315 lines
9.0 KiB
JavaScript
315 lines
9.0 KiB
JavaScript
|
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||
|
//
|
||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
||
|
import Gdk from 'gi://Gdk';
|
||
|
import Gio from 'gi://Gio';
|
||
|
import GLib from 'gi://GLib';
|
||
|
import GObject from 'gi://GObject';
|
||
|
import Gtk from '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
|
||
|
*/
|
||
|
export const ResponseType = {
|
||
|
CANCEL: Gtk.ResponseType.CANCEL,
|
||
|
SET: Gtk.ResponseType.APPLY,
|
||
|
UNSET: 2,
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* A simplified version of the shortcut editor from GNOME Control Center
|
||
|
*/
|
||
|
export const 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
|
||
|
*/
|
||
|
export 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.
|
||
|
*/
|
||
|
export 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;
|
||
|
}
|
||
|
}
|
||
|
|