Skip to content

Commit a667a97

Browse files
committed
Add application shortcuts backend
1 parent de708fc commit a667a97

6 files changed

Lines changed: 329 additions & 0 deletions

File tree

data/io.elementary.settings-daemon.gschema.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,17 @@
127127
<description></description>
128128
</key>
129129
</schema>
130+
131+
<schema path="/io/elementary/settings-daemon/applications/" id="io.elementary.settings-daemon.applications">
132+
<key type="a(isa{sv}as)" name="application-shortcuts">
133+
<default>[(0, 'io.elementary.settings.desktop', {}, <![CDATA[['<Super>I']]]>), (0, 'io.elementary.files.desktop', {}, <![CDATA[['<Super>E']]]>)]</default>
134+
<summary>Application shortcuts</summary>
135+
<description>
136+
The first argument is a type (0 - launch desktop file, 1 - launch cli).
137+
The second argument is the 'target', desktop file name if type is 0, commandline if it's 1.
138+
The third argument is 'parameters'
139+
And the last argument is a list of keyboard shortcuts.
140+
</description>
141+
</key>
142+
</schema>
130143
</schemalist>

src/Application.vala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
2020

2121
private Backends.Housekeeping housekeeping;
2222
private Backends.PowerProfilesSync power_profiles_sync;
23+
private Backends.ApplicationShortcuts application_shortcuts;
2324

2425
private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts";
2526
private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts";
@@ -56,6 +57,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
5657

5758
housekeeping = new Backends.Housekeeping ();
5859
power_profiles_sync = new Backends.PowerProfilesSync ();
60+
application_shortcuts = new Backends.ApplicationShortcuts ();
5961

6062
var check_firmware_updates_action = new GLib.SimpleAction ("check-firmware-updates", null);
6163
check_firmware_updates_action.activate.connect (check_firmware_updates);
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-or-later
3+
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
4+
*/
5+
6+
public class SettingsDaemon.Backends.ApplicationShortcuts : Object {
7+
private enum ActionType {
8+
DESKTOP_FILE,
9+
COMMAND_LINE
10+
}
11+
12+
private struct Parsed {
13+
ActionType type;
14+
string target;
15+
GLib.HashTable<string, Variant> parameters;
16+
string[] keybindings;
17+
}
18+
19+
private struct ActionInfo {
20+
ActionType type;
21+
string target;
22+
GLib.HashTable<string, Variant> parameters;
23+
}
24+
25+
private GLib.Settings application_settings;
26+
private ShellKeyGrabber? key_grabber = null;
27+
private DesktopIntegration? desktop_integration = null;
28+
private ulong key_grabber_id = 0;
29+
private GLib.HashTable<uint, ActionInfo?> saved_action_ids;
30+
31+
construct {
32+
application_settings = new GLib.Settings ("io.elementary.settings-daemon.applications");
33+
saved_action_ids = new GLib.HashTable<uint, ActionInfo?> (null, null);
34+
35+
migrate_gsd_shortcuts.begin ();
36+
37+
application_settings.changed.connect (() => {
38+
if (key_grabber != null) {
39+
try {
40+
key_grabber.ungrab_accelerators (saved_action_ids.get_keys_as_array ());
41+
} catch (Error e) {
42+
critical ("Couldn't ungrab accelerators: %s", e.message);
43+
}
44+
45+
if (key_grabber_id != 0) {
46+
key_grabber.disconnect (key_grabber_id);
47+
key_grabber_id = 0;
48+
}
49+
50+
setup_grabs ();
51+
}
52+
});
53+
54+
Bus.watch_name (BusType.SESSION,
55+
"org.gnome.Shell",
56+
BusNameWatcherFlags.NONE,
57+
(connection) => {
58+
connection.get_proxy.begin<ShellKeyGrabber> (
59+
"org.gnome.Shell", "/org/gnome/Shell", NONE, null,
60+
(obj, res) => {
61+
try {
62+
key_grabber = ((GLib.DBusConnection) obj).get_proxy.end<ShellKeyGrabber> (res);
63+
setup_grabs ();
64+
} catch (Error e) {
65+
critical (e.message);
66+
key_grabber = null;
67+
}
68+
}
69+
);
70+
},
71+
() => {
72+
if (key_grabber_id != 0) {
73+
key_grabber.disconnect (key_grabber_id);
74+
key_grabber_id = 0;
75+
}
76+
77+
key_grabber = null;
78+
critical ("Lost connection to org.gnome.Shell");
79+
}
80+
);
81+
82+
Bus.watch_name (
83+
BusType.SESSION,
84+
"org.pantheon.gala",
85+
BusNameWatcherFlags.NONE,
86+
(connection) => {
87+
connection.get_proxy.begin<DesktopIntegration> (
88+
"org.pantheon.gala", "/org/pantheon/gala/DesktopInterface", NONE, null,
89+
(obj, res) => {
90+
try {
91+
desktop_integration = ((GLib.DBusConnection) obj).get_proxy.end<DesktopIntegration> (res);
92+
} catch (Error e) {
93+
critical (e.message);
94+
desktop_integration = null;
95+
}
96+
}
97+
);
98+
},
99+
() => {
100+
desktop_integration = null;
101+
critical ("Lost connection to org.pantheon.gala.DesktopIntegration");
102+
}
103+
);
104+
}
105+
106+
private async void migrate_gsd_shortcuts () {
107+
unowned var settings_schema = GLib.SettingsSchemaSource.get_default ();
108+
if (settings_schema.lookup ("org.gnome.settings-daemon.plugins.media-keys", false) != null) {
109+
var value = (Parsed[]) application_settings.get_value ("application-shortcuts");
110+
111+
var gsd_settings = new GLib.Settings ("org.gnome.settings-daemon.plugins.media-keys");
112+
var enabled_keybindings = gsd_settings.get_strv ("custom-keybindings");
113+
114+
for (var i = 0; i < enabled_keybindings.length; i++) {
115+
var settings = new GLib.Settings.with_path ("org.gnome.settings-daemon.plugins.media-keys.custom-keybinding", enabled_keybindings[i]);
116+
Parsed new_shortcut = {
117+
ActionType.COMMAND_LINE,
118+
settings.get_string ("command"),
119+
new GLib.HashTable<string, Variant> (null, null),
120+
{ settings.get_string ("binding") }
121+
};
122+
value += new_shortcut;
123+
}
124+
125+
application_settings.set_value ("application-shortcuts", value);
126+
gsd_settings.set_strv ("custom-keybindings", {});
127+
}
128+
}
129+
130+
private void setup_grabs () requires (key_grabber != null) {
131+
Accelerator[] accelerators = {};
132+
133+
var parsed_value = (Parsed[]) application_settings.get_value ("application-shortcuts");
134+
for (var i = 0; i < parsed_value.length; i++) {
135+
var keybindings = parsed_value[i].keybindings;
136+
for (var j = 0; j < keybindings.length; j++) {
137+
accelerators += Accelerator () {
138+
name = keybindings[j],
139+
mode_flags = ActionMode.NONE,
140+
grab_flags = Meta.KeyBindingFlags.NONE
141+
};
142+
}
143+
}
144+
145+
uint[] action_ids;
146+
try {
147+
action_ids = key_grabber.grab_accelerators (accelerators);
148+
} catch (Error e) {
149+
critical (e.message);
150+
return;
151+
}
152+
153+
for (int i = 0; i < action_ids.length; i++) {
154+
var parsed_value_i = parsed_value[i];
155+
saved_action_ids[action_ids[i]] = { parsed_value_i.type, parsed_value_i.target, parsed_value_i.parameters };
156+
}
157+
158+
key_grabber_id = key_grabber.accelerator_activated.connect (on_accelerator_activated);
159+
}
160+
161+
private void on_accelerator_activated (uint action, GLib.HashTable<string, GLib.Variant> parameters_dict) {
162+
var action_info = saved_action_ids[action];
163+
if (action_info == null) {
164+
return;
165+
}
166+
167+
var context = Gdk.Display.get_default ().get_app_launch_context ();
168+
context.set_timestamp ("timestamp" in parameters_dict ? (uint32) parameters_dict["timestamp"] : Gdk.CURRENT_TIME);
169+
170+
var action_parameters = action_info.parameters;
171+
172+
switch (action_info.type) {
173+
case DESKTOP_FILE:
174+
var desktop_file_name = action_info.target;
175+
176+
DesktopIntegration.RunningApplication[] apps = {};
177+
if (desktop_integration != null) {
178+
try {
179+
apps = desktop_integration.get_running_applications ();
180+
} catch (Error e) {
181+
warning (e.message);
182+
}
183+
}
184+
185+
var already_launched = false;
186+
for (var i = 0; i < apps.length; i++) {
187+
if (apps[i].app_id == desktop_file_name) {
188+
already_launched = true;
189+
break;
190+
}
191+
}
192+
193+
if ("action" in action_parameters) {
194+
unowned var action_name = action_parameters["action"].get_string ();
195+
new DesktopAppInfo (desktop_file_name).launch_action (action_name, context);
196+
} else if (!already_launched || desktop_integration == null) {
197+
launch_app (desktop_file_name, context);
198+
} else {
199+
try {
200+
var found_window = false;
201+
var windows = desktop_integration.get_windows ();
202+
for (var i = 0; i < windows.length; i++) {
203+
if (windows[i].properties["app-id"].get_string () == desktop_file_name) {
204+
found_window = true;
205+
desktop_integration.focus_window (windows[i].uid);
206+
break;
207+
}
208+
}
209+
210+
if (!found_window) {
211+
launch_app (desktop_file_name, context);
212+
}
213+
} catch (Error e) {
214+
warning (e.message);
215+
launch_app (desktop_file_name, context);
216+
}
217+
}
218+
break;
219+
220+
case COMMAND_LINE:
221+
var commandline = action_info.target;
222+
var flags = GLib.AppInfoCreateFlags.NONE;
223+
if ("needs-terminal" in action_parameters && action_parameters["needs-terminal"].get_boolean ()) {
224+
flags = GLib.AppInfoCreateFlags.NEEDS_TERMINAL;
225+
}
226+
227+
try {
228+
AppInfo.create_from_commandline (commandline, null, flags).launch (null, context);
229+
} catch (Error e) {
230+
warning ("Couldn't launch %s: %s", commandline, e.message);
231+
}
232+
break;
233+
}
234+
}
235+
236+
private void launch_app (string desktop_file_name, Gdk.AppLaunchContext context) {
237+
try {
238+
new DesktopAppInfo (desktop_file_name).launch (null, context);
239+
} catch (Error e) {
240+
warning ("Couldn't launch %s: %s", desktop_file_name, e.message);
241+
}
242+
}
243+
}

src/DBus/DesktopIntegration.vala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[DBus (name="org.pantheon.gala.DesktopIntegration")]
2+
public interface DesktopIntegration : GLib.Object {
3+
public struct RunningApplication {
4+
string app_id;
5+
GLib.HashTable<unowned string, Variant> details;
6+
}
7+
8+
public struct Window {
9+
uint64 uid;
10+
GLib.HashTable<unowned string, Variant> properties;
11+
}
12+
13+
public abstract RunningApplication[] get_running_applications () throws GLib.DBusError, GLib.IOError;
14+
public abstract Window[] get_windows () throws GLib.DBusError, GLib.IOError;
15+
public abstract void focus_window (uint64 uid) throws GLib.DBusError, GLib.IOError;
16+
}

src/DBus/ShellKeyGrabber.vala

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ActionMode:
3+
* @NONE: block action
4+
* @NORMAL: allow action when in window mode, e.g. when the focus is in an application window
5+
* @OVERVIEW: allow action while the overview is active
6+
* @LOCK_SCREEN: allow action when the screen is locked, e.g. when the screen shield is shown
7+
* @UNLOCK_SCREEN: allow action in the unlock dialog
8+
* @LOGIN_SCREEN: allow action in the login screen
9+
* @SYSTEM_MODAL: allow action when a system modal dialog (e.g. authentification or session dialogs) is open
10+
* @LOOKING_GLASS: allow action in looking glass
11+
* @POPUP: allow action while a shell menu is open
12+
*/
13+
[Flags]
14+
public enum ActionMode {
15+
NONE = 0,
16+
NORMAL = 1 << 0,
17+
OVERVIEW = 1 << 1,
18+
LOCK_SCREEN = 1 << 2,
19+
UNLOCK_SCREEN = 1 << 3,
20+
LOGIN_SCREEN = 1 << 4,
21+
SYSTEM_MODAL = 1 << 5,
22+
LOOKING_GLASS = 1 << 6,
23+
POPUP = 1 << 7,
24+
}
25+
26+
[Flags]
27+
public enum Meta.KeyBindingFlags {
28+
NONE = 0,
29+
PER_WINDOW = 1 << 0,
30+
BUILTIN = 1 << 1,
31+
IS_REVERSED = 1 << 2,
32+
NON_MASKABLE = 1 << 3,
33+
IGNORE_AUTOREPEAT = 1 << 4,
34+
}
35+
36+
public struct Accelerator {
37+
public string name;
38+
public ActionMode mode_flags;
39+
public Meta.KeyBindingFlags grab_flags;
40+
}
41+
42+
[DBus (name = "org.gnome.Shell")]
43+
public interface ShellKeyGrabber : GLib.Object {
44+
public abstract signal void accelerator_activated (uint action, GLib.HashTable<string, GLib.Variant> parameters_dict);
45+
46+
public abstract uint grab_accelerator (string accelerator, ActionMode mode_flags, Meta.KeyBindingFlags grab_flags) throws GLib.DBusError, GLib.IOError;
47+
public abstract uint[] grab_accelerators (Accelerator[] accelerators) throws GLib.DBusError, GLib.IOError;
48+
public abstract bool ungrab_accelerator (uint action) throws GLib.DBusError, GLib.IOError;
49+
public abstract bool ungrab_accelerators (uint[] actions) throws GLib.DBusError, GLib.IOError;
50+
[DBus (name = "ShowOSD")]
51+
public abstract void show_osd (GLib.HashTable<string, GLib.Variant> parameters_dict) throws GLib.DBusError, GLib.IOError;
52+
}

src/meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ sources = files(
22
'AccountsService.vala',
33
'Application.vala',
44
'Backends/AccentColorManager.vala',
5+
'Backends/ApplicationShortcuts.vala',
56
'Backends/Housekeeping.vala',
67
'Backends/InterfaceSettings.vala',
78
'Backends/KeyboardSettings.vala',
@@ -10,6 +11,8 @@ sources = files(
1011
'Backends/PowerProfilesSync.vala',
1112
'Backends/PrefersColorSchemeSettings.vala',
1213
'Backends/SystemUpdate.vala',
14+
'DBus/DesktopIntegration.vala',
15+
'DBus/ShellKeyGrabber.vala',
1316
'Utils/PkUtils.vala',
1417
'Utils/SessionUtils.vala',
1518
'Utils/SunriseSunsetCalculator.vala',

0 commit comments

Comments
 (0)