mousepad.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const GLib = imports.gi.GLib;
  6. const Gdk = imports.gi.Gdk;
  7. const GObject = imports.gi.GObject;
  8. const Gtk = imports.gi.Gtk;
  9. /**
  10. * A map of Gdk to "KDE Connect" keyvals
  11. */
  12. const ReverseKeyMap = new Map([
  13. [Gdk.KEY_BackSpace, 1],
  14. [Gdk.KEY_Tab, 2],
  15. [Gdk.KEY_Linefeed, 3],
  16. [Gdk.KEY_Left, 4],
  17. [Gdk.KEY_Up, 5],
  18. [Gdk.KEY_Right, 6],
  19. [Gdk.KEY_Down, 7],
  20. [Gdk.KEY_Page_Up, 8],
  21. [Gdk.KEY_Page_Down, 9],
  22. [Gdk.KEY_Home, 10],
  23. [Gdk.KEY_End, 11],
  24. [Gdk.KEY_Return, 12],
  25. [Gdk.KEY_Delete, 13],
  26. [Gdk.KEY_Escape, 14],
  27. [Gdk.KEY_Sys_Req, 15],
  28. [Gdk.KEY_Scroll_Lock, 16],
  29. [Gdk.KEY_F1, 21],
  30. [Gdk.KEY_F2, 22],
  31. [Gdk.KEY_F3, 23],
  32. [Gdk.KEY_F4, 24],
  33. [Gdk.KEY_F5, 25],
  34. [Gdk.KEY_F6, 26],
  35. [Gdk.KEY_F7, 27],
  36. [Gdk.KEY_F8, 28],
  37. [Gdk.KEY_F9, 29],
  38. [Gdk.KEY_F10, 30],
  39. [Gdk.KEY_F11, 31],
  40. [Gdk.KEY_F12, 32],
  41. ]);
  42. /*
  43. * A list of keyvals we consider modifiers
  44. */
  45. const MOD_KEYS = [
  46. Gdk.KEY_Alt_L,
  47. Gdk.KEY_Alt_R,
  48. Gdk.KEY_Caps_Lock,
  49. Gdk.KEY_Control_L,
  50. Gdk.KEY_Control_R,
  51. Gdk.KEY_Meta_L,
  52. Gdk.KEY_Meta_R,
  53. Gdk.KEY_Num_Lock,
  54. Gdk.KEY_Shift_L,
  55. Gdk.KEY_Shift_R,
  56. Gdk.KEY_Super_L,
  57. Gdk.KEY_Super_R,
  58. ];
  59. /*
  60. * Some convenience functions for checking keyvals for modifiers
  61. */
  62. const isAlt = (key) => [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R].includes(key);
  63. const isCtrl = (key) => [Gdk.KEY_Control_L, Gdk.KEY_Control_R].includes(key);
  64. const isShift = (key) => [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R].includes(key);
  65. const isSuper = (key) => [Gdk.KEY_Super_L, Gdk.KEY_Super_R].includes(key);
  66. var InputDialog = GObject.registerClass({
  67. GTypeName: 'GSConnectMousepadInputDialog',
  68. Properties: {
  69. 'device': GObject.ParamSpec.object(
  70. 'device',
  71. 'Device',
  72. 'The device associated with this window',
  73. GObject.ParamFlags.READWRITE,
  74. GObject.Object
  75. ),
  76. 'plugin': GObject.ParamSpec.object(
  77. 'plugin',
  78. 'Plugin',
  79. 'The mousepad plugin associated with this window',
  80. GObject.ParamFlags.READWRITE,
  81. GObject.Object
  82. ),
  83. },
  84. Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/mousepad-input-dialog.ui',
  85. Children: [
  86. 'infobar', 'infobar-label',
  87. 'touchpad-eventbox', 'mouse-left-button', 'mouse-middle-button', 'mouse-right-button',
  88. 'touchpad-drag', 'touchpad-long-press',
  89. 'shift-label', 'ctrl-label', 'alt-label', 'super-label', 'entry',
  90. ],
  91. }, class InputDialog extends Gtk.Dialog {
  92. _init(params) {
  93. super._init(Object.assign({
  94. use_header_bar: true,
  95. }, params));
  96. const headerbar = this.get_titlebar();
  97. headerbar.title = _('Remote Input');
  98. headerbar.subtitle = this.device.name;
  99. // Main Box
  100. const content = this.get_content_area();
  101. content.border_width = 0;
  102. // TRANSLATORS: Displayed when the remote keyboard is not ready to accept input
  103. this.infobar_label.label = _('Remote keyboard on %s is not active').format(this.device.name);
  104. // Text Input
  105. this.entry.buffer.connect(
  106. 'insert-text',
  107. this._onInsertText.bind(this)
  108. );
  109. this.infobar.connect('notify::reveal-child', this._onState.bind(this));
  110. this.plugin.bind_property('state', this.infobar, 'reveal-child', 6);
  111. // Mouse Pad
  112. this._resetTouchpadMotion();
  113. this.touchpad_motion_timeout_id = 0;
  114. this.touchpad_holding = false;
  115. // Scroll Input
  116. this.add_events(Gdk.EventMask.SCROLL_MASK);
  117. this.show_all();
  118. }
  119. vfunc_delete_event(event) {
  120. this._ungrab();
  121. return this.hide_on_delete();
  122. }
  123. vfunc_grab_broken_event(event) {
  124. if (event.keyboard)
  125. this._ungrab();
  126. return false;
  127. }
  128. vfunc_key_release_event(event) {
  129. if (!this.plugin.state)
  130. debug('ignoring remote keyboard state');
  131. const keyvalLower = Gdk.keyval_to_lower(event.keyval);
  132. const realMask = event.state & Gtk.accelerator_get_default_mod_mask();
  133. this.alt_label.sensitive = !isAlt(keyvalLower) && (realMask & Gdk.ModifierType.MOD1_MASK);
  134. this.ctrl_label.sensitive = !isCtrl(keyvalLower) && (realMask & Gdk.ModifierType.CONTROL_MASK);
  135. this.shift_label.sensitive = !isShift(keyvalLower) && (realMask & Gdk.ModifierType.SHIFT_MASK);
  136. this.super_label.sensitive = !isSuper(keyvalLower) && (realMask & Gdk.ModifierType.SUPER_MASK);
  137. return super.vfunc_key_release_event(event);
  138. }
  139. vfunc_key_press_event(event) {
  140. if (!this.plugin.state)
  141. debug('ignoring remote keyboard state');
  142. let keyvalLower = Gdk.keyval_to_lower(event.keyval);
  143. let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
  144. this.alt_label.sensitive = isAlt(keyvalLower) || (realMask & Gdk.ModifierType.MOD1_MASK);
  145. this.ctrl_label.sensitive = isCtrl(keyvalLower) || (realMask & Gdk.ModifierType.CONTROL_MASK);
  146. this.shift_label.sensitive = isShift(keyvalLower) || (realMask & Gdk.ModifierType.SHIFT_MASK);
  147. this.super_label.sensitive = isSuper(keyvalLower) || (realMask & Gdk.ModifierType.SUPER_MASK);
  148. // Wait for a real key before sending
  149. if (MOD_KEYS.includes(keyvalLower))
  150. return false;
  151. // Normalize Tab
  152. if (keyvalLower === Gdk.KEY_ISO_Left_Tab)
  153. keyvalLower = Gdk.KEY_Tab;
  154. // Put shift back if it changed the case of the key, not otherwise.
  155. if (keyvalLower !== event.keyval)
  156. realMask |= Gdk.ModifierType.SHIFT_MASK;
  157. // HACK: we don't want to use SysRq as a keybinding (but we do want
  158. // Alt+Print), so we avoid translation from Alt+Print to SysRq
  159. if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0)
  160. keyvalLower = Gdk.KEY_Print;
  161. // CapsLock isn't supported as a keybinding modifier, so keep it from
  162. // confusing us
  163. realMask &= ~Gdk.ModifierType.LOCK_MASK;
  164. if (keyvalLower === 0)
  165. return false;
  166. debug(`keyval: ${event.keyval}, mask: ${realMask}`);
  167. const request = {
  168. alt: !!(realMask & Gdk.ModifierType.MOD1_MASK),
  169. ctrl: !!(realMask & Gdk.ModifierType.CONTROL_MASK),
  170. shift: !!(realMask & Gdk.ModifierType.SHIFT_MASK),
  171. super: !!(realMask & Gdk.ModifierType.SUPER_MASK),
  172. sendAck: true,
  173. };
  174. // specialKey
  175. if (ReverseKeyMap.has(event.keyval)) {
  176. request.specialKey = ReverseKeyMap.get(event.keyval);
  177. // key
  178. } else {
  179. const codePoint = Gdk.keyval_to_unicode(event.keyval);
  180. request.key = String.fromCodePoint(codePoint);
  181. }
  182. this.device.sendPacket({
  183. type: 'kdeconnect.mousepad.request',
  184. body: request,
  185. });
  186. // Pass these key combinations rather than using the echo reply
  187. if (request.alt || request.ctrl || request.super)
  188. return super.vfunc_key_press_event(event);
  189. return false;
  190. }
  191. vfunc_scroll_event(event) {
  192. if (event.delta_x === 0 && event.delta_y === 0)
  193. return true;
  194. this.device.sendPacket({
  195. type: 'kdeconnect.mousepad.request',
  196. body: {
  197. scroll: true,
  198. dx: event.delta_x * 200,
  199. dy: event.delta_y * 200,
  200. },
  201. });
  202. return true;
  203. }
  204. vfunc_window_state_event(event) {
  205. if (!this.plugin.state)
  206. debug('ignoring remote keyboard state');
  207. if (event.new_window_state & Gdk.WindowState.FOCUSED)
  208. this._grab();
  209. else
  210. this._ungrab();
  211. return super.vfunc_window_state_event(event);
  212. }
  213. _onInsertText(buffer, location, text, len) {
  214. if (this._isAck)
  215. return;
  216. debug(`insert-text: ${text} (chars ${[...text].length})`);
  217. for (const char of [...text]) {
  218. if (!char)
  219. continue;
  220. // TODO: modifiers?
  221. this.device.sendPacket({
  222. type: 'kdeconnect.mousepad.request',
  223. body: {
  224. alt: false,
  225. ctrl: false,
  226. shift: false,
  227. super: false,
  228. sendAck: false,
  229. key: char,
  230. },
  231. });
  232. }
  233. }
  234. _onState(widget) {
  235. if (!this.plugin.state)
  236. debug('ignoring remote keyboard state');
  237. if (this.is_active)
  238. this._grab();
  239. else
  240. this._ungrab();
  241. }
  242. _grab() {
  243. if (!this.visible || this._keyboard)
  244. return;
  245. const seat = Gdk.Display.get_default().get_default_seat();
  246. const status = seat.grab(
  247. this.get_window(),
  248. Gdk.SeatCapabilities.KEYBOARD,
  249. false,
  250. null,
  251. null,
  252. null
  253. );
  254. if (status !== Gdk.GrabStatus.SUCCESS) {
  255. logError(new Error('Grabbing keyboard failed'));
  256. return;
  257. }
  258. this._keyboard = seat.get_keyboard();
  259. this.grab_add();
  260. this.entry.has_focus = true;
  261. }
  262. _ungrab() {
  263. if (this._keyboard) {
  264. this._keyboard.get_seat().ungrab();
  265. this._keyboard = null;
  266. this.grab_remove();
  267. }
  268. this.entry.buffer.text = '';
  269. }
  270. _resetTouchpadMotion() {
  271. this.touchpad_motion_prev_x = 0;
  272. this.touchpad_motion_prev_y = 0;
  273. this.touchpad_motion_x = 0;
  274. this.touchpad_motion_y = 0;
  275. }
  276. _onMouseLeftButtonClicked(button) {
  277. this.device.sendPacket({
  278. type: 'kdeconnect.mousepad.request',
  279. body: {
  280. singleclick: true,
  281. },
  282. });
  283. }
  284. _onMouseMiddleButtonClicked(button) {
  285. this.device.sendPacket({
  286. type: 'kdeconnect.mousepad.request',
  287. body: {
  288. middleclick: true,
  289. },
  290. });
  291. }
  292. _onMouseRightButtonClicked(button) {
  293. this.device.sendPacket({
  294. type: 'kdeconnect.mousepad.request',
  295. body: {
  296. rightclick: true,
  297. },
  298. });
  299. }
  300. _onTouchpadDragBegin(gesture) {
  301. this._resetTouchpadMotion();
  302. this.touchpad_motion_timeout_id =
  303. GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10,
  304. this._onTouchpadMotionTimeout.bind(this));
  305. }
  306. _onTouchpadDragUpdate(gesture, offset_x, offset_y) {
  307. this.touchpad_motion_x = offset_x;
  308. this.touchpad_motion_y = offset_y;
  309. }
  310. _onTouchpadDragEnd(gesture) {
  311. this._resetTouchpadMotion();
  312. GLib.Source.remove(this.touchpad_motion_timeout_id);
  313. this.touchpad_motion_timeout_id = 0;
  314. }
  315. _onTouchpadLongPressCancelled(gesture) {
  316. const gesture_button = gesture.get_current_button();
  317. // Check user dragged less than certain distances.
  318. const is_click =
  319. (Math.abs(this.touchpad_motion_x) < 4) &&
  320. (Math.abs(this.touchpad_motion_y) < 4);
  321. if (is_click) {
  322. var click_body = {};
  323. switch (gesture_button) {
  324. case 1:
  325. click_body.singleclick = true;
  326. break;
  327. case 2:
  328. click_body.middleclick = true;
  329. break;
  330. case 3:
  331. click_body.rightclick = true;
  332. break;
  333. default:
  334. return;
  335. }
  336. this.device.sendPacket({
  337. type: 'kdeconnect.mousepad.request',
  338. body: click_body,
  339. });
  340. }
  341. }
  342. _onTouchpadLongPressPressed(gesture) {
  343. const gesture_button = gesture.get_current_button();
  344. if (gesture_button !== 1) {
  345. debug('Long press on other type of buttons are not handled.');
  346. } else {
  347. this.device.sendPacket({
  348. type: 'kdeconnect.mousepad.request',
  349. body: {
  350. singlehold: true,
  351. },
  352. });
  353. this.touchpad_holding = true;
  354. }
  355. }
  356. _onTouchpadLongPressEnd(gesture) {
  357. if (this.touchpad_holding) {
  358. this.device.sendPacket({
  359. type: 'kdeconnect.mousepad.request',
  360. body: {
  361. singlerelease: true,
  362. },
  363. });
  364. this.touchpad_holding = false;
  365. }
  366. }
  367. _onTouchpadMotionTimeout() {
  368. var diff_x = this.touchpad_motion_x - this.touchpad_motion_prev_x;
  369. var diff_y = this.touchpad_motion_y - this.touchpad_motion_prev_y;
  370. this.device.sendPacket({
  371. type: 'kdeconnect.mousepad.request',
  372. body: {
  373. dx: diff_x,
  374. dy: diff_y,
  375. },
  376. });
  377. this.touchpad_motion_prev_x = this.touchpad_motion_x;
  378. this.touchpad_motion_prev_y = this.touchpad_motion_y;
  379. return true;
  380. }
  381. });