From ff8a5506ea67755ab39ecb43cb8e03518613165d Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 18 May 2025 17:03:59 +0300 Subject: [PATCH 1/2] Add settings-daemon application launcher support --- po/POTFILES | 10 +- src/Shortcuts/Backend/ConflictsManager.vala | 17 +- .../Backend/CustomShortcutSettings.vala | 158 --------------- src/Shortcuts/Backend/CustomShortcuts.vala | 21 ++ src/Shortcuts/Shortcuts.vala | 9 +- src/Shortcuts/Widgets/AppChooser.vala | 116 +++++++++++ src/Shortcuts/Widgets/AppChooserRow.vala | 42 ++++ src/Shortcuts/Widgets/ConflictDialog.vala | 46 ----- .../Widgets/CustomShortcutListBox.vala | 91 +++++++-- src/Shortcuts/Widgets/CustomShortcutRow.vala | 185 +++++++----------- src/meson.build | 5 +- 11 files changed, 349 insertions(+), 351 deletions(-) delete mode 100644 src/Shortcuts/Backend/CustomShortcutSettings.vala create mode 100644 src/Shortcuts/Backend/CustomShortcuts.vala create mode 100644 src/Shortcuts/Widgets/AppChooser.vala create mode 100644 src/Shortcuts/Widgets/AppChooserRow.vala delete mode 100644 src/Shortcuts/Widgets/ConflictDialog.vala diff --git a/po/POTFILES b/po/POTFILES index 6d3fb67e..4336f914 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -20,11 +20,13 @@ src/Layout/Widgets/AdvancedSettingsPanel.vala src/Layout/Widgets/Display.vala src/Plug.vala src/Shortcuts/Backend/ConflictsManager.vala -src/Shortcuts/Backend/CustomShortcutSettings.vala +src/Shortcuts/Backend/CustomShortcuts.vala src/Shortcuts/Backend/Settings.vala -src/Shortcuts/Backend/ShortcutsList.vala src/Shortcuts/Backend/Shortcut.vala -src/Shortcuts/Shortcuts.vala -src/Shortcuts/Widgets/ConflictDialog.vala +src/Shortcuts/Backend/ShortcutsList.vala +src/Shortcuts/Widgets/AppChooser.vala +src/Shortcuts/Widgets/AppChooserRow.vala src/Shortcuts/Widgets/CustomShortcutListBox.vala +src/Shortcuts/Widgets/CustomShortcutRow.vala src/Shortcuts/Widgets/ShortcutListBox.vala +src/Shortcuts/Shortcuts.vala diff --git a/src/Shortcuts/Backend/ConflictsManager.vala b/src/Shortcuts/Backend/ConflictsManager.vala index e28dc40d..9902df18 100644 --- a/src/Shortcuts/Backend/ConflictsManager.vala +++ b/src/Shortcuts/Backend/ConflictsManager.vala @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: GPL-2.0-or-later - * SPDX-FileCopyrightText: 2017-2023 elementary, Inc. (https://elementary.io) + * SPDX-FileCopyrightText: 2017-2025 elementary, Inc. (https://elementary.io) */ class Keyboard.Shortcuts.ConflictsManager : GLib.Object { @@ -40,6 +40,19 @@ private static bool custom_shortcut_conflicts (Shortcut shortcut, out string name, out string group) { name = ""; group = SectionID.CUSTOM.to_string (); - return CustomShortcutSettings.shortcut_conflicts (shortcut, out name, null); + + var application_shortcuts = new GLib.Settings (CustomShortcuts.SETTINGS_SCHEMA); + var shortcuts = (CustomShortcuts.ParsedShortcut[]) application_shortcuts.get_value (CustomShortcuts.APPLICATION_SHORTCUTS); + for (int i = 0; i < shortcuts.length; i++) { + for (int j = 0; j < shortcuts[i].keybindings.length; j++) { + var action_shortcut = new Shortcut.parse (shortcuts[i].keybindings[j]); + if (shortcut.is_equal (action_shortcut)) { + name = shortcuts[i].target; + return true; + } + } + } + + return false; } } diff --git a/src/Shortcuts/Backend/CustomShortcutSettings.vala b/src/Shortcuts/Backend/CustomShortcutSettings.vala deleted file mode 100644 index 8e199dc9..00000000 --- a/src/Shortcuts/Backend/CustomShortcutSettings.vala +++ /dev/null @@ -1,158 +0,0 @@ -/* -* Copyright 2017-2020 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 2 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - -public struct Keyboard.Shortcuts.CustomShortcut { - public string shortcut; // Shortcut in gsettings format - public string command; - public string relocatable_schema; -} - -public class Keyboard.Shortcuts.CustomShortcutSettings : Object { - public static bool available = false; - - private const int MAX_SHORTCUTS = 100; - private const string KEY = "custom-keybinding"; - private const string RELOCATABLE_SCHEMA_PATH_TEMPLATE = "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom%d/"; - private const string SCHEMA = "org.gnome.settings-daemon.plugins.media-keys"; - - private static GLib.Settings settings; - - public static void init () { - var schema_source = GLib.SettingsSchemaSource.get_default (); - - var schema = schema_source.lookup (SCHEMA, true); - - if (schema == null) { - warning ("Schema \"%s\" is not installed on your system.", SCHEMA); - return; - } - - settings = new GLib.Settings (SCHEMA); - available = true; - } - - public static string? create_shortcut () requires (available) { - for (int i = 0; i < MAX_SHORTCUTS; i++) { - var new_relocatable_schema = RELOCATABLE_SCHEMA_PATH_TEMPLATE.printf (i); - - if (!relocatable_schema_is_used (new_relocatable_schema)) { - reset_relocatable_schema (new_relocatable_schema); - - var relocatable_schemas = settings.get_strv (KEY + "s"); - relocatable_schemas += new_relocatable_schema; - - settings.set_strv (KEY + "s", relocatable_schemas); - - return new_relocatable_schema; - } - } - - return null; - } - - private static bool relocatable_schema_is_used (string new_relocatable_schema) { - var relocatable_schemas = settings.get_strv (KEY + "s"); - - foreach (var relocatable_schema in relocatable_schemas) - if (relocatable_schema == new_relocatable_schema) - return true; - - return false; - } - - private static void reset_relocatable_schema (string relocatable_schema) { - var relocatable_settings = new GLib.Settings.with_path (SCHEMA + "." + KEY, relocatable_schema); - relocatable_settings.reset ("name"); - relocatable_settings.reset ("command"); - relocatable_settings.reset ("binding"); - } - - public static void remove_shortcut (string relocatable_schema) - requires (available) { - - string []relocatable_schemas = {}; - - foreach (var schema in settings.get_strv (KEY + "s")) - if (schema != relocatable_schema) - relocatable_schemas += schema; - - reset_relocatable_schema (relocatable_schema); - settings.set_strv (KEY + "s", relocatable_schemas); - } - - public static bool edit_shortcut (string relocatable_schema, string shortcut) - requires (available) { - - var relocatable_settings = new GLib.Settings.with_path (SCHEMA + "." + KEY, relocatable_schema); - relocatable_settings.set_string ("binding", shortcut); - - return true; - } - - public static bool edit_command (string relocatable_schema, string command) - requires (available) { - - var relocatable_settings = new GLib.Settings.with_path (SCHEMA + "." + KEY, relocatable_schema); - relocatable_settings.set_string ("command", command); - relocatable_settings.set_string ("name", command); - - return true; - } - - public static GLib.List list_custom_shortcuts () - requires (available) { - - var list = new GLib.List (); - foreach (var relocatable_schema in settings.get_strv (KEY + "s")) { - var relocatable_settings = new GLib.Settings.with_path (SCHEMA + "." + KEY, relocatable_schema); - - list.append ({ - relocatable_settings.get_string ("binding"), - relocatable_settings.get_string ("command"), - relocatable_schema - }); - } - - return list; - } - - public static GLib.Settings get_gsettings_for_relocatable_schema (string relocatable_schema) { - return new GLib.Settings.with_path (SCHEMA + "." + KEY, relocatable_schema); - } - - public static bool shortcut_conflicts (Shortcut new_shortcut, out string command, - out string relocatable_schema) { - command = ""; - relocatable_schema = ""; - var new_gsettings_shortcut = new_shortcut.to_gsettings (); - if (new_gsettings_shortcut == "") { - return false; - } - - foreach (var custom_shortcut in list_custom_shortcuts ()) { - if (custom_shortcut.shortcut == new_gsettings_shortcut) { - command = custom_shortcut.command; - relocatable_schema = custom_shortcut.relocatable_schema; - return true; - } - } - - return false; - } -} diff --git a/src/Shortcuts/Backend/CustomShortcuts.vala b/src/Shortcuts/Backend/CustomShortcuts.vala new file mode 100644 index 00000000..a11c7815 --- /dev/null +++ b/src/Shortcuts/Backend/CustomShortcuts.vala @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +namespace Keyboard.Shortcuts.CustomShortcuts { + public enum ActionType { + DESKTOP_FILE, + COMMAND_LINE + } + + public struct ParsedShortcut { + ActionType type; + string target; + GLib.HashTable parameters; + string[] keybindings; + } + + public const string SETTINGS_SCHEMA = "io.elementary.settings-daemon.applications"; + public const string APPLICATION_SHORTCUTS = "application-shortcuts"; +} diff --git a/src/Shortcuts/Shortcuts.vala b/src/Shortcuts/Shortcuts.vala index 684dce76..4abb46e5 100644 --- a/src/Shortcuts/Shortcuts.vala +++ b/src/Shortcuts/Shortcuts.vala @@ -62,8 +62,6 @@ namespace Keyboard.Shortcuts { private SwitcherRow custom_shortcuts_row; construct { - CustomShortcutSettings.init (); - unowned var list = Shortcuts.ShortcutsList.get_default (); section_switcher = new Gtk.ListBox (); @@ -113,12 +111,7 @@ namespace Keyboard.Shortcuts { for (int id = 0; id < SectionID.CUSTOM; id++) { shortcut_views += new ShortcutListBox ((SectionID) id); } - - if (CustomShortcutSettings.available) { - var custom_tree = new CustomShortcutListBox (); - - shortcut_views += custom_tree; - } + shortcut_views += new CustomShortcutListBox (); foreach (unowned Gtk.Widget view in shortcut_views) { stack.add_child (view); diff --git a/src/Shortcuts/Widgets/AppChooser.vala b/src/Shortcuts/Widgets/AppChooser.vala new file mode 100644 index 00000000..c35a2f6d --- /dev/null +++ b/src/Shortcuts/Widgets/AppChooser.vala @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Keyboard.AppChooser : Granite.Dialog { + public signal void app_chosen (string filename, GLib.HashTable parameters); + public signal void custom_command_chosen (string command, GLib.HashTable parameters); + + private Gtk.ListBox list; + private Gtk.SearchEntry search_entry; + private Gtk.Entry custom_entry; + + construct { + search_entry = new Gtk.SearchEntry () { + placeholder_text = _("Search Applications") + }; + + list = new Gtk.ListBox () { + hexpand = true, + vexpand = true + }; + list.add_css_class (Granite.STYLE_CLASS_RICH_LIST); + list.set_sort_func (sort_function); + list.set_filter_func (filter_function); + + var scrolled = new Gtk.ScrolledWindow () { + child = list + }; + + var frame = new Gtk.Frame (null) { + child = scrolled + }; + + custom_entry = new Gtk.Entry () { + placeholder_text = _("Type in a custom command"), + primary_icon_activatable = false, + primary_icon_name = "utilities-terminal-symbolic" + }; + + var box = new Gtk.Box (VERTICAL, 6); + box.append (search_entry); + box.append (frame); + box.append (custom_entry); + + modal = true; + default_height = 500; + default_width = 400; + get_content_area ().append (box); + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + + // TRANSLATORS: This string is used by screen reader + update_property (Gtk.AccessibleProperty.LABEL, _("Select startup app"), -1); + + search_entry.grab_focus (); + search_entry.search_changed.connect (() => { + list.invalidate_filter (); + }); + + response.connect (hide); + + list.row_activated.connect (on_app_selected); + custom_entry.activate.connect (on_custom_command_entered); + } + + public void init_list (GLib.List app_infos) { + foreach (var app_info in app_infos) { + var icon = app_info.get_icon () ?? new ThemedIcon ("application-default-icon"); + var name = app_info.get_name (); + var filename = File.new_for_path (app_info.get_filename ()).get_basename (); + + list.prepend (new AppChooserRow ({icon, name, app_info.get_description (), null, filename})); + + var actions = app_info.list_actions (); + for (var i = 0; i < actions.length; i++) { + var action = actions[i]; + list.prepend (new AppChooserRow ({icon, name, app_info.get_action_name (action), action, filename})); + } + } + } + + private int sort_function (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) { + unowned AppChooserRow row_1 = (AppChooserRow) row1.get_child (); + unowned AppChooserRow row_2 = (AppChooserRow) row2.get_child (); + + var name_1 = row_1.info.name; + var name_2 = row_2.info.name; + + return name_1.collate (name_2); + } + + private bool filter_function (Gtk.ListBoxRow list_box_row) { + var app_row = (AppChooserRow) list_box_row.get_child (); + return search_entry.text.down () in app_row.info.name.down () + || search_entry.text.down () in app_row.info.name.down (); + } + + private void on_app_selected (Gtk.ListBoxRow list_box_row) { + var app_row = (AppChooserRow) list_box_row.get_child (); + + var parameters = new GLib.HashTable (null, null); + if (app_row.info.action != null) { + parameters["action"] = app_row.info.action; + } + + app_chosen (app_row.info.filename, parameters); + hide (); + } + + private void on_custom_command_entered () { + custom_command_chosen ( + custom_entry.text, + new GLib.HashTable (null, null) + ); + hide (); + } +} diff --git a/src/Shortcuts/Widgets/AppChooserRow.vala b/src/Shortcuts/Widgets/AppChooserRow.vala new file mode 100644 index 00000000..b1b28c94 --- /dev/null +++ b/src/Shortcuts/Widgets/AppChooserRow.vala @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Keyboard.AppChooserRow : Gtk.Grid { + public struct Info { + GLib.Icon icon; + string? name; + string? description; + string? action; + string filename; + } + + public Info info { get; construct; } + + public AppChooserRow (Info info) { + Object (info: info); + } + + construct { + var image = new Gtk.Image () { + pixel_size = 32, + gicon = info.icon + }; + + var app_name = new Gtk.Label (info.name) { + xalign = 0, + ellipsize = Pango.EllipsizeMode.END + }; + + var app_comment = new Gtk.Label (info.description) { + xalign = 0, + ellipsize = Pango.EllipsizeMode.END + }; + app_comment.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + + column_spacing = 6; + attach (image, 0, 0, 1, 2); + attach (app_name, 1, 0); + attach (app_comment, 1, 1); + } +} diff --git a/src/Shortcuts/Widgets/ConflictDialog.vala b/src/Shortcuts/Widgets/ConflictDialog.vala deleted file mode 100644 index d6d917ac..00000000 --- a/src/Shortcuts/Widgets/ConflictDialog.vala +++ /dev/null @@ -1,46 +0,0 @@ -/* -* Copyright (c) 2017-2018 elementary, LLC. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public -* License as published by the Free Software Foundation; either -* version 2 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - -public class ConflictDialog : Granite.MessageDialog { - public signal void responded (int response_id); - - public ConflictDialog (string shortcut, string conflict_action, string this_action) { - Object ( - image_icon: new GLib.ThemedIcon ("dialog-warning"), - primary_text: _("%s is already used for %s").printf (shortcut, conflict_action), - secondary_text: _("If you reassign the shortcut to %s, %s will be disabled.").printf (this_action, conflict_action) - ); - } - - construct { - deletable = false; - modal = true; - resizable = false; - - add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - - var reassign_button = add_button (_("Reassign"), Gtk.ResponseType.ACCEPT); - reassign_button.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); - - response.connect ((response_id) => { - responded (response_id); - destroy (); - }); - } -} diff --git a/src/Shortcuts/Widgets/CustomShortcutListBox.vala b/src/Shortcuts/Widgets/CustomShortcutListBox.vala index 5cb06c18..751c8e55 100644 --- a/src/Shortcuts/Widgets/CustomShortcutListBox.vala +++ b/src/Shortcuts/Widgets/CustomShortcutListBox.vala @@ -4,7 +4,13 @@ */ class Keyboard.Shortcuts.CustomShortcutListBox : Gtk.Box { + private GLib.Settings settings; + private string[] preferred_languages; + private ulong settings_load_id = 0; + private Gtk.ListBox list_box; + private AppChooser app_chooser; + construct { list_box = new Gtk.ListBox () { hexpand = true, @@ -37,9 +43,34 @@ class Keyboard.Shortcuts.CustomShortcutListBox : Gtk.Box { append (list_box); append (actionbar); + settings = new GLib.Settings ("io.elementary.settings-daemon.applications"); + preferred_languages = Intl.get_language_names (); + load_and_display_custom_shortcuts (); + settings_load_id = settings.changed.connect (load_and_display_custom_shortcuts); + + var app_infos = new GLib.List (); + foreach (var app_info in GLib.AppInfo.get_all ()) { + if (app_info is GLib.DesktopAppInfo && app_info.should_show ()) { + app_infos.append ((GLib.DesktopAppInfo) app_info); + } + } - add_button.clicked.connect (on_add_clicked); + app_chooser = new AppChooser (); + app_chooser.init_list (app_infos); + + add_button.clicked.connect (() => { + app_chooser.transient_for = (Gtk.Window) get_root (); + app_chooser.present (); + }); + + app_chooser.app_chosen.connect ((filename, parameters) => { + add_new_shortcut (CustomShortcuts.ActionType.DESKTOP_FILE, filename, parameters); + }); + + app_chooser.custom_command_chosen.connect ((command, parameters) => { + add_new_shortcut (CustomShortcuts.ActionType.COMMAND_LINE, command, parameters); + }); } private void load_and_display_custom_shortcuts () { @@ -47,27 +78,53 @@ class Keyboard.Shortcuts.CustomShortcutListBox : Gtk.Box { list_box.remove (list_box.get_row_at_index (0)); } - foreach (var custom_shortcut in CustomShortcutSettings.list_custom_shortcuts ()) { - list_box.append (new CustomShortcutRow (custom_shortcut)); + var shortcuts = (CustomShortcuts.ParsedShortcut[]) settings.get_value (CustomShortcuts.APPLICATION_SHORTCUTS); + for (var i = 0; i < shortcuts.length; i++) { + var row = new CustomShortcutRow (shortcuts[i]); + row.shortcut_changed.connect (sync_shortcuts); + list_box.append (row); } } - private void add_row (CustomShortcut? shortcut) { - CustomShortcutRow new_row; - if (shortcut != null) { - new_row = new CustomShortcutRow (shortcut); - } else { - var relocatable_schema = CustomShortcutSettings.create_shortcut (); - CustomShortcut new_custom_shortcut = {"", "", relocatable_schema}; - new_row = new CustomShortcutRow (new_custom_shortcut); - } + private void add_new_shortcut (CustomShortcuts.ActionType type, string target, GLib.HashTable parameters) { + CustomShortcuts.ParsedShortcut new_shortcut = { + type, + target, + parameters, + {} + }; - list_box.append (new_row); - list_box.select_row (new_row); + var shortcuts = (CustomShortcuts.ParsedShortcut[]) settings.get_value (CustomShortcuts.APPLICATION_SHORTCUTS); + shortcuts += new_shortcut; + settings.set_value (CustomShortcuts.APPLICATION_SHORTCUTS, shortcuts); } - private void on_add_clicked () { - add_row (null); - list_box.unselect_all (); + private void sync_shortcuts () { + Gtk.Widget? _row = list_box.get_first_child (); + if (_row == null) { + return; + } + + var should_rebuild_list = false; + CustomShortcuts.ParsedShortcut[] shortcuts = {}; + do { + var row = (CustomShortcutRow) _row; + if (row.shortcut == null) { + should_rebuild_list = true; + continue; + } + + shortcuts += row.shortcut; + } while ((_row = _row.get_next_sibling ()) != null); + + if (!should_rebuild_list) { + GLib.SignalHandler.block (settings, settings_load_id); + } + + settings.set_value (CustomShortcuts.APPLICATION_SHORTCUTS, shortcuts); + + if (!should_rebuild_list) { + GLib.SignalHandler.unblock (settings, settings_load_id); + } } } diff --git a/src/Shortcuts/Widgets/CustomShortcutRow.vala b/src/Shortcuts/Widgets/CustomShortcutRow.vala index 730e6eb5..a3540dc1 100644 --- a/src/Shortcuts/Widgets/CustomShortcutRow.vala +++ b/src/Shortcuts/Widgets/CustomShortcutRow.vala @@ -1,41 +1,65 @@ /* - * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-License-Identifier: GPL-3.0-or-later * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { - private const string BINDING_KEY = "binding"; - private const string COMMAND_KEY = "command"; - private const string NAME_KEY = "name"; - private Gtk.Entry command_entry; - private Variant previous_binding; - - public string relocatable_schema { get; construct; } - public GLib.Settings gsettings { get; construct; } - private bool is_editing_shortcut = false; + public signal void shortcut_changed (); + + public CustomShortcuts.ParsedShortcut? shortcut { get; construct set; } private Gtk.Button clear_button; private Gtk.Box keycap_box; private Gtk.Label status_label; private Gtk.Stack keycap_stack; - public CustomShortcutRow (CustomShortcut _custom_shortcut) { - Object ( - relocatable_schema: _custom_shortcut.relocatable_schema, - gsettings: CustomShortcutSettings.get_gsettings_for_relocatable_schema (_custom_shortcut.relocatable_schema) - ); + private bool is_editing_shortcut = false; - command_entry.text = _custom_shortcut.command; + public CustomShortcutRow (CustomShortcuts.ParsedShortcut shortcut) { + Object (shortcut: shortcut); } construct { - command_entry = new Gtk.Entry () { - max_width_chars = 500, - has_frame = false, + GLib.Icon icon; + string name, description; + if (shortcut.type == DESKTOP_FILE) { + var desktop_file = new DesktopAppInfo (shortcut.target); + icon = desktop_file.get_icon () ?? new ThemedIcon ("application-default-icon"); + name = desktop_file.get_name (); + description = ( + "action" in shortcut.parameters ? + desktop_file.get_action_name (shortcut.parameters["action"].get_string ()) : + desktop_file.get_description () + ); + } else { + icon = new ThemedIcon ("application-default-icon"); + name = _("Custom Command"); + description = shortcut.target; + } + + var image = new Gtk.Image () { + pixel_size = 32, + gicon = icon + }; + + var app_name = new Gtk.Label (name) { + xalign = 0 + }; + + var app_comment = new Gtk.Label (description) { + ellipsize = Pango.EllipsizeMode.END, hexpand = true, - halign = START, - placeholder_text = _("Enter a command here") + xalign = 0 }; + app_comment.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + + var app_grid = new Gtk.Grid () { + column_spacing = 6, + hexpand = true + }; + app_grid.attach (image, 0, 0, 1, 2); + app_grid.attach (app_name, 1, 0); + app_grid.attach (app_comment, 1, 1); status_label = new Gtk.Label (_("Disabled")) { halign = END @@ -95,6 +119,7 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { var menubutton = new Gtk.MenuButton () { has_frame = false, + valign = CENTER, icon_name = "open-menu-symbolic", popover = popover }; @@ -106,7 +131,7 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { margin_start = 6, valign = CENTER }; - box.append (command_entry); + box.append (app_grid); box.append (keycap_stack); box.append (menubutton); @@ -114,107 +139,65 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { render_keycaps (); - gsettings.changed[BINDING_KEY].connect (render_keycaps); - gsettings.changed[COMMAND_KEY].connect (() => { - var new_text = gsettings.get_string (COMMAND_KEY); - if (new_text != command_entry.text) { - command_entry.text = new_text; - } - }); - clear_button.clicked.connect (() => { popover.popdown (); if (!is_editing_shortcut) { - gsettings.set_string (BINDING_KEY, ""); + shortcut.keybindings = {}; + render_keycaps (); + shortcut_changed (); } }); remove_button.clicked.connect (() => { popover.popdown (); - CustomShortcutSettings.remove_shortcut (relocatable_schema); + shortcut = null; + shortcut_changed (); unparent (); }); set_accel_button.clicked.connect (() => { popover.popdown (); - if (!is_editing_shortcut) { - edit_shortcut (true); - } + edit_shortcut (true); }); var keycap_controller = new Gtk.GestureClick (); keycap_stack.add_controller (keycap_controller); keycap_controller.released.connect (() => { - if (!is_editing_shortcut) { - edit_shortcut (true); - } + edit_shortcut (true); }); var status_controller = new Gtk.GestureClick (); status_label.add_controller (status_controller); status_controller.released.connect (() => { - if (!is_editing_shortcut) { - edit_shortcut (true); - } - }); - - var command_entry_focus_controller = new Gtk.EventControllerFocus (); - command_entry.add_controller (command_entry_focus_controller); - command_entry_focus_controller.enter.connect (() => { - cancel_editing_shortcut (); - ((Gtk.ListBox)parent).select_row (this); - }); - - command_entry.changed.connect (() => { - assert (is_editing_shortcut == false); - var command = command_entry.text; - gsettings.set_string (COMMAND_KEY, command); - gsettings.set_string (NAME_KEY, command); + edit_shortcut (true); }); var key_controller = new Gtk.EventControllerKey (); key_controller.key_released.connect (on_key_released); - add_controller (key_controller); - } - private void cancel_editing_shortcut () { - if (is_editing_shortcut) { - gsettings.set_value (BINDING_KEY, previous_binding); + var focus_controller = new Gtk.EventControllerFocus (); + focus_controller.leave.connect (() => { edit_shortcut (false); - } + }); + add_controller (focus_controller); } private void edit_shortcut (bool start_editing) { //Ensure device grabs are paired if (start_editing && !is_editing_shortcut) { ((Gdk.Toplevel) get_root ().get_surface ()).inhibit_system_shortcuts (null); - ((Gtk.ListBox)parent).select_row (this); - grab_focus (); - - var focus_controller = new Gtk.EventControllerFocus (); - focus_controller.leave.connect (() => { - focus_controller.dispose (); - cancel_editing_shortcut (); - }); - add_controller (focus_controller); - - previous_binding = gsettings.get_value (BINDING_KEY); - gsettings.set_string (BINDING_KEY, ""); + keycap_stack.visible_child = status_label; + status_label.label = _("Enter new shortcut…"); + ((Gtk.ListBox) parent).select_row (this); + grab_focus (); } else if (!start_editing && is_editing_shortcut) { ((Gdk.Toplevel) get_root ().get_surface ()).restore_system_shortcuts (); + render_keycaps (); } is_editing_shortcut = start_editing; - - if (is_editing_shortcut) { - keycap_stack.visible_child = status_label; - status_label.label = _("Enter new shortcut…"); - } else { - keycap_stack.visible_child = keycap_box; - render_keycaps (); - } } private void on_key_released (Gtk.EventControllerKey controller, uint keyval, uint keycode, Gdk.ModifierType state) { @@ -231,7 +214,6 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { switch (keyval) { case Gdk.Key.Escape: // Cancel editing - gsettings.set_value (BINDING_KEY, previous_binding); break; // case Gdk.Key.F1: May be used for system help case Gdk.Key.F2: @@ -274,15 +256,14 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { return ; } - private void update_binding (Shortcut shortcut) { + private void update_binding (Shortcut new_shortcut) { string conflict_name = ""; string group = ""; - string relocatable_schema = ""; - if (ConflictsManager.shortcut_conflicts (shortcut, out conflict_name, out group)) { + if (ConflictsManager.shortcut_conflicts (new_shortcut, out conflict_name, out group)) { var message_dialog = new Granite.MessageDialog ( _("Unable to set new shortcut due to conflicts"), _("“%s” is already used for “%s → %s”.").printf ( - shortcut.to_readable (), group, conflict_name + new_shortcut.to_readable (), group, conflict_name ), new ThemedIcon ("preferences-desktop-keyboard"), Gtk.ButtonsType.CLOSE @@ -297,39 +278,15 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { }); message_dialog.present (); - gsettings.set_value (BINDING_KEY, previous_binding); return; - } else if (CustomShortcutSettings.shortcut_conflicts (shortcut, out conflict_name, out relocatable_schema)) { - var dialog = new ConflictDialog (shortcut.to_readable (), conflict_name, command_entry.text); - dialog.responded.connect ((response_id) => { - if (response_id == Gtk.ResponseType.ACCEPT) { - gsettings.set_string (BINDING_KEY, shortcut.to_gsettings ()); - var conflict_gsettings = CustomShortcutSettings.get_gsettings_for_relocatable_schema (relocatable_schema); - conflict_gsettings.set_string (BINDING_KEY, ""); - } else { - gsettings.set_value (BINDING_KEY, previous_binding); - } - }); - - dialog.transient_for = (Gtk.Window) this.get_root (); - dialog.present (); } else { - gsettings.set_string (BINDING_KEY, shortcut.to_gsettings ()); + shortcut.keybindings = { new_shortcut.to_gsettings () }; + shortcut_changed (); } } private void render_keycaps () { - var key_value = gsettings.get_value (BINDING_KEY); - var value_string = ""; - - if (key_value.is_of_type (VariantType.ARRAY)) { - var key_value_strv = key_value.get_strv (); - if (key_value_strv.length > 0) { - value_string = key_value_strv[0]; - } - } else { - value_string = key_value.dup_string (); - } + var value_string = shortcut.keybindings.length > 0 ? shortcut.keybindings[0] : ""; if (value_string != "") { build_keycap_box (value_string, ref keycap_box); @@ -340,7 +297,7 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { keycap_stack.visible_child = status_label; status_label.label = _("Disabled"); } - } + } private void build_keycap_box (string value_string, ref Gtk.Box box) { var accels_string = Granite.accel_to_string (value_string); diff --git a/src/meson.build b/src/meson.build index 3645467a..1f3f48af 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,11 +33,12 @@ plug_files = files( 'Layout/Widgets/Display.vala', 'Layout/Layout.vala', 'Shortcuts/Backend/ConflictsManager.vala', - 'Shortcuts/Backend/CustomShortcutSettings.vala', + 'Shortcuts/Backend/CustomShortcuts.vala', 'Shortcuts/Backend/Settings.vala', 'Shortcuts/Backend/Shortcut.vala', 'Shortcuts/Backend/ShortcutsList.vala', - 'Shortcuts/Widgets/ConflictDialog.vala', + 'Shortcuts/Widgets/AppChooser.vala', + 'Shortcuts/Widgets/AppChooserRow.vala', 'Shortcuts/Widgets/CustomShortcutListBox.vala', 'Shortcuts/Widgets/CustomShortcutRow.vala', 'Shortcuts/Widgets/ShortcutListBox.vala', From 3efd30b2960d1c8594a36cd19b12270c0e78f12e Mon Sep 17 00:00:00 2001 From: lenemter Date: Tue, 20 May 2025 00:36:38 +0300 Subject: [PATCH 2/2] Address review comments --- po/POTFILES | 1 + src/Shortcuts/Backend/Utils.vala | 29 ++++++++++ src/Shortcuts/Widgets/AppChooser.vala | 22 ++++---- src/Shortcuts/Widgets/AppChooserRow.vala | 24 +++++++- src/Shortcuts/Widgets/CustomShortcutRow.vala | 59 +++++++++++++++----- src/meson.build | 1 + 6 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 src/Shortcuts/Backend/Utils.vala diff --git a/po/POTFILES b/po/POTFILES index 4336f914..3da102ad 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -24,6 +24,7 @@ src/Shortcuts/Backend/CustomShortcuts.vala src/Shortcuts/Backend/Settings.vala src/Shortcuts/Backend/Shortcut.vala src/Shortcuts/Backend/ShortcutsList.vala +src/Shortcuts/Backend/Utils.vala src/Shortcuts/Widgets/AppChooser.vala src/Shortcuts/Widgets/AppChooserRow.vala src/Shortcuts/Widgets/CustomShortcutListBox.vala diff --git a/src/Shortcuts/Backend/Utils.vala b/src/Shortcuts/Backend/Utils.vala new file mode 100644 index 00000000..45da3a8e --- /dev/null +++ b/src/Shortcuts/Backend/Utils.vala @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +namespace Keyboard.Shortcuts.Utils { + public GLib.Icon? get_action_icon (GLib.DesktopAppInfo app_info, string action) { + unowned var icon_theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()); + + GLib.Icon? action_icon = null; + try { + var keyfile = new GLib.KeyFile (); + keyfile.load_from_file (app_info.get_filename (), GLib.KeyFileFlags.NONE); + + var group = "Desktop Action %s".printf (action); + if (keyfile.has_key (group, GLib.KeyFileDesktop.KEY_ICON)) { + var icon_name = keyfile.get_string (group, GLib.KeyFileDesktop.KEY_ICON); + + if (icon_theme.has_icon (icon_name)) { + action_icon = new ThemedIcon (icon_name); + } + } + } catch (Error e) { + warning (e.message); + } + + return action_icon; + } +} diff --git a/src/Shortcuts/Widgets/AppChooser.vala b/src/Shortcuts/Widgets/AppChooser.vala index c35a2f6d..3f2de893 100644 --- a/src/Shortcuts/Widgets/AppChooser.vala +++ b/src/Shortcuts/Widgets/AppChooser.vala @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class Keyboard.AppChooser : Granite.Dialog { +public class Keyboard.Shortcuts.AppChooser : Granite.Dialog { public signal void app_chosen (string filename, GLib.HashTable parameters); public signal void custom_command_chosen (string command, GLib.HashTable parameters); @@ -14,6 +14,7 @@ public class Keyboard.AppChooser : Granite.Dialog { search_entry = new Gtk.SearchEntry () { placeholder_text = _("Search Applications") }; + search_entry.set_key_capture_widget (this); list = new Gtk.ListBox () { hexpand = true, @@ -24,11 +25,8 @@ public class Keyboard.AppChooser : Granite.Dialog { list.set_filter_func (filter_function); var scrolled = new Gtk.ScrolledWindow () { - child = list - }; - - var frame = new Gtk.Frame (null) { - child = scrolled + child = list, + has_frame = true }; custom_entry = new Gtk.Entry () { @@ -39,7 +37,7 @@ public class Keyboard.AppChooser : Granite.Dialog { var box = new Gtk.Box (VERTICAL, 6); box.append (search_entry); - box.append (frame); + box.append (scrolled); box.append (custom_entry); modal = true; @@ -49,7 +47,7 @@ public class Keyboard.AppChooser : Granite.Dialog { add_button (_("Cancel"), Gtk.ResponseType.CANCEL); // TRANSLATORS: This string is used by screen reader - update_property (Gtk.AccessibleProperty.LABEL, _("Select startup app"), -1); + update_property (Gtk.AccessibleProperty.LABEL, _("Select an app"), -1); search_entry.grab_focus (); search_entry.search_changed.connect (() => { @@ -66,14 +64,18 @@ public class Keyboard.AppChooser : Granite.Dialog { foreach (var app_info in app_infos) { var icon = app_info.get_icon () ?? new ThemedIcon ("application-default-icon"); var name = app_info.get_name (); + var description = app_info.get_description (); var filename = File.new_for_path (app_info.get_filename ()).get_basename (); - list.prepend (new AppChooserRow ({icon, name, app_info.get_description (), null, filename})); + list.prepend (new AppChooserRow ({icon, null, name, description, null, filename})); + unowned var icon_theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()); var actions = app_info.list_actions (); for (var i = 0; i < actions.length; i++) { var action = actions[i]; - list.prepend (new AppChooserRow ({icon, name, app_info.get_action_name (action), action, filename})); + var action_icon = Utils.get_action_icon (app_info, action); + var action_name = "%s → %s".printf (name, app_info.get_action_name (action)); + list.prepend (new AppChooserRow ({icon, action_icon, action_name, description, action, filename})); } } } diff --git a/src/Shortcuts/Widgets/AppChooserRow.vala b/src/Shortcuts/Widgets/AppChooserRow.vala index b1b28c94..dd5bd447 100644 --- a/src/Shortcuts/Widgets/AppChooserRow.vala +++ b/src/Shortcuts/Widgets/AppChooserRow.vala @@ -2,9 +2,10 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class Keyboard.AppChooserRow : Gtk.Grid { +public class Keyboard.Shortcuts.AppChooserRow : Gtk.Grid { public struct Info { GLib.Icon icon; + GLib.Icon? overlay_icon; string? name; string? description; string? action; @@ -23,6 +24,21 @@ public class Keyboard.AppChooserRow : Gtk.Grid { gicon = info.icon }; + Gtk.Overlay? overlay = null; + if (info.overlay_icon != null) { + var overlay_image = new Gtk.Image () { + pixel_size = 16, + gicon = info.overlay_icon, + halign = END, + valign = END + }; + + overlay = new Gtk.Overlay () { + child = image + }; + overlay.add_overlay (overlay_image); + } + var app_name = new Gtk.Label (info.name) { xalign = 0, ellipsize = Pango.EllipsizeMode.END @@ -35,7 +51,11 @@ public class Keyboard.AppChooserRow : Gtk.Grid { app_comment.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); column_spacing = 6; - attach (image, 0, 0, 1, 2); + if (overlay != null) { + attach (overlay, 0, 0, 1, 2); + } else { + attach (image, 0, 0, 1, 2); + } attach (app_name, 1, 0); attach (app_comment, 1, 1); } diff --git a/src/Shortcuts/Widgets/CustomShortcutRow.vala b/src/Shortcuts/Widgets/CustomShortcutRow.vala index a3540dc1..4e78a6e7 100644 --- a/src/Shortcuts/Widgets/CustomShortcutRow.vala +++ b/src/Shortcuts/Widgets/CustomShortcutRow.vala @@ -20,28 +20,57 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { } construct { - GLib.Icon icon; string name, description; + Gtk.Widget icon_widget; if (shortcut.type == DESKTOP_FILE) { var desktop_file = new DesktopAppInfo (shortcut.target); - icon = desktop_file.get_icon () ?? new ThemedIcon ("application-default-icon"); - name = desktop_file.get_name (); - description = ( - "action" in shortcut.parameters ? - desktop_file.get_action_name (shortcut.parameters["action"].get_string ()) : - desktop_file.get_description () - ); + + string? action = null; + if ("action" in shortcut.parameters) { + action = shortcut.parameters["action"].get_string (); + } + + if (action != null) { + name = "%s -> %s".printf (desktop_file.get_name (), desktop_file.get_action_name (action)); + } else { + name = desktop_file.get_name (); + } + + description = desktop_file.get_description (); + + var image = new Gtk.Image () { + pixel_size = 32, + gicon = desktop_file.get_icon () ?? new ThemedIcon ("application-default-icon") + }; + + if (action != null) { + var action_icon = Utils.get_action_icon (desktop_file, action); + + var overlay_image = new Gtk.Image () { + pixel_size = 16, + gicon = action_icon, + halign = END, + valign = END + }; + + var overlay = new Gtk.Overlay () { + child = image + }; + overlay.add_overlay (overlay_image); + + icon_widget = overlay; + } else { + icon_widget = image; + } } else { - icon = new ThemedIcon ("application-default-icon"); name = _("Custom Command"); description = shortcut.target; + icon_widget = new Gtk.Image () { + pixel_size = 32, + gicon = new ThemedIcon ("application-default-icon") + }; } - var image = new Gtk.Image () { - pixel_size = 32, - gicon = icon - }; - var app_name = new Gtk.Label (name) { xalign = 0 }; @@ -57,7 +86,7 @@ private class Keyboard.Shortcuts.CustomShortcutRow : Gtk.ListBoxRow { column_spacing = 6, hexpand = true }; - app_grid.attach (image, 0, 0, 1, 2); + app_grid.attach (icon_widget, 0, 0, 1, 2); app_grid.attach (app_name, 1, 0); app_grid.attach (app_comment, 1, 1); diff --git a/src/meson.build b/src/meson.build index 1f3f48af..0bac447b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,6 +37,7 @@ plug_files = files( 'Shortcuts/Backend/Settings.vala', 'Shortcuts/Backend/Shortcut.vala', 'Shortcuts/Backend/ShortcutsList.vala', + 'Shortcuts/Backend/Utils.vala', 'Shortcuts/Widgets/AppChooser.vala', 'Shortcuts/Widgets/AppChooserRow.vala', 'Shortcuts/Widgets/CustomShortcutListBox.vala',