import Clutter from 'gi://Clutter'; import Meta from 'gi://Meta'; import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js'; import { MouseSpriteContent } from './cursor.js'; import Globals from './globals.js'; // Taken from https://github.com/jkitching/soft-brightness-plus export class CursorManager { constructor(mainActor, refreshRate) { this._mainActor = mainActor; this._refreshRate = refreshRate; // 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; this._cursorUnfocusInhibited = false; // Set/destroyed by _startCloningMouse / _stopCloningMouse this._cursorWatch = null; this._cursorChangedConnection = null; this._cursorVisibilityChangedConnection = null; this._redraw_timeline = null; } enable() { Globals.logger.log_debug('CursorManager enable'); this._enableCloningMouse(); this.startCloning(); } disable() { Globals.logger.log_debug('CursorManager disable'); this._disableCloningMouse(); } startCloning() { Globals.logger.log_debug('CursorManager startCloning'); this._startCloningMouse(); } stopCloning() { Globals.logger.log_debug('CursorManager stopCloning'); this._stopCloningMouse(); } // After this: // * real cursor is disabled // * cloning is "on" // * cloned cursor not visible, but ready for _startCloningMouse to make it visible // // okay if _startCloningMouse is not immediately called since set_pointer_visible is bound to our replacement function // and will trigger _startCloningMouse when the cursor should be shown _enableCloningMouse() { Globals.logger.log_debug('CursorManager _enableCloningMouse'); this._cursorTracker = Meta.CursorTracker.get_for_display(global.display); this._cursorWantedVisible = this._cursorTracker.get_pointer_visible(); 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._cursorTrackerSetPointerVisibleBound(false); this._cursorSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE }); this._cursorSprite.content = new MouseSpriteContent(); this._cursorActor = new Clutter.Actor(); this._cursorActor.add_child(this._cursorSprite); this._cursorWatcher = PointerWatcher.getPointerWatcher(); this._cursorSeat = Clutter.get_default_backend().get_default_seat(); } // After this: // * real cursor enabled, manages its own visibility // * cloning is "off" // * no cloned cursor // // completely reverts _enableCloningMouse _disableCloningMouse() { Globals.logger.log_debug('CursorManager _disableCloningMouse'); this._stopCloningMouse(); Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible; this._cursorTracker.set_pointer_visible(this._cursorWantedVisible); if (this._cursorSprite) { this._cursorSprite.content = null; if (this._cursorActor) this._cursorActor.remove_child(this._cursorSprite); } this._cursorWantedVisible = null; this._cursorTracker = null; this._cursorTrackerSetPointerVisible = null; this._cursorTrackerSetPointerVisibleBound = null; this._cursorSprite = null; this._cursorActor = null; this._cursorWatcher = null; this._cursorSeat = null; } // bound to Meta.CursorTracker.prototype.set_pointer_visible when cloning is "on" // original function available in this._cursorTrackerSetPointerVisibleBound _cursorTrackerSetPointerVisibleReplacement(visible) { Globals.logger.log_debug('CursorManager _cursorTrackerSetPointerVisibleReplacement'); this._cursorWantedVisible = visible; if (visible) { this._startCloningMouse(); } else { this._stopCloningMouse(); } } // After this: // * real cursor is hidden // * cloning is "on" // * clone cursor is visible // // add the clone cursor actor, watch for pointer movement and cursor changes, reflect them in the cloned cursor // prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true _startCloningMouse() { Globals.logger.log_debug('CursorManager _startCloningMouse'); this._mainActor.add_child(this._cursorActor); this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._queueSpriteUpdate.bind(this)); this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._queueVisibilityUpdate.bind(this)); const interval = 1000.0 / this._refreshRate; this._redraw_timeline = Clutter.Timeline.new_for_actor(this._mainActor, interval * 10); this._redraw_timeline.connect('new-frame', (() => { this.handleNewFrame(); }).bind(this)); // Some elements will occasionally appear above the cursor, so we periodically reset the actor stacking. // This could theoretically be fixed "better" by attaching to all events that might affect actor ordering, // but finding a comprehensive list is difficult and not future proof. So this ugly solution helps us // catch everything. this._redraw_timeline.connect('completed', (() => { this._periodicReset(); }).bind(this)); this._redraw_timeline.set_repeat_count(-1); this._redraw_timeline.start(); this._cursorWatch = this._cursorWatcher.addWatch(interval, this._queuePositionUpdate.bind(this)); const [x, y] = global.get_pointer(); this._queuePositionUpdate(x, y); this._queueSpriteUpdate(); if (this._cursorTracker.set_keep_focus_while_hidden) { this._cursorTracker.set_keep_focus_while_hidden(true); } if (!this._cursorUnfocusInhibited) { Globals.logger.log_debug('inhibit_unfocus'); this._cursorSeat.inhibit_unfocus(); this._cursorUnfocusInhibited = true; } } // After this: // * real cursor is hidden // * cloning is "on" // * cloned cursor not visible, but ready for _startCloningMouse to make it visible // // completely reverts _startCloningMouse _stopCloningMouse() { Globals.logger.log_debug('CursorManager _stopCloningMouse'); if (this._cursorWatch != null) { this._cursorWatch.remove(); this._cursorWatch = null; } if (this._cursorChangedConnection) { this._cursorTracker.disconnect(this._cursorChangedConnection); this._cursorChangedConnection = null; } if (this._cursorVisibilityChangedConnection) { this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection); this._cursorVisibilityChangedConnection = null; } if (this._redraw_timeline) { this._redraw_timeline.stop(); this._redraw_timeline = null; } if (this._cursorActor) this._mainActor.remove_child(this._cursorActor); if (this._cursorUnfocusInhibited) { Globals.logger.log_debug('uninhibit_unfocus'); this._cursorSeat.uninhibit_unfocus(); this._cursorUnfocusInhibited = false; } } _queuePositionUpdate(x, y) { this._queued_cursor_position = [x, y]; } _queueSpriteUpdate() { this._queued_sprite_update = true; } _queueVisibilityUpdate() { this._queued_visibility_update = true; this._cursorTrackerSetPointerVisibleBound(false); this._queueSpriteUpdate(); } // updates the stacking and other attributes that are hard to track and may periodically get out of sync _periodicReset() { this._queue_reset = true; this._queueVisibilityUpdate(); } handleNewFrame() { let redraw = false; if (this._queued_cursor_position) { const [x, y] = this._queued_cursor_position; this._cursorActor.set_position(x, y); this._queued_cursor_position = null; redraw = true; } if (this._queued_sprite_update) { 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._queued_sprite_update = false; redraw = true; } if (this._queued_visibility_update) { this._queued_visibility_update = false; redraw = true; } if (this._queue_reset) { if (this._mainActor.get_last_child() !== this._cursorActor) this._mainActor.set_child_above_sibling(this._cursorActor, null); // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) { Globals.logger.log_debug('reinhibiting'); this._cursorSeat.inhibit_unfocus(); } this._queue_reset = false; redraw = true; } return redraw; } }