Skip to content

Commit 934be3e

Browse files
committed
Add application shortcuts backend
1 parent de708fc commit 934be3e

6 files changed

Lines changed: 293 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>[('System Settings', 0, 'io.elementary.settings.desktop', {}, <![CDATA[['<Super>I']]]>), ('Files', 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: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
application_settings.changed.connect (() => {
36+
if (key_grabber != null) {
37+
try {
38+
key_grabber.ungrab_accelerators (saved_action_ids.get_keys_as_array ());
39+
} catch (Error e) {
40+
critical ("Couldn't ungrab accelerators: %s", e.message);
41+
}
42+
43+
if (key_grabber_id != 0) {
44+
key_grabber.disconnect (key_grabber_id);
45+
key_grabber_id = 0;
46+
}
47+
48+
setup_grabs ();
49+
}
50+
});
51+
52+
Bus.watch_name (BusType.SESSION,
53+
"org.gnome.Shell",
54+
BusNameWatcherFlags.NONE,
55+
(connection) => {
56+
connection.get_proxy.begin<ShellKeyGrabber> (
57+
"org.gnome.Shell", "/org/gnome/Shell", NONE, null,
58+
(obj, res) => {
59+
try {
60+
key_grabber = ((GLib.DBusConnection) obj).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+
() => {
70+
if (key_grabber_id != 0) {
71+
key_grabber.disconnect (key_grabber_id);
72+
key_grabber_id = 0;
73+
}
74+
75+
key_grabber = null;
76+
critical ("Lost connection to org.gnome.Shell");
77+
}
78+
);
79+
80+
Bus.watch_name (
81+
BusType.SESSION,
82+
"org.pantheon.gala",
83+
BusNameWatcherFlags.NONE,
84+
(connection) => {
85+
connection.get_proxy.begin<DesktopIntegration> (
86+
"org.pantheon.gala", "/org/pantheon/gala/DesktopInterface", NONE, null,
87+
(obj, res) => {
88+
try {
89+
desktop_integration = ((GLib.DBusConnection) obj).get_proxy.end<DesktopIntegration> (res);
90+
} catch (Error e) {
91+
critical (e.message);
92+
desktop_integration = null;
93+
}
94+
}
95+
);
96+
},
97+
() => {
98+
desktop_integration = null;
99+
critical ("Lost connection to org.pantheon.gala.DesktopIntegration");
100+
}
101+
);
102+
}
103+
104+
private void setup_grabs () requires (key_grabber != null) {
105+
Accelerator[] accelerators = {};
106+
107+
var parsed_value = (Parsed[]) application_settings.get_value ("application-shortcuts");
108+
for (var i = 0; i < parsed_value.length; i++) {
109+
var keybindings = parsed_value[i].keybindings;
110+
for (var j = 0; j < keybindings.length; j++) {
111+
accelerators += Accelerator () {
112+
name = keybindings[j],
113+
mode_flags = ActionMode.NONE,
114+
grab_flags = Meta.KeyBindingFlags.NONE
115+
};
116+
}
117+
}
118+
119+
uint[] action_ids;
120+
try {
121+
action_ids = key_grabber.grab_accelerators (accelerators);
122+
} catch (Error e) {
123+
critical (e.message);
124+
return;
125+
}
126+
127+
for (int i = 0; i < action_ids.length; i++) {
128+
var parsed_value_i = parsed_value[i];
129+
saved_action_ids[action_ids[i]] = { parsed_value_i.type, parsed_value_i.target, parsed_value_i.parameters };
130+
}
131+
132+
key_grabber_id = key_grabber.accelerator_activated.connect (on_accelerator_activated);
133+
}
134+
135+
private void on_accelerator_activated (uint action, GLib.HashTable<string, GLib.Variant> parameters_dict) {
136+
var action_info = saved_action_ids[action];
137+
if (action_info == null) {
138+
return;
139+
}
140+
141+
var context = Gdk.Display.get_default ().get_app_launch_context ();
142+
context.set_timestamp ("timestamp" in parameters_dict ? (uint32) parameters_dict["timestamp"] : Gdk.CURRENT_TIME);
143+
144+
var action_parameters = action_info.parameters;
145+
146+
switch (action_info.type) {
147+
case DESKTOP_FILE:
148+
var desktop_file_name = action_info.target;
149+
150+
DesktopIntegration.RunningApplication[] apps = {};
151+
if (desktop_integration != null) {
152+
try {
153+
apps = desktop_integration.get_running_applications ();
154+
} catch (Error e) {
155+
warning (e.message);
156+
}
157+
}
158+
159+
var already_launched = false;
160+
for (var i = 0; i < apps.length; i++) {
161+
if (apps[i].app_id == desktop_file_name) {
162+
already_launched = true;
163+
break;
164+
}
165+
}
166+
167+
if (!already_launched || desktop_integration == null) {
168+
launch_app (desktop_file_name, context);
169+
} else {
170+
try {
171+
var windows = desktop_integration.get_windows ();
172+
for (var i = 0; i < windows.length; i++) {
173+
if (windows[i].properties["app-id"].get_string () == desktop_file_name) {
174+
desktop_integration.focus_window (windows[i].uid);
175+
}
176+
}
177+
} catch (Error e) {
178+
warning (e.message);
179+
launch_app (desktop_file_name, context);
180+
}
181+
}
182+
break;
183+
184+
case COMMAND_LINE:
185+
var commandline = action_info.target;
186+
var flags = GLib.AppInfoCreateFlags.NONE;
187+
if ("needs-terminal" in action_parameters && action_parameters["needs-terminal"].get_boolean ()) {
188+
flags = GLib.AppInfoCreateFlags.NEEDS_TERMINAL;
189+
}
190+
191+
try {
192+
AppInfo.create_from_commandline (commandline, null, flags).launch (null, context);
193+
} catch (Error e) {
194+
warning ("Couldn't launch %s: %s", commandline, e.message);
195+
}
196+
break;
197+
}
198+
}
199+
200+
private void launch_app (string desktop_file_name, Gdk.AppLaunchContext context) {
201+
try {
202+
new DesktopAppInfo (desktop_file_name).launch (null, context);
203+
} catch (Error e) {
204+
warning ("Couldn't launch %s: %s", desktop_file_name, e.message);
205+
}
206+
}
207+
}

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)