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..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 *
@@ -90,8 +91,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 +271,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/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 da728fd28a..b81bf7a779 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 { MprisController } = imports.ui.gestures.mprisController;
+const { GestureType } = imports.ui.gestures.gestureTypes;
+const { getMprisPlayerManager } = imports.misc.mprisPlayer;
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;
@@ -65,10 +65,7 @@ var cleanup = () => {
mixer = null;
}
- if (mpris_controller != null) {
- mpris_controller.shutdown();
- mpris_controller = null;
- }
+ mpris_manager = null;
}
var BaseAction = class {
@@ -142,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
}
@@ -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,28 +450,26 @@ 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();
}
}
}
-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 +479,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..a5b9369dca 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 Main = imports.ui.main;
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,24 @@ 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.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 +144,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 +159,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,13 +229,13 @@ 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;
}
- if (this.screenSaverProxy.screenSaverActive) {
+ if (Main.screensaverController?.locked) {
debug_gesture(`Ignoring 'gesture-begin', screensaver is active`);
return;
}
@@ -277,7 +256,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 +273,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/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;
- }
-}
-
-
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..a4828eed5b
--- /dev/null
+++ b/js/ui/gestures/nativeGestures.js
@@ -0,0 +1,574 @@
+// -*- 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);
+
+ const handling = this._state === TouchpadState.HANDLING;
+
+ 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 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);
+
+ const handling = this._state === TouchpadState.HANDLING;
+
+ 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 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..5faa03870c 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,12 @@ 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