keybindings.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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 Gio from 'gi://Gio';
  6. import GLib from 'gi://GLib';
  7. import GObject from 'gi://GObject';
  8. import Gtk from 'gi://Gtk';
  9. /*
  10. * A list of modifier keysyms we ignore
  11. */
  12. const _MODIFIERS = [
  13. Gdk.KEY_Alt_L,
  14. Gdk.KEY_Alt_R,
  15. Gdk.KEY_Caps_Lock,
  16. Gdk.KEY_Control_L,
  17. Gdk.KEY_Control_R,
  18. Gdk.KEY_Meta_L,
  19. Gdk.KEY_Meta_R,
  20. Gdk.KEY_Num_Lock,
  21. Gdk.KEY_Shift_L,
  22. Gdk.KEY_Shift_R,
  23. Gdk.KEY_Super_L,
  24. Gdk.KEY_Super_R,
  25. ];
  26. /**
  27. * Response enum for ShortcutChooserDialog
  28. */
  29. export const ResponseType = {
  30. CANCEL: Gtk.ResponseType.CANCEL,
  31. SET: Gtk.ResponseType.APPLY,
  32. UNSET: 2,
  33. };
  34. /**
  35. * A simplified version of the shortcut editor from GNOME Control Center
  36. */
  37. export const ShortcutChooserDialog = GObject.registerClass({
  38. GTypeName: 'GSConnectPreferencesShortcutEditor',
  39. Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-shortcut-editor.ui',
  40. Children: [
  41. 'cancel-button', 'set-button',
  42. 'stack', 'summary-label',
  43. 'shortcut-label', 'conflict-label',
  44. ],
  45. }, class ShortcutChooserDialog extends Gtk.Dialog {
  46. _init(params) {
  47. super._init({
  48. transient_for: Gio.Application.get_default().get_active_window(),
  49. use_header_bar: true,
  50. });
  51. this._seat = Gdk.Display.get_default().get_default_seat();
  52. // Current accelerator or %null
  53. this.accelerator = params.accelerator;
  54. // TRANSLATORS: Summary of a keyboard shortcut function
  55. // Example: Enter a new shortcut to change Messaging
  56. this.summary = _('Enter a new shortcut to change <b>%s</b>').format(
  57. params.summary
  58. );
  59. }
  60. get accelerator() {
  61. return this.shortcut_label.accelerator;
  62. }
  63. set accelerator(value) {
  64. this.shortcut_label.accelerator = value;
  65. }
  66. get summary() {
  67. return this.summary_label.label;
  68. }
  69. set summary(value) {
  70. this.summary_label.label = value;
  71. }
  72. vfunc_key_press_event(event) {
  73. let keyvalLower = Gdk.keyval_to_lower(event.keyval);
  74. let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
  75. // TODO: Critical: 'WIDGET_REALIZED_FOR_EVENT (widget, event)' failed
  76. if (_MODIFIERS.includes(keyvalLower))
  77. return true;
  78. // Normalize Tab
  79. if (keyvalLower === Gdk.KEY_ISO_Left_Tab)
  80. keyvalLower = Gdk.KEY_Tab;
  81. // Put shift back if it changed the case of the key, not otherwise.
  82. if (keyvalLower !== event.keyval)
  83. realMask |= Gdk.ModifierType.SHIFT_MASK;
  84. // HACK: we don't want to use SysRq as a keybinding (but we do want
  85. // Alt+Print), so we avoid translation from Alt+Print to SysRq
  86. if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0)
  87. keyvalLower = Gdk.KEY_Print;
  88. // A single Escape press cancels the editing
  89. if (realMask === 0 && keyvalLower === Gdk.KEY_Escape) {
  90. this.response(ResponseType.CANCEL);
  91. return false;
  92. }
  93. // Backspace disables the current shortcut
  94. if (realMask === 0 && keyvalLower === Gdk.KEY_BackSpace) {
  95. this.response(ResponseType.UNSET);
  96. return false;
  97. }
  98. // CapsLock isn't supported as a keybinding modifier, so keep it from
  99. // confusing us
  100. realMask &= ~Gdk.ModifierType.LOCK_MASK;
  101. if (keyvalLower !== 0 && realMask !== 0) {
  102. this._ungrab();
  103. // Set the accelerator property/label
  104. this.accelerator = Gtk.accelerator_name(keyvalLower, realMask);
  105. // TRANSLATORS: When a keyboard shortcut is unavailable
  106. // Example: [Ctrl]+[S] is already being used
  107. this.conflict_label.label = _('%s is already being used').format(
  108. Gtk.accelerator_get_label(keyvalLower, realMask)
  109. );
  110. // Show Cancel button and switch to confirm/conflict page
  111. this.cancel_button.visible = true;
  112. this.stack.visible_child_name = 'confirm';
  113. this._check();
  114. }
  115. return true;
  116. }
  117. async _check() {
  118. try {
  119. const available = await checkAccelerator(this.accelerator);
  120. this.set_button.visible = available;
  121. this.conflict_label.visible = !available;
  122. } catch (e) {
  123. logError(e);
  124. this.response(ResponseType.CANCEL);
  125. }
  126. }
  127. _grab() {
  128. const success = this._seat.grab(
  129. this.get_window(),
  130. Gdk.SeatCapabilities.KEYBOARD,
  131. true, // owner_events
  132. null, // cursor
  133. null, // event
  134. null
  135. );
  136. if (success !== Gdk.GrabStatus.SUCCESS)
  137. return this.response(ResponseType.CANCEL);
  138. if (!this._seat.get_keyboard() && !this._seat.get_pointer())
  139. return this.response(ResponseType.CANCEL);
  140. this.grab_add();
  141. }
  142. _ungrab() {
  143. this._seat.ungrab();
  144. this.grab_remove();
  145. }
  146. // Override to use our own ungrab process
  147. response(response_id) {
  148. this.hide();
  149. this._ungrab();
  150. return super.response(response_id);
  151. }
  152. // Override with a non-blocking version of Gtk.Dialog.run()
  153. run() {
  154. this.show();
  155. // Wait a bit before attempting grab
  156. GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
  157. this._grab();
  158. return GLib.SOURCE_REMOVE;
  159. });
  160. }
  161. });
  162. /**
  163. * Check the availability of an accelerator using GNOME Shell's DBus interface.
  164. *
  165. * @param {string} accelerator - An accelerator
  166. * @param {number} [modeFlags] - Mode Flags
  167. * @param {number} [grabFlags] - Grab Flags
  168. * @param {boolean} %true if available, %false on error or unavailable
  169. */
  170. export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) {
  171. try {
  172. let result = false;
  173. // Try to grab the accelerator
  174. const action = await new Promise((resolve, reject) => {
  175. Gio.DBus.session.call(
  176. 'org.gnome.Shell',
  177. '/org/gnome/Shell',
  178. 'org.gnome.Shell',
  179. 'GrabAccelerator',
  180. new GLib.Variant('(suu)', [accelerator, modeFlags, grabFlags]),
  181. null,
  182. Gio.DBusCallFlags.NONE,
  183. -1,
  184. null,
  185. (connection, res) => {
  186. try {
  187. res = connection.call_finish(res);
  188. resolve(res.deepUnpack()[0]);
  189. } catch (e) {
  190. reject(e);
  191. }
  192. }
  193. );
  194. });
  195. // If successful, use the result of ungrabbing as our return
  196. if (action !== 0) {
  197. result = await new Promise((resolve, reject) => {
  198. Gio.DBus.session.call(
  199. 'org.gnome.Shell',
  200. '/org/gnome/Shell',
  201. 'org.gnome.Shell',
  202. 'UngrabAccelerator',
  203. new GLib.Variant('(u)', [action]),
  204. null,
  205. Gio.DBusCallFlags.NONE,
  206. -1,
  207. null,
  208. (connection, res) => {
  209. try {
  210. res = connection.call_finish(res);
  211. resolve(res.deepUnpack()[0]);
  212. } catch (e) {
  213. reject(e);
  214. }
  215. }
  216. );
  217. });
  218. }
  219. return result;
  220. } catch (e) {
  221. logError(e);
  222. return false;
  223. }
  224. }
  225. /**
  226. * Show a dialog to get a keyboard shortcut from a user.
  227. *
  228. * @param {string} summary - A description of the keybinding's function
  229. * @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel
  230. * @return {string} An accelerator or %null if it should be unset.
  231. */
  232. export async function getAccelerator(summary, accelerator = null) {
  233. try {
  234. const dialog = new ShortcutChooserDialog({
  235. summary: summary,
  236. accelerator: accelerator,
  237. });
  238. accelerator = await new Promise((resolve, reject) => {
  239. dialog.connect('response', (dialog, response) => {
  240. switch (response) {
  241. case ResponseType.SET:
  242. accelerator = dialog.accelerator;
  243. break;
  244. case ResponseType.UNSET:
  245. accelerator = null;
  246. break;
  247. case ResponseType.CANCEL:
  248. // leave the accelerator as passed in
  249. break;
  250. }
  251. dialog.destroy();
  252. resolve(accelerator);
  253. });
  254. dialog.run();
  255. });
  256. return accelerator;
  257. } catch (e) {
  258. logError(e);
  259. return accelerator;
  260. }
  261. }