'use strict'; const Gdk = imports.gi.Gdk; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const SESSION_TIMEOUT = 15; const RemoteSession = GObject.registerClass({ GTypeName: 'GSConnectRemoteSession', Implements: [Gio.DBusInterface], Signals: { 'closed': { flags: GObject.SignalFlags.RUN_FIRST } } }, class RemoteSession extends Gio.DBusProxy { _init(objectPath) { super._init({ g_bus_type: Gio.BusType.SESSION, g_name: 'org.gnome.Mutter.RemoteDesktop', g_object_path: objectPath, g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session', g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES }); this._started = false; } vfunc_g_signal(sender_name, signal_name, parameters) { if (signal_name === 'Closed') { this.emit('closed'); } } _call(name, parameters = null) { if (!this._started) return; this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null); } async start() { try { if (this._started) return; // Initialize the proxy await new Promise((resolve, reject) => { this.init_async( GLib.PRIORITY_DEFAULT, null, (proxy, res) => { try { proxy.init_finish(res); resolve(); } catch (e) { reject(e); } } ); }); // Start the session await new Promise((resolve, reject) => { this.call( 'Start', null, Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { try { resolve(proxy.call_finish(res)); } catch (e) { reject(e); } } ); }); this._started = true; } catch (e) { this.destroy(); Gio.DBusError.strip_remote_error(e); throw e; } } stop() { if (this._started) { this._started = false; this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null); } } _translateButton(button) { switch (button) { case Gdk.BUTTON_PRIMARY: return 0x110; case Gdk.BUTTON_MIDDLE: return 0x112; case Gdk.BUTTON_SECONDARY: return 0x111; case 4: return 0; // FIXME case 5: return 0x10F; // up } } movePointer(dx, dy) { this._call( 'NotifyPointerMotionRelative', GLib.Variant.new('(dd)', [dx, dy]) ); } pressPointer(button) { button = this._translateButton(button); this._call( 'NotifyPointerButton', GLib.Variant.new('(ib)', [button, true]) ); } releasePointer(button) { button = this._translateButton(button); this._call( 'NotifyPointerButton', GLib.Variant.new('(ib)', [button, false]) ); } clickPointer(button) { button = this._translateButton(button); this._call( 'NotifyPointerButton', GLib.Variant.new('(ib)', [button, true]) ); this._call( 'NotifyPointerButton', GLib.Variant.new('(ib)', [button, false]) ); } doubleclickPointer(button) { this.clickPointer(button); this.clickPointer(button); } scrollPointer(dx, dy) { // TODO: NotifyPointerAxis only seems to work on Wayland, but maybe // NotifyPointerAxisDiscrete is the better choice anyways if (_WAYLAND) { this._call( 'NotifyPointerAxis', GLib.Variant.new('(ddu)', [dx, dy, 0]) ); this._call( 'NotifyPointerAxis', GLib.Variant.new('(ddu)', [0, 0, 1]) ); } else { if (dy > 0) { this._call( 'NotifyPointerAxisDiscrete', GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1]) ); } else if (dy < 0) { this._call( 'NotifyPointerAxisDiscrete', GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1]) ); } } } /** * Keyboard Events */ pressKeysym(keysym) { this._call( 'NotifyKeyboardKeysym', GLib.Variant.new('(ub)', [keysym, true]) ); } releaseKeysym(keysym) { this._call( 'NotifyKeyboardKeysym', GLib.Variant.new('(ub)', [keysym, false]) ); } pressreleaseKeysym(keysym) { this._call( 'NotifyKeyboardKeysym', GLib.Variant.new('(ub)', [keysym, true]) ); this._call( 'NotifyKeyboardKeysym', GLib.Variant.new('(ub)', [keysym, false]) ); } /** * High-level keyboard input */ pressKey(input, modifiers) { // Press Modifiers if (modifiers & Gdk.ModifierType.MOD1_MASK) this.pressKeysym(Gdk.KEY_Alt_L); if (modifiers & Gdk.ModifierType.CONTROL_MASK) this.pressKeysym(Gdk.KEY_Control_L); if (modifiers & Gdk.ModifierType.SHIFT_MASK) this.pressKeysym(Gdk.KEY_Shift_L); if (modifiers & Gdk.ModifierType.SUPER_MASK) this.pressKeysym(Gdk.KEY_Super_L); if (typeof input === 'string') { let keysym = Gdk.unicode_to_keyval(input.codePointAt(0)); this.pressreleaseKeysym(keysym); } else { this.pressreleaseKeysym(input); } // Release Modifiers if (modifiers & Gdk.ModifierType.MOD1_MASK) this.releaseKeysym(Gdk.KEY_Alt_L); if (modifiers & Gdk.ModifierType.CONTROL_MASK) this.releaseKeysym(Gdk.KEY_Control_L); if (modifiers & Gdk.ModifierType.SHIFT_MASK) this.releaseKeysym(Gdk.KEY_Shift_L); if (modifiers & Gdk.ModifierType.SUPER_MASK) this.releaseKeysym(Gdk.KEY_Super_L); } destroy() { if (this.__disposed === undefined) { this.__disposed = true; this.run_dispose(); } } }); const Controller = class Controller { constructor() { this._nameAppearedId = 0; this._session = null; this._sessionCloseId = 0; this._sessionExpiry = 0; this._sessionExpiryId = 0; this._sessionStarting = false; // Watch for the RemoteDesktop portal this._nameWatcherId = Gio.bus_watch_name( Gio.BusType.SESSION, 'org.gnome.Mutter.RemoteDesktop', Gio.BusNameWatcherFlags.NONE, this._onNameAppeared.bind(this), this._onNameVanished.bind(this) ); } get connection() { if (this._connection === undefined) { this._connection = null; } return this._connection; } _checkWayland() { if (_WAYLAND) { // eslint-disable-next-line no-global-assign HAVE_REMOTEINPUT = false; let service = Gio.Application.get_default(); // First we're going to disabled the mousepad plugin on all devices for (let device of service.devices) { let supported = device.settings.get_strv('supported-plugins'); supported = supported.splice(supported.indexOf('mousepad'), 1); device.settings.set_strv('supported-plugins', supported); } // Second we need to amend the service identity and broadcast service._identity = undefined; service._identify(); return true; } return false; } _onNameAppeared(connection, name, name_owner) { try { this._connection = connection; } catch (e) { logError(e); } } _onNameVanished(connection, name) { try { if (this._session !== null) { this._onSessionClosed(this._session); } } catch (e) { logError(e); } } _onSessionClosed(session) { // Disconnect from the session if (this._sessionClosedId > 0) { session.disconnect(this._sessionClosedId); this._sessionClosedId = 0; } // Destroy the session session.destroy(); this._session = null; } _onSessionExpired() { // If the session has been used recently, schedule a new expiry let remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000)); if (remainder > 0) { this._sessionExpiryId = GLib.timeout_add_seconds( GLib.PRIORITY_DEFAULT, remainder, this._onSessionExpired.bind(this) ); return GLib.SOURCE_REMOVE; } // Otherwise if there's an active session, close it if (this._session !== null) { this._session.stop(); } // Reset the GSource Id this._sessionExpiryId = 0; return GLib.SOURCE_REMOVE; } _createSession() { return new Promise((resolve, reject) => { if (this.connection === null) { reject(new Error('No DBus connection')); return; } this.connection.call( 'org.gnome.Mutter.RemoteDesktop', '/org/gnome/Mutter/RemoteDesktop', 'org.gnome.Mutter.RemoteDesktop', 'CreateSession', null, null, Gio.DBusCallFlags.NONE, -1, null, (connection, res) => { try { res = connection.call_finish(res); resolve(res.deepUnpack()[0]); } catch (e) { reject(e); } } ); }); } async _ensureAdapter() { try { // Update the timestamp of the last event this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT); // Session is active if (this._session !== null) return; // Mutter's RemoteDesktop is not available, fall back to Atspi if (this.connection === null) { debug('Falling back to Atspi'); // If we got here in Wayland, we need to re-adjust and bail if (this._checkWayland()) return; let fallback = imports.service.components.atspi; this._session = new fallback.Controller(); // Mutter is available and there isn't another session starting } else if (this._sessionStarting === false) { this._sessionStarting = true; debug('Creating Mutter RemoteDesktop session'); let objectPath = await this._createSession(); this._session = new RemoteSession(objectPath); await this._session.start(); this._sessionClosedId = this._session.connect( 'closed', this._onSessionClosed.bind(this) ); if (this._sessionExpiryId === 0) { this._sessionExpiryId = GLib.timeout_add_seconds( GLib.PRIORITY_DEFAULT, SESSION_TIMEOUT, this._onSessionExpired.bind(this) ); } this._sessionStarting = false; } } catch (e) { logError(e); if (this._session !== null) { this._session.destroy(); this._session = null; } this._sessionStarting = false; } } /** * Pointer Events */ movePointer(dx, dy) { try { if (dx === 0 && dy === 0) return; this._ensureAdapter(); this._session.movePointer(dx, dy); } catch (e) { debug(e); } } pressPointer(button) { try { this._ensureAdapter(); this._session.pressPointer(button); } catch (e) { debug(e); } } releasePointer(button) { try { this._ensureAdapter(); this._session.releasePointer(button); } catch (e) { debug(e); } } clickPointer(button) { try { this._ensureAdapter(); this._session.clickPointer(button); } catch (e) { debug(e); } } doubleclickPointer(button) { try { this._ensureAdapter(); this._session.doubleclickPointer(button); } catch (e) { debug(e); } } scrollPointer(dx, dy) { if (dx === 0 && dy === 0) return; try { this._ensureAdapter(); this._session.scrollPointer(dx, dy); } catch (e) { debug(e); } } /** * Keyboard Events */ pressKeysym(keysym) { try { this._ensureAdapter(); this._session.pressKeysym(keysym); } catch (e) { debug(e); } } releaseKeysym(keysym) { try { this._ensureAdapter(); this._session.releaseKeysym(keysym); } catch (e) { debug(e); } } pressreleaseKeysym(keysym) { try { this._ensureAdapter(); this._session.pressreleaseKeysym(keysym); } catch (e) { debug(e); } } /** * High-level keyboard input */ pressKey(input, modifiers) { try { this._ensureAdapter(); this._session.pressKey(input, modifiers); } catch (e) { debug(e); } } destroy() { if (this._session !== null) { // Disconnect from the session if (this._sessionClosedId > 0) { this._session.disconnect(this._sessionClosedId); this._sessionClosedId = 0; } this._session.destroy(); this._session = null; } if (this._nameWatcherId > 0) { Gio.bus_unwatch_name(this._nameWatcherId); this._nameWatcherId = 0; } } }; /** * The service class for this component */ var Component = Controller;