Skip to content

Commit cb7fb56

Browse files
lenemterzeebok
andauthored
Add application shortcuts backend (#190)
* Add application shortcuts backend * Use get_proxy, drop 'needs-terminal' parameter * Update signatures * Remove default shortcuts * Oops --------- Co-authored-by: Ryan Kornheisl <ryan@skarva.tech>
1 parent dd0b6ac commit cb7fb56

6 files changed

Lines changed: 303 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>[]</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: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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;
27+
private DesktopIntegration? desktop_integration;
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.get_proxy.begin<ShellKeyGrabber> (
55+
BusType.SESSION,
56+
"org.gnome.Shell", "/org/gnome/Shell",
57+
NONE, null,
58+
(obj, res) => {
59+
try {
60+
key_grabber = Bus.get_proxy.end<ShellKeyGrabber> (res);
61+
setup_grabs ();
62+
} catch (Error e) {
63+
critical (e.message);
64+
key_grabber = null;
65+
}
66+
}
67+
);
68+
69+
Bus.get_proxy.begin<DesktopIntegration> (
70+
BusType.SESSION,
71+
"org.pantheon.gala", "/org/pantheon/gala/DesktopInterface",
72+
NONE, null,
73+
(obj, res) => {
74+
try {
75+
desktop_integration = Bus.get_proxy.end<DesktopIntegration> (res);
76+
} catch (Error e) {
77+
critical (e.message);
78+
desktop_integration = null;
79+
}
80+
}
81+
);
82+
}
83+
84+
private async void migrate_gsd_shortcuts () {
85+
unowned var settings_schema = GLib.SettingsSchemaSource.get_default ();
86+
if (settings_schema.lookup ("org.gnome.settings-daemon.plugins.media-keys", false) != null) {
87+
var value = (Parsed[]) application_settings.get_value ("application-shortcuts");
88+
89+
var gsd_settings = new GLib.Settings ("org.gnome.settings-daemon.plugins.media-keys");
90+
var enabled_keybindings = gsd_settings.get_strv ("custom-keybindings");
91+
92+
for (var i = 0; i < enabled_keybindings.length; i++) {
93+
var settings = new GLib.Settings.with_path ("org.gnome.settings-daemon.plugins.media-keys.custom-keybinding", enabled_keybindings[i]);
94+
Parsed new_shortcut = {
95+
ActionType.COMMAND_LINE,
96+
settings.get_string ("command"),
97+
new GLib.HashTable<string, Variant> (null, null),
98+
{ settings.get_string ("binding") }
99+
};
100+
value += new_shortcut;
101+
}
102+
103+
application_settings.set_value ("application-shortcuts", value);
104+
gsd_settings.set_strv ("custom-keybindings", {});
105+
}
106+
}
107+
108+
private void setup_grabs () requires (key_grabber != null) {
109+
Accelerator[] accelerators = {};
110+
111+
var parsed_value = (Parsed[]) application_settings.get_value ("application-shortcuts");
112+
for (var i = 0; i < parsed_value.length; i++) {
113+
var keybindings = parsed_value[i].keybindings;
114+
for (var j = 0; j < keybindings.length; j++) {
115+
accelerators += Accelerator () {
116+
name = keybindings[j],
117+
mode_flags = ActionMode.NONE,
118+
grab_flags = Meta.KeyBindingFlags.NONE
119+
};
120+
}
121+
}
122+
123+
uint[] action_ids;
124+
try {
125+
action_ids = key_grabber.grab_accelerators (accelerators);
126+
} catch (Error e) {
127+
critical (e.message);
128+
return;
129+
}
130+
131+
for (int i = 0; i < action_ids.length; i++) {
132+
var parsed_value_i = parsed_value[i];
133+
saved_action_ids[action_ids[i]] = { parsed_value_i.type, parsed_value_i.target, parsed_value_i.parameters };
134+
}
135+
136+
key_grabber_id = key_grabber.accelerator_activated.connect (on_accelerator_activated);
137+
}
138+
139+
private void on_accelerator_activated (uint action, GLib.HashTable<string, GLib.Variant> parameters_dict) {
140+
var action_info = saved_action_ids[action];
141+
if (action_info == null) {
142+
return;
143+
}
144+
145+
var context = Gdk.Display.get_default ().get_app_launch_context ();
146+
context.set_timestamp ("timestamp" in parameters_dict ? (uint32) parameters_dict["timestamp"] : Gdk.CURRENT_TIME);
147+
148+
var action_parameters = action_info.parameters;
149+
150+
switch (action_info.type) {
151+
case DESKTOP_FILE:
152+
var desktop_file_name = action_info.target;
153+
154+
DesktopIntegration.RunningApplication[] apps = {};
155+
if (desktop_integration != null) {
156+
try {
157+
apps = desktop_integration.get_running_applications ();
158+
} catch (Error e) {
159+
warning (e.message);
160+
}
161+
}
162+
163+
var already_launched = false;
164+
for (var i = 0; i < apps.length; i++) {
165+
if (apps[i].app_id == desktop_file_name) {
166+
already_launched = true;
167+
break;
168+
}
169+
}
170+
171+
if ("action" in action_parameters) {
172+
unowned var action_name = action_parameters["action"].get_string ();
173+
new DesktopAppInfo (desktop_file_name).launch_action (action_name, context);
174+
} else if (!already_launched || desktop_integration == null) {
175+
launch_app (desktop_file_name, context);
176+
} else {
177+
try {
178+
var found_window = false;
179+
var windows = desktop_integration.get_windows ();
180+
for (var i = 0; i < windows.length; i++) {
181+
if (windows[i].properties["app-id"].get_string () == desktop_file_name) {
182+
found_window = true;
183+
desktop_integration.focus_window (windows[i].uid);
184+
break;
185+
}
186+
}
187+
188+
if (!found_window) {
189+
launch_app (desktop_file_name, context);
190+
}
191+
} catch (Error e) {
192+
warning (e.message);
193+
launch_app (desktop_file_name, context);
194+
}
195+
}
196+
break;
197+
198+
case COMMAND_LINE:
199+
var commandline = action_info.target;
200+
201+
try {
202+
AppInfo.create_from_commandline (commandline, null, NONE).launch (null, context);
203+
} catch (Error e) {
204+
warning ("Couldn't launch %s: %s", commandline, e.message);
205+
}
206+
break;
207+
}
208+
}
209+
210+
private void launch_app (string desktop_file_name, Gdk.AppLaunchContext context) {
211+
try {
212+
new DesktopAppInfo (desktop_file_name).launch (null, context);
213+
} catch (Error e) {
214+
warning ("Couldn't launch %s: %s", desktop_file_name, e.message);
215+
}
216+
}
217+
}

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)