mousepad.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  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 GObject = imports.gi.GObject;
  7. const Components = imports.service.components;
  8. const {InputDialog} = imports.service.ui.mousepad;
  9. const PluginBase = imports.service.plugin;
  10. var Metadata = {
  11. label: _('Mousepad'),
  12. description: _('Enables the paired device to act as a remote mouse and keyboard'),
  13. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Mousepad',
  14. incomingCapabilities: [
  15. 'kdeconnect.mousepad.echo',
  16. 'kdeconnect.mousepad.request',
  17. 'kdeconnect.mousepad.keyboardstate',
  18. ],
  19. outgoingCapabilities: [
  20. 'kdeconnect.mousepad.echo',
  21. 'kdeconnect.mousepad.request',
  22. 'kdeconnect.mousepad.keyboardstate',
  23. ],
  24. actions: {
  25. keyboard: {
  26. label: _('Remote Input'),
  27. icon_name: 'input-keyboard-symbolic',
  28. parameter_type: null,
  29. incoming: [
  30. 'kdeconnect.mousepad.echo',
  31. 'kdeconnect.mousepad.keyboardstate',
  32. ],
  33. outgoing: ['kdeconnect.mousepad.request'],
  34. },
  35. },
  36. };
  37. /**
  38. * A map of "KDE Connect" keyvals to Gdk
  39. */
  40. const KeyMap = new Map([
  41. [1, Gdk.KEY_BackSpace],
  42. [2, Gdk.KEY_Tab],
  43. [3, Gdk.KEY_Linefeed],
  44. [4, Gdk.KEY_Left],
  45. [5, Gdk.KEY_Up],
  46. [6, Gdk.KEY_Right],
  47. [7, Gdk.KEY_Down],
  48. [8, Gdk.KEY_Page_Up],
  49. [9, Gdk.KEY_Page_Down],
  50. [10, Gdk.KEY_Home],
  51. [11, Gdk.KEY_End],
  52. [12, Gdk.KEY_Return],
  53. [13, Gdk.KEY_Delete],
  54. [14, Gdk.KEY_Escape],
  55. [15, Gdk.KEY_Sys_Req],
  56. [16, Gdk.KEY_Scroll_Lock],
  57. [17, 0],
  58. [18, 0],
  59. [19, 0],
  60. [20, 0],
  61. [21, Gdk.KEY_F1],
  62. [22, Gdk.KEY_F2],
  63. [23, Gdk.KEY_F3],
  64. [24, Gdk.KEY_F4],
  65. [25, Gdk.KEY_F5],
  66. [26, Gdk.KEY_F6],
  67. [27, Gdk.KEY_F7],
  68. [28, Gdk.KEY_F8],
  69. [29, Gdk.KEY_F9],
  70. [30, Gdk.KEY_F10],
  71. [31, Gdk.KEY_F11],
  72. [32, Gdk.KEY_F12],
  73. ]);
  74. const KeyMapCodes = new Map([
  75. [1, 14],
  76. [2, 15],
  77. [3, 101],
  78. [4, 105],
  79. [5, 103],
  80. [6, 106],
  81. [7, 108],
  82. [8, 104],
  83. [9, 109],
  84. [10, 102],
  85. [11, 107],
  86. [12, 28],
  87. [13, 111],
  88. [14, 1],
  89. [15, 99],
  90. [16, 70],
  91. [17, 0],
  92. [18, 0],
  93. [19, 0],
  94. [20, 0],
  95. [21, 59],
  96. [22, 60],
  97. [23, 61],
  98. [24, 62],
  99. [25, 63],
  100. [26, 64],
  101. [27, 65],
  102. [28, 66],
  103. [29, 67],
  104. [30, 68],
  105. [31, 87],
  106. [32, 88],
  107. ]);
  108. /**
  109. * Mousepad Plugin
  110. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mousepad
  111. *
  112. * TODO: support outgoing mouse events?
  113. */
  114. var Plugin = GObject.registerClass({
  115. GTypeName: 'GSConnectMousepadPlugin',
  116. Properties: {
  117. 'state': GObject.ParamSpec.boolean(
  118. 'state',
  119. 'State',
  120. 'Remote keyboard state',
  121. GObject.ParamFlags.READABLE,
  122. false
  123. ),
  124. },
  125. }, class Plugin extends PluginBase.Plugin {
  126. _init(device) {
  127. super._init(device, 'mousepad');
  128. if (!globalThis.HAVE_GNOME)
  129. this._input = Components.acquire('ydotool');
  130. else
  131. this._input = Components.acquire('input');
  132. this._shareControlChangedId = this.settings.connect(
  133. 'changed::share-control',
  134. this._sendState.bind(this)
  135. );
  136. }
  137. get state() {
  138. if (this._state === undefined)
  139. this._state = false;
  140. return this._state;
  141. }
  142. connected() {
  143. super.connected();
  144. this._sendState();
  145. }
  146. disconnected() {
  147. super.disconnected();
  148. this._state = false;
  149. this.notify('state');
  150. }
  151. handlePacket(packet) {
  152. switch (packet.type) {
  153. case 'kdeconnect.mousepad.request':
  154. this._handleInput(packet.body);
  155. break;
  156. case 'kdeconnect.mousepad.echo':
  157. this._handleEcho(packet.body);
  158. break;
  159. case 'kdeconnect.mousepad.keyboardstate':
  160. this._handleState(packet);
  161. break;
  162. }
  163. }
  164. /**
  165. * Handle a input event.
  166. *
  167. * @param {Object} input - The body of a `kdeconnect.mousepad.request`
  168. */
  169. _handleInput(input) {
  170. if (!this.settings.get_boolean('share-control'))
  171. return;
  172. let keysym;
  173. let modifiers = 0;
  174. const modifiers_codes = [];
  175. // These are ordered, as much as possible, to create the shortest code
  176. // path for high-frequency, low-latency events (eg. mouse movement)
  177. switch (true) {
  178. case input.hasOwnProperty('scroll'):
  179. this._input.scrollPointer(input.dx, input.dy);
  180. break;
  181. case (input.hasOwnProperty('dx') && input.hasOwnProperty('dy')):
  182. this._input.movePointer(input.dx, input.dy);
  183. break;
  184. case (input.hasOwnProperty('key') || input.hasOwnProperty('specialKey')):
  185. // NOTE: \u0000 sometimes sent in advance of a specialKey packet
  186. if (input.key && input.key === '\u0000')
  187. return;
  188. // Modifiers
  189. if (input.alt) {
  190. modifiers |= Gdk.ModifierType.MOD1_MASK;
  191. modifiers_codes.push(56);
  192. }
  193. if (input.ctrl) {
  194. modifiers |= Gdk.ModifierType.CONTROL_MASK;
  195. modifiers_codes.push(29);
  196. }
  197. if (input.shift) {
  198. modifiers |= Gdk.ModifierType.SHIFT_MASK;
  199. modifiers_codes.push(42);
  200. }
  201. if (input.super) {
  202. modifiers |= Gdk.ModifierType.SUPER_MASK;
  203. modifiers_codes.push(125);
  204. }
  205. // Regular key (printable ASCII or Unicode)
  206. if (input.key) {
  207. if (!globalThis.HAVE_GNOME)
  208. this._input.pressKeys(input.key, modifiers_codes);
  209. else
  210. this._input.pressKeys(input.key, modifiers);
  211. this._sendEcho(input);
  212. // Special key (eg. non-printable ASCII)
  213. } else if (input.specialKey && KeyMap.has(input.specialKey)) {
  214. if (!globalThis.HAVE_GNOME) {
  215. keysym = KeyMapCodes.get(input.specialKey);
  216. this._input.pressKeys(keysym, modifiers_codes);
  217. } else {
  218. keysym = KeyMap.get(input.specialKey);
  219. this._input.pressKeys(keysym, modifiers);
  220. }
  221. this._sendEcho(input);
  222. }
  223. break;
  224. case input.hasOwnProperty('singleclick'):
  225. this._input.clickPointer(Gdk.BUTTON_PRIMARY);
  226. break;
  227. case input.hasOwnProperty('doubleclick'):
  228. this._input.doubleclickPointer(Gdk.BUTTON_PRIMARY);
  229. break;
  230. case input.hasOwnProperty('middleclick'):
  231. this._input.clickPointer(Gdk.BUTTON_MIDDLE);
  232. break;
  233. case input.hasOwnProperty('rightclick'):
  234. this._input.clickPointer(Gdk.BUTTON_SECONDARY);
  235. break;
  236. case input.hasOwnProperty('singlehold'):
  237. this._input.pressPointer(Gdk.BUTTON_PRIMARY);
  238. break;
  239. case input.hasOwnProperty('singlerelease'):
  240. this._input.releasePointer(Gdk.BUTTON_PRIMARY);
  241. break;
  242. default:
  243. logError(new Error('Unknown input'));
  244. }
  245. }
  246. /**
  247. * Handle an echo/ACK of a event we sent, displaying it the dialog entry.
  248. *
  249. * @param {Object} input - The body of a `kdeconnect.mousepad.echo`
  250. */
  251. _handleEcho(input) {
  252. if (!this._dialog || !this._dialog.visible)
  253. return;
  254. // Skip modifiers
  255. if (input.alt || input.ctrl || input.super)
  256. return;
  257. if (input.key) {
  258. this._dialog._isAck = true;
  259. this._dialog.entry.buffer.text += input.key;
  260. this._dialog._isAck = false;
  261. } else if (KeyMap.get(input.specialKey) === Gdk.KEY_BackSpace) {
  262. this._dialog.entry.emit('backspace');
  263. }
  264. }
  265. /**
  266. * Handle a state change from the remote keyboard. This is an indication
  267. * that the remote keyboard is ready to accept input.
  268. *
  269. * @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet
  270. */
  271. _handleState(packet) {
  272. this._state = !!packet.body.state;
  273. this.notify('state');
  274. }
  275. /**
  276. * Send an echo/ACK of @input, if requested
  277. *
  278. * @param {Object} input - The body of a 'kdeconnect.mousepad.request'
  279. */
  280. _sendEcho(input) {
  281. if (!input.sendAck)
  282. return;
  283. delete input.sendAck;
  284. input.isAck = true;
  285. this.device.sendPacket({
  286. type: 'kdeconnect.mousepad.echo',
  287. body: input,
  288. });
  289. }
  290. /**
  291. * Send the local keyboard state
  292. *
  293. * @param {boolean} state - Whether we're ready to accept input
  294. */
  295. _sendState() {
  296. this.device.sendPacket({
  297. type: 'kdeconnect.mousepad.keyboardstate',
  298. body: {
  299. state: this.settings.get_boolean('share-control'),
  300. },
  301. });
  302. }
  303. /**
  304. * Open the Keyboard Input dialog
  305. */
  306. keyboard() {
  307. if (this._dialog === undefined) {
  308. this._dialog = new InputDialog({
  309. device: this.device,
  310. plugin: this,
  311. });
  312. }
  313. this._dialog.present();
  314. }
  315. destroy() {
  316. if (this._input !== undefined) {
  317. if (!globalThis.HAVE_GNOME)
  318. this._input = Components.release('ydotool');
  319. else
  320. this._input = Components.release('input');
  321. }
  322. if (this._dialog !== undefined)
  323. this._dialog.destroy();
  324. this.settings.disconnect(this._shareControlChangedId);
  325. super.destroy();
  326. }
  327. });