diff --git a/gnome/breezydesktop@org.xronlinux/cursor.js b/gnome/breezydesktop@org.xronlinux/cursor.js
new file mode 100644
index 0000000..d7ff3e3
--- /dev/null
+++ b/gnome/breezydesktop@org.xronlinux/cursor.js
@@ -0,0 +1,68 @@
+// Taken from https://github.com/jkitching/soft-brightness-plus
+//
+// Copyright (C) 2023 Joel Kitching (jkitching on Github)
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+
+// Copied almost verbatim from ui/magnifier.js.
+export const MouseSpriteContent = GObject.registerClass({
+ Implements: [Clutter.Content],
+}, class MouseSpriteContent extends GObject.Object {
+ _init() {
+ super._init();
+ this._texture = null;
+ }
+
+ vfunc_get_preferred_size() {
+ if (!this._texture)
+ return [false, 0, 0];
+
+ return [true, this._texture.get_width(), this._texture.get_height()];
+ }
+
+ vfunc_paint_content(actor, node, _paintContext) {
+ if (!this._texture)
+ return;
+
+ let color = Clutter.Color.from_string('#ffffff'); // white
+ let [minFilter, magFilter] = actor.get_content_scaling_filters();
+ let textureNode = new Clutter.TextureNode(this._texture,
+ color, minFilter, magFilter);
+ textureNode.set_name('SoftBrightnessPlusMouseSpriteContent');
+ node.add_child(textureNode);
+
+ textureNode.add_rectangle(actor.get_content_box());
+ }
+
+ get texture() {
+ return this._texture;
+ }
+
+ set texture(coglTexture) {
+ if (this._texture === coglTexture)
+ return;
+
+ let oldTexture = this._texture;
+ this._texture = coglTexture;
+ this.invalidate();
+
+ if (!oldTexture || !coglTexture ||
+ oldTexture.get_width() !== coglTexture.get_width() ||
+ oldTexture.get_height() !== coglTexture.get_height())
+ this.invalidate_size();
+ }
+});
\ No newline at end of file
diff --git a/gnome/breezydesktop@org.xronlinux/cursormanager.js b/gnome/breezydesktop@org.xronlinux/cursormanager.js
new file mode 100644
index 0000000..a98e258
--- /dev/null
+++ b/gnome/breezydesktop@org.xronlinux/cursormanager.js
@@ -0,0 +1,312 @@
+
+
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
+import { MouseSpriteContent } from './cursor.js';
+
+// Taken from https://github.com/jkitching/soft-brightness-plus
+export class CursorManager {
+ constructor(logger, settings, mainActor) {
+ this._logger = logger;
+ this._settings = settings;
+ this._mainActor = mainActor;
+
+ this._enableTimeoutId = null;
+ this._changeHookFn = null;
+
+ this._cloneMouseSetting = null;
+ this._cloneMouseSettingChangedConnection = null;
+
+ // Set/destroyed by _enableCloningMouse/_disableCloningMouse
+ this._cursorWantedVisible = null;
+ this._cursorTracker = null;
+ this._cursorTrackerSetPointerVisible = null;
+ this._cursorTrackerSetPointerVisibleBound = null;
+ this._cursorSprite = null;
+ this._cursorActor = null;
+ this._cursorWatcher = null;
+ this._cursorSeat = null;
+ // Set/destroyed by _startCloningMouse / _stopCloningMouse
+ this._cursorWatch = null;
+ this._cursorChangedConnection = null;
+ this._cursorVisibilityChangedConnection = null;
+ // Set/destroyed by _delayedSetPointerInvisible/_clearDelayedSetPointerInvibleCallbacks
+ this._delayedSetPointerInvisibleIdleSource = null;
+ }
+
+ setChangeHook(fn) {
+ this._changeHookFn = fn;
+ }
+
+ enable() {
+ // First 500ms: For some reason, starting the mouse cloning at this
+ // stage fails when gnome-shell is restarting on x11 and the mouse
+ // listener doesn't receive any events. Adding a small delay before
+ // starting the whole mouse cloning business helps.
+ this._enableTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
+ // Wait 500ms before starting to check for the _brightness object.
+ this._enableTimeoutId = null;
+ this._enable();
+ // Ensure proper stacking order for cursor and overlay.
+ if (this._changeHookFn !== null) {
+ this._changeHookFn();
+ }
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _enable() {
+ this._cloneMouseSetting = true; // this._settings.get_boolean('clone-mouse');
+ this._enableCloningMouse();
+ // this._cloneMouseSettingChangedConnection = this._settings.connect('changed::clone-mouse', this._on_clone_mouse_change.bind(this));
+ }
+
+ disable() {
+ // If _enableTimeoutId is non-null, _enable() has not run yet, and will
+ // not run. Do not run _disable() in this case.
+ GLib.source_remove(this._enableTimeoutId);
+ if (this._enableTimeoutId !== null) {
+ return;
+ }
+ this._enableTimeoutId = null;
+ this._changeHookFn = null;
+
+ // this._settings.disconnect(this._cloneMouseSettingChangedConnection);
+ // this._cloneMouseSettingChangedConnection = null;
+ this._disableCloningMouse();
+ this._cloneMouseSetting = null;
+
+ // Set/destroyed by _enableCloningMouse/_disableCloningMouse
+ this._cursorWantedVisible = null;
+ this._cursorTracker = null;
+ this._cursorTrackerSetPointerVisible = null;
+ this._cursorTrackerSetPointerVisibleBound = null;
+ this._cursorSprite = null;
+ this._cursorActor = null;
+ this._cursorWatcher = null;
+ this._cursorSeat = null;
+ // Set/destroyed by _startCloningMouse / _stopCloningMouse
+ this._cursorWatch = null;
+ this._cursorChangedConnection = null;
+ this._cursorVisibilityChangedConnection = null;
+ // Set/destroyed by _delayedSetPointerInvisible/_clearDelayedSetPointerInvibleCallbacks
+ this._delayedSetPointerInvisibleIdleSource = null;
+ }
+
+ startCloning() {
+ if (this._cursorWantedVisible) {
+ this._startCloningMouse();
+ }
+ }
+
+ stopCloning() {
+ this._stopCloningShowMouse();
+ }
+
+ hidePointer() {
+ this._setPointerVisible(false);
+ }
+
+ _isMouseClonable() {
+ return this._cloneMouseSetting;
+ }
+
+ _on_clone_mouse_change() {
+ const cloneMouse = true; // this._settings.get_boolean('clone-mouse');
+ if (cloneMouse == this._cloneMouseSetting) {
+ this._logger.log_debug('_on_clone_mouse_change(): no setting change, no change');
+ return;
+ }
+ if (cloneMouse) {
+ // Starting to clone mouse
+ this._logger.log_debug('_on_clone_mouse_change(): starting mouse cloning');
+ this._cloneMouseSetting = true;
+ this._enableCloningMouse();
+ if (this._changeHookFn !== null) {
+ this._changeHookFn();
+ }
+ } else {
+ this._logger.log_debug('_on_clone_mouse_change(): stopping mouse cloning');
+ this._disableCloningMouse();
+ this._cloneMouseSetting = false;
+ }
+ }
+
+ _enableCloningMouse() {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ this._logger.log_debug('_enableCloningMouse()');
+
+ this._cursorWantedVisible = true;
+ this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
+ this._cursorTrackerSetPointerVisible = Meta.CursorTracker.prototype.set_pointer_visible;
+ this._cursorTrackerSetPointerVisibleBound = this._cursorTrackerSetPointerVisible.bind(this._cursorTracker);
+ Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisibleReplacement.bind(this);
+
+ this._cursorSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
+ this._cursorSprite.content = new MouseSpriteContent();
+
+ this._cursorActor = new Clutter.Actor();
+ this._cursorActor.add_actor(this._cursorSprite);
+ this._cursorWatcher = PointerWatcher.getPointerWatcher();
+ this._cursorSeat = Clutter.get_default_backend().get_default_seat();
+ }
+
+ _disableCloningMouse() {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ this._stopCloningShowMouse();
+ this._logger.log_debug('_disableCloningMouse()');
+
+ Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
+
+ this._cursorWantedVisible = null;
+ this._cursorTracker = null;
+ this._cursorTrackerSetPointerVisible = null;
+ this._cursorTrackerSetPointerVisibleBound = null;
+ this._cursorSprite = null;
+ this._cursorActor = null;
+ this._cursorWatcher = null;
+ this._cursorSeat = null;
+ }
+
+ _setPointerVisible(visible) {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ this._cursorTrackerSetPointerVisibleBound(visible);
+ }
+
+ _cursorTrackerSetPointerVisibleReplacement(visible) {
+ if (visible) {
+ this._startCloningMouse();
+ // For some reason, exiting the magnifier causes the
+ // stacking order for the cursor and overlay actors to be
+ // swapped around. Reassert stacking order whenever the
+ // pointer should become visible again.
+ if (this._changeHookFn !== null) {
+ this._changeHookFn();
+ }
+ } else {
+ this._stopCloningMouse();
+ this._setPointerVisible(false);
+ }
+ this._cursorWantedVisible = visible;
+ }
+
+ _startCloningMouse() {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ this._logger.log_debug('_startCloningMouse()');
+ if (this._cursorWatch == null) {
+ this._mainActor.add_actor(this._cursorActor);
+ this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
+ this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
+ const interval = 1000 / 60;
+ this._logger.log_debug('_startCloningMouse(): watch interval = ' + interval + ' ms');
+ this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
+
+ this._updateMouseSprite();
+ this._updateMousePosition();
+ }
+ this._setPointerVisible(false);
+
+ if (this._cursorTracker.set_keep_focus_while_hidden) {
+ this._cursorTracker.set_keep_focus_while_hidden(true);
+ }
+
+ if (!this._cursorSeat.is_unfocus_inhibited()) {
+ this._cursorSeat.inhibit_unfocus();
+ }
+ }
+
+ _stopCloningShowMouse() {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ this._logger.log_debug('_stopCloningShowMouse(), restoring cursor visibility to ' + this._cursorWantedVisible);
+ this._stopCloningMouse();
+ this._setPointerVisible(this._cursorWantedVisible);
+
+ if (this._cursorTracker.set_keep_focus_while_hidden) {
+ this._cursorTracker.set_keep_focus_while_hidden(false);
+ }
+
+ if (this._cursorSeat.is_unfocus_inhibited()) {
+ this._cursorSeat.uninhibit_unfocus();
+ }
+ }
+
+ _stopCloningMouse() {
+ if (!this._isMouseClonable()) {
+ return;
+ }
+ if (this._cursorWatch != null) {
+ this._logger.log_debug('_stopCloningMouse()');
+
+ this._cursorWatch.remove();
+ this._cursorWatch = null;
+
+ this._cursorTracker.disconnect(this._cursorChangedConnection);
+ this._cursorChangedConnection = null;
+
+ this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
+ this._cursorVisibilityChangedConnection = null;
+
+ this._mainActor.remove_actor(this._cursorActor);
+ }
+
+ this._clearDelayedSetPointerInvibleCallbacks();
+ }
+
+ _updateMousePosition(actor, event) {
+ const [x, y, mask] = global.get_pointer();
+ this._cursorActor.set_position(x, y);
+ this._delayedSetPointerInvisible();
+ }
+
+ _updateMouseSprite() {
+ const sprite = this._cursorTracker.get_sprite();
+ if (sprite) {
+ this._cursorSprite.content.texture = sprite;
+ this._cursorSprite.show();
+ } else {
+ this._cursorSprite.hide();
+ }
+
+ const [xHot, yHot] = this._cursorTracker.get_hot();
+ this._cursorSprite.set({
+ translation_x: -xHot,
+ translation_y: -yHot,
+ });
+ this._delayedSetPointerInvisible();
+ }
+
+ _delayedSetPointerInvisible() {
+ this._setPointerVisible(false);
+
+ // Clear the pointer upon entering idle loop
+ if (this._delayedSetPointerInvisibleIdleSource == null) {
+ this._delayedSetPointerInvisibleIdleSource = GLib.idle_add(
+ GLib.PRIORITY_DEFAULT,
+ () => {
+ this._setPointerVisible(false);
+ this._delayedSetPointerInvisibleIdleSource = null;
+ return false;
+ }
+ );
+ }
+ }
+
+ _clearDelayedSetPointerInvibleCallbacks() {
+ if (this._delayedSetPointerInvisibleIdleSource != null) {
+ GLib.source_remove(this._delayedSetPointerInvisibleIdleSource);
+ this._delayedSetPointerInvisibleIdleSource = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/gnome/breezydesktop@org.xronlinux/extension.js b/gnome/breezydesktop@org.xronlinux/extension.js
index d104469..34bdc0d 100644
--- a/gnome/breezydesktop@org.xronlinux/extension.js
+++ b/gnome/breezydesktop@org.xronlinux/extension.js
@@ -4,9 +4,12 @@ import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import Meta from 'gi://Meta';
-import ExtensionUtils from 'gi://ExtensionUtils';
-import Main from 'gi://Main';
-import PanelMenu from 'gi://PanelMenu';
+import * as Config from 'resource:///org/gnome/shell/misc/config.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Logger from './logger.js';
+import { CursorManager } from './cursormanager.js';
+
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
const UINT8_SIZE = 1;
const BOOL_SIZE = UINT8_SIZE;
@@ -195,12 +198,32 @@ function setIntermittentUniformVariables() {
}
-export default class ExampleExtension extends Extension {
+export default class BreezyDesktopExtension extends Extension {
+ constructor(metadata, uuid) {
+ super(metadata, uuid);
+ this._extensionPath = metadata.path;
+
+ // Set/destroyed by enable/disable
+ this._settings = null;
+ this._logger = null;
+ this._cursorManager = null;
+ this._removeSettingsCallbacks = [];
+ }
+
enable() {
+ // this._settings = this.getSettings();
+ this._logger = new Logger.Logger('soft-brightness-plus', this.metadata, Config.PACKAGE_VERSION);
+ // this._logger.set_debug(this._settings.get_boolean('debug'));
+ this._logger.log_debug('enable(), session mode = ' + Main.sessionMode.currentMode);
+ this._logger.logVersion();
+
+ this._cursorManager = new CursorManager(this._logger, this._settings, global.stage);
+ this._cursorManager.enable();
+
+ const extensionPath = this._extensionPath;
var XREffect = GObject.registerClass({}, class XREffect extends Shell.GLSLEffect {
vfunc_build_pipeline() {
- const shaderPath = GLib.getenv('BREEZY_GNOME_SHADER_PATH');
- const code = getShaderSource(shaderPath);
+ const code = getShaderSource(`${extensionPath}/IMUAdjust.frag`);
const main = 'PS_IMU_Transform(vec4(0, 0, 0, 0), cogl_tex_coord_in[0].xy, cogl_color_out);';
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false);
@@ -216,7 +239,6 @@ export default class ExampleExtension extends Extension {
if (data[0]) {
const buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer);
- var repaintNeeded = false;
if (!this._initialized) {
this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
@@ -227,8 +249,9 @@ export default class ExampleExtension extends Extension {
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
this.setIntermittentUniformVariables();
+ this._repaint_needed = false;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
- repaintNeeded = true;
+ this._repaint_needed = true;
this.queue_repaint();
return GLib.SOURCE_CONTINUE;
});
@@ -243,9 +266,10 @@ export default class ExampleExtension extends Extension {
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
- // if (repaintNeeded) {
+ if (this._repaint_needed) {
super.vfunc_paint_target(node, paintContext);
- // }
+ this._repaint_needed = false;
+ }
}
}
});
@@ -254,6 +278,9 @@ export default class ExampleExtension extends Extension {
}
disable() {
+ this._logger.log_debug('disable(), session mode = ' + Main.sessionMode.currentMode);
+ this._cursorManager.disable();
+ this._cursorManager = null;
}
}
diff --git a/gnome/breezydesktop@org.xronlinux/logger.js b/gnome/breezydesktop@org.xronlinux/logger.js
new file mode 100644
index 0000000..dd160ae
--- /dev/null
+++ b/gnome/breezydesktop@org.xronlinux/logger.js
@@ -0,0 +1,98 @@
+// Taken from https://github.com/jkitching/soft-brightness-plus
+//
+// Copyright (C) 2023 Joel Kitching (jkitching on Github)
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import * as Config from 'resource:///org/gnome/shell/misc/config.js';
+import GLib from 'gi://GLib';
+import System from 'system';
+
+export class Logger {
+ constructor(title, metadata, packageVersion) {
+ this._title = title;
+ this._metadata = metadata;
+ this._packageVersion = packageVersion;
+
+ this._first_log = true;
+ this._debug = false;
+ }
+
+ get_version() {
+ return this._metadata['version'] + ' / git ' + this._metadata['vcs_revision'];
+ }
+
+ logVersion() {
+ const gnomeShellVersion = Config.PACKAGE_VERSION;
+ if (gnomeShellVersion != undefined) {
+ const splitVersion = gnomeShellVersion.split('.').map((x) => {
+ x = Number(x);
+ if (Number.isNaN(x)) {
+ return 0;
+ } else {
+ return x;
+ }
+ });
+ const major = splitVersion[0];
+ const minor = splitVersion.length >= 2 ? splitVersion[1] : 0;
+ const patch = splitVersion.length >= 3 ? splitVersion[2] : 0;
+ const xdgSessionType = GLib.getenv('XDG_SESSION_TYPE');
+ const onWayland = xdgSessionType == 'wayland';
+ this.log_debug('_logVersion(): gnome-shell version major=' + major + ', minor=' + minor + ', patch=' + patch + ', system_version=' + System.version + ', XDG_SESSION_TYPE=' + xdgSessionType);
+ this.log_debug('_logVersion(): onWayland=' + onWayland);
+ }
+ }
+
+ log(text) {
+ if (this._first_log) {
+ this._first_log = false;
+ let msg = 'version ' + this.get_version();
+ const gnomeShellVersion = this._packageVersion;
+ if (gnomeShellVersion != undefined) {
+ msg += ' on Gnome-Shell ' + gnomeShellVersion;
+ }
+ const gjsVersion = System.version;
+ if (gjsVersion != undefined) {
+ const gjsVersionMajor = Math.floor(gjsVersion / 10000);
+ const gjsVersionMinor = Math.floor((gjsVersion % 10000) / 100);
+ const gjsVersionPatch = gjsVersion % 100;
+ msg += (' / gjs ' + gjsVersionMajor +
+ '.' + gjsVersionMinor +
+ '.' + gjsVersionPatch +
+ ' (' + gjsVersion + ')'
+ );
+ }
+ const sessionType = GLib.getenv('XDG_SESSION_TYPE');
+ if (sessionType != undefined) {
+ msg += ' / ' + sessionType;
+ }
+ this.log(msg);
+ }
+ console.log('' + this._title + ': ' + text);
+ }
+
+ log_debug(text) {
+ if (this._debug) {
+ this.log(text);
+ }
+ }
+
+ set_debug(debug) {
+ this._debug = debug;
+ }
+
+ get_debug() {
+ return this._debug;
+ }
+};
\ No newline at end of file
diff --git a/gnome/breezydesktop@org.xronlinux/metadata.json b/gnome/breezydesktop@org.xronlinux/metadata.json
index f1c65b0..b0009c0 100644
--- a/gnome/breezydesktop@org.xronlinux/metadata.json
+++ b/gnome/breezydesktop@org.xronlinux/metadata.json
@@ -1,7 +1,7 @@
{
"uuid": "breezydesktop@org.xronlinux",
- "name": "Breezy GNOME",
- "description": "XR virtual desktop for Linux.",
+ "name": "Breezy GNOME XR Desktop",
+ "description": "XR virtual desktop for GNOME.",
"shell-version": [
"45", "46"
],