mirror of
https://github.com/edu4rdshl/gnome-shell.git
synced 2026-07-18 07:34:54 +00:00
dbus: Add internal API suitable for GlobalShortcuts
https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/issues/47#note_2089569 This adds an interface in org.gnome.Shell that provides additional functionality for managing key grabs, suitable for the GlobalShortcuts portal.
This commit is contained in:
parent
dc6f7a1289
commit
681b846fb2
2 changed files with 355 additions and 14 deletions
|
|
@ -17,9 +17,82 @@
|
|||
<arg type="s" direction="in" name="id"/>
|
||||
</method>
|
||||
<method name="ShowApplications"/>
|
||||
|
||||
<!-- START SHORTCUTS
|
||||
Designed to handle shortcuts for the globalshortcuts portal.
|
||||
`group_id` identifies a group of shortcuts meant to be modified together.
|
||||
-->
|
||||
|
||||
<!-- (Re)binds shortcuts according to spec.
|
||||
`shortcuts` is made of pairs:
|
||||
(id: s, {'shortcuts'=Variant(as)})
|
||||
for example:
|
||||
('sstop', {'description': 'Stop the show', 'shortcuts': <['<alt>C', '<ctrl>Q']>})
|
||||
|
||||
`results['shortcuts']` is the list of shortcuts now in effect:
|
||||
(id: s, {'trigger_description'=Variant(s)})
|
||||
for example:
|
||||
('sstop', {'description': 'Stop the show', 'trigger_description': <['hold alt and then C', 'the Q key while holding Control']>})
|
||||
'trigger_description's are free-form, human-readable text.
|
||||
|
||||
`changed` is nonzero if registered shortcuts changed because of this call, zero otherwise.
|
||||
|
||||
The 'description' key is optional.
|
||||
|
||||
FIXME: trigger release events when a combo is being pressed while unbinding?
|
||||
-->
|
||||
<method name="BindShortcuts">
|
||||
<arg type="s" name="group_id" direction="in"/>
|
||||
<arg type="a(sa{sv})" name="shortcuts" direction="in"/>
|
||||
<arg type="a{sv}" name="results" direction="out"/>
|
||||
<arg type="u" name="changed" direction="out"/>
|
||||
</method>
|
||||
|
||||
<!-- Unbinds shortcuts in the selected group.
|
||||
Equivalent to calling BindShortcuts with an empty list.
|
||||
-->
|
||||
<method name="UnbindShortcuts">
|
||||
<arg type="s" name="group_id" direction="in"/>
|
||||
</method>
|
||||
|
||||
<!-- Lists all shortcuts of the group, together with their triggers.
|
||||
If the group never bound shortcuts, this returns an error.
|
||||
|
||||
`results['shortcuts']` is a list of pairs:
|
||||
(id: s, {'description'=Variant(s), 'trigger_description'=Variant(as))})
|
||||
for example:
|
||||
('sstop', {'description': 'Stop the show', 'trigger_description': <['hold alt and then C', 'the Q key while holding Control']>})
|
||||
|
||||
The 'description' key is optional.
|
||||
-->
|
||||
<method name="ListShortcuts">
|
||||
<arg type="s" name="group_id" direction="in"/>
|
||||
<arg type="a{sv}" name="results" direction="out"/>
|
||||
</method>
|
||||
|
||||
<!-- Sent whenever a registered combo becomes pressed.
|
||||
Sent back to the client that registered it. -->
|
||||
<signal name="Activated">
|
||||
<arg type="s" name="group_id" direction="out"/>
|
||||
<arg type="s" name="shortcut_id" direction="out"/>
|
||||
<arg type="t" name="timestamp" direction="out"/>
|
||||
</signal>
|
||||
|
||||
<!-- Sent whenever a registered combo stops being pressed.
|
||||
Sent back to the client that registered it. -->
|
||||
<signal name="Deactivated">
|
||||
<arg type="s" name="group_id" direction="out"/>
|
||||
<arg type="s" name="shortcut_id" direction="out"/>
|
||||
<arg type="t" name="timestamp" direction="out"/>
|
||||
</signal>
|
||||
|
||||
<!-- END SHORTCUTS -->
|
||||
|
||||
<method name="GrabAccelerator">
|
||||
<arg type="s" direction="in" name="accelerator"/>
|
||||
<!-- Shell.ActionMode -->
|
||||
<arg type="u" direction="in" name="modeFlags"/>
|
||||
<!-- Meta.KeyBindingFlags -->
|
||||
<arg type="u" direction="in" name="grabFlags"/>
|
||||
<arg type="u" direction="out" name="action"/>
|
||||
</method>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ import {ControlsState} from './overviewControls.js';
|
|||
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell');
|
||||
const ScreenSaverIface = loadInterfaceXML('org.gnome.ScreenSaver');
|
||||
|
||||
|
||||
function dbg_(v) {
|
||||
console.error(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
const eqSet = (xs, ys) =>
|
||||
xs.size === ys.size &&
|
||||
[...xs].every(x => ys.has(x));
|
||||
|
||||
|
||||
export class GnomeShell {
|
||||
constructor() {
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellIface, this);
|
||||
|
|
@ -34,10 +46,24 @@ export class GnomeShell {
|
|||
this._grabbedAccelerators = new Map();
|
||||
this._grabbers = new Map();
|
||||
|
||||
/* session: string → [
|
||||
* sender: string,
|
||||
* [(
|
||||
* name: string,
|
||||
* shortcuts: [trigger: string, action: int],
|
||||
* )]
|
||||
* ]
|
||||
*/
|
||||
this._accelerators = new Map();
|
||||
|
||||
global.display.connect('accelerator-activated',
|
||||
(display, action, device, timestamp) => {
|
||||
this._emitAcceleratorActivated(action, device, timestamp);
|
||||
});
|
||||
global.display.connect('accelerator-deactivated',
|
||||
(display, action, device, timestamp) => {
|
||||
this._emitAcceleratorDeactivated(action, device, timestamp);
|
||||
});
|
||||
|
||||
this._cachedOverviewVisible = false;
|
||||
Main.overview.connect('showing',
|
||||
|
|
@ -194,6 +220,66 @@ export class GnomeShell {
|
|||
invocation.return_value(null);
|
||||
}
|
||||
|
||||
async BindShortcutsAsync(params, invocation) {
|
||||
try {
|
||||
await this._senderChecker.checkInvocation(invocation);
|
||||
} catch (e) {
|
||||
invocation.return_gerror(e);
|
||||
return;
|
||||
}
|
||||
|
||||
let [groupId, shortcuts] = params;
|
||||
|
||||
let [remainingShortcuts, changed] = this._bindShortcuts(groupId, shortcuts, invocation.get_sender());
|
||||
|
||||
invocation.return_value(GLib.Variant.new(
|
||||
'(a{sv}u)',
|
||||
[
|
||||
{'shortcuts': GLib.Variant.new('a(sa{sv})', remainingShortcuts)},
|
||||
changed,
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
async UnbindShortcutsAsync(params, invocation) {
|
||||
try {
|
||||
await this._senderChecker.checkInvocation(invocation);
|
||||
} catch (e) {
|
||||
invocation.return_gerror(e);
|
||||
return;
|
||||
}
|
||||
let [groupId] = params;
|
||||
|
||||
this._bindShortcuts(groupId, [], invocation.get_sender());
|
||||
|
||||
invocation.return_value(null);
|
||||
}
|
||||
|
||||
|
||||
async ListShortcutsAsync(params, invocation) {
|
||||
try {
|
||||
await this._senderChecker.checkInvocation(invocation);
|
||||
} catch (e) {
|
||||
invocation.return_gerror(e);
|
||||
return;
|
||||
}
|
||||
|
||||
let [groupId] = params;
|
||||
if (this._accelerators.has(groupId)) {
|
||||
invocation.return_value(GLib.Variant.new(
|
||||
'(a{sv})',
|
||||
[{'shortcuts': GLib.Variant.new('a(sa{sv})', this._listShortcuts(groupId))}]
|
||||
));
|
||||
} else {
|
||||
invocation.return_error_literal(
|
||||
Gio.DBusError,
|
||||
Gio.DBusError.INVALID_ARGS,
|
||||
'No such group'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async GrabAcceleratorAsync(params, invocation) {
|
||||
try {
|
||||
await this._senderChecker.checkInvocation(invocation);
|
||||
|
|
@ -272,28 +358,206 @@ export class GnomeShell {
|
|||
invocation.return_value(null);
|
||||
}
|
||||
|
||||
_emitAcceleratorActivated(action, device, timestamp) {
|
||||
_bindShortcuts(groupId, shortcuts, sender) {
|
||||
let cleanedShortcuts = shortcuts.map(([name, info]) => [
|
||||
name,
|
||||
info['shortcuts'] ? info['shortcuts'].unpack() : [],
|
||||
info['description'] ? info['description'].unpack() : undefined,
|
||||
]);
|
||||
|
||||
let existing = [];
|
||||
|
||||
if (this._accelerators.has(groupId)) {
|
||||
let [sender_, ex] = this._accelerators.get(groupId);
|
||||
existing = ex;
|
||||
}
|
||||
|
||||
// MDN says the last key is the one preserved in the final Map.
|
||||
let descriptions = new Map([
|
||||
...existing.map(([name, _, desc]) => [name, desc]),
|
||||
...cleanedShortcuts.map(([name, _, desc]) => [name, desc]),
|
||||
]);
|
||||
|
||||
let requested = new Map(cleanedShortcuts);
|
||||
// Shortcuts to (re)bind.
|
||||
let changed = [];
|
||||
// The following 2 mark things to unbind.
|
||||
// Names may get immediately be bound again.
|
||||
let removedNames = [];
|
||||
|
||||
for (let [name, actions, desc_] of existing) {
|
||||
if (requested.has(name)) {
|
||||
let reqTriggers = requested.get(name);
|
||||
let triggers = actions.map(([trigger, _]) => trigger);
|
||||
const unpack = arr => arr.map(t => t.unpack());
|
||||
if (!eqSet(new Set(unpack(reqTriggers)), new Set(unpack(triggers)))) {
|
||||
changed.push([name, reqTriggers]);
|
||||
removedNames.push(name);
|
||||
}
|
||||
} else {
|
||||
removedNames.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
let present = new Map(existing);
|
||||
for (let [name, triggers, desc_] of cleanedShortcuts) {
|
||||
if (!present.has(name))
|
||||
changed.push([name, triggers]);
|
||||
}
|
||||
|
||||
let addedShortcuts = changed.map(([name, triggers]) => [name, triggers, descriptions.get(name)]);
|
||||
|
||||
// FIXME: is this a race condition? Can input events be processed between these calls?
|
||||
if (removedNames.length > 0)
|
||||
this._unbindShortcuts(groupId, removedNames);
|
||||
|
||||
this._bindShortcutsForSender(groupId, addedShortcuts, sender);
|
||||
|
||||
let kept = new Map(changed);
|
||||
let removedIds = removedNames.filter(name => !kept.has(name));
|
||||
|
||||
let remainingShortcuts = this._listShortcuts(groupId);
|
||||
if (remainingShortcuts.length === 0)
|
||||
this._accelerators.delete(groupId);
|
||||
return [remainingShortcuts, 0 + (changed.length + removedIds.length > 0)];
|
||||
}
|
||||
|
||||
// Each shortcut is being bound separately and as a whole.
|
||||
// If there's an existing bound shortcut with the same name,
|
||||
// the new one will not take effect.
|
||||
_bindShortcutsForSender(groupId, shortcuts, sender) {
|
||||
let [sender_, existing] = this._accelerators.get(groupId) || [null, []];
|
||||
|
||||
let existingNames = new Set(existing.map(([name, _actions, _desc]) => name));
|
||||
|
||||
let addedShortcuts = [];
|
||||
for (let [name, triggers, description] of shortcuts) {
|
||||
if (existingNames.has(name)) {
|
||||
console.error('Tried to bind over an existing shortcut', name, 'in group', groupId);
|
||||
continue;
|
||||
}
|
||||
let actions = [];
|
||||
for (let trigger of triggers) {
|
||||
let bindingAction = this._grabAcceleratorForSender(
|
||||
trigger.unpack(),
|
||||
Shell.ActionMode.ALL,
|
||||
Meta.KeyBindingFlags.TRIGGER_RELEASE,
|
||||
sender
|
||||
);
|
||||
actions.push([trigger, bindingAction]);
|
||||
}
|
||||
addedShortcuts.push([name, actions, description]);
|
||||
}
|
||||
|
||||
this._accelerators.set(groupId, [sender, existing.concat(addedShortcuts)]);
|
||||
}
|
||||
|
||||
_listShortcuts(groupId) {
|
||||
let [sender_, rawShortcuts] = this._accelerators.get(groupId);
|
||||
|
||||
function shortcutToDbus(name, triggers, description) {
|
||||
let info = {'trigger_description': GLib.Variant.new('as', triggers.map(t => t.unpack()))};
|
||||
if (description !== undefined)
|
||||
info.description = GLib.Variant.new('s', description);
|
||||
|
||||
return [name, info];
|
||||
}
|
||||
|
||||
return rawShortcuts.map(
|
||||
([name, shortcuts, description]) => shortcutToDbus(
|
||||
name,
|
||||
shortcuts.map(([trigger, _]) => trigger),
|
||||
description
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
_unbindShortcuts(groupId, names) {
|
||||
let namesSet = new Set(names);
|
||||
let [sender, rawShortcuts] = this._accelerators.get(groupId);
|
||||
let ungrabFailed = 0;
|
||||
let newRaw = [];
|
||||
|
||||
for (let [name, shortcuts, desc] of rawShortcuts) {
|
||||
if (namesSet.has(name)) {
|
||||
for (let [trigger_, action] of shortcuts)
|
||||
ungrabFailed += this._ungrabAcceleratorForSender(action, sender) ? 0 : 1;
|
||||
} else {
|
||||
newRaw.push([name, shortcuts, desc]);
|
||||
}
|
||||
}
|
||||
|
||||
if (ungrabFailed)
|
||||
console.error(`Failed to remove ${ungrabFailed} shortcuts for ${groupId}`);
|
||||
|
||||
|
||||
if (newRaw.length > 0)
|
||||
this._accelerators.set(groupId, [sender, newRaw]);
|
||||
else
|
||||
this._accelerators.delete(groupId);
|
||||
|
||||
console.warn(`Some shortcuts were unbound in group ${groupId}`);
|
||||
}
|
||||
|
||||
_emitAcceleratorTriggered(action, device, timestamp, activated) {
|
||||
let destination = this._grabbedAccelerators.get(action);
|
||||
if (!destination)
|
||||
return;
|
||||
|
||||
let connection = this._dbusImpl.get_connection();
|
||||
let info = this._dbusImpl.get_info();
|
||||
let params = {
|
||||
'timestamp': GLib.Variant.new('u', timestamp),
|
||||
'action-mode': GLib.Variant.new('u', Main.actionMode),
|
||||
};
|
||||
let shortcutData;
|
||||
for (let [group, data] of this._accelerators) {
|
||||
let [sender, rawShortcuts] = data;
|
||||
if (sender === destination) {
|
||||
for (let [name, shortcuts] of rawShortcuts) {
|
||||
for (let [_, savedAction] of shortcuts) {
|
||||
if (action === savedAction) {
|
||||
shortcutData = [group, name];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deviceNode = device.get_device_node();
|
||||
if (deviceNode)
|
||||
params['device-node'] = GLib.Variant.new('s', deviceNode);
|
||||
if (shortcutData === undefined) {
|
||||
if (!activated)
|
||||
return;
|
||||
|
||||
connection.emit_signal(
|
||||
destination,
|
||||
this._dbusImpl.get_object_path(),
|
||||
info?.name ?? null,
|
||||
'AcceleratorActivated',
|
||||
GLib.Variant.new('(ua{sv})', [action, params]));
|
||||
let params = {
|
||||
'timestamp': GLib.Variant.new('u', timestamp),
|
||||
'action-mode': GLib.Variant.new('u', Main.actionMode),
|
||||
};
|
||||
|
||||
let deviceNode = device.get_device_node();
|
||||
if (deviceNode)
|
||||
params['device-node'] = GLib.Variant.new('s', deviceNode);
|
||||
|
||||
connection.emit_signal(
|
||||
destination,
|
||||
this._dbusImpl.get_object_path(),
|
||||
info?.name ?? null,
|
||||
'AcceleratorActivated',
|
||||
GLib.Variant.new('(ua{sv})', [action, params]));
|
||||
} else {
|
||||
let [group, name] = shortcutData;
|
||||
|
||||
connection.emit_signal(
|
||||
destination,
|
||||
this._dbusImpl.get_object_path(),
|
||||
info?.name ?? null,
|
||||
activated ? 'Activated' : 'Deactivated',
|
||||
GLib.Variant.new('(sst)', [group, name, timestamp]));
|
||||
}
|
||||
}
|
||||
|
||||
_emitAcceleratorActivated(action, device, timestamp) {
|
||||
return this._emitAcceleratorTriggered(action, device, timestamp, true);
|
||||
}
|
||||
|
||||
_emitAcceleratorDeactivated(action, device, timestamp) {
|
||||
return this._emitAcceleratorTriggered(action, device, timestamp, false);
|
||||
}
|
||||
|
||||
_grabAcceleratorForSender(accelerator, modeFlags, grabFlags, sender) {
|
||||
|
|
@ -337,6 +601,10 @@ export class GnomeShell {
|
|||
if (sender === name)
|
||||
this._ungrabAccelerator(action);
|
||||
}
|
||||
for (let [session, [sender, data_]] of this._accelerators) {
|
||||
if (sender === name)
|
||||
this._accelerators.delete(session);
|
||||
}
|
||||
Gio.bus_unwatch_name(this._grabbers.get(name));
|
||||
this._grabbers.delete(name);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue