Skip to content

Commit 40e360d

Browse files
committed
keybindings.js: Add dedicated xlet keybinding methods to improve
multi-instance xlet handling. Multi-instance applets with duplicate shortcuts (which will be their default, at least when first added), will now allow the shortcut to open the instance on the current monitor, if there is one, rather than ending up always being the most-recently-loaded instance. For example, two default menu applets, one per monitor. - By default, the super keys will open whichever instance is on the active monitor. - If you assign a shortcut to one instance that is different from the other, it will open only that instance, regardless of which monitor is active. ref: #13079
1 parent 178cb82 commit 40e360d

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

js/ui/keybindings.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
const Signals = imports.signals;
22
const Main = imports.ui.main;
33
const Gio = imports.gi.Gio;
4+
const GLib = imports.gi.GLib;
45
const Lang = imports.lang;
56
const Util = imports.misc.util;
67
const Meta = imports.gi.Meta;
8+
const AppletManager = imports.ui.appletManager;
9+
const DeskletManager = imports.ui.deskletManager;
710

811
const MK = imports.gi.CDesktopEnums.MediaKeyType;
912
const CinnamonDesktop = imports.gi.CinnamonDesktop;
@@ -52,6 +55,7 @@ KeybindingManager.prototype = {
5255
* This dict will contain [name, bindings, callback] and keyed on the id returned by
5356
* add_custom_keybinding. */
5457
this.bindings = new Map();
58+
this.applet_bindings = new Map();
5559
this.kb_schema = Gio.Settings.new(CUSTOM_KEYS_PARENT_SCHEMA);
5660
this.setup_custom_keybindings();
5761
this.kb_schema.connect("changed::custom-list", Lang.bind(this, this.on_customs_changed));
@@ -72,6 +76,171 @@ KeybindingManager.prototype = {
7276
return this.addHotKeyArray(name, bindings_string.split("::"), callback);
7377
},
7478

79+
_makeXletKey: function(xlet, name, binding) {
80+
return `${xlet._uuid}::${name}::${binding}`;
81+
},
82+
83+
_uuidFromXletKey: function(xlet_key) {
84+
return xlet_key.split("::")[0];
85+
},
86+
87+
/* Menu applet example
88+
*
89+
* uuid: menu@cinnamon.org
90+
* binding name: overlay-key
91+
* instances:
92+
* 49: super-l, super-r
93+
* 52: super-l, ctrl-shift-f7
94+
*
95+
* is in applet_bindings as:
96+
*
97+
* {
98+
* "menu@cinnamon.org::overlay-key:super-l" : {
99+
* "49": callback49,
100+
* "52": callback52
101+
* },
102+
* "menu@cinnamon.org::overlay-key:super-r" : {
103+
* "49": callback49
104+
* },
105+
* "menu@cinnamon.org::overlay-key:ctrl-shift-f7" : {
106+
* "52": callback52
107+
* }
108+
* }
109+
*/
110+
111+
addXletHotKey: function(xlet, name, bindings_string, callback) {
112+
this._removeMatchingXletBindings(xlet, name);
113+
114+
if (!bindings_string)
115+
return false;
116+
117+
let xlet_set = null;
118+
const instanceId = xlet.instance_id || 0; // extensions == undefined
119+
const binding_array = bindings_string.split("::");
120+
121+
for (const binding of binding_array) {
122+
const xlet_key = this._makeXletKey(xlet, name, binding);
123+
xlet_set = this.applet_bindings.get(xlet_key);
124+
125+
if (xlet_set === undefined) {
126+
xlet_set = new Map([
127+
["commitTimeoutId", 0]
128+
]);
129+
this.applet_bindings.set(xlet_key, xlet_set);
130+
}
131+
132+
xlet_set.set(instanceId, callback);
133+
134+
this._queueCommitXletHotKey(xlet_key, binding, xlet_set);
135+
}
136+
},
137+
138+
_removeMatchingXletBindings: function(xlet, name) {
139+
// This sucks, but since the individual binding string is part of the name
140+
// name we send to muffin, we can't just call display.remove_keybinding(name),
141+
// and need to iterate thru the list finding our matching uuid and instance ids.
142+
const key_prefix = `${xlet._uuid}::${name}::`;
143+
const instanceId = xlet.instance_id || 0;
144+
const iter = this.applet_bindings.keys();
145+
146+
for (const xlet_key of iter) {
147+
if (xlet_key.startsWith(key_prefix)) {
148+
const xlet_set = this.applet_bindings.get(xlet_key);
149+
if (xlet_set.has(instanceId)) {
150+
xlet_set.delete(instanceId);
151+
if (xlet_set.size === 1) { // only commitTimeoutId left
152+
this.applet_bindings.delete(xlet_key);
153+
this.removeHotKey(xlet_key);
154+
}
155+
}
156+
}
157+
}
158+
},
159+
160+
_xletCallback: function(lookup_key, display, window, kb, action) {
161+
const xlet_set = this.applet_bindings.get(lookup_key);
162+
if (!xlet_set) {
163+
return;
164+
}
165+
166+
/* This should catch extensions also. The minimum size is 2 - 1 will be a
167+
* binding, plus the commitTimeoutId. */
168+
if (xlet_set.size === 2) {
169+
const iter = xlet_set.keys();
170+
for (const instanceId of iter) {
171+
if (instanceId === "commitTimeoutId") {
172+
continue;
173+
}
174+
const callback = xlet_set.get(instanceId);
175+
callback(display, window, kb, action);
176+
break;
177+
}
178+
return;
179+
}
180+
181+
const iter = xlet_set.keys();
182+
const uuid = this._uuidFromXletKey(lookup_key);
183+
const currentMonitor = Main.layoutManager.currentMonitor.index;
184+
185+
let xlet = null
186+
let xletMonitor = 0;
187+
let primary_callback = null;
188+
let current_callback = null;
189+
190+
for (instanceId of iter) {
191+
current_callback = xlet_set.get(instanceId);
192+
193+
xlet = AppletManager.get_object_for_uuid(uuid, instanceId);
194+
195+
if (!xlet) {
196+
xlet = DeskletManager.get_object_for_uuid(uuid, instanceId);
197+
}
198+
199+
if (xlet) {
200+
const actor = xlet.actor;
201+
if (actor) {
202+
xletMonitor = Main.layoutManager.findMonitorIndexForActor(actor);
203+
204+
if (xletMonitor === Main.layoutManager.primaryMonitor.index) {
205+
primary_callback = current_callback;
206+
}
207+
if (xletMonitor == currentMonitor) {
208+
current_callback(display, window, kb, action);
209+
return;
210+
}
211+
}
212+
}
213+
}
214+
215+
// No match... more monitors than instances? Prefer the primary monitor's if we encountered it.
216+
if (primary_callback) {
217+
primary_callback(display, window, kb, action);
218+
} else {
219+
// Fallback to the last one we looked at otherwise.
220+
current_callback(display, window, kb, action);
221+
}
222+
},
223+
224+
removeXletHotKey: function(xlet, name) {
225+
this._removeMatchingXletBindings(xlet, name);
226+
},
227+
228+
_queueCommitXletHotKey: function(xlet_key, binding, xlet_set) {
229+
let id = xlet_set.get("commitTimeoutId") ?? 0;
230+
231+
if (id > 0) {
232+
GLib.source_remove(id);
233+
}
234+
235+
id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
236+
this.addHotKeyArray(xlet_key, [binding], this._xletCallback.bind(this, xlet_key));
237+
xlet_set.set("commitTimeoutId", 0);
238+
return GLib.SOURCE_REMOVE;
239+
});
240+
241+
xlet_set.set("commitTimeoutId", id);
242+
},
243+
75244
_lookupEntry: function(name) {
76245
let found = 0;
77246
for (const action_id of this.bindings.keys()) {

0 commit comments

Comments
 (0)