mousepad.js 9.9 KB

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