mousepad.js 13 KB

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