|
| 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 | +} |
0 commit comments