Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions data/io.elementary.settings-daemon.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,17 @@
<description></description>
</key>
</schema>

<schema path="/io/elementary/settings-daemon/applications/" id="io.elementary.settings-daemon.applications">
<key type="a(isa{sv}as)" name="application-shortcuts">
<default>[]</default>
<summary>Application shortcuts</summary>
<description>
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.
</description>
</key>
</schema>
</schemalist>
2 changes: 2 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
217 changes: 217 additions & 0 deletions src/Backends/ApplicationShortcuts.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* 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<string, Variant> parameters;
string[] keybindings;
}

private struct ActionInfo {
ActionType type;
string target;
GLib.HashTable<string, Variant> parameters;
}

private GLib.Settings application_settings;
private ShellKeyGrabber? key_grabber;
private DesktopIntegration? desktop_integration;
private ulong key_grabber_id = 0;
private GLib.HashTable<uint, ActionInfo?> saved_action_ids;

construct {
application_settings = new GLib.Settings ("io.elementary.settings-daemon.applications");
saved_action_ids = new GLib.HashTable<uint, ActionInfo?> (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.get_proxy.begin<ShellKeyGrabber> (
BusType.SESSION,
"org.gnome.Shell", "/org/gnome/Shell",
NONE, null,
(obj, res) => {
try {
key_grabber = Bus.get_proxy.end<ShellKeyGrabber> (res);
setup_grabs ();
} catch (Error e) {
critical (e.message);
key_grabber = null;
}
}
);

Bus.get_proxy.begin<DesktopIntegration> (
BusType.SESSION,
"org.pantheon.gala", "/org/pantheon/gala/DesktopInterface",
NONE, null,
(obj, res) => {
try {
desktop_integration = Bus.get_proxy.end<DesktopIntegration> (res);
} catch (Error e) {
critical (e.message);
desktop_integration = null;
}
}
);
}

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<string, Variant> (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<string, GLib.Variant> 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;

try {
AppInfo.create_from_commandline (commandline, null, NONE).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);
}
}
}
16 changes: 16 additions & 0 deletions src/DBus/DesktopIntegration.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[DBus (name="org.pantheon.gala.DesktopIntegration")]
public interface DesktopIntegration : GLib.Object {
public struct RunningApplication {
string app_id;
GLib.HashTable<unowned string, Variant> details;
}

public struct Window {
uint64 uid;
GLib.HashTable<unowned string, Variant> 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;
}
52 changes: 52 additions & 0 deletions src/DBus/ShellKeyGrabber.vala
Original file line number Diff line number Diff line change
@@ -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<string, GLib.Variant> 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<string, GLib.Variant> parameters_dict) throws GLib.DBusError, GLib.IOError;
}
3 changes: 3 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down