diff --git a/po/POTFILES b/po/POTFILES index ac40e96f2..80b46b822 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -14,6 +14,7 @@ src/Shortcuts/Shortcut.vala src/Shortcuts/Settings.vala src/Shortcuts/List.vala src/Shortcuts/CustomShortcutSettings.vala +src/Shortcuts/ApplicationShortcutSettings.vala src/Layout/XkbModifier.vala src/Layout/Settings.vala src/Layout/Handler.vala diff --git a/src/Shortcuts/ApplicationShortcutSettings.vala b/src/Shortcuts/ApplicationShortcutSettings.vala new file mode 100644 index 000000000..4aa1136c3 --- /dev/null +++ b/src/Shortcuts/ApplicationShortcutSettings.vala @@ -0,0 +1,165 @@ +/* +* Copyright (c) 2017 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 +*/ + +class Pantheon.Keyboard.Shortcuts.ApplicationShortcutSettings : Object { + + const string SCHEMA_DEFAULT = "org.pantheon.desktop.gala.keybindings.applications"; + const string SCHEMA_CUSTOM = "org.pantheon.desktop.gala.keybindings.applications.custom"; + const string KEY_TEMPLATE = "applications-custom%d"; + const string KEY_DESKTOP_IDS = "desktop-ids"; + const int MAX_SHORTCUTS = 10; + static HashTable keybindings_to_types; + static GLib.Settings settings_custom; + static GLib.Settings settings_default; + + public struct CustomShortcut { + string name; + string desktop_id; + string shortcut; + string key; + } + + public static void init () { + keybindings_to_types = new HashTable (str_hash, str_equal); + keybindings_to_types.insert ("applications-webbrowser", "x-scheme-handler/http"); + keybindings_to_types.insert ("applications-emailclient", "x-scheme-handler/mailto"); + keybindings_to_types.insert ("applications-calendar", "text/calendar"); + keybindings_to_types.insert ("applications-videoplayer", "video/x-ogm+ogg"); + keybindings_to_types.insert ("applications-musicplayer", "audio/x-vorbis+ogg"); + keybindings_to_types.insert ("applications-imageviewer", "image/jpeg"); + keybindings_to_types.insert ("applications-texteditor", "text/plain"); + keybindings_to_types.insert ("applications-filebrowser", "inode/directory"); + keybindings_to_types.insert ("applications-terminal", ""); + + var schema_source = GLib.SettingsSchemaSource.get_default (); + + var schema_default = schema_source.lookup (SCHEMA_DEFAULT, true); + + if (schema_default == null) { + warning ("Schema \"%s\" is not installed on your system.", SCHEMA_DEFAULT); + return; + } + + settings_default = new GLib.Settings.full (schema_default, null, null); + + var schema_custom = schema_source.lookup (SCHEMA_CUSTOM, true); + + if (schema_custom == null) { + warning ("Schema \"%s\" is not installed on your system.", SCHEMA_CUSTOM); + return; + } + + settings_custom = new GLib.Settings.full (schema_custom, null, null); + } + + public static GLib.List list_custom_shortcuts () { + var desktop_ids = settings_custom.get_strv (KEY_DESKTOP_IDS); + var l = new GLib.List (); + for (int i = 0; i < MAX_SHORTCUTS; i++) { + var desktop_id = desktop_ids [i]; + if (desktop_id != "") { + var key = KEY_TEMPLATE.printf (i); + l.append ({ + (new DesktopAppInfo (desktop_id)).get_name (), + desktop_id, + settings_custom.get_strv (key) [0], + key + }); + } + } + + return l; + } + + public static GLib.List list_default_shortcuts () { + GLib.List l = null; + var keys = list.launchers_group.keys; + var actions = list.launchers_group.actions; + + for (var i=0; i < keys.length; i++) { + var key = keys [i]; + var action = actions [i]; + var type = keybindings_to_types.get (key); + string desktop_id; + + if (key == "applications-terminal") { // can't set default application for terminal + desktop_id = "io.elementary.terminal.desktop"; + } else { + desktop_id = AppInfo.get_default_for_type (type, false).get_id (); + } + + l.append ({ + action, + desktop_id, + settings_default.get_strv (key) [0], + key + }); + } + + return l; + } + + public static string? create_shortcut (AppInfo info) { + var desktop_ids = settings_custom.get_strv (KEY_DESKTOP_IDS); + + for (int i = 0; i < MAX_SHORTCUTS; i++) { + if (desktop_ids [i] == "") { + desktop_ids [i] = info.get_id (); + settings_custom.set_strv (KEY_DESKTOP_IDS, desktop_ids); + return KEY_TEMPLATE.printf (i); + } + } + + return (string) null; + } + + public static void remove_shortcut (string key) { + var index = int.parse(key.substring (-1)); + var desktop_ids = settings_custom.get_strv (KEY_DESKTOP_IDS); + desktop_ids [index] = ""; + settings_custom.set_strv (KEY_DESKTOP_IDS, desktop_ids); + settings_custom.set_strv (key, {""}); + } + + public static bool edit_shortcut (string key, Shortcut shortcut) { + var custom = key.slice (0, -1) == KEY_TEMPLATE.slice (0, -2); + var settings = custom ? settings_custom : settings_default; + settings.set_strv (key, {shortcut.to_gsettings ()}); + return true; + } + + public static bool shortcut_conflicts (Shortcut new_shortcut, out string name, out string key) { + var shortcuts = list_default_shortcuts (); + shortcuts.concat (list_custom_shortcuts ()); + + name = ""; + key = ""; + + foreach (var sc in shortcuts) { + var shortcut = new Shortcut.parse (sc.shortcut); + if (shortcut.is_equal (new_shortcut)) { + name = sc.name; + key = sc.key; + return true; + } + } + + return false; + } +} diff --git a/src/Shortcuts/CustomShortcutSettings.vala b/src/Shortcuts/CustomShortcutSettings.vala index e7fbc84df..a4a02cda8 100644 --- a/src/Shortcuts/CustomShortcutSettings.vala +++ b/src/Shortcuts/CustomShortcutSettings.vala @@ -63,6 +63,7 @@ class Pantheon.Keyboard.Shortcuts.CustomShortcutSettings : Object { } public static string? create_shortcut () requires (available) { + debug ("create shortcut!"); for (int i = 0; i < MAX_SHORTCUTS; i++) { var new_relocatable_schema = get_relocatable_schema_path (i); diff --git a/src/Shortcuts/List.vala b/src/Shortcuts/List.vala index 11fe42d63..7e3098195 100644 --- a/src/Shortcuts/List.vala +++ b/src/Shortcuts/List.vala @@ -102,11 +102,15 @@ namespace Pantheon.Keyboard.Shortcuts { launchers_group = {}; launchers_group.icon_name = "preferences-desktop-applications"; launchers_group.label = _("Applications"); - add_action (ref launchers_group, Schema.MEDIA, _("Email"), "email"); - add_action (ref launchers_group, Schema.MEDIA, _("Home Folder"), "home"); - add_action (ref launchers_group, Schema.MEDIA, _("Music"), "media"); - add_action (ref launchers_group, Schema.MEDIA, _("Terminal"), "terminal"); - add_action (ref launchers_group, Schema.MEDIA, _("Internet Browser"), "www"); + add_action (ref launchers_group, Schema.APPS, _("Web Browser"), "applications-webbrowser"); + add_action (ref launchers_group, Schema.APPS, _("Email Client"), "applications-emailclient"); + add_action (ref launchers_group, Schema.APPS, _("Calendar"), "applications-calendar"); + add_action (ref launchers_group, Schema.APPS, _("Video Player"), "applications-videoplayer"); + add_action (ref launchers_group, Schema.APPS, _("Music Player"), "applications-musicplayer"); + add_action (ref launchers_group, Schema.APPS, _("Image Viewer"), "applications-imageviewer"); + add_action (ref launchers_group, Schema.APPS, _("Text Editor"), "applications-texteditor"); + add_action (ref launchers_group, Schema.APPS, _("File Browser"), "applications-filebrowser"); + add_action (ref launchers_group, Schema.APPS, _("Terminal"), "applications-terminal"); media_group = {}; media_group.icon_name = "applications-multimedia"; diff --git a/src/Shortcuts/Settings.vala b/src/Shortcuts/Settings.vala index 9ea0206a3..3540db367 100644 --- a/src/Shortcuts/Settings.vala +++ b/src/Shortcuts/Settings.vala @@ -19,7 +19,7 @@ namespace Pantheon.Keyboard.Shortcuts { - private enum Schema { WM, MUTTER, GALA, MEDIA, COUNT } + private enum Schema { WM, MUTTER, GALA, APPS, MEDIA, COUNT } // helper class for gsettings // note that media key are stored as strings, all others as string vectors @@ -34,6 +34,7 @@ namespace Pantheon.Keyboard.Shortcuts "org.gnome.desktop.wm.keybindings", "org.gnome.mutter.keybindings", "org.pantheon.desktop.gala.keybindings", + "org.pantheon.desktop.gala.keybindings.applications", "org.gnome.settings-daemon.plugins.media-keys" }; diff --git a/src/Views/Shortcuts.vala b/src/Views/Shortcuts.vala index d872bd29a..0885630c0 100644 --- a/src/Views/Shortcuts.vala +++ b/src/Views/Shortcuts.vala @@ -51,12 +51,17 @@ namespace Pantheon.Keyboard.Shortcuts { construct { CustomShortcutSettings.init (); + ApplicationShortcutSettings.init (); list = new List (); settings = new Shortcuts.Settings (); for (int id = 0; id < SectionID.CUSTOM; id++) { - trees += new Tree ((SectionID) id); + if (id == SectionID.APPS) { + trees += new ApplicationTree (); + } else { + trees += new Tree ((SectionID) id); + } } if (CustomShortcutSettings.available) { @@ -73,7 +78,7 @@ namespace Pantheon.Keyboard.Shortcuts { section_switcher.add_section (list.system_group); section_switcher.add_section (list.custom_group); - section_switcher.set_selected (0); + section_switcher.set_selected (3); var shortcut_display = new ShortcutDisplay (trees); diff --git a/src/Widgets/Shortcuts/ApplicationTree.vala b/src/Widgets/Shortcuts/ApplicationTree.vala new file mode 100644 index 000000000..4bff93c33 --- /dev/null +++ b/src/Widgets/Shortcuts/ApplicationTree.vala @@ -0,0 +1,272 @@ +/* +* Copyright (c) 2017 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 +*/ + +namespace Pantheon.Keyboard.Shortcuts { + + private class ApplicationTree : Gtk.Viewport, DisplayTree { + Gtk.CellRendererText cell_desc; + Gtk.CellRendererAccel cell_edit_default; + Gtk.CellRendererAccel cell_edit_custom; + Gtk.CellRendererPixbuf cell_icon; + Gtk.TreeView tv_default_apps; + Gtk.TreeView tv_custom_apps; + + enum Column { + NAME, + DESKTOP_ID, + SHORTCUT, + ICON, + KEY, + COUNT, + } + + public signal void row_selected (); + public signal void row_unselected (); + + public ApplicationTree () { + setup_gui (); + load_and_display_shortcuts (); + connect_signals (); + } + + void setup_gui () { + var container = new Gtk.Grid (); + + var label_default_apps = new Gtk.Label (_("Default Applications")); + label_default_apps.get_style_context ().add_class ("h4"); + label_default_apps.halign = Gtk.Align.CENTER; + + var label_custom_apps = new Gtk.Label (_("Custom Applications")); + label_custom_apps.get_style_context ().add_class ("h4"); + label_custom_apps.halign = Gtk.Align.CENTER; + + tv_default_apps = new Gtk.TreeView (); + tv_custom_apps = new Gtk.TreeView (); + + var store_default_apps = new Gtk.ListStore (Column.COUNT, + typeof (string), typeof (string), typeof (string), typeof (GLib.Icon), typeof (string)); + + var store_custom_apps = new Gtk.ListStore (Column.COUNT, + typeof (string), typeof (string), typeof (string), typeof (GLib.Icon), typeof (string)); + + cell_desc = new Gtk.CellRendererText (); + cell_edit_default = new Gtk.CellRendererAccel (); + cell_edit_custom = new Gtk.CellRendererAccel (); + cell_icon = new Gtk.CellRendererPixbuf (); + + cell_desc.editable = false; + cell_edit_default.editable = true; + cell_edit_default.accel_mode = Gtk.CellRendererAccelMode.OTHER; + cell_edit_custom.editable = true; + cell_edit_custom.accel_mode = Gtk.CellRendererAccelMode.OTHER; + + tv_default_apps.set_model (store_default_apps); + tv_default_apps.insert_column_with_attributes (-1, _("Icon"), cell_icon, "gicon", Column.ICON); + tv_default_apps.insert_column_with_attributes (-1, _("Application"), cell_desc, "markup", Column.NAME); + tv_default_apps.insert_column_with_attributes (-1, _("Shortcut"), cell_edit_default, "text", Column.SHORTCUT); + + tv_default_apps.headers_visible = false; + tv_default_apps.hexpand = true; + tv_default_apps.get_column (1).expand = true; + + tv_custom_apps.set_model (store_custom_apps); + tv_custom_apps.insert_column_with_attributes (-1, _("Icon"), cell_icon, "gicon", Column.ICON); + tv_custom_apps.insert_column_with_attributes (-1, _("Application"), cell_desc, "markup", Column.NAME); + tv_custom_apps.insert_column_with_attributes (-1, _("Shortcut"), cell_edit_custom, "text", Column.SHORTCUT); + + tv_custom_apps.headers_visible = false; + tv_custom_apps.expand = true; + tv_custom_apps.get_column (1).expand = true; + + container.attach (label_default_apps, 0, 0, 1, 1); + container.attach (tv_default_apps, 0, 1, 1, 1); + container.attach (label_custom_apps, 0, 2, 1, 1); + container.attach (tv_custom_apps, 0, 3, 1, 1); + + add (container); + } + + public void load_and_display_shortcuts () { + Gtk.TreeIter iter; + var store_default_apps = tv_default_apps.model as Gtk.ListStore; + store_default_apps.clear (); + + foreach (var default_shortcut in ApplicationShortcutSettings.list_default_shortcuts ()) { + var shortcut = new Shortcut.parse (default_shortcut.shortcut); + + if (shortcut == null) + continue; + + var desktop_appinfo = new DesktopAppInfo (default_shortcut.desktop_id); + store_default_apps.append (out iter); + store_default_apps.set (iter, + Column.NAME, default_shortcut.name, + Column.DESKTOP_ID, default_shortcut.desktop_id, + Column.SHORTCUT, shortcut.to_readable (), + Column.ICON, desktop_appinfo.get_icon (), + Column.KEY, default_shortcut.key); + } + + var store_custom_apps = tv_custom_apps.model as Gtk.ListStore; + store_custom_apps.clear (); + + foreach (var custom_shortcut in ApplicationShortcutSettings.list_custom_shortcuts ()) { + var shortcut = new Shortcut.parse (custom_shortcut.shortcut); + + var desktop_appinfo = new DesktopAppInfo (custom_shortcut.desktop_id); + store_custom_apps.append (out iter); + store_custom_apps.set (iter, + Column.NAME, custom_shortcut.name, + Column.DESKTOP_ID, custom_shortcut.desktop_id, + Column.SHORTCUT, shortcut.to_readable (), + Column.ICON, desktop_appinfo.get_icon (), + Column.KEY, custom_shortcut.key); + } + } + + void connect_signals () { + var selection = tv_custom_apps.get_selection (); + selection.changed.connect (() => { + if (selection.count_selected_rows () > 0) { + row_selected (); + } else { + row_unselected (); + } + }); + + tv_default_apps.focus_in_event.connect (() => {row_unselected (); return false;}); + + cell_edit_default.accel_edited.connect ((path, key, mods) => { + var shortcut = new Shortcut (key, mods); + change_shortcut (path, shortcut, tv_default_apps); + }); + + cell_edit_default.accel_cleared.connect ((path) => { + change_shortcut (path, (Shortcut) null, tv_default_apps); + }); + + cell_edit_custom.accel_edited.connect ((path, key, mods) => { + var shortcut = new Shortcut (key, mods); + change_shortcut (path, shortcut, tv_custom_apps); + }); + + cell_edit_custom.accel_cleared.connect ((path) => { + change_shortcut (path, (Shortcut) null, tv_custom_apps); + }); + } + + + AppInfo? get_app_info_dialog () { + Gtk.AppChooserDialog dialog = new Gtk.AppChooserDialog.for_content_type ((Gtk.Window) this.get_toplevel (), 0, ""); + ((Gtk.AppChooserWidget) dialog.get_widget ()).show_all = true; + AppInfo? info = null; + if (dialog.run () == Gtk.ResponseType.OK) { + info = dialog.get_app_info (); + } + dialog.close (); + return info; + } + + public void on_add_clicked () { + var info = get_app_info_dialog (); + if (info == null) + return; + + var store = tv_custom_apps.model as Gtk.ListStore; + Gtk.TreeIter iter; + var key = ApplicationShortcutSettings.create_shortcut (info); + + if (key == null) + return; + + store.append (out iter); + store.set (iter, Column.DESKTOP_ID, info.get_name ()); + store.set (iter, Column.SHORTCUT, (new Shortcut.parse ("")).to_readable ()); + store.set (iter, Column.KEY, key); + + load_and_display_shortcuts (); + } + + public void on_remove_clicked () { + Gtk.TreeIter iter; + Gtk.TreePath path; + + tv_custom_apps.get_cursor (out path, null); + tv_custom_apps.model.get_iter (out iter, path); + remove_shortcut_for_iter (iter); + } + + public bool shortcut_conflicts (Shortcut shortcut, out string name) { + return ApplicationShortcutSettings.shortcut_conflicts (shortcut, out name, null); + } + + public void reset_shortcut (Shortcut shortcut) { + string key; + ApplicationShortcutSettings.shortcut_conflicts (shortcut, null, out key); + ApplicationShortcutSettings.edit_shortcut (key, new Shortcut ()); + load_and_display_shortcuts (); + } + + public bool change_shortcut (string path, Shortcut? shortcut, Gtk.TreeView tv) { + Gtk.TreeIter iter; + GLib.Value key, name; + + tv.model.get_iter (out iter, new Gtk.TreePath.from_string (path)); + tv.model.get_value (iter, Column.NAME, out name); + tv.model.get_value (iter, Column.KEY, out key); + + string conflict_name; + + if (shortcut != null) { + foreach (var tree in trees) { + if (tree.shortcut_conflicts (shortcut, out conflict_name) == false || conflict_name == (string) name) { + continue; + } + + var dialog = new ConflictDialog (shortcut.to_readable (), conflict_name, (string) name); + dialog.reassign.connect (() => { + tree.reset_shortcut (shortcut); + ApplicationShortcutSettings.edit_shortcut ((string) key, shortcut); + load_and_display_shortcuts (); + }); + dialog.transient_for = (Gtk.Window) this.get_toplevel (); + dialog.present (); + return false; + } + } + + ApplicationShortcutSettings.edit_shortcut ((string) key, shortcut ?? new Shortcut ()); + load_and_display_shortcuts (); + return true; + } + + void remove_shortcut_for_iter (Gtk.TreeIter iter) { + GLib.Value key; + tv_custom_apps.model.get_value (iter, Column.KEY, out key); + var store = tv_custom_apps.model as Gtk.ListStore; + + ApplicationShortcutSettings.remove_shortcut ((string) key); +#if VALA_0_36 + store.remove (ref iter); +#else + store.remove (iter); +#endif + } + } +} diff --git a/src/Widgets/Shortcuts/Display.vala b/src/Widgets/Shortcuts/Display.vala index 45c0e5f26..296abcea6 100644 --- a/src/Widgets/Shortcuts/Display.vala +++ b/src/Widgets/Shortcuts/Display.vala @@ -57,10 +57,29 @@ namespace Pantheon.Keyboard.Shortcuts { attach (scroll, 0, 0, 1, 1); attach (actionbar, 0, 1, 1, 1); - add_button.clicked.connect (() => (trees[selected] as CustomTree).on_add_clicked ()); - remove_button.clicked.connect (() => (trees[selected] as CustomTree).on_remove_clicked ()); + actionbar.no_show_all = selected != SectionID.CUSTOM && selected != SectionID.APPS; + actionbar.visible = selected == SectionID.CUSTOM || selected == SectionID.APPS; + + add_button.clicked.connect (() => { + if (selected == SectionID.CUSTOM) { + (trees[selected] as CustomTree).on_add_clicked (); + } else if (selected == SectionID.APPS) { + (trees[selected] as ApplicationTree).on_add_clicked (); + } + }); + + remove_button.clicked.connect (() => { + if (selected == SectionID.CUSTOM) { + (trees[selected] as CustomTree).on_remove_clicked (); + } else if (selected == SectionID.APPS) { + (trees[selected] as ApplicationTree).on_remove_clicked (); + } + }); + + change_selection (3); } + // replace old tree view with new one public void change_selection (int new_selection) { if (new_selection == selected) { @@ -77,6 +96,10 @@ namespace Pantheon.Keyboard.Shortcuts { custom_tree.command_editing_started.connect (disable_add); custom_tree.command_editing_ended.connect (enable_add); + } else if (new_selection == SectionID.APPS) { + var application_tree = trees[new_selection] as ApplicationTree; + application_tree.row_selected.connect (row_selected); + application_tree.row_unselected.connect (row_unselected); } if (selected == SectionID.CUSTOM) { @@ -86,13 +109,16 @@ namespace Pantheon.Keyboard.Shortcuts { custom_tree.command_editing_started.disconnect (disable_add); custom_tree.command_editing_ended.disconnect (enable_add); - + } else if (selected == SectionID.APPS) { + var application_tree = trees[selected] as ApplicationTree; + application_tree.row_selected.disconnect (row_selected); + application_tree.row_unselected.disconnect (row_unselected); } selected = new_selection; - actionbar.no_show_all = new_selection != SectionID.CUSTOM; - actionbar.visible = new_selection == SectionID.CUSTOM; + actionbar.no_show_all = new_selection != SectionID.CUSTOM && new_selection != SectionID.APPS; + actionbar.visible = new_selection == SectionID.CUSTOM || new_selection == SectionID.APPS; show_all (); diff --git a/src/meson.build b/src/meson.build index b3254b131..24ad11070 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ plug_files = files( 'Widgets/Shortcuts/SectionSwitcher.vala', 'Widgets/Shortcuts/DisplayTree.vala', 'Widgets/Shortcuts/Display.vala', + 'Widgets/Shortcuts/ApplicationTree.vala', 'Widgets/Shortcuts/CustomTree.vala', 'Widgets/Layout/Display.vala', 'Widgets/Layout/AddLayoutPopover.vala', @@ -14,6 +15,7 @@ plug_files = files( 'Shortcuts/Shortcut.vala', 'Shortcuts/Settings.vala', 'Shortcuts/List.vala', + 'Shortcuts/ApplicationShortcutSettings.vala', 'Shortcuts/CustomShortcutSettings.vala', 'Layout/XkbModifier.vala', 'Layout/Settings.vala',