atspi.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Atspi from 'gi://Atspi?version=2.0';
  5. import Gdk from 'gi://Gdk';
  6. /**
  7. * Printable ASCII range
  8. */
  9. const _ASCII = /[\x20-\x7E]/;
  10. /**
  11. * Modifier Keycode Defaults
  12. */
  13. const XKeycode = {
  14. Alt_L: 0x40,
  15. Control_L: 0x25,
  16. Shift_L: 0x32,
  17. Super_L: 0x85,
  18. };
  19. /**
  20. * A thin wrapper around Atspi for X11 sessions without Pipewire support.
  21. */
  22. export default class Controller {
  23. constructor() {
  24. // Atspi.init() return 2 on fail, but still marks itself as inited. We
  25. // uninit before throwing an error otherwise any future call to init()
  26. // will appear successful and other calls will cause GSConnect to exit.
  27. // See: https://gitlab.gnome.org/GNOME/at-spi2-core/blob/master/atspi/atspi-misc.c
  28. if (Atspi.init() === 2) {
  29. this.destroy();
  30. throw new Error('Failed to start AT-SPI');
  31. }
  32. try {
  33. this._display = Gdk.Display.get_default();
  34. this._seat = this._display.get_default_seat();
  35. this._pointer = this._seat.get_pointer();
  36. } catch (e) {
  37. this.destroy();
  38. throw e;
  39. }
  40. // Try to read modifier keycodes from Gdk
  41. try {
  42. const keymap = Gdk.Keymap.get_for_display(this._display);
  43. let modifier;
  44. modifier = keymap.get_entries_for_keyval(Gdk.KEY_Alt_L)[1][0];
  45. XKeycode.Alt_L = modifier.keycode;
  46. modifier = keymap.get_entries_for_keyval(Gdk.KEY_Control_L)[1][0];
  47. XKeycode.Control_L = modifier.keycode;
  48. modifier = keymap.get_entries_for_keyval(Gdk.KEY_Shift_L)[1][0];
  49. XKeycode.Shift_L = modifier.keycode;
  50. modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
  51. XKeycode.Super_L = modifier.keycode;
  52. } catch (e) {
  53. debug('using default modifier keycodes');
  54. }
  55. }
  56. /*
  57. * Pointer events
  58. */
  59. clickPointer(button) {
  60. try {
  61. const [, x, y] = this._pointer.get_position();
  62. const monitor = this._display.get_monitor_at_point(x, y);
  63. const scale = monitor.get_scale_factor();
  64. Atspi.generate_mouse_event(scale * x, scale * y, `b${button}c`);
  65. } catch (e) {
  66. logError(e);
  67. }
  68. }
  69. doubleclickPointer(button) {
  70. try {
  71. const [, x, y] = this._pointer.get_position();
  72. const monitor = this._display.get_monitor_at_point(x, y);
  73. const scale = monitor.get_scale_factor();
  74. Atspi.generate_mouse_event(scale * x, scale * y, `b${button}d`);
  75. } catch (e) {
  76. logError(e);
  77. }
  78. }
  79. movePointer(dx, dy) {
  80. try {
  81. const [, x, y] = this._pointer.get_position();
  82. const monitor = this._display.get_monitor_at_point(x, y);
  83. const scale = monitor.get_scale_factor();
  84. Atspi.generate_mouse_event(scale * dx, scale * dy, 'rel');
  85. } catch (e) {
  86. logError(e);
  87. }
  88. }
  89. pressPointer(button) {
  90. try {
  91. const [, x, y] = this._pointer.get_position();
  92. const monitor = this._display.get_monitor_at_point(x, y);
  93. const scale = monitor.get_scale_factor();
  94. Atspi.generate_mouse_event(scale * x, scale * y, `b${button}p`);
  95. } catch (e) {
  96. logError(e);
  97. }
  98. }
  99. releasePointer(button) {
  100. try {
  101. const [, x, y] = this._pointer.get_position();
  102. const monitor = this._display.get_monitor_at_point(x, y);
  103. const scale = monitor.get_scale_factor();
  104. Atspi.generate_mouse_event(scale * x, scale * y, `b${button}r`);
  105. } catch (e) {
  106. logError(e);
  107. }
  108. }
  109. scrollPointer(dx, dy) {
  110. if (dy > 0)
  111. this.clickPointer(4);
  112. else if (dy < 0)
  113. this.clickPointer(5);
  114. }
  115. /*
  116. * Phony virtual keyboard helpers
  117. */
  118. _modeLock(keycode) {
  119. Atspi.generate_keyboard_event(
  120. keycode,
  121. null,
  122. Atspi.KeySynthType.PRESS
  123. );
  124. }
  125. _modeUnlock(keycode) {
  126. Atspi.generate_keyboard_event(
  127. keycode,
  128. null,
  129. Atspi.KeySynthType.RELEASE
  130. );
  131. }
  132. /*
  133. * Simulate a printable-ASCII character.
  134. *
  135. */
  136. _pressASCII(key, modifiers) {
  137. try {
  138. // Press Modifiers
  139. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  140. this._modeLock(XKeycode.Alt_L);
  141. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  142. this._modeLock(XKeycode.Control_L);
  143. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  144. this._modeLock(XKeycode.Shift_L);
  145. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  146. this._modeLock(XKeycode.Super_L);
  147. Atspi.generate_keyboard_event(
  148. 0,
  149. key,
  150. Atspi.KeySynthType.STRING
  151. );
  152. // Release Modifiers
  153. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  154. this._modeUnlock(XKeycode.Alt_L);
  155. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  156. this._modeUnlock(XKeycode.Control_L);
  157. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  158. this._modeUnlock(XKeycode.Shift_L);
  159. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  160. this._modeUnlock(XKeycode.Super_L);
  161. } catch (e) {
  162. logError(e);
  163. }
  164. }
  165. _pressKeysym(keysym, modifiers) {
  166. try {
  167. // Press Modifiers
  168. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  169. this._modeLock(XKeycode.Alt_L);
  170. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  171. this._modeLock(XKeycode.Control_L);
  172. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  173. this._modeLock(XKeycode.Shift_L);
  174. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  175. this._modeLock(XKeycode.Super_L);
  176. Atspi.generate_keyboard_event(
  177. keysym,
  178. null,
  179. Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
  180. );
  181. // Release Modifiers
  182. if (modifiers & Gdk.ModifierType.MOD1_MASK)
  183. this._modeUnlock(XKeycode.Alt_L);
  184. if (modifiers & Gdk.ModifierType.CONTROL_MASK)
  185. this._modeUnlock(XKeycode.Control_L);
  186. if (modifiers & Gdk.ModifierType.SHIFT_MASK)
  187. this._modeUnlock(XKeycode.Shift_L);
  188. if (modifiers & Gdk.ModifierType.SUPER_MASK)
  189. this._modeUnlock(XKeycode.Super_L);
  190. } catch (e) {
  191. logError(e);
  192. }
  193. }
  194. /**
  195. * Simulate the composition of a unicode character with:
  196. * Control+Shift+u, [hex], Return
  197. *
  198. * @param {number} key - An XKeycode
  199. * @param {number} modifiers - A modifier mask
  200. */
  201. _pressUnicode(key, modifiers) {
  202. try {
  203. if (modifiers > 0)
  204. log('GSConnect: ignoring modifiers for unicode keyboard event');
  205. // TODO: Using Control and Shift keysym is not working (it triggers
  206. // key release). Probably using LOCKMODIFIERS will not work either
  207. // as unlocking the modifier will not trigger a release
  208. // Activate compose sequence
  209. this._modeLock(XKeycode.Control_L);
  210. this._modeLock(XKeycode.Shift_L);
  211. this.pressreleaseKeysym(Gdk.KEY_U);
  212. this._modeUnlock(XKeycode.Control_L);
  213. this._modeUnlock(XKeycode.Shift_L);
  214. // Enter the unicode sequence
  215. const ucode = key.charCodeAt(0).toString(16);
  216. let keysym;
  217. for (let h = 0, len = ucode.length; h < len; h++) {
  218. keysym = Gdk.unicode_to_keyval(ucode.charAt(h).codePointAt(0));
  219. this.pressreleaseKeysym(keysym);
  220. }
  221. // Finish the compose sequence
  222. this.pressreleaseKeysym(Gdk.KEY_Return);
  223. } catch (e) {
  224. logError(e);
  225. }
  226. }
  227. /*
  228. * Keyboard Events
  229. */
  230. pressKeysym(keysym) {
  231. Atspi.generate_keyboard_event(
  232. keysym,
  233. null,
  234. Atspi.KeySynthType.PRESS | Atspi.KeySynthType.SYM
  235. );
  236. }
  237. releaseKeysym(keysym) {
  238. Atspi.generate_keyboard_event(
  239. keysym,
  240. null,
  241. Atspi.KeySynthType.RELEASE | Atspi.KeySynthType.SYM
  242. );
  243. }
  244. pressreleaseKeysym(keysym) {
  245. Atspi.generate_keyboard_event(
  246. keysym,
  247. null,
  248. Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM
  249. );
  250. }
  251. pressKey(input, modifiers) {
  252. // We were passed a keysym
  253. if (typeof input === 'number')
  254. this._pressKeysym(input, modifiers);
  255. // Regular ASCII
  256. else if (_ASCII.test(input))
  257. this._pressASCII(input, modifiers);
  258. // Unicode
  259. else
  260. this._pressUnicode(input, modifiers);
  261. }
  262. destroy() {
  263. try {
  264. Atspi.exit();
  265. } catch (e) {
  266. // Silence errors
  267. }
  268. }
  269. }