From c0961807fb4ecb202c86bfd7c34c940829aece36 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Mon, 16 Mar 2026 20:28:46 -0400 Subject: [PATCH 1/5] gestures: Add support for wayland sessions. Unfortunately the way relevant events are handled in muffin differs enough between session types that we need to keep touchegg support for x11, at least for now. - Refactor the GesturesManager to support both touchegg and native clutter/libinput events. - Actual gesture actions behavior are unchanged. Todo: - Touchpad gestures work only if the touchpad is enabled. This would seem logical, but is only the case for Wayland - in x11, gestures work regardless. - Only tested with touchpads so far. Need to test touchscreen behavior. ref: linuxmint/wayland#99 --- data/org.cinnamon.gestures.gschema.xml | 2 +- .../cinnamon-settings/modules/cs_gestures.py | 34 +- js/ui/gestures/actions.js | 8 +- .../{ToucheggTypes.js => gestureTypes.js} | 0 js/ui/gestures/gesturesManager.js | 78 +-- js/ui/gestures/nativeGestureSource.js | 110 ++++ js/ui/gestures/nativeGestures.js | 570 ++++++++++++++++++ js/ui/gestures/toucheggGestureSource.js | 60 ++ po/POTFILES.in | 7 +- src/cinnamon-touchegg-client.c | 3 +- 10 files changed, 802 insertions(+), 70 deletions(-) rename js/ui/gestures/{ToucheggTypes.js => gestureTypes.js} (100%) create mode 100644 js/ui/gestures/nativeGestureSource.js create mode 100644 js/ui/gestures/nativeGestures.js create mode 100644 js/ui/gestures/toucheggGestureSource.js diff --git a/data/org.cinnamon.gestures.gschema.xml b/data/org.cinnamon.gestures.gschema.xml index 8c1402aa80..cce5e2a609 100644 --- a/data/org.cinnamon.gestures.gschema.xml +++ b/data/org.cinnamon.gestures.gschema.xml @@ -3,7 +3,7 @@ gettext-domain="@GETTEXT_PACKAGE@"> false - Enables gesture support using Touchegg. + Enables gesture support. 60 diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py index 82ca69465f..b3f0b24d9a 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py @@ -90,8 +90,15 @@ def __init__(self, content_box): self.disabled_box = None def on_module_selected(self): - installed = GLib.find_program_in_path("touchegg") - alive = self.test_daemon_alive() + self.is_wayland = util.get_session_type() == "wayland" + + # On X11, check for touchegg; on Wayland, native gestures are used + if self.is_wayland: + installed = True + alive = True + else: + installed = GLib.find_program_in_path("touchegg") + alive = self.test_daemon_alive() if self.gesture_settings is None: self.gesture_settings = Gio.Settings(schema_id=SCHEMA) @@ -263,21 +270,24 @@ def sort_by_direction(key1, key2): self.disabled_retry_button.set_visible(False) self.disabled_page_disable_button.set_visible(False) - if not installed: - text = _("The touchegg package must be installed for gesture support.") - self.disabled_retry_button.show() - elif not self.gesture_settings.get_boolean("enabled"): + text = "" + if not self.gesture_settings.get_boolean("enabled"): self.disabled_page_switch.set_visible(True) text = _("Gestures are disabled") - elif not alive: - text = _("The Touchegg service is not running") - if self.gesture_settings.get_boolean("enabled"): - self.disabled_page_disable_button.set_visible(True) - self.disabled_retry_button.show() + elif not self.is_wayland: + # X11-specific: check for touchegg + if not installed: + text = _("The touchegg package must be installed for gesture support.") + self.disabled_retry_button.show() + elif not alive: + text = _("The Touchegg service is not running") + if self.gesture_settings.get_boolean("enabled"): + self.disabled_page_disable_button.set_visible(True) + self.disabled_retry_button.show() self.sidePage.stack.set_transition_type(Gtk.StackTransitionType.NONE) - if not enabled or not alive or not installed: + if not enabled or (not self.is_wayland and (not alive or not installed)): self.disabled_label.set_markup(f"{text}") page = "disabled" else: diff --git a/js/ui/gestures/actions.js b/js/ui/gestures/actions.js index da728fd28a..a5f261d43a 100644 --- a/js/ui/gestures/actions.js +++ b/js/ui/gestures/actions.js @@ -2,13 +2,13 @@ const { GLib, Gio, Cinnamon, Meta, Cvc } = imports.gi; const Main = imports.ui.main; -const { GestureType } = imports.ui.gestures.ToucheggTypes; +const { GestureType } = imports.ui.gestures.gestureTypes; const { MprisController } = imports.ui.gestures.mprisController; const Magnifier = imports.ui.magnifier; const touchpad_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.peripherals.touchpad" }); -const CONTINUOUS_ACTION_POLL_INTERVAL = 50 * 1000; +const CONTINUOUS_ACTION_POLL_INTERVAL = 50; // milliseconds var make_action = (settings, definition, device) => { var threshold = 100; @@ -473,8 +473,6 @@ var MediaAction = class extends BaseAction { } } -const ZOOM_SAMPLE_RATE = 20 * 1000 // 20 ms; g_get_monotonic_time() returns microseconds - var ZoomAction = class extends BaseAction { constructor(definition, device, threshold) { super(definition, device, threshold); @@ -484,7 +482,7 @@ var ZoomAction = class extends BaseAction { if (definition.custom_value !== "") { try { - let adjust = parseInt(definition.custom_value) * 1000; + let adjust = parseInt(definition.custom_value); this.poll_interval = this.poll_interval + adjust; } catch (e) {} } diff --git a/js/ui/gestures/ToucheggTypes.js b/js/ui/gestures/gestureTypes.js similarity index 100% rename from js/ui/gestures/ToucheggTypes.js rename to js/ui/gestures/gestureTypes.js diff --git a/js/ui/gestures/gesturesManager.js b/js/ui/gestures/gesturesManager.js index 2213c6c730..61243e6bc1 100644 --- a/js/ui/gestures/gesturesManager.js +++ b/js/ui/gestures/gesturesManager.js @@ -1,23 +1,21 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -const { Gio, GObject, Cinnamon, Meta } = imports.gi; -const Util = imports.misc.util; +const { Gio, Meta } = imports.gi; const SignalManager = imports.misc.signalManager; const ScreenSaver = imports.misc.screenSaver; const actions = imports.ui.gestures.actions; -const { +const { GestureType, GestureDirection, - DeviceType, GestureTypeString, GestureDirectionString, - GesturePhaseString, DeviceTypeString -} = imports.ui.gestures.ToucheggTypes; +} = imports.ui.gestures.gestureTypes; +const { NativeGestureSource } = imports.ui.gestures.nativeGestureSource; +const { ToucheggGestureSource } = imports.ui.gestures.toucheggGestureSource; const SCHEMA = "org.cinnamon.gestures"; -const TOUCHPAD_SCHEMA = "org.cinnamon.desktop.peripherals.touchpad" const NON_GESTURE_KEYS = [ "enabled", @@ -86,20 +84,25 @@ var GestureDefinition = class { var GesturesManager = class { constructor(wm) { - if (Meta.is_wayland_compositor()) { - global.log("Gestures disabled on Wayland"); - return; - } - this.signalManager = new SignalManager.SignalManager(null); this.settings = new Gio.Settings({ schema_id: SCHEMA }) + this.screenSaverProxy = new ScreenSaver.ScreenSaverProxy(); + this.current_gesture = null; + this.live_actions = new Map(); + + if (Meta.is_wayland_compositor()) { + this.gestureSource = new NativeGestureSource(); + } else { + this.gestureSource = new ToucheggGestureSource(); + } this.migrate_settings(); this.signalManager.connect(this.settings, "changed", this.settings_or_devices_changed, this); - this.screenSaverProxy = new ScreenSaver.ScreenSaverProxy(); - this.client = null; - this.current_gesture = null; + + this.gestureSource.connect('gesture-begin', this.gesture_begin.bind(this)); + this.gestureSource.connect('gesture-update', this.gesture_update.bind(this)); + this.gestureSource.connect('gesture-end', this.gesture_end.bind(this)); this.settings_or_devices_changed() } @@ -142,41 +145,14 @@ var GesturesManager = class { } } - setup_client() { - if (this.client == null) { - global.log('Set up Touchegg client'); - actions.init_mixer(); - actions.init_mpris_controller(); - - this.client = new Cinnamon.ToucheggClient(); - - this.signalManager.connect(this.client, "gesture-begin", this.gesture_begin, this); - this.signalManager.connect(this.client, "gesture-update", this.gesture_update, this); - this.signalManager.connect(this.client, "gesture-end", this.gesture_end, this); - } - } - - shutdown_client() { - if (this.client == null) { - return; - } - - global.log('Shutdown Touchegg client'); - this.signalManager.disconnect("gesture-begin"); - this.signalManager.disconnect("gesture-update"); - this.signalManager.disconnect("gesture-end"); - this.client = null; - - actions.cleanup(); - } - settings_or_devices_changed(settings, key) { if (this.settings.get_boolean("enabled")) { this.setup_actions(); return; } - this.shutdown_client(); + this.gestureSource.shutdown(); + actions.cleanup(); } gesture_active() { @@ -184,8 +160,12 @@ var GesturesManager = class { } setup_actions() { - // Make sure the client is setup - this.setup_client(); + // Make sure gesture source is set up + if (!this.gestureSource.isActive()) { + actions.init_mixer(); + actions.init_mpris_controller(); + this.gestureSource.setup(); + } this.live_actions = new Map(); @@ -250,7 +230,7 @@ var GesturesManager = class { return definition; } - gesture_begin(client, type, direction, percentage, fingers, device, elapsed_time) { + gesture_begin(source, type, direction, percentage, fingers, device, elapsed_time) { if (this.current_gesture != null) { global.logWarning("New gesture started before another was completed. Clearing the old one"); this.current_gesture = null; @@ -277,7 +257,7 @@ var GesturesManager = class { this.current_gesture.begin(direction, percentage, elapsed_time); } - gesture_update(client, type, direction, percentage, fingers, device, elapsed_time) { + gesture_update(source, type, direction, percentage, fingers, device, elapsed_time) { if (this.current_gesture == null) { debug_gesture("Gesture update but there's no current one."); return; @@ -294,7 +274,7 @@ var GesturesManager = class { this.current_gesture.update(direction, percentage, elapsed_time); } - gesture_end(client, type, direction, percentage, fingers, device, elapsed_time) { + gesture_end(source, type, direction, percentage, fingers, device, elapsed_time) { if (this.current_gesture == null) { debug_gesture("Gesture end but there's no current one."); return; diff --git a/js/ui/gestures/nativeGestureSource.js b/js/ui/gestures/nativeGestureSource.js new file mode 100644 index 0000000000..9be631df45 --- /dev/null +++ b/js/ui/gestures/nativeGestureSource.js @@ -0,0 +1,110 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Signals = imports.signals; + +const { + TouchpadSwipeGesture, + TouchpadPinchGesture, + TouchSwipeGesture, + TouchPinchGesture, + TouchTapGesture +} = imports.ui.gestures.nativeGestures; + +const TOUCHPAD_SWIPE_FINGER_COUNTS = [3, 4]; +const TOUCHPAD_PINCH_FINGER_COUNTS = [2, 3, 4]; +const TOUCHSCREEN_FINGER_COUNTS = [2, 3, 4, 5]; + +/** + * NativeGestureSource - Gesture source using native Clutter touchpad events + */ +var NativeGestureSource = class { + constructor() { + this._touchpadSwipeGesture = null; + this._touchpadPinchGesture = null; + this._touchSwipeGestures = []; + this._touchPinchGestures = []; + this._touchTapGestures = []; + } + + setup() { + global.log('Setting up native gesture source'); + + this._touchpadSwipeGesture = new TouchpadSwipeGesture(TOUCHPAD_SWIPE_FINGER_COUNTS); + this._touchpadSwipeGesture.connect('detected-begin', this._onGestureBegin.bind(this)); + this._touchpadSwipeGesture.connect('detected-update', this._onGestureUpdate.bind(this)); + this._touchpadSwipeGesture.connect('detected-end', this._onGestureEnd.bind(this)); + + this._touchpadPinchGesture = new TouchpadPinchGesture(TOUCHPAD_PINCH_FINGER_COUNTS); + this._touchpadPinchGesture.connect('detected-begin', this._onGestureBegin.bind(this)); + this._touchpadPinchGesture.connect('detected-update', this._onGestureUpdate.bind(this)); + this._touchpadPinchGesture.connect('detected-end', this._onGestureEnd.bind(this)); + + for (let fingers of TOUCHSCREEN_FINGER_COUNTS) { + const swipeGesture = new TouchSwipeGesture(fingers); + swipeGesture.connect('detected-begin', this._onGestureBegin.bind(this)); + swipeGesture.connect('detected-update', this._onGestureUpdate.bind(this)); + swipeGesture.connect('detected-end', this._onGestureEnd.bind(this)); + global.stage.add_action_with_name(`touch-swipe-${fingers}`, swipeGesture); + this._touchSwipeGestures.push(swipeGesture); + + const pinchGesture = new TouchPinchGesture(fingers); + pinchGesture.connect('detected-begin', this._onGestureBegin.bind(this)); + pinchGesture.connect('detected-update', this._onGestureUpdate.bind(this)); + pinchGesture.connect('detected-end', this._onGestureEnd.bind(this)); + global.stage.add_action_with_name(`touch-pinch-${fingers}`, pinchGesture); + this._touchPinchGestures.push(pinchGesture); + + const tapGesture = new TouchTapGesture(fingers); + tapGesture.connect('detected-begin', this._onGestureBegin.bind(this)); + tapGesture.connect('detected-end', this._onGestureEnd.bind(this)); + global.stage.add_action_with_name(`touch-tap-${fingers}`, tapGesture); + this._touchTapGestures.push(tapGesture); + } + } + + shutdown() { + global.log('Shutting down native gesture source'); + + if (this._touchpadSwipeGesture) { + this._touchpadSwipeGesture.destroy(); + this._touchpadSwipeGesture = null; + } + + if (this._touchpadPinchGesture) { + this._touchpadPinchGesture.destroy(); + this._touchpadPinchGesture = null; + } + + for (let gesture of this._touchSwipeGestures) { + global.stage.remove_action(gesture); + } + this._touchSwipeGestures = []; + + for (let gesture of this._touchPinchGestures) { + global.stage.remove_action(gesture); + } + this._touchPinchGestures = []; + + for (let gesture of this._touchTapGestures) { + global.stage.remove_action(gesture); + } + this._touchTapGestures = []; + } + + isActive() { + return this._touchpadSwipeGesture !== null; + } + + _onGestureBegin(source, type, direction, percentage, fingers, device, time) { + this.emit('gesture-begin', type, direction, percentage, fingers, device, time); + } + + _onGestureUpdate(source, type, direction, percentage, fingers, device, time) { + this.emit('gesture-update', type, direction, percentage, fingers, device, time); + } + + _onGestureEnd(source, type, direction, percentage, fingers, device, time) { + this.emit('gesture-end', type, direction, percentage, fingers, device, time); + } +}; +Signals.addSignalMethods(NativeGestureSource.prototype); diff --git a/js/ui/gestures/nativeGestures.js b/js/ui/gestures/nativeGestures.js new file mode 100644 index 0000000000..3879a88588 --- /dev/null +++ b/js/ui/gestures/nativeGestures.js @@ -0,0 +1,570 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const { Clutter, GObject, Gio } = imports.gi; +const Signals = imports.signals; + +const { + GestureType, + GestureDirection, + DeviceType +} = imports.ui.gestures.gestureTypes; + +// Distance thresholds for gesture detection (in pixels) +const TOUCHPAD_BASE_HEIGHT = 300; +const TOUCHPAD_BASE_WIDTH = 400; +const DRAG_THRESHOLD_DISTANCE = 16; + +const TouchpadState = { + NONE: 0, + PENDING: 1, + HANDLING: 2, + IGNORED: 3, +}; + +var TouchpadSwipeGesture = class { + constructor(fingerCounts) { + this._fingerCounts = fingerCounts; // Array of supported finger counts + this._state = TouchpadState.NONE; + this._cumulativeX = 0; + this._cumulativeY = 0; + this._direction = GestureDirection.UNKNOWN; + this._fingers = 0; + this._percentage = 0; + this._baseDistance = 0; + this._startTime = 0; + + this._touchpadSettings = new Gio.Settings({ + schema_id: 'org.cinnamon.desktop.peripherals.touchpad', + }); + + this._stageEventId = global.stage.connect( + 'captured-event::touchpad', this._handleEvent.bind(this)); + } + + _handleEvent(actor, event) { + if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE) + return Clutter.EVENT_PROPAGATE; + + const phase = event.get_gesture_phase(); + const fingers = event.get_touchpad_gesture_finger_count(); + + // Reset state on gesture begin regardless of finger count + if (phase === Clutter.TouchpadGesturePhase.BEGIN) { + this._state = TouchpadState.NONE; + this._direction = GestureDirection.UNKNOWN; + this._cumulativeX = 0; + this._cumulativeY = 0; + this._percentage = 0; + } + + // Only handle if finger count matches one we're listening for + if (!this._fingerCounts.includes(fingers)) { + return Clutter.EVENT_PROPAGATE; + } + + if (this._state === TouchpadState.IGNORED) { + return Clutter.EVENT_PROPAGATE; + } + + const time = event.get_time(); + const [dx, dy] = event.get_gesture_motion_delta_unaccelerated(); + + // Apply natural scroll setting + let adjDx = dx; + let adjDy = dy; + if (this._touchpadSettings.get_boolean('natural-scroll')) { + adjDx = -dx; + adjDy = -dy; + } + + if (this._state === TouchpadState.NONE) { + if (dx === 0 && dy === 0) { + return Clutter.EVENT_PROPAGATE; + } + + this._fingers = fingers; + this._startTime = time; + this._state = TouchpadState.PENDING; + } + + if (this._state === TouchpadState.PENDING) { + this._cumulativeX += adjDx; + this._cumulativeY += adjDy; + + const distance = Math.sqrt(this._cumulativeX ** 2 + this._cumulativeY ** 2); + + if (distance >= DRAG_THRESHOLD_DISTANCE) { + // Determine direction + // Note: dx/dy are inverted for horizontal to match touchegg convention + if (Math.abs(this._cumulativeX) > Math.abs(this._cumulativeY)) { + this._direction = this._cumulativeX > 0 ? GestureDirection.LEFT : GestureDirection.RIGHT; + this._baseDistance = TOUCHPAD_BASE_WIDTH; + } else { + this._direction = this._cumulativeY > 0 ? GestureDirection.DOWN : GestureDirection.UP; + this._baseDistance = TOUCHPAD_BASE_HEIGHT; + } + + this._cumulativeX = 0; + this._cumulativeY = 0; + this._state = TouchpadState.HANDLING; + + this.emit('detected-begin', + GestureType.SWIPE, + this._direction, + 0, + this._fingers, + DeviceType.TOUCHPAD, + this._startTime); + } else { + return Clutter.EVENT_PROPAGATE; + } + } + + // Calculate delta along the gesture direction + // Note: horizontal is inverted to match touchegg convention + let delta = 0; + if (this._direction === GestureDirection.LEFT || this._direction === GestureDirection.RIGHT) { + delta = -adjDx; // Inverted for horizontal + if (this._direction === GestureDirection.LEFT) { + delta = -delta; + } + } else { + delta = adjDy; + if (this._direction === GestureDirection.UP) { + delta = -delta; + } + } + + // Update percentage (can exceed 100%) + this._percentage += (delta / this._baseDistance) * 100; + this._percentage = Math.max(0, this._percentage); + + switch (phase) { + case Clutter.TouchpadGesturePhase.BEGIN: + case Clutter.TouchpadGesturePhase.UPDATE: + this.emit('detected-update', + GestureType.SWIPE, + this._direction, + this._percentage, + this._fingers, + DeviceType.TOUCHPAD, + time); + break; + + case Clutter.TouchpadGesturePhase.END: + case Clutter.TouchpadGesturePhase.CANCEL: + this.emit('detected-end', + GestureType.SWIPE, + this._direction, + this._percentage, + this._fingers, + DeviceType.TOUCHPAD, + time); + this._state = TouchpadState.NONE; + break; + } + + return this._state === TouchpadState.HANDLING + ? Clutter.EVENT_STOP + : Clutter.EVENT_PROPAGATE; + } + + destroy() { + if (this._stageEventId) { + global.stage.disconnect(this._stageEventId); + this._stageEventId = 0; + } + } +}; +Signals.addSignalMethods(TouchpadSwipeGesture.prototype); + +var TouchpadPinchGesture = class { + constructor(fingerCounts) { + this._fingerCounts = fingerCounts; + this._state = TouchpadState.NONE; + this._direction = GestureDirection.UNKNOWN; + this._fingers = 0; + this._percentage = 0; + this._initialScale = 1.0; + this._startTime = 0; + + this._stageEventId = global.stage.connect( + 'captured-event::touchpad', this._handleEvent.bind(this)); + } + + _handleEvent(actor, event) { + if (event.type() !== Clutter.EventType.TOUCHPAD_PINCH) + return Clutter.EVENT_PROPAGATE; + + const phase = event.get_gesture_phase(); + const fingers = event.get_touchpad_gesture_finger_count(); + const scale = event.get_gesture_pinch_scale(); + + // Reset state on gesture begin (but don't capture scale yet - it's 0.0 on BEGIN) + if (phase === Clutter.TouchpadGesturePhase.BEGIN) { + this._state = TouchpadState.NONE; + this._direction = GestureDirection.UNKNOWN; + this._initialScale = 0; + this._percentage = 0; + return Clutter.EVENT_PROPAGATE; // Wait for UPDATE events + } + + if (!this._fingerCounts.includes(fingers)) { + return Clutter.EVENT_PROPAGATE; + } + + if (this._state === TouchpadState.IGNORED) { + return Clutter.EVENT_PROPAGATE; + } + + const time = event.get_time(); + + // Capture initial scale on first UPDATE event + if (this._state === TouchpadState.NONE && phase === Clutter.TouchpadGesturePhase.UPDATE) { + this._fingers = fingers; + this._startTime = time; + this._initialScale = scale; + this._state = TouchpadState.PENDING; + return Clutter.EVENT_PROPAGATE; // Wait for more updates to determine direction + } + + if (this._state === TouchpadState.PENDING) { + const scaleDelta = scale - this._initialScale; + + // Wait for significant scale change to determine direction + if (Math.abs(scaleDelta) >= 0.05) { + this._direction = scaleDelta > 0 ? GestureDirection.OUT : GestureDirection.IN; + this._state = TouchpadState.HANDLING; + + this.emit('detected-begin', + GestureType.PINCH, + this._direction, + 0, + this._fingers, + DeviceType.TOUCHPAD, + this._startTime); + } else { + return Clutter.EVENT_PROPAGATE; + } + } + + // Calculate percentage based on scale change from initial + // Scale typically ranges from ~0.5 to ~1.5, so a change of 0.5 = 100% + if (this._direction === GestureDirection.IN) { + // Pinching in: scale decreases from initial + this._percentage = (this._initialScale - scale) * 200; + } else { + // Pinching out: scale increases from initial + this._percentage = (scale - this._initialScale) * 200; + } + this._percentage = Math.max(0, this._percentage); + + switch (phase) { + case Clutter.TouchpadGesturePhase.BEGIN: + case Clutter.TouchpadGesturePhase.UPDATE: + this.emit('detected-update', + GestureType.PINCH, + this._direction, + this._percentage, + this._fingers, + DeviceType.TOUCHPAD, + time); + break; + + case Clutter.TouchpadGesturePhase.END: + case Clutter.TouchpadGesturePhase.CANCEL: + this.emit('detected-end', + GestureType.PINCH, + this._direction, + this._percentage, + this._fingers, + DeviceType.TOUCHPAD, + time); + this._state = TouchpadState.NONE; + break; + } + + return this._state === TouchpadState.HANDLING + ? Clutter.EVENT_STOP + : Clutter.EVENT_PROPAGATE; + } + + destroy() { + if (this._stageEventId) { + global.stage.disconnect(this._stageEventId); + this._stageEventId = 0; + } + } +}; +Signals.addSignalMethods(TouchpadPinchGesture.prototype); + +var TouchSwipeGesture = GObject.registerClass({ + Signals: { + 'detected-begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + }, +}, class TouchSwipeGesture extends Clutter.GestureAction { + _init(nTouchPoints) { + super._init(); + this.set_n_touch_points(nTouchPoints); + this.set_threshold_trigger_edge(Clutter.GestureTriggerEdge.AFTER); + + this._direction = GestureDirection.UNKNOWN; + this._lastPosition = { x: 0, y: 0 }; + this._startPosition = { x: 0, y: 0 }; + this._percentage = 0; + this._distance = global.screen_height; + this._nTouchPoints = nTouchPoints; + } + + vfunc_gesture_prepare(actor) { + if (!super.vfunc_gesture_prepare(actor)) { + return false; + } + + const [xPress, yPress] = this.get_press_coords(0); + const [x, y] = this.get_motion_coords(0); + const xDelta = x - xPress; + const yDelta = y - yPress; + + // Determine direction + if (Math.abs(xDelta) > Math.abs(yDelta)) { + this._direction = xDelta > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT; + this._distance = global.screen_width; + } else { + this._direction = yDelta > 0 ? GestureDirection.DOWN : GestureDirection.UP; + this._distance = global.screen_height; + } + + this._startPosition = { x: xPress, y: yPress }; + this._lastPosition = { x, y }; + this._percentage = 0; + + const time = this.get_last_event(0).get_time(); + this.emit('detected-begin', + GestureType.SWIPE, + this._direction, + 0, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + + return true; + } + + vfunc_gesture_progress(_actor) { + const [x, y] = this.get_motion_coords(0); + const time = this.get_last_event(0).get_time(); + + let delta = 0; + if (this._direction === GestureDirection.LEFT || this._direction === GestureDirection.RIGHT) { + delta = x - this._lastPosition.x; + if (this._direction === GestureDirection.LEFT) { + delta = -delta; + } + } else { + delta = y - this._lastPosition.y; + if (this._direction === GestureDirection.UP) { + delta = -delta; + } + } + + this._percentage += (delta / this._distance) * 100; + this._percentage = Math.max(0, this._percentage); + + this._lastPosition = { x, y }; + + this.emit('detected-update', + GestureType.SWIPE, + this._direction, + this._percentage, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + + return true; + } + + vfunc_gesture_end(_actor) { + const time = this.get_last_event(0).get_time(); + this.emit('detected-end', + GestureType.SWIPE, + this._direction, + this._percentage, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + } + + vfunc_gesture_cancel(_actor) { + const time = Clutter.get_current_event_time(); + this.emit('detected-end', + GestureType.SWIPE, + this._direction, + 0, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + } +}); + +var TouchPinchGesture = GObject.registerClass({ + Signals: { + 'detected-begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + }, +}, class TouchPinchGesture extends Clutter.GestureAction { + _init(nTouchPoints) { + super._init(); + // Pinch requires at least 2 touch points + this.set_n_touch_points(Math.max(2, nTouchPoints)); + this.set_threshold_trigger_edge(Clutter.GestureTriggerEdge.AFTER); + + this._direction = GestureDirection.UNKNOWN; + this._initialDistance = 0; + this._percentage = 0; + this._nTouchPoints = nTouchPoints; + } + + _getPointsDistance() { + // Calculate distance between first two touch points + if (this.get_n_current_points() < 2) { + return 0; + } + + const [x1, y1] = this.get_motion_coords(0); + const [x2, y2] = this.get_motion_coords(1); + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + } + + vfunc_gesture_prepare(actor) { + if (!super.vfunc_gesture_prepare(actor)) { + return false; + } + + this._initialDistance = this._getPointsDistance(); + if (this._initialDistance === 0) { + return false; + } + + this._direction = GestureDirection.UNKNOWN; + this._percentage = 0; + + return true; + } + + vfunc_gesture_progress(_actor) { + const currentDistance = this._getPointsDistance(); + if (this._initialDistance === 0) { + return true; + } + + const time = this.get_last_event(0).get_time(); + const scale = currentDistance / this._initialDistance; + + // Determine direction on first significant change + if (this._direction === GestureDirection.UNKNOWN) { + if (Math.abs(scale - 1.0) >= 0.05) { + this._direction = scale > 1.0 ? GestureDirection.OUT : GestureDirection.IN; + + this.emit('detected-begin', + GestureType.PINCH, + this._direction, + 0, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + } else { + return true; + } + } + + // Calculate percentage + if (this._direction === GestureDirection.IN) { + this._percentage = (1.0 - scale) * 200; + } else { + this._percentage = (scale - 1.0) * 200; + } + this._percentage = Math.max(0, this._percentage); + + this.emit('detected-update', + GestureType.PINCH, + this._direction, + this._percentage, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + + return true; + } + + vfunc_gesture_end(_actor) { + if (this._direction === GestureDirection.UNKNOWN) { + return; + } + + const time = this.get_last_event(0).get_time(); + this.emit('detected-end', + GestureType.PINCH, + this._direction, + this._percentage, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + } + + vfunc_gesture_cancel(_actor) { + if (this._direction === GestureDirection.UNKNOWN) { + return; + } + + const time = Clutter.get_current_event_time(); + this.emit('detected-end', + GestureType.PINCH, + this._direction, + 0, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + } +}); + +var TouchTapGesture = GObject.registerClass({ + Signals: { + 'detected-begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + 'detected-end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_INT, GObject.TYPE_UINT, GObject.TYPE_INT64] }, + }, +}, class TouchTapGesture extends Clutter.TapAction { + _init(nTouchPoints) { + super._init(); + this.set_n_touch_points(nTouchPoints); + + this._nTouchPoints = nTouchPoints; + } + + vfunc_tap(actor) { + const time = Clutter.get_current_event_time(); + + // For tap gestures, we emit begin and end immediately with 100% completion + this.emit('detected-begin', + GestureType.TAP, + GestureDirection.UNKNOWN, + 100, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + + this.emit('detected-end', + GestureType.TAP, + GestureDirection.UNKNOWN, + 100, + this._nTouchPoints, + DeviceType.TOUCHSCREEN, + time); + + return true; + } +}); diff --git a/js/ui/gestures/toucheggGestureSource.js b/js/ui/gestures/toucheggGestureSource.js new file mode 100644 index 0000000000..10a100e37c --- /dev/null +++ b/js/ui/gestures/toucheggGestureSource.js @@ -0,0 +1,60 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const { Cinnamon } = imports.gi; +const Signals = imports.signals; +const SignalManager = imports.misc.signalManager; + +/** + * ToucheggGestureSource - Gesture source using touchegg daemon + */ +var ToucheggGestureSource = class { + constructor() { + this._client = null; + this._signalManager = new SignalManager.SignalManager(null); + } + + setup() { + if (this._client !== null) { + return; + } + + global.log('Setting up touchegg gesture source'); + + this._client = new Cinnamon.ToucheggClient(); + + // Touchegg client already emits 'gesture-begin/update/end' signals + // Just forward them + this._signalManager.connect(this._client, "gesture-begin", this._onGestureBegin, this); + this._signalManager.connect(this._client, "gesture-update", this._onGestureUpdate, this); + this._signalManager.connect(this._client, "gesture-end", this._onGestureEnd, this); + } + + shutdown() { + if (this._client === null) { + return; + } + + global.log('Shutting down touchegg gesture source'); + this._signalManager.disconnect("gesture-begin"); + this._signalManager.disconnect("gesture-update"); + this._signalManager.disconnect("gesture-end"); + this._client = null; + } + + isActive() { + return this._client !== null; + } + + _onGestureBegin(client, type, direction, percentage, fingers, device, time) { + this.emit('gesture-begin', type, direction, percentage, fingers, device, time); + } + + _onGestureUpdate(client, type, direction, percentage, fingers, device, time) { + this.emit('gesture-update', type, direction, percentage, fingers, device, time); + } + + _onGestureEnd(client, type, direction, percentage, fingers, device, time) { + this.emit('gesture-end', type, direction, percentage, fingers, device, time); + } +}; +Signals.addSignalMethods(ToucheggGestureSource.prototype); diff --git a/po/POTFILES.in b/po/POTFILES.in index c057d66da5..839cb40942 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -12,7 +12,6 @@ data/cinnamon2d.desktop.in.in # data/org.cinnamon.gestures.gschema.xml # data/org.cinnamon.gschema.xml -src/cinnamon-touchegg-client.c src/cinnamon-doc-system.c src/cinnamon-tray-manager.c src/cinnamon-window-tracker.c @@ -107,6 +106,7 @@ src/cinnamon-recorder.c src/cinnamon-screen.c src/cinnamon-screenshot.c src/cinnamon-secure-text-buffer.c +src/cinnamon-touchegg-client.c files/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui files/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui @@ -200,10 +200,13 @@ js/ui/expoThumbnail.js js/ui/extension.js js/ui/extensionSystem.js js/ui/flashspot.js -js/ui/gestures/ToucheggTypes.js +js/ui/gestures/gestureTypes.js js/ui/gestures/actions.js js/ui/gestures/gesturesManager.js js/ui/gestures/mprisController.js +js/ui/gestures/nativeGestures.js +js/ui/gestures/nativeGestureSource.js +js/ui/gestures/toucheggGestureSource.js js/ui/hotCorner.js js/ui/ibusCandidatePopup.js js/ui/iconGrid.js diff --git a/src/cinnamon-touchegg-client.c b/src/cinnamon-touchegg-client.c index 073586b2e2..d1d8b11589 100644 --- a/src/cinnamon-touchegg-client.c +++ b/src/cinnamon-touchegg-client.c @@ -59,7 +59,8 @@ emit_our_signal (CinnamonToucheggClient *client, g_debug ("CinnamonToucheggClient signal: %s: type %u, direction %u, progress %0.1f, fingers %d, device %u, elapsed_time %lu", our_signal, type, direction, percentage, fingers, device, elapsed_time); - g_signal_emit_by_name (client, our_signal, type, direction, percentage, fingers, device, g_get_monotonic_time ()); + // Use milliseconds for consistency with Clutter event.get_time() + g_signal_emit_by_name (client, our_signal, type, direction, percentage, fingers, device, g_get_monotonic_time () / 1000); } static void From f1fa225c5ca8f67d25da0d2dcd21000aa3090951 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Mon, 16 Mar 2026 21:19:20 -0400 Subject: [PATCH 2/5] gesturesManager.js: Connect to screensaverController, not a proxy. --- js/ui/gestures/gesturesManager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/ui/gestures/gesturesManager.js b/js/ui/gestures/gesturesManager.js index 61243e6bc1..a5b9369dca 100644 --- a/js/ui/gestures/gesturesManager.js +++ b/js/ui/gestures/gesturesManager.js @@ -2,7 +2,7 @@ const { Gio, Meta } = imports.gi; const SignalManager = imports.misc.signalManager; -const ScreenSaver = imports.misc.screenSaver; +const Main = imports.ui.main; const actions = imports.ui.gestures.actions; const { @@ -86,7 +86,6 @@ var GesturesManager = class { constructor(wm) { this.signalManager = new SignalManager.SignalManager(null); this.settings = new Gio.Settings({ schema_id: SCHEMA }) - this.screenSaverProxy = new ScreenSaver.ScreenSaverProxy(); this.current_gesture = null; this.live_actions = new Map(); @@ -236,7 +235,7 @@ var GesturesManager = class { this.current_gesture = null; } - if (this.screenSaverProxy.screenSaverActive) { + if (Main.screensaverController?.locked) { debug_gesture(`Ignoring 'gesture-begin', screensaver is active`); return; } From 2d56b7b9a85498b91f3ec72b0e25fbdce9cd58f4 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Mon, 16 Mar 2026 21:33:34 -0400 Subject: [PATCH 3/5] actions: Use js/misc/mprisPlayer, remove mprisController.js. One mpris manager for all consumers now (applet, screensaver, gestures). --- js/misc/mprisPlayer.js | 18 +- js/ui/gestures/actions.js | 21 +-- js/ui/gestures/mprisController.js | 294 ------------------------------ 3 files changed, 26 insertions(+), 307 deletions(-) delete mode 100644 js/ui/gestures/mprisController.js diff --git a/js/misc/mprisPlayer.js b/js/misc/mprisPlayer.js index 643b2f0044..d8a40b1261 100644 --- a/js/misc/mprisPlayer.js +++ b/js/misc/mprisPlayer.js @@ -538,9 +538,25 @@ var MprisPlayerManager = class MprisPlayerManager { }); } + _isInstance(busName) { + // MPRIS instances are in the form + // org.mpris.MediaPlayer2.name.instanceXXXX + // ...except for VLC, which to this day uses + // org.mpris.MediaPlayer2.name-XXXX + return busName.split('.').length > 4 || + /^org\.mpris\.MediaPlayer2\.vlc-\d+$/.test(busName); + } + _addPlayer(busName, owner) { if (this._players[owner]) { - return; // Already tracking this player + // If we already have a player for this owner, prefer the instance + // bus name over the base name - it's more specific and some players + // register both. + let existing = this._players[owner]; + if (this._isInstance(busName) && !this._isInstance(existing.getBusName())) { + existing._busName = busName; + } + return; } let player = new MprisPlayer(busName, owner); diff --git a/js/ui/gestures/actions.js b/js/ui/gestures/actions.js index a5f261d43a..11d58dbd4e 100644 --- a/js/ui/gestures/actions.js +++ b/js/ui/gestures/actions.js @@ -3,7 +3,7 @@ const { GLib, Gio, Cinnamon, Meta, Cvc } = imports.gi; const Main = imports.ui.main; const { GestureType } = imports.ui.gestures.gestureTypes; -const { MprisController } = imports.ui.gestures.mprisController; +const { getMprisPlayerManager } = imports.misc.mprisPlayer; const Magnifier = imports.ui.magnifier; const touchpad_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.peripherals.touchpad" }); @@ -65,10 +65,7 @@ var cleanup = () => { mixer = null; } - if (mpris_controller != null) { - mpris_controller.shutdown(); - mpris_controller = null; - } + mpris_manager = null; } var BaseAction = class { @@ -438,13 +435,13 @@ var VolumeAction = class extends BaseAction { } } -var mpris_controller = null; +var mpris_manager = null; var init_mpris_controller = () => { - if (mpris_controller != null) { + if (mpris_manager != null) { return; } - mpris_controller = new MprisController(); + mpris_manager = getMprisPlayerManager(); } var MediaAction = class extends BaseAction { @@ -453,22 +450,22 @@ var MediaAction = class extends BaseAction { } do_action(direction, percentage, time) { - const player = mpris_controller.get_player(); + const player = mpris_manager.getBestPlayer(); if (player == null) { return; } if (this.definition.action === "MEDIA_PLAY_PAUSE") { - player.toggle_play() + player.playPause(); } else if (this.definition.action === "MEDIA_NEXT") { - player.next_track(); + player.next(); } else if (this.definition.action === "MEDIA_PREVIOUS") { - player.previous_track(); + player.previous(); } } } diff --git a/js/ui/gestures/mprisController.js b/js/ui/gestures/mprisController.js deleted file mode 100644 index d9e088c4d0..0000000000 --- a/js/ui/gestures/mprisController.js +++ /dev/null @@ -1,294 +0,0 @@ -// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - -// TODO: Have both the sound applet and gestures use this? -const Interfaces = imports.misc.interfaces; - -const MEDIA_PLAYER_2_PATH = "/org/mpris/MediaPlayer2"; -const MEDIA_PLAYER_2_NAME = "org.mpris.MediaPlayer2"; -const MEDIA_PLAYER_2_PLAYER_IFACE_NAME = "org.mpris.MediaPlayer2.Player"; - -const DEBUG_MPRIS = false; - -var debug_mpris = (...args) => { - if (DEBUG_MPRIS) { - global.log(...args); - } -} - -var Player = class { - constructor(controller, bus_name, owner) { - this.controller = controller; - this.bus_name = bus_name; - this.owner = owner; - - this.player_control = null; - this.prop_handler = null; - this.prop_changed_id = 0; - - this.can_control = false; - this.is_playing = false; - this.can_play = false; - this.can_pause = false; - this.can_go_next = false; - this.can_go_previous = false; - - let async_ready_cb = (proxy, error, property) => { - if (error) - log(error); - else { - this[property] = proxy; - this.dbus_acquired(); - } - }; - - Interfaces.getDBusProxyWithOwnerAsync(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, - this.bus_name, - (p, e) => async_ready_cb(p, e, 'player_control')); - - Interfaces.getDBusPropertiesAsync(this.bus_name, - MEDIA_PLAYER_2_PATH, - (p, e) => async_ready_cb(p, e, 'prop_handler')); - } - - dbus_acquired() { - if (!this.prop_handler || !this.player_control) - return; - - this.prop_changed_id = this.prop_handler.connectSignal('PropertiesChanged', (proxy, sender, [iface, props]) => { - if (iface !== MEDIA_PLAYER_2_PLAYER_IFACE_NAME) { - return; - } - - this.update_from_props(Object.keys(props)); - }); - - this.update(); - } - - update() { - this.update_from_props(null); - } - - update_from_props(prop_names) { - debug_mpris("updated props: ", prop_names); - if (!prop_names || prop_names.includes("CanControl")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'CanControl', (value, error) => { - if (!error) - this.can_control = value[0].unpack(); - debug_mpris("update can_control:", this.can_control); - }); - - if (!prop_names || prop_names.includes("PlaybackStatus")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'PlaybackStatus', (value, error) => { - if (!error) - this.is_playing = ["Playing", "Paused"].includes(value[0].unpack()); - debug_mpris("update status:", this.is_playing); - }); - - if (!prop_names || prop_names.includes("CanGoNext")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'CanGoNext', (value, error) => { - if (!error) - this.can_go_next = value[0].unpack(); - debug_mpris("update can_go_next ", this.can_go_next); - }); - - if (!prop_names || prop_names.includes("CanGoPrevious")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'CanGoPrevious', (value, error) => { - if (!error) - this.can_go_previous = value[0].unpack(); - debug_mpris("update can_go_previous ", this.can_go_previous); - }); - - if (!prop_names || prop_names.includes("CanPlay")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'CanPlay', (value, error) => { - if (!error) - this.can_play = value[0].unpack(); - debug_mpris("update can_play ", this.can_play); - - }); - - if (!prop_names || prop_names.includes("CanPause")) - this.prop_handler.GetRemote(MEDIA_PLAYER_2_PLAYER_IFACE_NAME, 'CanPause', (value, error) => { - if (!error) - this.can_pause = value[0].unpack(); - debug_mpris("update can_pause ", this.can_pause); - }); - } - - toggle_play() { - debug_mpris("toggle play"); - if (!this.can_control) { - return; - } - - // Should we rely on the CanPlay/Pause properties or just try? - this.player_control.PlayPauseRemote(); - } - - next_track() { - debug_mpris("next track"); - if (!this.can_control) { - return; - } - if (!this.can_go_next) { - return; - } - - this.player_control.NextRemote(); - } - - previous_track() { - debug_mpris("previous track"); - if (!this.can_control) { - return; - } - - if (!this.can_go_previous) { - return; - } - - this.player_control.PreviousRemote(); - } - - destroy() { - if (this.prop_handler != null) { - this.prop_handler.disconnectSignal(this.prop_changed_id); - this.prop_changed_id = 0; - } - - this.prop_handler = null; - this.player_control = null; - } -} - -var MprisController = class { - constructor() { - this._dbus = null; - - this._players = {}; - this._active_player = null; - this._owner_changed_id = 0; - - Interfaces.getDBusAsync((proxy, error) => { - if (error) { - global.logError(error); - return; - } - - this._dbus = proxy; - - // player DBus name pattern - let name_regex = /^org\.mpris\.MediaPlayer2\./; - // load players - this._dbus.ListNamesRemote((names) => { - for (let n in names[0]) { - let name = names[0][n]; - if (name_regex.test(name)) - this._dbus.GetNameOwnerRemote(name, (owner) => this._add_player(name, owner[0])); - } - }); - - // watch players - this._owner_changed_id = this._dbus.connectSignal('NameOwnerChanged', - (proxy, sender, [name, old_owner, new_owner]) => { - if (name_regex.test(name)) { - if (new_owner && !old_owner) - this._add_player(name, new_owner); - else if (old_owner && !new_owner) - this._remove_player(name, old_owner); - else - this._change_player_owner(name, old_owner, new_owner); - } - } - ); - }); - } - - shutdown() { - if (this._owner_changed_id > 0) { - this._dbus.disconnectSignal(this._owner_changed_id); - this._owner_changed_id = 0; - this._dbus = null; - } - - for (let player in this._players) { - this._players[player].destroy(); - delete this._players[player]; - } - - this._players = null; - } - - _is_instance(busName) { - // MPRIS instances are in the form - // org.mpris.MediaPlayer2.name.instanceXXXX - // ...except for VLC, which to this day uses - // org.mpris.MediaPlayer2.name-XXXX - return busName.split('.').length > 4 || - /^org\.mpris\.MediaPlayer2\.vlc-\d+$/.test(busName); - } - - _add_player(bus_name, owner) { - debug_mpris("Add player: ", bus_name, owner); - if (this._players[owner]) { - let prev_name = this._players[owner].bus_name; - if (this._isInstance(bus_name) && !this._isInstance(prev_name)) { - this._players[owner].bus_name = bus_name; - this._players[owner].update(); - } - else { - return; - } - } else if (owner) { - let player = new Player(this, bus_name, owner); - this._players[owner] = player; - } - } - - _remove_player(bus_name, owner) { - debug_mpris("Remove player: ", bus_name, owner); - if (this._players[owner] && this._players[owner].bus_name == bus_name) { - this._players[owner].destroy(); - delete this._players[owner]; - } - } - - _change_player_owner(bus_name, old_owner, new_owner) { - if (this._players[old_owner] && bus_name == this._players[old_owner].bus_name) { - this._players[new_owner] = this._players[old_owner]; - this._players[new_owner].owner = new_owner; - delete this._players[old_owner]; - this._players[new_owner].update(); - } - } - - get_player() { - let chosen_player = null; - let first_can_control = null; - - for (let name in this._players) { - let maybe_player = this._players[name]; - - if (maybe_player.is_playing && maybe_player.can_control) { - chosen_player = maybe_player; - break; - } - - if (maybe_player.can_control && first_can_control == null) { - first_can_control = maybe_player; - } - } - - if (chosen_player) { - return chosen_player; - } - - if (first_can_control != null) { - return first_can_control; - } - - return null; - } -} - - From ccd4b05795ee5434a9f3ba239a5d9cf228bc56b1 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Mon, 16 Mar 2026 21:56:26 -0400 Subject: [PATCH 4/5] misc: Some fixes. --- .../usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py | 1 + js/ui/gestures/actions.js | 2 +- po/POTFILES.in | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py index b3f0b24d9a..6e6d5f07c4 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_gestures.py @@ -6,6 +6,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gio, Gtk +from bin import util from bin.SettingsWidgets import SidePage, SettingsWidget from xapp.GSettingsWidgets import * diff --git a/js/ui/gestures/actions.js b/js/ui/gestures/actions.js index 11d58dbd4e..b81bf7a779 100644 --- a/js/ui/gestures/actions.js +++ b/js/ui/gestures/actions.js @@ -139,7 +139,7 @@ var WindowOpAction = class extends BaseAction { const window = global.display.get_focus_window(); if (window == null) { - global.logWarning("WorkspaceSwitchAction: no focus window"); + global.logWarning("WindowOpAction: no focus window"); return } diff --git a/po/POTFILES.in b/po/POTFILES.in index 839cb40942..5faa03870c 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -203,7 +203,6 @@ js/ui/flashspot.js js/ui/gestures/gestureTypes.js js/ui/gestures/actions.js js/ui/gestures/gesturesManager.js -js/ui/gestures/mprisController.js js/ui/gestures/nativeGestures.js js/ui/gestures/nativeGestureSource.js js/ui/gestures/toucheggGestureSource.js From ec17226d03d33ee5ad1f0c0737d37102b3e2e74d Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Mon, 16 Mar 2026 22:11:56 -0400 Subject: [PATCH 5/5] nativeGestures.js: halt event propagation when cancelling a gesture. --- js/ui/gestures/nativeGestures.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/js/ui/gestures/nativeGestures.js b/js/ui/gestures/nativeGestures.js index 3879a88588..a4828eed5b 100644 --- a/js/ui/gestures/nativeGestures.js +++ b/js/ui/gestures/nativeGestures.js @@ -139,6 +139,8 @@ var TouchpadSwipeGesture = class { this._percentage += (delta / this._baseDistance) * 100; this._percentage = Math.max(0, this._percentage); + const handling = this._state === TouchpadState.HANDLING; + switch (phase) { case Clutter.TouchpadGesturePhase.BEGIN: case Clutter.TouchpadGesturePhase.UPDATE: @@ -164,7 +166,7 @@ var TouchpadSwipeGesture = class { break; } - return this._state === TouchpadState.HANDLING + return handling ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE; } @@ -259,6 +261,8 @@ var TouchpadPinchGesture = class { } this._percentage = Math.max(0, this._percentage); + const handling = this._state === TouchpadState.HANDLING; + switch (phase) { case Clutter.TouchpadGesturePhase.BEGIN: case Clutter.TouchpadGesturePhase.UPDATE: @@ -284,7 +288,7 @@ var TouchpadPinchGesture = class { break; } - return this._state === TouchpadState.HANDLING + return handling ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE; }