input.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const Gdk = imports.gi.Gdk;
  6. const Gio = imports.gi.Gio;
  7. const GLib = imports.gi.GLib;
  8. const GObject = imports.gi.GObject;
  9. const SESSION_TIMEOUT = 15;
  10. const RemoteSession = GObject.registerClass({
  11. GTypeName: 'GSConnectRemoteSession',
  12. Implements: [Gio.DBusInterface],
  13. Signals: {
  14. 'closed': {
  15. flags: GObject.SignalFlags.RUN_FIRST,
  16. },
  17. },
  18. }, class RemoteSession extends Gio.DBusProxy {
  19. _init(objectPath) {
  20. super._init({
  21. g_bus_type: Gio.BusType.SESSION,
  22. g_name: 'org.gnome.Mutter.RemoteDesktop',
  23. g_object_path: objectPath,
  24. g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session',
  25. g_flags: Gio.DBusProxyFlags.NONE,
  26. });
  27. this._started = false;
  28. }
  29. vfunc_g_signal(sender_name, signal_name, parameters) {
  30. if (signal_name === 'Closed')
  31. this.emit('closed');
  32. }
  33. _call(name, parameters = null) {
  34. if (!this._started)
  35. return;
  36. // Pass a null callback to allow this call to finish itself
  37. this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null);
  38. }
  39. get session_id() {
  40. try {
  41. return this.get_cached_property('SessionId').unpack();
  42. } catch (e) {
  43. return null;
  44. }
  45. }
  46. async start() {
  47. try {
  48. if (this._started)
  49. return;
  50. // Initialize the proxy, and start the session
  51. await this.init_async(GLib.PRIORITY_DEFAULT, null);
  52. await this.call('Start', null, Gio.DBusCallFlags.NONE, -1, null);
  53. this._started = true;
  54. } catch (e) {
  55. this.destroy();
  56. Gio.DBusError.strip_remote_error(e);
  57. throw e;
  58. }
  59. }
  60. stop() {
  61. if (this._started) {
  62. this._started = false;
  63. // Pass a null callback to allow this call to finish itself
  64. this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null);
  65. }
  66. }
  67. _translateButton(button) {
  68. switch (button) {
  69. case Gdk.BUTTON_PRIMARY:
  70. return 0x110;
  71. case Gdk.BUTTON_MIDDLE:
  72. return 0x112;
  73. case Gdk.BUTTON_SECONDARY:
  74. return 0x111;
  75. case 4:
  76. return 0; // FIXME
  77. case 5:
  78. return 0x10F; // up
  79. }
  80. }
  81. movePointer(dx, dy) {
  82. this._call(
  83. 'NotifyPointerMotionRelative',
  84. GLib.Variant.new('(dd)', [dx, dy])
  85. );
  86. }
  87. pressPointer(button) {
  88. button = this._translateButton(button);
  89. this._call(
  90. 'NotifyPointerButton',
  91. GLib.Variant.new('(ib)', [button, true])
  92. );
  93. }
  94. releasePointer(button) {
  95. button = this._translateButton(button);
  96. this._call(
  97. 'NotifyPointerButton',
  98. GLib.Variant.new('(ib)', [button, false])
  99. );
  100. }
  101. clickPointer(button) {
  102. button = this._translateButton(button);
  103. this._call(
  104. 'NotifyPointerButton',
  105. GLib.Variant.new('(ib)', [button, true])
  106. );
  107. this._call(
  108. 'NotifyPointerButton',
  109. GLib.Variant.new('(ib)', [button, false])
  110. );
  111. }
  112. doubleclickPointer(button) {
  113. this.clickPointer(button);
  114. this.clickPointer(button);
  115. }
  116. scrollPointer(dx, dy) {
  117. // NOTE: NotifyPointerAxis only seems to work on Wayland, but maybe
  118. // NotifyPointerAxisDiscrete is the better choice anyways
  119. if (HAVE_WAYLAND) {
  120. this._call(
  121. 'NotifyPointerAxis',
  122. GLib.Variant.new('(ddu)', [dx, dy, 0])
  123. );
  124. this._call(
  125. 'NotifyPointerAxis',
  126. GLib.Variant.new('(ddu)', [0, 0, 1])
  127. );
  128. } else if (dy > 0) {
  129. this._call(
  130. 'NotifyPointerAxisDiscrete',
  131. GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1])
  132. );
  133. } else if (dy < 0) {
  134. this._call(
  135. 'NotifyPointerAxisDiscrete',
  136. GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1])
  137. );
  138. }
  139. }
  140. /*
  141. * Keyboard Events
  142. */
  143. pressKeysym(keysym) {
  144. this._call(
  145. 'NotifyKeyboardKeysym',
  146. GLib.Variant.new('(ub)', [keysym, true])
  147. );
  148. }
  149. releaseKeysym(keysym) {
  150. this._call(
  151. 'NotifyKeyboardKeysym',
  152. GLib.Variant.new('(ub)', [keysym, false])
  153. );
  154. }
  155. pressreleaseKeysym(keysym) {
  156. this._call(
  157. 'NotifyKeyboardKeysym',
  158. GLib.Variant.new('(ub)', [keysym, true])
  159. );
  160. this._call(
  161. 'NotifyKeyboardKeysym',
  162. GLib.Variant.new('(ub)', [keysym, false])
  163. );
  164. }
  165. /*
  166. * High-level keyboard input
  167. */
  168. pressKey(input, modifiers) {
  169. // Press Modifiers
  170. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  171. this.pressKeysym(Gdk.KEY_Alt_L);
  172. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  173. this.pressKeysym(Gdk.KEY_Control_L);
  174. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  175. this.pressKeysym(Gdk.KEY_Shift_L);
  176. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  177. this.pressKeysym(Gdk.KEY_Super_L);
  178. if (typeof input === 'string') {
  179. const keysym = Gdk.unicode_to_keyval(input.codePointAt(0));
  180. this.pressreleaseKeysym(keysym);
  181. } else {
  182. this.pressreleaseKeysym(input);
  183. }
  184. // Release Modifiers
  185. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  186. this.releaseKeysym(Gdk.KEY_Alt_L);
  187. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  188. this.releaseKeysym(Gdk.KEY_Control_L);
  189. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  190. this.releaseKeysym(Gdk.KEY_Shift_L);
  191. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  192. this.releaseKeysym(Gdk.KEY_Super_L);
  193. }
  194. destroy() {
  195. if (this.__disposed === undefined) {
  196. this.__disposed = true;
  197. GObject.signal_handlers_destroy(this);
  198. }
  199. }
  200. });
  201. class Controller {
  202. constructor() {
  203. this._nameAppearedId = 0;
  204. this._session = null;
  205. this._sessionCloseId = 0;
  206. this._sessionExpiry = 0;
  207. this._sessionExpiryId = 0;
  208. this._sessionStarting = false;
  209. // Watch for the RemoteDesktop portal
  210. this._nameWatcherId = Gio.bus_watch_name(
  211. Gio.BusType.SESSION,
  212. 'org.gnome.Mutter.RemoteDesktop',
  213. Gio.BusNameWatcherFlags.NONE,
  214. this._onNameAppeared.bind(this),
  215. this._onNameVanished.bind(this)
  216. );
  217. }
  218. get connection() {
  219. if (this._connection === undefined)
  220. this._connection = null;
  221. return this._connection;
  222. }
  223. /**
  224. * Check if this is a Wayland session, specifically for distributions that
  225. * don't ship pipewire support (eg. Debian/Ubuntu).
  226. *
  227. * FIXME: this is a super ugly hack that should go away
  228. *
  229. * @return {boolean} %true if wayland is not supported
  230. */
  231. _checkWayland() {
  232. if (HAVE_WAYLAND) {
  233. // eslint-disable-next-line no-global-assign
  234. HAVE_REMOTEINPUT = false;
  235. const service = Gio.Application.get_default();
  236. if (service === null)
  237. return true;
  238. // First we're going to disabled the affected plugins on all devices
  239. for (const device of service.manager.devices.values()) {
  240. const supported = device.settings.get_strv('supported-plugins');
  241. let index;
  242. if ((index = supported.indexOf('mousepad')) > -1)
  243. supported.splice(index, 1);
  244. if ((index = supported.indexOf('presenter')) > -1)
  245. supported.splice(index, 1);
  246. device.settings.set_strv('supported-plugins', supported);
  247. }
  248. // Second we need each backend to rebuild its identity packet and
  249. // broadcast the amended capabilities to the network
  250. for (const backend of service.manager.backends.values())
  251. backend.buildIdentity();
  252. service.manager.identify();
  253. return true;
  254. }
  255. return false;
  256. }
  257. _onNameAppeared(connection, name, name_owner) {
  258. try {
  259. this._connection = connection;
  260. } catch (e) {
  261. logError(e);
  262. }
  263. }
  264. _onNameVanished(connection, name) {
  265. try {
  266. if (this._session !== null)
  267. this._onSessionClosed(this._session);
  268. } catch (e) {
  269. logError(e);
  270. }
  271. }
  272. _onSessionClosed(session) {
  273. // Disconnect from the session
  274. if (this._sessionClosedId > 0) {
  275. session.disconnect(this._sessionClosedId);
  276. this._sessionClosedId = 0;
  277. }
  278. // Destroy the session
  279. session.destroy();
  280. this._session = null;
  281. }
  282. _onSessionExpired() {
  283. // If the session has been used recently, schedule a new expiry
  284. const remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000));
  285. if (remainder > 0) {
  286. this._sessionExpiryId = GLib.timeout_add_seconds(
  287. GLib.PRIORITY_DEFAULT,
  288. remainder,
  289. this._onSessionExpired.bind(this)
  290. );
  291. return GLib.SOURCE_REMOVE;
  292. }
  293. // Otherwise if there's an active session, close it
  294. if (this._session !== null)
  295. this._session.stop();
  296. // Reset the GSource Id
  297. this._sessionExpiryId = 0;
  298. return GLib.SOURCE_REMOVE;
  299. }
  300. async _createRemoteDesktopSession() {
  301. if (this.connection === null)
  302. return Promise.reject(new Error('No DBus connection'));
  303. const reply = await this.connection.call(
  304. 'org.gnome.Mutter.RemoteDesktop',
  305. '/org/gnome/Mutter/RemoteDesktop',
  306. 'org.gnome.Mutter.RemoteDesktop',
  307. 'CreateSession',
  308. null,
  309. null,
  310. Gio.DBusCallFlags.NONE,
  311. -1,
  312. null);
  313. return reply.deepUnpack()[0];
  314. }
  315. async _ensureAdapter() {
  316. try {
  317. // Update the timestamp of the last event
  318. this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
  319. // Session is active
  320. if (this._session !== null)
  321. return;
  322. // Mutter's RemoteDesktop is not available, fall back to Atspi
  323. if (this.connection === null) {
  324. debug('Falling back to Atspi');
  325. // If we got here in Wayland, we need to re-adjust and bail
  326. if (this._checkWayland())
  327. return;
  328. const fallback = imports.service.components.atspi;
  329. this._session = new fallback.Controller();
  330. // Mutter is available and there isn't another session starting
  331. } else if (this._sessionStarting === false) {
  332. this._sessionStarting = true;
  333. debug('Creating Mutter RemoteDesktop session');
  334. // This takes three steps: creating the remote desktop session,
  335. // starting the session, and creating a screencast session for
  336. // the remote desktop session.
  337. const objectPath = await this._createRemoteDesktopSession();
  338. this._session = new RemoteSession(objectPath);
  339. await this._session.start();
  340. // Watch for the session ending
  341. this._sessionClosedId = this._session.connect(
  342. 'closed',
  343. this._onSessionClosed.bind(this)
  344. );
  345. if (this._sessionExpiryId === 0) {
  346. this._sessionExpiryId = GLib.timeout_add_seconds(
  347. GLib.PRIORITY_DEFAULT,
  348. SESSION_TIMEOUT,
  349. this._onSessionExpired.bind(this)
  350. );
  351. }
  352. this._sessionStarting = false;
  353. }
  354. } catch (e) {
  355. logError(e);
  356. if (this._session !== null) {
  357. this._session.destroy();
  358. this._session = null;
  359. }
  360. this._sessionStarting = false;
  361. }
  362. }
  363. /*
  364. * Pointer Events
  365. */
  366. movePointer(dx, dy) {
  367. try {
  368. if (dx === 0 && dy === 0)
  369. return;
  370. this._ensureAdapter();
  371. this._session.movePointer(dx, dy);
  372. } catch (e) {
  373. debug(e);
  374. }
  375. }
  376. pressPointer(button) {
  377. try {
  378. this._ensureAdapter();
  379. this._session.pressPointer(button);
  380. } catch (e) {
  381. debug(e);
  382. }
  383. }
  384. releasePointer(button) {
  385. try {
  386. this._ensureAdapter();
  387. this._session.releasePointer(button);
  388. } catch (e) {
  389. debug(e);
  390. }
  391. }
  392. clickPointer(button) {
  393. try {
  394. this._ensureAdapter();
  395. this._session.clickPointer(button);
  396. } catch (e) {
  397. debug(e);
  398. }
  399. }
  400. doubleclickPointer(button) {
  401. try {
  402. this._ensureAdapter();
  403. this._session.doubleclickPointer(button);
  404. } catch (e) {
  405. debug(e);
  406. }
  407. }
  408. scrollPointer(dx, dy) {
  409. if (dx === 0 && dy === 0)
  410. return;
  411. try {
  412. this._ensureAdapter();
  413. this._session.scrollPointer(dx, dy);
  414. } catch (e) {
  415. debug(e);
  416. }
  417. }
  418. /*
  419. * Keyboard Events
  420. */
  421. pressKeysym(keysym) {
  422. try {
  423. this._ensureAdapter();
  424. this._session.pressKeysym(keysym);
  425. } catch (e) {
  426. debug(e);
  427. }
  428. }
  429. releaseKeysym(keysym) {
  430. try {
  431. this._ensureAdapter();
  432. this._session.releaseKeysym(keysym);
  433. } catch (e) {
  434. debug(e);
  435. }
  436. }
  437. pressreleaseKeysym(keysym) {
  438. try {
  439. this._ensureAdapter();
  440. this._session.pressreleaseKeysym(keysym);
  441. } catch (e) {
  442. debug(e);
  443. }
  444. }
  445. /*
  446. * High-level keyboard input
  447. */
  448. pressKeys(input, modifiers) {
  449. try {
  450. this._ensureAdapter();
  451. if (typeof input === 'string') {
  452. for (let i = 0; i < input.length; i++)
  453. this._session.pressKey(input[i], modifiers);
  454. } else {
  455. this._session.pressKey(input, modifiers);
  456. }
  457. } catch (e) {
  458. debug(e);
  459. }
  460. }
  461. destroy() {
  462. if (this._session !== null) {
  463. // Disconnect from the session
  464. if (this._sessionClosedId > 0) {
  465. this._session.disconnect(this._sessionClosedId);
  466. this._sessionClosedId = 0;
  467. }
  468. this._session.destroy();
  469. this._session = null;
  470. }
  471. if (this._nameWatcherId > 0) {
  472. Gio.bus_unwatch_name(this._nameWatcherId);
  473. this._nameWatcherId = 0;
  474. }
  475. }
  476. }
  477. /**
  478. * The service class for this component
  479. */
  480. var Component = Controller;