From a667a9742c3a47eb60fb9848bcce7da5273925ec Mon Sep 17 00:00:00 2001 From: lenemter Date: Sat, 17 May 2025 13:41:23 +0300 Subject: [PATCH 1/5] Add application shortcuts backend --- .../io.elementary.settings-daemon.gschema.xml | 13 + src/Application.vala | 2 + src/Backends/ApplicationShortcuts.vala | 243 ++++++++++++++++++ src/DBus/DesktopIntegration.vala | 16 ++ src/DBus/ShellKeyGrabber.vala | 52 ++++ src/meson.build | 3 + 6 files changed, 329 insertions(+) create mode 100644 src/Backends/ApplicationShortcuts.vala create mode 100644 src/DBus/DesktopIntegration.vala create mode 100644 src/DBus/ShellKeyGrabber.vala diff --git a/data/io.elementary.settings-daemon.gschema.xml b/data/io.elementary.settings-daemon.gschema.xml index 637e4a7a..40058ede 100644 --- a/data/io.elementary.settings-daemon.gschema.xml +++ b/data/io.elementary.settings-daemon.gschema.xml @@ -127,4 +127,17 @@ + + + + [(0, 'io.elementary.settings.desktop', {}, I']]]>), (0, 'io.elementary.files.desktop', {}, E']]]>)] + Application shortcuts + + The first argument is a type (0 - launch desktop file, 1 - launch cli). + The second argument is the 'target', desktop file name if type is 0, commandline if it's 1. + The third argument is 'parameters' + And the last argument is a list of keyboard shortcuts. + + + diff --git a/src/Application.vala b/src/Application.vala index 018b7b9b..ae9f8542 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -20,6 +20,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application { private Backends.Housekeeping housekeeping; private Backends.PowerProfilesSync power_profiles_sync; + private Backends.ApplicationShortcuts application_shortcuts; private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts"; private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts"; @@ -56,6 +57,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application { housekeeping = new Backends.Housekeeping (); power_profiles_sync = new Backends.PowerProfilesSync (); + application_shortcuts = new Backends.ApplicationShortcuts (); var check_firmware_updates_action = new GLib.SimpleAction ("check-firmware-updates", null); check_firmware_updates_action.activate.connect (check_firmware_updates); diff --git a/src/Backends/ApplicationShortcuts.vala b/src/Backends/ApplicationShortcuts.vala new file mode 100644 index 00000000..33f240dc --- /dev/null +++ b/src/Backends/ApplicationShortcuts.vala @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class SettingsDaemon.Backends.ApplicationShortcuts : Object { + private enum ActionType { + DESKTOP_FILE, + COMMAND_LINE + } + + private struct Parsed { + ActionType type; + string target; + GLib.HashTable parameters; + string[] keybindings; + } + + private struct ActionInfo { + ActionType type; + string target; + GLib.HashTable parameters; + } + + private GLib.Settings application_settings; + private ShellKeyGrabber? key_grabber = null; + private DesktopIntegration? desktop_integration = null; + private ulong key_grabber_id = 0; + private GLib.HashTable saved_action_ids; + + construct { + application_settings = new GLib.Settings ("io.elementary.settings-daemon.applications"); + saved_action_ids = new GLib.HashTable (null, null); + + migrate_gsd_shortcuts.begin (); + + application_settings.changed.connect (() => { + if (key_grabber != null) { + try { + key_grabber.ungrab_accelerators (saved_action_ids.get_keys_as_array ()); + } catch (Error e) { + critical ("Couldn't ungrab accelerators: %s", e.message); + } + + if (key_grabber_id != 0) { + key_grabber.disconnect (key_grabber_id); + key_grabber_id = 0; + } + + setup_grabs (); + } + }); + + Bus.watch_name (BusType.SESSION, + "org.gnome.Shell", + BusNameWatcherFlags.NONE, + (connection) => { + connection.get_proxy.begin ( + "org.gnome.Shell", "/org/gnome/Shell", NONE, null, + (obj, res) => { + try { + key_grabber = ((GLib.DBusConnection) obj).get_proxy.end (res); + setup_grabs (); + } catch (Error e) { + critical (e.message); + key_grabber = null; + } + } + ); + }, + () => { + if (key_grabber_id != 0) { + key_grabber.disconnect (key_grabber_id); + key_grabber_id = 0; + } + + key_grabber = null; + critical ("Lost connection to org.gnome.Shell"); + } + ); + + Bus.watch_name ( + BusType.SESSION, + "org.pantheon.gala", + BusNameWatcherFlags.NONE, + (connection) => { + connection.get_proxy.begin ( + "org.pantheon.gala", "/org/pantheon/gala/DesktopInterface", NONE, null, + (obj, res) => { + try { + desktop_integration = ((GLib.DBusConnection) obj).get_proxy.end (res); + } catch (Error e) { + critical (e.message); + desktop_integration = null; + } + } + ); + }, + () => { + desktop_integration = null; + critical ("Lost connection to org.pantheon.gala.DesktopIntegration"); + } + ); + } + + private async void migrate_gsd_shortcuts () { + unowned var settings_schema = GLib.SettingsSchemaSource.get_default (); + if (settings_schema.lookup ("org.gnome.settings-daemon.plugins.media-keys", false) != null) { + var value = (Parsed[]) application_settings.get_value ("application-shortcuts"); + + var gsd_settings = new GLib.Settings ("org.gnome.settings-daemon.plugins.media-keys"); + var enabled_keybindings = gsd_settings.get_strv ("custom-keybindings"); + + for (var i = 0; i < enabled_keybindings.length; i++) { + var settings = new GLib.Settings.with_path ("org.gnome.settings-daemon.plugins.media-keys.custom-keybinding", enabled_keybindings[i]); + Parsed new_shortcut = { + ActionType.COMMAND_LINE, + settings.get_string ("command"), + new GLib.HashTable (null, null), + { settings.get_string ("binding") } + }; + value += new_shortcut; + } + + application_settings.set_value ("application-shortcuts", value); + gsd_settings.set_strv ("custom-keybindings", {}); + } + } + + private void setup_grabs () requires (key_grabber != null) { + Accelerator[] accelerators = {}; + + var parsed_value = (Parsed[]) application_settings.get_value ("application-shortcuts"); + for (var i = 0; i < parsed_value.length; i++) { + var keybindings = parsed_value[i].keybindings; + for (var j = 0; j < keybindings.length; j++) { + accelerators += Accelerator () { + name = keybindings[j], + mode_flags = ActionMode.NONE, + grab_flags = Meta.KeyBindingFlags.NONE + }; + } + } + + uint[] action_ids; + try { + action_ids = key_grabber.grab_accelerators (accelerators); + } catch (Error e) { + critical (e.message); + return; + } + + for (int i = 0; i < action_ids.length; i++) { + var parsed_value_i = parsed_value[i]; + saved_action_ids[action_ids[i]] = { parsed_value_i.type, parsed_value_i.target, parsed_value_i.parameters }; + } + + key_grabber_id = key_grabber.accelerator_activated.connect (on_accelerator_activated); + } + + private void on_accelerator_activated (uint action, GLib.HashTable parameters_dict) { + var action_info = saved_action_ids[action]; + if (action_info == null) { + return; + } + + var context = Gdk.Display.get_default ().get_app_launch_context (); + context.set_timestamp ("timestamp" in parameters_dict ? (uint32) parameters_dict["timestamp"] : Gdk.CURRENT_TIME); + + var action_parameters = action_info.parameters; + + switch (action_info.type) { + case DESKTOP_FILE: + var desktop_file_name = action_info.target; + + DesktopIntegration.RunningApplication[] apps = {}; + if (desktop_integration != null) { + try { + apps = desktop_integration.get_running_applications (); + } catch (Error e) { + warning (e.message); + } + } + + var already_launched = false; + for (var i = 0; i < apps.length; i++) { + if (apps[i].app_id == desktop_file_name) { + already_launched = true; + break; + } + } + + if ("action" in action_parameters) { + unowned var action_name = action_parameters["action"].get_string (); + new DesktopAppInfo (desktop_file_name).launch_action (action_name, context); + } else if (!already_launched || desktop_integration == null) { + launch_app (desktop_file_name, context); + } else { + try { + var found_window = false; + var windows = desktop_integration.get_windows (); + for (var i = 0; i < windows.length; i++) { + if (windows[i].properties["app-id"].get_string () == desktop_file_name) { + found_window = true; + desktop_integration.focus_window (windows[i].uid); + break; + } + } + + if (!found_window) { + launch_app (desktop_file_name, context); + } + } catch (Error e) { + warning (e.message); + launch_app (desktop_file_name, context); + } + } + break; + + case COMMAND_LINE: + var commandline = action_info.target; + var flags = GLib.AppInfoCreateFlags.NONE; + if ("needs-terminal" in action_parameters && action_parameters["needs-terminal"].get_boolean ()) { + flags = GLib.AppInfoCreateFlags.NEEDS_TERMINAL; + } + + try { + AppInfo.create_from_commandline (commandline, null, flags).launch (null, context); + } catch (Error e) { + warning ("Couldn't launch %s: %s", commandline, e.message); + } + break; + } + } + + private void launch_app (string desktop_file_name, Gdk.AppLaunchContext context) { + try { + new DesktopAppInfo (desktop_file_name).launch (null, context); + } catch (Error e) { + warning ("Couldn't launch %s: %s", desktop_file_name, e.message); + } + } +} diff --git a/src/DBus/DesktopIntegration.vala b/src/DBus/DesktopIntegration.vala new file mode 100644 index 00000000..a7157f88 --- /dev/null +++ b/src/DBus/DesktopIntegration.vala @@ -0,0 +1,16 @@ +[DBus (name="org.pantheon.gala.DesktopIntegration")] +public interface DesktopIntegration : GLib.Object { + public struct RunningApplication { + string app_id; + GLib.HashTable details; + } + + public struct Window { + uint64 uid; + GLib.HashTable properties; + } + + public abstract RunningApplication[] get_running_applications () throws GLib.DBusError, GLib.IOError; + public abstract Window[] get_windows () throws GLib.DBusError, GLib.IOError; + public abstract void focus_window (uint64 uid) throws GLib.DBusError, GLib.IOError; +} diff --git a/src/DBus/ShellKeyGrabber.vala b/src/DBus/ShellKeyGrabber.vala new file mode 100644 index 00000000..17d5788d --- /dev/null +++ b/src/DBus/ShellKeyGrabber.vala @@ -0,0 +1,52 @@ +/** + * ActionMode: + * @NONE: block action + * @NORMAL: allow action when in window mode, e.g. when the focus is in an application window + * @OVERVIEW: allow action while the overview is active + * @LOCK_SCREEN: allow action when the screen is locked, e.g. when the screen shield is shown + * @UNLOCK_SCREEN: allow action in the unlock dialog + * @LOGIN_SCREEN: allow action in the login screen + * @SYSTEM_MODAL: allow action when a system modal dialog (e.g. authentification or session dialogs) is open + * @LOOKING_GLASS: allow action in looking glass + * @POPUP: allow action while a shell menu is open + */ +[Flags] +public enum ActionMode { + NONE = 0, + NORMAL = 1 << 0, + OVERVIEW = 1 << 1, + LOCK_SCREEN = 1 << 2, + UNLOCK_SCREEN = 1 << 3, + LOGIN_SCREEN = 1 << 4, + SYSTEM_MODAL = 1 << 5, + LOOKING_GLASS = 1 << 6, + POPUP = 1 << 7, +} + +[Flags] +public enum Meta.KeyBindingFlags { + NONE = 0, + PER_WINDOW = 1 << 0, + BUILTIN = 1 << 1, + IS_REVERSED = 1 << 2, + NON_MASKABLE = 1 << 3, + IGNORE_AUTOREPEAT = 1 << 4, +} + +public struct Accelerator { + public string name; + public ActionMode mode_flags; + public Meta.KeyBindingFlags grab_flags; +} + +[DBus (name = "org.gnome.Shell")] +public interface ShellKeyGrabber : GLib.Object { + public abstract signal void accelerator_activated (uint action, GLib.HashTable parameters_dict); + + public abstract uint grab_accelerator (string accelerator, ActionMode mode_flags, Meta.KeyBindingFlags grab_flags) throws GLib.DBusError, GLib.IOError; + public abstract uint[] grab_accelerators (Accelerator[] accelerators) throws GLib.DBusError, GLib.IOError; + public abstract bool ungrab_accelerator (uint action) throws GLib.DBusError, GLib.IOError; + public abstract bool ungrab_accelerators (uint[] actions) throws GLib.DBusError, GLib.IOError; + [DBus (name = "ShowOSD")] + public abstract void show_osd (GLib.HashTable parameters_dict) throws GLib.DBusError, GLib.IOError; +} diff --git a/src/meson.build b/src/meson.build index 472148b9..e5cb5503 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,6 +2,7 @@ sources = files( 'AccountsService.vala', 'Application.vala', 'Backends/AccentColorManager.vala', + 'Backends/ApplicationShortcuts.vala', 'Backends/Housekeeping.vala', 'Backends/InterfaceSettings.vala', 'Backends/KeyboardSettings.vala', @@ -10,6 +11,8 @@ sources = files( 'Backends/PowerProfilesSync.vala', 'Backends/PrefersColorSchemeSettings.vala', 'Backends/SystemUpdate.vala', + 'DBus/DesktopIntegration.vala', + 'DBus/ShellKeyGrabber.vala', 'Utils/PkUtils.vala', 'Utils/SessionUtils.vala', 'Utils/SunriseSunsetCalculator.vala', From f56907410dee87de71cb688fdfe588caaf5ebe4d Mon Sep 17 00:00:00 2001 From: lenemter Date: Tue, 3 Jun 2025 18:33:54 +0300 Subject: [PATCH 2/5] Use get_proxy, drop 'needs-terminal' parameter --- src/Backends/ApplicationShortcuts.vala | 73 ++++++++------------------ 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/src/Backends/ApplicationShortcuts.vala b/src/Backends/ApplicationShortcuts.vala index 33f240dc..0a5f32ca 100644 --- a/src/Backends/ApplicationShortcuts.vala +++ b/src/Backends/ApplicationShortcuts.vala @@ -23,8 +23,8 @@ public class SettingsDaemon.Backends.ApplicationShortcuts : Object { } private GLib.Settings application_settings; - private ShellKeyGrabber? key_grabber = null; - private DesktopIntegration? desktop_integration = null; + private ShellKeyGrabber key_grabber; + private DesktopIntegration desktop_integration; private ulong key_grabber_id = 0; private GLib.HashTable saved_action_ids; @@ -51,54 +51,31 @@ public class SettingsDaemon.Backends.ApplicationShortcuts : Object { } }); - Bus.watch_name (BusType.SESSION, - "org.gnome.Shell", - BusNameWatcherFlags.NONE, - (connection) => { - connection.get_proxy.begin ( - "org.gnome.Shell", "/org/gnome/Shell", NONE, null, - (obj, res) => { - try { - key_grabber = ((GLib.DBusConnection) obj).get_proxy.end (res); - setup_grabs (); - } catch (Error e) { - critical (e.message); - key_grabber = null; - } - } - ); - }, - () => { - if (key_grabber_id != 0) { - key_grabber.disconnect (key_grabber_id); - key_grabber_id = 0; + Bus.get_proxy.begin ( + BusType.SESSION, + "org.gnome.Shell", "/org/gnome/Shell", + NONE, null, + (obj, res) => { + try { + key_grabber = Bus.get_proxy.end (res); + } catch (Error e) { + critical (e.message); + key_grabber = null; } - - key_grabber = null; - critical ("Lost connection to org.gnome.Shell"); } ); - Bus.watch_name ( + Bus.get_proxy.begin ( BusType.SESSION, - "org.pantheon.gala", - BusNameWatcherFlags.NONE, - (connection) => { - connection.get_proxy.begin ( - "org.pantheon.gala", "/org/pantheon/gala/DesktopInterface", NONE, null, - (obj, res) => { - try { - desktop_integration = ((GLib.DBusConnection) obj).get_proxy.end (res); - } catch (Error e) { - critical (e.message); - desktop_integration = null; - } - } - ); - }, - () => { - desktop_integration = null; - critical ("Lost connection to org.pantheon.gala.DesktopIntegration"); + "org.pantheon.gala", "/org/pantheon/gala/DesktopInterface", + NONE, null, + (obj, res) => { + try { + desktop_integration = Bus.get_proxy.end (res); + } catch (Error e) { + critical (e.message); + desktop_integration = null; + } } ); } @@ -219,13 +196,9 @@ public class SettingsDaemon.Backends.ApplicationShortcuts : Object { case COMMAND_LINE: var commandline = action_info.target; - var flags = GLib.AppInfoCreateFlags.NONE; - if ("needs-terminal" in action_parameters && action_parameters["needs-terminal"].get_boolean ()) { - flags = GLib.AppInfoCreateFlags.NEEDS_TERMINAL; - } try { - AppInfo.create_from_commandline (commandline, null, flags).launch (null, context); + AppInfo.create_from_commandline (commandline, null, NONE).launch (null, context); } catch (Error e) { warning ("Couldn't launch %s: %s", commandline, e.message); } From 10a5e538bfde8ec7fc818af6c0f48976ba7fc3fd Mon Sep 17 00:00:00 2001 From: lenemter Date: Tue, 3 Jun 2025 18:34:34 +0300 Subject: [PATCH 3/5] Update signatures --- src/Backends/ApplicationShortcuts.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Backends/ApplicationShortcuts.vala b/src/Backends/ApplicationShortcuts.vala index 0a5f32ca..5e9e938c 100644 --- a/src/Backends/ApplicationShortcuts.vala +++ b/src/Backends/ApplicationShortcuts.vala @@ -23,8 +23,8 @@ public class SettingsDaemon.Backends.ApplicationShortcuts : Object { } private GLib.Settings application_settings; - private ShellKeyGrabber key_grabber; - private DesktopIntegration desktop_integration; + private ShellKeyGrabber? key_grabber; + private DesktopIntegration? desktop_integration; private ulong key_grabber_id = 0; private GLib.HashTable saved_action_ids; From b26df498c0a58170b468908a7db0ef9d7dcb5d22 Mon Sep 17 00:00:00 2001 From: lenemter Date: Tue, 3 Jun 2025 18:40:12 +0300 Subject: [PATCH 4/5] Remove default shortcuts --- data/io.elementary.settings-daemon.gschema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/io.elementary.settings-daemon.gschema.xml b/data/io.elementary.settings-daemon.gschema.xml index 40058ede..6fb29f83 100644 --- a/data/io.elementary.settings-daemon.gschema.xml +++ b/data/io.elementary.settings-daemon.gschema.xml @@ -130,7 +130,7 @@ - [(0, 'io.elementary.settings.desktop', {}, I']]]>), (0, 'io.elementary.files.desktop', {}, E']]]>)] + Application shortcuts The first argument is a type (0 - launch desktop file, 1 - launch cli). From 62382908afe76212a10434a530922b1800331fd3 Mon Sep 17 00:00:00 2001 From: lenemter Date: Tue, 3 Jun 2025 18:48:08 +0300 Subject: [PATCH 5/5] Oops --- data/io.elementary.settings-daemon.gschema.xml | 2 +- src/Backends/ApplicationShortcuts.vala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/data/io.elementary.settings-daemon.gschema.xml b/data/io.elementary.settings-daemon.gschema.xml index 6fb29f83..f3ec639c 100644 --- a/data/io.elementary.settings-daemon.gschema.xml +++ b/data/io.elementary.settings-daemon.gschema.xml @@ -130,7 +130,7 @@ - + [] Application shortcuts The first argument is a type (0 - launch desktop file, 1 - launch cli). diff --git a/src/Backends/ApplicationShortcuts.vala b/src/Backends/ApplicationShortcuts.vala index 5e9e938c..94df8215 100644 --- a/src/Backends/ApplicationShortcuts.vala +++ b/src/Backends/ApplicationShortcuts.vala @@ -58,6 +58,7 @@ public class SettingsDaemon.Backends.ApplicationShortcuts : Object { (obj, res) => { try { key_grabber = Bus.get_proxy.end (res); + setup_grabs (); } catch (Error e) { critical (e.message); key_grabber = null;