From 49f0af4ae19e5406493856f1ed43a2d126b87fa7 Mon Sep 17 00:00:00 2001 From: upstream Date: Mon, 12 Oct 2026 19:30:19 +0200 Subject: [PATCH] Upstream linuxmint/cinnamon state of the touched files before c1610f1a6c --- js/ui/keybindings.js | 489 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 js/ui/keybindings.js diff --git a/js/ui/keybindings.js b/js/ui/keybindings.js new file mode 100644 index 0000000..c4f9f6b --- /dev/null +++ b/js/ui/keybindings.js @@ -0,0 +1,489 @@ +const Signals = imports.signals; +const Main = imports.ui.main; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Util = imports.misc.util; +const Meta = imports.gi.Meta; +const Cinnamon = imports.gi.Cinnamon; +const AppletManager = imports.ui.appletManager; +const DeskletManager = imports.ui.deskletManager; + +const MK = imports.gi.CDesktopEnums.MediaKeyType; +const CinnamonDesktop = imports.gi.CinnamonDesktop; + +const CUSTOM_KEYS_PARENT_SCHEMA = "org.cinnamon.desktop.keybindings"; +const CUSTOM_KEYS_BASENAME = "/org/cinnamon/desktop/keybindings/custom-keybindings"; +const CUSTOM_KEYS_SCHEMA = "org.cinnamon.desktop.keybindings.custom-keybinding"; + +const MEDIA_KEYS_SCHEMA = "org.cinnamon.desktop.keybindings.media-keys"; + +const MEDIA_KEYS = [ + // Volume/audio + { key: MK.MUTE, mode: Cinnamon.ActionMode.ALL }, + { key: MK.MUTE_QUIET, mode: Cinnamon.ActionMode.ALL }, + { key: MK.VOLUME_UP, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.VOLUME_UP_QUIET, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.VOLUME_DOWN, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.VOLUME_DOWN_QUIET, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.MIC_MUTE, mode: Cinnamon.ActionMode.ALL }, + + // Disc/media + { key: MK.EJECT, mode: Cinnamon.ActionMode.ALL }, + { key: MK.MEDIA, mode: Cinnamon.ActionMode.ALL }, + + // Playback + { key: MK.PLAY, mode: Cinnamon.ActionMode.ALL }, + { key: MK.PAUSE, mode: Cinnamon.ActionMode.ALL }, + { key: MK.STOP, mode: Cinnamon.ActionMode.ALL }, + { key: MK.PREVIOUS, mode: Cinnamon.ActionMode.ALL }, + { key: MK.NEXT, mode: Cinnamon.ActionMode.ALL }, + { key: MK.REWIND, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.FORWARD, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.REPEAT, mode: Cinnamon.ActionMode.ALL }, + { key: MK.RANDOM, mode: Cinnamon.ActionMode.ALL }, + + // Screenshots + { key: MK.SCREENSHOT, mode: Cinnamon.ActionMode.ALL }, + { key: MK.SCREENSHOT_CLIP, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.WINDOW_SCREENSHOT, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.WINDOW_SCREENSHOT_CLIP, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.AREA_SCREENSHOT, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.AREA_SCREENSHOT_CLIP, mode: Cinnamon.ActionMode.NORMAL }, + + // Touchpad + { key: MK.TOUCHPAD, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.TOUCHPAD_ON, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.TOUCHPAD_OFF, mode: Cinnamon.ActionMode.NORMAL }, + + // Session + { key: MK.LOGOUT, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.SHUTDOWN, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.SUSPEND, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.HIBERNATE, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.SCREENSAVER, mode: Cinnamon.ActionMode.NORMAL }, + + // App launchers + { key: MK.HOME, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.CALCULATOR, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.SEARCH, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.EMAIL, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.HELP, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.TERMINAL, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.WWW, mode: Cinnamon.ActionMode.NORMAL }, + + // Display + { key: MK.ROTATE_VIDEO_LOCK, mode: Cinnamon.ActionMode.ALL }, + + // Accessibility + { key: MK.SCREENREADER, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.ON_SCREEN_KEYBOARD, mode: Cinnamon.ActionMode.NORMAL | + Cinnamon.ActionMode.POPUP | + Cinnamon.ActionMode.UNLOCK_SCREEN | + Cinnamon.ActionMode.SYSTEM_MODAL }, + { key: MK.INCREASE_TEXT, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.DECREASE_TEXT, mode: Cinnamon.ActionMode.NORMAL }, + { key: MK.TOGGLE_CONTRAST, mode: Cinnamon.ActionMode.NORMAL }, + + // Brightness + { key: MK.SCREEN_BRIGHTNESS_UP, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.SCREEN_BRIGHTNESS_DOWN, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.KEYBOARD_BRIGHTNESS_UP, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.KEYBOARD_BRIGHTNESS_DOWN, mode: Cinnamon.ActionMode.ALL, repeatable: true }, + { key: MK.KEYBOARD_BRIGHTNESS_TOGGLE, mode: Cinnamon.ActionMode.ALL }, + + // Battery + { key: MK.BATTERY, mode: Cinnamon.ActionMode.NORMAL }, +]; + +const iface = "\ + \ + \ + \ + \ + \ + \ + \ + "; + +const proxy = Gio.DBusProxy.makeProxyWrapper(iface); + +function KeybindingManager() { + this._init(); +} + +KeybindingManager.prototype = { + _init: function() { + this._proxy = new proxy(Gio.DBus.session, + 'org.cinnamon.SettingsDaemon.KeybindingHandler', + '/org/cinnamon/SettingsDaemon/KeybindingHandler'); + + /* Keep track of bindings so we can a) check if they've change (and avoid the work + * if the haven't), b) handle the callbacks when the keystrokes are captured by + * Main._stageEventHandler. + * + * This dict will contain [name, bindings, callback] and keyed on the id returned by + * add_custom_keybinding. */ + this.bindings = new Map(); + this.applet_bindings = new Map(); + this.kb_schema = Gio.Settings.new(CUSTOM_KEYS_PARENT_SCHEMA); + this.setup_custom_keybindings(); + this.kb_schema.connect("changed::custom-list", Lang.bind(this, this.on_customs_changed)); + + this.media_key_settings = new Gio.Settings({ schema_id: MEDIA_KEYS_SCHEMA }); + this.media_key_settings.connect("changed", Lang.bind(this, this.setup_media_keys)); + + this.setup_media_keys(); + }, + + on_customs_changed: function(settings, key) { + this.remove_custom_keybindings(); + this.setup_custom_keybindings(); + }, + + addHotKey: function(name, bindings_string, callback, flags, allowedModes) { + if (!bindings_string) + return false; + return this.addHotKeyArray(name, bindings_string.split("::"), callback, flags, allowedModes); + }, + + _makeXletKey: function(xlet, name, binding) { + return `${xlet._uuid}::${name}::${binding}`; + }, + + _uuidFromXletKey: function(xlet_key) { + return xlet_key.split("::")[0]; + }, + + /* Menu applet example + * + * uuid: menu@cinnamon.org + * binding name: overlay-key + * instances: + * 49: super-l, super-r + * 52: super-l, ctrl-shift-f7 + * + * is in applet_bindings as: + * + * { + * "menu@cinnamon.org::overlay-key:super-l" : { + * "49": callback49, + * "52": callback52 + * }, + * "menu@cinnamon.org::overlay-key:super-r" : { + * "49": callback49 + * }, + * "menu@cinnamon.org::overlay-key:ctrl-shift-f7" : { + * "52": callback52 + * } + * } + */ + + addXletHotKey: function(xlet, name, bindings_string, callback, flags, allowedModes) { + this._removeMatchingXletBindings(xlet, name); + + if (!bindings_string) + return false; + + let xlet_set = null; + const instanceId = xlet.instance_id || 0; // extensions == undefined + const binding_array = bindings_string.split("::"); + + for (const binding of binding_array) { + const xlet_key = this._makeXletKey(xlet, name, binding); + xlet_set = this.applet_bindings.get(xlet_key); + + if (xlet_set === undefined) { + xlet_set = new Map([ + ["commitTimeoutId", 0] + ]); + this.applet_bindings.set(xlet_key, xlet_set); + } + + xlet_set.set(instanceId, callback); + + this._queueCommitXletHotKey(xlet_key, binding, xlet_set, flags, allowedModes); + } + }, + + _removeMatchingXletBindings: function(xlet, name) { + // This sucks, but since the individual binding string is part of the name + // name we send to muffin, we can't just call display.remove_keybinding(name), + // and need to iterate thru the list finding our matching uuid and instance ids. + const key_prefix = `${xlet._uuid}::${name}::`; + const instanceId = xlet.instance_id || 0; + const iter = this.applet_bindings.keys(); + + for (const xlet_key of iter) { + if (xlet_key.startsWith(key_prefix)) { + const xlet_set = this.applet_bindings.get(xlet_key); + if (xlet_set.has(instanceId)) { + xlet_set.delete(instanceId); + if (xlet_set.size === 1) { // only commitTimeoutId left + this.applet_bindings.delete(xlet_key); + this.removeHotKey(xlet_key); + } + } + } + } + }, + + _xletCallback: function(lookup_key, display, window, kb, action) { + const xlet_set = this.applet_bindings.get(lookup_key); + if (!xlet_set) { + return; + } + + /* This should catch extensions also. The minimum size is 2 - 1 will be a + * binding, plus the commitTimeoutId. */ + if (xlet_set.size === 2) { + const iter = xlet_set.keys(); + for (const instanceId of iter) { + if (instanceId === "commitTimeoutId") { + continue; + } + const callback = xlet_set.get(instanceId); + callback(display, window, kb, action); + break; + } + return; + } + + const iter = xlet_set.keys(); + const uuid = this._uuidFromXletKey(lookup_key); + const currentMonitor = Main.layoutManager.currentMonitor.index; + + let xlet = null + let xletMonitor = 0; + let primary_callback = null; + let current_callback = null; + + for (instanceId of iter) { + current_callback = xlet_set.get(instanceId); + + xlet = AppletManager.get_object_for_uuid(uuid, instanceId); + + if (!xlet) { + xlet = DeskletManager.get_object_for_uuid(uuid, instanceId); + } + + if (xlet) { + const actor = xlet.actor; + if (actor) { + xletMonitor = Main.layoutManager.findMonitorIndexForActor(actor); + + if (xletMonitor === Main.layoutManager.primaryMonitor.index) { + primary_callback = current_callback; + } + if (xletMonitor == currentMonitor) { + current_callback(display, window, kb, action); + return; + } + } + } + } + + // No match... more monitors than instances? Prefer the primary monitor's if we encountered it. + if (primary_callback) { + primary_callback(display, window, kb, action); + } else { + // Fallback to the last one we looked at otherwise. + current_callback(display, window, kb, action); + } + }, + + removeXletHotKey: function(xlet, name) { + this._removeMatchingXletBindings(xlet, name); + }, + + _queueCommitXletHotKey: function(xlet_key, binding, xlet_set, flags, allowedModes) { + let id = xlet_set.get("commitTimeoutId") ?? 0; + + if (id > 0) { + GLib.source_remove(id); + } + + id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + this.addHotKeyArray(xlet_key, [binding], this._xletCallback.bind(this, xlet_key), flags, allowedModes); + xlet_set.set("commitTimeoutId", 0); + return GLib.SOURCE_REMOVE; + }); + + xlet_set.set("commitTimeoutId", id); + }, + + _lookupEntry: function(name) { + let found = 0; + for (const action_id of this.bindings.keys()) { + let entry = this.bindings.get(action_id); + if (entry !== undefined && entry.name === name) { + return [action_id, entry]; + } + } + + return [Meta.KeyBindingAction.NONE, undefined]; + }, + + getBindingById: function(action_id) { + return this.bindings.get(action_id); + }, + + addHotKeyArray: function(name, bindings, callback, + flags=Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + allowedModes=Cinnamon.ActionMode.NORMAL | + Cinnamon.ActionMode.POPUP) { + let [existing_action_id, entry] = this._lookupEntry(name); + + if (entry !== undefined) { + if (entry.bindings.toString() === bindings.toString()) { + return true; + } + global.display.remove_keybinding(name); + this.bindings.delete(existing_action_id); + } + + if (!bindings) { + global.logError("Missing bindings array for keybinding: " + name); + return false; + } + + let empty = true; + for (let i = 0; empty && (i < bindings.length); i++) { + empty = bindings[i].toString().trim() == ""; + } + + if (empty) { + return true; + } + + action_id = global.display.add_custom_keybinding_full(name, bindings, flags, callback); + // log(`set keybinding: ${name}, bindings: ${bindings}, flags: ${flags}, allowedModes: ${allowedModes} - action id: ${action_id}`); + + if (action_id === Meta.KeyBindingAction.NONE) { + global.logError("Warning, unable to bind hotkey with name '" + name + "'. The selected keybinding could already be in use."); + return false; + } + this.bindings.set(action_id, { + "name" : name, + "bindings" : bindings, + "callback" : callback, + "allowedModes": allowedModes + }); + + return true; + }, + + removeHotKey: function(name) { + let [action_id, entry] = this._lookupEntry(name); + + if (entry === undefined) { + return; + } + + global.display.remove_keybinding(name); + this.bindings.delete(action_id); + }, + + setup_custom_keybindings: function() { + let list = this.kb_schema.get_strv("custom-list"); + + for (let i = 0; i < list.length; i++) { + if (list[i] === "__dummy__") { + continue; + } + + let custom_path = CUSTOM_KEYS_BASENAME + "/" + list[i] + "/"; + let schema = Gio.Settings.new_with_path(CUSTOM_KEYS_SCHEMA, custom_path); + let command = schema.get_string("command"); + let binding = schema.get_strv("binding"); + let name = list[i]; + this.addHotKeyArray(name, binding, Lang.bind(this, function() { + Util.spawnCommandLine(command); + })) + } + }, + + remove_custom_keybindings: function() { + for (const action_id of this.bindings.keys()) { + name = this.bindings.get(action_id).name; + if (name && name.indexOf("custom") > -1) { + global.display.remove_keybinding(name); + this.bindings.delete(action_id); + } + } + }, + + setup_media_keys: function() { + for (const mk of MEDIA_KEYS) { + let flags = mk.repeatable + ? Meta.KeyBindingFlags.NONE + : Meta.KeyBindingFlags.IGNORE_AUTOREPEAT; + + let bindings = this.media_key_settings.get_strv( + CinnamonDesktop.desktop_get_media_key_string(mk.key) + ); + + this.addHotKeyArray( + "media-keys-" + mk.key.toString(), + bindings, + Lang.bind(this, this.on_media_key_pressed, mk.key), + flags, + mk.mode + ); + } + return true; + }, + + on_media_key_pressed: function(display, window, kb, action) { + let [, entry] = this._lookupEntry("media-keys-" + action.toString()); + if (Main._shouldFilterKeybinding(entry)) + return; + + // The screensaver key always goes through the controller - it resolves + // custom-screensaver-command, internal shield, and external daemon modes. + if (action === MK.SCREENSAVER) { + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + Main.screensaverController.lockScreen(false); + return GLib.SOURCE_REMOVE; + }); + return; + } + + this._proxy.HandleKeybindingRemote(action); + }, + + _onBuiltinKeyPressed: function(display, window, binding, actionId) { + let entry = this.bindings.get(actionId); + + if (entry === undefined || Main._shouldFilterKeybinding(entry)) + return; + + entry.callback(display, window, binding); + }, + + setBuiltinHandler: function(name, actionId, callback, allowedModes=Cinnamon.ActionMode.NORMAL) { + Meta.keybindings_set_custom_handler(name, + (display, window, binding) => this._onBuiltinKeyPressed(display, window, binding, actionId) + ); + + this.bindings.set(actionId, { + name: name, + bindings: [], + callback: callback, + allowedModes: allowedModes + }); + }, + + invoke_keybinding_action_by_id: function(id, mask=0) { + const entry = this.bindings.get(id); + if (entry !== undefined) { + entry.callback(global.display, global.display.get_focus_window(), { + get_name: () => entry.name, + get_mask: () => mask, + is_reversed: () => entry.name.endsWith('-backward') + }); + } + } +}; +Signals.addSignalMethods(KeybindingManager.prototype); +