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" ],