123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gdk = imports.gi.Gdk;
- const Gio = imports.gi.Gio;
- const GLib = imports.gi.GLib;
- const GObject = imports.gi.GObject;
- const Gtk = imports.gi.Gtk;
- /*
- * A list of modifier keysyms we ignore
- */
- const _MODIFIERS = [
- Gdk.KEY_Alt_L,
- Gdk.KEY_Alt_R,
- Gdk.KEY_Caps_Lock,
- Gdk.KEY_Control_L,
- Gdk.KEY_Control_R,
- Gdk.KEY_Meta_L,
- Gdk.KEY_Meta_R,
- Gdk.KEY_Num_Lock,
- Gdk.KEY_Shift_L,
- Gdk.KEY_Shift_R,
- Gdk.KEY_Super_L,
- Gdk.KEY_Super_R,
- ];
- /**
- * Response enum for ShortcutChooserDialog
- */
- var ResponseType = {
- CANCEL: Gtk.ResponseType.CANCEL,
- SET: Gtk.ResponseType.APPLY,
- UNSET: 2,
- };
- /**
- * A simplified version of the shortcut editor from GNOME Control Center
- */
- var ShortcutChooserDialog = GObject.registerClass({
- GTypeName: 'GSConnectPreferencesShortcutEditor',
- Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-shortcut-editor.ui',
- Children: [
- 'cancel-button', 'set-button',
- 'stack', 'summary-label',
- 'shortcut-label', 'conflict-label',
- ],
- }, class ShortcutChooserDialog extends Gtk.Dialog {
- _init(params) {
- super._init({
- transient_for: Gio.Application.get_default().get_active_window(),
- use_header_bar: true,
- });
- this._seat = Gdk.Display.get_default().get_default_seat();
- // Current accelerator or %null
- this.accelerator = params.accelerator;
- // TRANSLATORS: Summary of a keyboard shortcut function
- // Example: Enter a new shortcut to change Messaging
- this.summary = _('Enter a new shortcut to change <b>%s</b>').format(
- params.summary
- );
- }
- get accelerator() {
- return this.shortcut_label.accelerator;
- }
- set accelerator(value) {
- this.shortcut_label.accelerator = value;
- }
- get summary() {
- return this.summary_label.label;
- }
- set summary(value) {
- this.summary_label.label = value;
- }
- vfunc_key_press_event(event) {
- let keyvalLower = Gdk.keyval_to_lower(event.keyval);
- let realMask = event.state & Gtk.accelerator_get_default_mod_mask();
- // TODO: Critical: 'WIDGET_REALIZED_FOR_EVENT (widget, event)' failed
- if (_MODIFIERS.includes(keyvalLower))
- return true;
- // Normalize Tab
- if (keyvalLower === Gdk.KEY_ISO_Left_Tab)
- keyvalLower = Gdk.KEY_Tab;
- // Put shift back if it changed the case of the key, not otherwise.
- if (keyvalLower !== event.keyval)
- realMask |= Gdk.ModifierType.SHIFT_MASK;
- // HACK: we don't want to use SysRq as a keybinding (but we do want
- // Alt+Print), so we avoid translation from Alt+Print to SysRq
- if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0)
- keyvalLower = Gdk.KEY_Print;
- // A single Escape press cancels the editing
- if (realMask === 0 && keyvalLower === Gdk.KEY_Escape) {
- this.response(ResponseType.CANCEL);
- return false;
- }
- // Backspace disables the current shortcut
- if (realMask === 0 && keyvalLower === Gdk.KEY_BackSpace) {
- this.response(ResponseType.UNSET);
- return false;
- }
- // CapsLock isn't supported as a keybinding modifier, so keep it from
- // confusing us
- realMask &= ~Gdk.ModifierType.LOCK_MASK;
- if (keyvalLower !== 0 && realMask !== 0) {
- this._ungrab();
- // Set the accelerator property/label
- this.accelerator = Gtk.accelerator_name(keyvalLower, realMask);
- // TRANSLATORS: When a keyboard shortcut is unavailable
- // Example: [Ctrl]+[S] is already being used
- this.conflict_label.label = _('%s is already being used').format(
- Gtk.accelerator_get_label(keyvalLower, realMask)
- );
- // Show Cancel button and switch to confirm/conflict page
- this.cancel_button.visible = true;
- this.stack.visible_child_name = 'confirm';
- this._check();
- }
- return true;
- }
- async _check() {
- try {
- const available = await checkAccelerator(this.accelerator);
- this.set_button.visible = available;
- this.conflict_label.visible = !available;
- } catch (e) {
- logError(e);
- this.response(ResponseType.CANCEL);
- }
- }
- _grab() {
- const success = this._seat.grab(
- this.get_window(),
- Gdk.SeatCapabilities.KEYBOARD,
- true, // owner_events
- null, // cursor
- null, // event
- null
- );
- if (success !== Gdk.GrabStatus.SUCCESS)
- return this.response(ResponseType.CANCEL);
- if (!this._seat.get_keyboard() && !this._seat.get_pointer())
- return this.response(ResponseType.CANCEL);
- this.grab_add();
- }
- _ungrab() {
- this._seat.ungrab();
- this.grab_remove();
- }
- // Override to use our own ungrab process
- response(response_id) {
- this.hide();
- this._ungrab();
- return super.response(response_id);
- }
- // Override with a non-blocking version of Gtk.Dialog.run()
- run() {
- this.show();
- // Wait a bit before attempting grab
- GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
- this._grab();
- return GLib.SOURCE_REMOVE;
- });
- }
- });
- /**
- * Check the availability of an accelerator using GNOME Shell's DBus interface.
- *
- * @param {string} accelerator - An accelerator
- * @param {number} [modeFlags] - Mode Flags
- * @param {number} [grabFlags] - Grab Flags
- * @param {boolean} %true if available, %false on error or unavailable
- */
- async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) {
- try {
- let result = false;
- // Try to grab the accelerator
- const action = await new Promise((resolve, reject) => {
- Gio.DBus.session.call(
- 'org.gnome.Shell',
- '/org/gnome/Shell',
- 'org.gnome.Shell',
- 'GrabAccelerator',
- new GLib.Variant('(suu)', [accelerator, modeFlags, grabFlags]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- (connection, res) => {
- try {
- res = connection.call_finish(res);
- resolve(res.deepUnpack()[0]);
- } catch (e) {
- reject(e);
- }
- }
- );
- });
- // If successful, use the result of ungrabbing as our return
- if (action !== 0) {
- result = await new Promise((resolve, reject) => {
- Gio.DBus.session.call(
- 'org.gnome.Shell',
- '/org/gnome/Shell',
- 'org.gnome.Shell',
- 'UngrabAccelerator',
- new GLib.Variant('(u)', [action]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- (connection, res) => {
- try {
- res = connection.call_finish(res);
- resolve(res.deepUnpack()[0]);
- } catch (e) {
- reject(e);
- }
- }
- );
- });
- }
- return result;
- } catch (e) {
- logError(e);
- return false;
- }
- }
- /**
- * Show a dialog to get a keyboard shortcut from a user.
- *
- * @param {string} summary - A description of the keybinding's function
- * @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel
- * @return {string} An accelerator or %null if it should be unset.
- */
- async function getAccelerator(summary, accelerator = null) {
- try {
- const dialog = new ShortcutChooserDialog({
- summary: summary,
- accelerator: accelerator,
- });
- accelerator = await new Promise((resolve, reject) => {
- dialog.connect('response', (dialog, response) => {
- switch (response) {
- case ResponseType.SET:
- accelerator = dialog.accelerator;
- break;
- case ResponseType.UNSET:
- accelerator = null;
- break;
- case ResponseType.CANCEL:
- // leave the accelerator as passed in
- break;
- }
- dialog.destroy();
- resolve(accelerator);
- });
- dialog.run();
- });
- return accelerator;
- } catch (e) {
- logError(e);
- return accelerator;
- }
- }
|