dot/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js

370 lines
11 KiB
JavaScript

'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;
/**
* 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: 'ShortcutChooserDialog'
}, class ShortcutChooserDialog extends Gtk.Dialog {
_init(params) {
super._init({
transient_for: Gio.Application.get_default().get_active_window(),
use_header_bar: true,
modal: true,
// TRANSLATORS: Title of keyboard shortcut dialog
title: _('Set Shortcut')
});
this.seat = Gdk.Display.get_default().get_default_seat();
// Content
let content = this.get_content_area();
content.spacing = 18;
content.margin = 12;
// Action Buttons
this.cancel_button = this.add_button(_('Cancel'), ResponseType.CANCEL);
this.cancel_button.visible = false;
// TRANSLATORS: Button to confirm the new shortcut
this.set_button = this.add_button(_('Set'), ResponseType.SET);
this.set_button.visible = false;
this.set_default_response(ResponseType.SET);
let summaryLabel = new Gtk.Label({
// TRANSLATORS: Summary of a keyboard shortcut function
// Example: Enter a new shortcut to change Messaging
label: _('Enter a new shortcut to change <b>%s</b>').format(
params.summary
),
use_markup: true,
visible: true
});
content.add(summaryLabel);
this.stack = new Gtk.Stack({
transition_type: Gtk.StackTransitionType.CROSSFADE,
visible: true
});
content.add(this.stack);
// Edit page
let editPage = new Gtk.Grid({
row_spacing: 18,
visible: true
});
this.stack.add_named(editPage, 'edit');
let editImage = new Gtk.Image({
resource: '/org/gnome/Shell/Extensions/GSConnect/images/enter-keyboard-shortcut.svg',
visible: true
});
editPage.attach(editImage, 0, 0, 1, 1);
let editLabel = new Gtk.Label({
// TRANSLATORS: Keys for cancelling (␛) or resetting (␈) a shortcut
label: _('Press Esc to cancel or Backspace to reset the keyboard shortcut.'),
visible: true
});
editLabel.get_style_context().add_class('dim-label');
editPage.attach(editLabel, 0, 1, 1, 1);
// Confirm page
let confirmPage = new Gtk.Grid({
row_spacing: 18,
visible: true
});
this.stack.add_named(confirmPage, 'confirm');
this.shortcut_label = new Gtk.ShortcutLabel({
accelerator: params.accelerator,
hexpand: true,
halign: Gtk.Align.CENTER,
visible: true
});
confirmPage.attach(this.shortcut_label, 0, 0, 1, 1);
this.conflict_label = new Gtk.Label({
hexpand: true,
halign: Gtk.Align.CENTER
});
confirmPage.attach(this.conflict_label, 0, 1, 1, 1);
}
get accelerator() {
return this.shortcut_label.accelerator;
}
set accelerator(value) {
this.shortcut_label.accelerator = 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: Remove modifier keys
let mods = [
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
];
if (mods.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 {
let available = await check_accelerator(this.accelerator);
this.set_button.visible = available;
this.conflict_label.visible = !available;
} catch (e) {
logError(e);
this.response(ResponseType.CANCEL);
}
}
_grab() {
let seat = Gdk.Display.get_default().get_default_seat();
let success = 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);
}
let device = seat.get_keyboard() || seat.get_pointer();
if (!device) {
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} - An accelerator
* @param {number} - Mode Flags
* @param {number} - Grab Flags
* @param {boolean} - %true if available, %false on error or unavailable
*/
async function check_accelerator(accelerator, modeFlags = 0, grabFlags = 0) {
try {
let result = false;
// Try to grab the accelerator
let 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) {
debug (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 get_accelerator(summary, accelerator = null) {
try {
let 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;
}
}