[pitivi] prefs: Add section for customising shortcuts



commit 2a1695bd5990fa62770165beb1d11862e1d92f7f
Author: Jakub Brindza <jakub brindza gmail com>
Date:   Tue Jul 26 16:03:30 2016 +0100

    prefs: Add section for customising shortcuts
    
    Fixes https://phabricator.freedesktop.org/T7452
    
    Differential Revision: https://phabricator.freedesktop.org/D1220

 pitivi/dialogs/prefs.py |  330 ++++++++++++++++++++++++++++++++++++++++++++++-
 pitivi/shortcuts.py     |   20 +++-
 2 files changed, 346 insertions(+), 4 deletions(-)
---
diff --git a/pitivi/dialogs/prefs.py b/pitivi/dialogs/prefs.py
index 5636e69..511bb80 100644
--- a/pitivi/dialogs/prefs.py
+++ b/pitivi/dialogs/prefs.py
@@ -20,12 +20,16 @@
 import os
 from gettext import gettext as _
 
+from gi.repository import Gdk
+from gi.repository import Gio
+from gi.repository import GObject
 from gi.repository import Gtk
 
 from pitivi.configure import get_ui_dir
 from pitivi.settings import GlobalSettings
 from pitivi.utils import widgets
 from pitivi.utils.loggable import Loggable
+from pitivi.utils.ui import PADDING
 from pitivi.utils.ui import SPACING
 
 
@@ -44,17 +48,19 @@ GlobalSettings.addConfigOption('prefsDialogHeight',
 
 class PreferencesDialog(Loggable):
     """Preferences for how the app works."""
-
     prefs = {}
     section_names = {"timeline": _("Timeline")}
 
     def __init__(self, app):
         Loggable.__init__(self)
+        self.app = app
+        self.app.shortcuts.connect("accel-changed", self.__do_accel_changed_cb)
 
         self.settings = app.settings
         self.widgets = {}
         self.resets = {}
         self.original_values = {}
+        self.action_ids = {}
 
         # Identify the widgets we'll need
         builder = Gtk.Builder()
@@ -62,14 +68,26 @@ class PreferencesDialog(Loggable):
         builder.connect_signals(self)
         self.dialog = builder.get_object("dialog1")
         self.sidebar = builder.get_object("sidebar")
+        self.sidebar.set_size_request(205, -1)
         self.stack = builder.get_object("stack")
         self.revert_button = builder.get_object("revertButton")
         self.factory_settings = builder.get_object("resetButton")
         self.restart_warning = builder.get_object("restartWarning")
 
-        self.__fillContents()
+        self.__add_settings_sections()
+        self.__add_shortcuts_section()
         self.dialog.set_transient_for(app.gui)
 
+    def __do_accel_changed_cb(self, shortcuts_manager, action_name):
+        if action_name:
+            index = self.action_ids[action_name]
+            title = self.list_store.get_item(index).title
+            updated_item = ModelItem(self.app, action_name, title)
+            self.list_store.remove(index)
+            self.list_store.insert(index, updated_item)
+            self.list_store.emit("items-changed", index, 1, 1)
+            self.content_box.get_row_at_index(index).show()
+
     def run(self):
         """Runs the dialog."""
         self.dialog.run()
@@ -144,7 +162,8 @@ class PreferencesDialog(Loggable):
         cls._add_preference(attrname, label, description, section,
                             widgets.FontWidget)
 
-    def __fillContents(self):
+    def __add_settings_sections(self):
+        """Adds sections for the preferences which have been registered."""
         for section_id, options in sorted(self.prefs.items()):
             grid = Gtk.Grid()
             grid.set_border_width(SPACING)
@@ -206,6 +225,123 @@ class PreferencesDialog(Loggable):
             self.stack.add_titled(grid, section_id, self.section_names[section_id])
         self.factory_settings.set_sensitive(self._canReset())
 
+    def __add_shortcuts_section(self):
+        """Adds a section with keyboard shortcuts."""
+        shortcuts_manager = self.app.shortcuts
+        self.description_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+        self.accel_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
+        self.content_box = Gtk.ListBox()
+        self.list_store = Gio.ListStore.new(ModelItem)
+        index = 0
+        for group, actions in shortcuts_manager.group_actions.items():
+            for action, title in actions:
+                item = ModelItem(self.app, action, title)
+                self.list_store.append(item)
+                self.action_ids[action] = index
+                index += 1
+        self.content_box.bind_model(self.list_store, self._create_widget_func, None)
+        self.content_box.set_header_func(self._add_header_func, None)
+        self.content_box.connect("row_activated", self.__row_activated_cb)
+        self.content_box.set_selection_mode(Gtk.SelectionMode.NONE)
+        self.content_box.props.margin = PADDING * 3
+        viewport = Gtk.Viewport()
+        viewport.add(self.content_box)
+
+        scrolled_window = Gtk.ScrolledWindow()
+        scrolled_window.add_with_viewport(viewport)
+        scrolled_window.set_min_content_height(500)
+        scrolled_window.set_min_content_width(600)
+
+        outside_box = Gtk.Box()
+        outside_box.add(scrolled_window)
+        outside_box.show_all()
+
+        self.stack.add_titled(outside_box, "shortcuts", _("Shortcuts"))
+
+    def __row_activated_cb(self, list_box, row):
+        index = row.get_index()
+        item = self.list_store.get_item(index)
+        customsation_dialog = CustomShortcutDialog(self.app, self.dialog, item)
+        customsation_dialog.show_all()
+
+    def _create_widget_func(self, item, user_data):
+        """Generates and fills up the contents for the model."""
+        defaults = self.app.shortcuts.default_accelerators
+        accel_changed = item.get_accel(formatted=False) not in defaults[item.action_name]
+
+        title_label = Gtk.Label()
+        accel_label = Gtk.Label()
+        title_label.set_text(item.title)
+        accel_label.set_text(item.get_accel(formatted=True))
+        if not accel_changed:
+            accel_label.set_state_flags(Gtk.StateFlags.INSENSITIVE, True)
+        title_label.props.xalign = 0
+        title_label.props.margin_left = PADDING * 2
+        title_label.props.margin_right = PADDING * 2
+        self.description_size_group.add_widget(title_label)
+        accel_label.props.xalign = 0
+        accel_label.props.margin_left = PADDING * 2
+        accel_label.props.margin_right = PADDING * 2
+        self.accel_size_group.add_widget(accel_label)
+
+        # Add the third column with the reset button.
+        button = Gtk.Button.new_from_icon_name("edit-clear-all-symbolic",
+                                               Gtk.IconSize.MENU)
+        button.set_tooltip_text(_("Reset the shortcut to the default accelerator"))
+        button.set_relief(Gtk.ReliefStyle.NONE)
+        button.connect("clicked", self.__reset_accelerator_cb, item)
+        button.set_sensitive(accel_changed)
+
+        title_label.show()
+        accel_label.show()
+        button.show()
+
+        # Pack the three widgets above into a row and add to parent_box.
+        contents_box = Gtk.Box()
+        contents_box.pack_start(title_label, True, True, 0)
+        contents_box.pack_start(accel_label, True, False, 0)
+        contents_box.pack_start(button, True, True, 0)
+
+        return contents_box
+
+    def _add_header_func(self, row, before, unused_user_data):
+        """Adds a header for a new section in the model."""
+        if before:
+            row.set_header(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
+
+        shortcuts_manager = self.app.shortcuts
+        curr_prefix = self.list_store.get_item(row.get_index()).action_name.split(".")[0]
+        try:
+            prev_prefix = self.list_store.get_item(row.get_index() - 1).action_name.split(".")[0]
+        except OverflowError:
+            prev_prefix = None
+
+        if prev_prefix != curr_prefix:
+            header = Gtk.Label()
+            header.set_use_markup(True)
+            header.set_markup("<b>%s</b>" % shortcuts_manager.group_titles[curr_prefix])
+            header_box = Gtk.Box()
+            header_box.add(header)
+            header_box.props.margin_top = PADDING
+            header_box.props.margin_bottom = PADDING
+            header_box.props.margin_left = PADDING * 2
+            header_box.props.margin_right = PADDING * 2
+            # COMMENTED OUT UNTIL I FIND OUT HOW TO DO IT:
+            # header_box.override_background_color(Gtk.StateType.NORMAL,
+            #                                      Gdk.RGBA(.4, .4, .4, .4))
+            header_box.show_all()
+            row.set_header(header_box)
+
+    def __reset_accelerator_cb(self, unused_button, item):
+        """Resets the accelerator and updates the displayed value to default."""
+        self.app.shortcuts.reset_accels(item.action_name)
+        # Update the row's accelerator value.
+        for index in range(self.list_store.get_n_items()):
+            if self.list_store.get_item(index) == item:
+                self.list_store.remove(index)
+                self.list_store.insert(index, ModelItem(self.app, item.action_name,
+                                                        item.title))
+
     def _factorySettingsButtonCb(self, unused_button):
         """Resets all settings to the defaults."""
         for section in self.prefs.values():
@@ -268,3 +404,191 @@ class PreferencesDialog(Loggable):
                 if not self.settings.isDefault(attrname):
                     return True
         return False
+
+
+class ModelItem(GObject.Object):
+    """Holds the data of a keyboard shortcut for a Gio.ListStore."""
+
+    def __init__(self, app, action_name, title):
+        GObject.Object.__init__(self)
+        self.app = app
+        self.action_name = action_name
+        self.title = title
+
+    def get_accel(self, formatted=True):
+        """Returns the corresponding accelerator in a viewable format."""
+        try:
+            accels = self.app.get_accels_for_action(self.action_name)[0]
+        except IndexError:
+            accels = ""
+
+        if formatted:
+            keyval, mods = Gtk.accelerator_parse(accels)
+            accelerator = Gtk.accelerator_get_label(keyval, mods)
+            return accelerator
+        else:
+            return accels
+
+
+class CustomShortcutDialog(Gtk.Dialog):
+    """Dialog for customising accelerator invoked by activating a row in preferences."""
+    FORBIDDEN_KEYVALS = [Gdk.KEY_Escape]
+
+    def __init__(self, app, pref_dialog, customised_item):
+        Gtk.Dialog.__init__(self, use_header_bar=True, flags=Gtk.DialogFlags.MODAL)
+        self.app = app
+        self.preferences = pref_dialog
+        self.customised_item = customised_item
+        self.shortcut_changed = False
+        self.valid_shortcut = False
+
+        # Initialise all potential widgets used in the dialog.
+        self.accelerator_label = Gtk.Label()
+        self.currently_used = Gtk.Label()
+        self.invalid_used = Gtk.Label()
+        self.conflicting_action = None
+        self.conflicting_action_name = None
+        self.conflict_label = Gtk.Label()
+        self.apply_button = Gtk.Button()
+        self.replace_button = Gtk.Button()
+
+        self.set_size_request(500, 300)
+        self.set_transient_for(self.preferences)
+        self.get_titlebar().set_decoration_layout('close:')
+        self.add_events(Gdk.EventMask.KEY_PRESS_MASK)
+        self.display_customisation_dialog(customised_item)
+        self.replace_button.set_visible(False)
+
+    def display_customisation_dialog(self, customised_item):
+        """Populates the dialog with relevant information and displays it."""
+        self.set_title(_("Set shortcut"))
+        content_area = self.get_content_area()
+        prompt_label = Gtk.Label()
+        prompt_label.set_markup(_("Enter new shortcut for <b>%s</b>,\nor press Esc to "
+                                  "cancel.") % customised_item.title)
+        prompt_label.props.margin_top = PADDING * 3
+        prompt_label.props.margin_bottom = PADDING * 3
+        self.accelerator_label.set_markup("<span size='20000'><b>%s</b></span>"
+                                          % customised_item.get_accel())
+        self.accelerator_label.props.margin_bottom = PADDING
+
+        content_area.add(prompt_label)
+        content_area.add(self.accelerator_label)
+        content_area.add(self.conflict_label)
+        content_area.add(self.currently_used)
+        content_area.add(self.invalid_used)
+
+    def do_key_press_event(self, event):
+        """Decides if the pressed accel combination is valid and sets widget visibility."""
+        custom_keyval = event.keyval
+        custom_mask = event.state
+
+        if custom_keyval == Gdk.KEY_Escape:
+            self.destroy()
+            return
+
+        accelerator = Gtk.accelerator_get_label(custom_keyval, custom_mask)
+        self.accelerator_label.set_markup("<span size='20000'><b>%s</b></span>"
+                                          % accelerator)
+        equal_accelerators = self.check_equal_to_set(custom_keyval, custom_mask)
+        if equal_accelerators:
+                self.currently_used.set_markup(_("This is the currently set accelerator "
+                                                 "for this shortcut.\n You may want to "
+                                                 "change it to something else."))
+
+        valid = Gtk.accelerator_valid(custom_keyval, custom_mask)
+        if not valid:
+            self.invalid_used.set_markup(_("The accelerator you are trying to set "
+                                           "might interfere with typing.\n "
+                                           "Try using Control, Shift or Alt "
+                                           "with some other key, please."))
+
+        already_used = self.verify_already_used(custom_keyval, custom_mask)
+        self.valid_shortcut = valid and not already_used
+        if self.valid_shortcut:
+            self.toggle_apply_accel_buttons(custom_keyval, custom_mask)
+        else:
+            if valid and not equal_accelerators:
+                self.toggle_conflict_buttons(custom_keyval, custom_mask)
+                self.conflict_label.set_markup(_("This shortcut is already used for <b>"
+                                                 "%s</b>.\nDo you want to replace it?")
+                                               % self.conflicting_action_name)
+
+        # Set visibility according to the booleans set above.
+        self.apply_button.set_visible(self.valid_shortcut)
+        self.conflict_label.set_visible(not self.valid_shortcut and valid and
+                                        not equal_accelerators)
+        self.replace_button.set_visible(not self.valid_shortcut and valid and
+                                        not equal_accelerators)
+        self.currently_used.set_visible(equal_accelerators)
+        self.invalid_used.set_visible(not valid)
+
+    def verify_already_used(self, keyval, mask):
+        """Checks if the customised accelerator is not already used for another action.
+
+        Compare the customised accelerator to other accelerators in the same group
+        of actions as well as actions in the 'win' and 'app' groups, because these
+        would get affected if identical accelerator were set to some other action in a
+        container.
+        """
+        customised_action = self.customised_item.action_name
+        group_name = customised_action.split(".")[0]
+        groups_to_check = set([group_name, "app", "win"])
+        for group in groups_to_check:
+            for action, title in self.app.shortcuts.group_actions[group]:
+                for accel in self.app.get_accels_for_action(action):
+                    if (keyval, mask) == Gtk.accelerator_parse(accel):
+                        self.conflicting_action = action
+                        self.conflicting_action_name = title
+                        return True
+        return False
+
+    def check_equal_to_set(self, keyval, mask):
+        """Checks if the customised accelerator is not already set for the action."""
+        action = self.customised_item.action_name
+        for accel in self.app.get_accels_for_action(action):
+            if (keyval, mask) == Gtk.accelerator_parse(accel):
+                return True
+        return False
+
+    def toggle_conflict_buttons(self, keyval, mask):
+        """Shows the buttons viewed when a conflicting accel is pressed."""
+        if self.conflicting_action and self.replace_button.get_visible() is False:
+            self.replace_button = self.add_button(_("Replace"), Gtk.ResponseType.OK)
+            self.replace_button.connect("clicked", self.__replace_accelerators_cb,
+                                        keyval, mask)
+            self.replace_button.get_style_context().\
+                add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION)
+            self.replace_button.set_tooltip_text(_("Remove this accelerator from where "
+                                                 "it was used previously and set it for "
+                                                   "this shortcut."))
+
+    def toggle_apply_accel_buttons(self, keyval, mask):
+        """Shows the buttons viewed when a valid accel is pressed."""
+        if not self.apply_button.get_visible():
+            self.apply_button = self.add_button(_("Apply"), Gtk.ResponseType.OK)
+            self.apply_button.connect("clicked",
+                                      self.__apply_accel_setting_cb, keyval, mask)
+            self.apply_button.get_style_context()\
+                .add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION)
+            self.apply_button.set_tooltip_text(_("Apply the accelerator to this"
+                                                 "shortcut."))
+
+    def __replace_accelerators_cb(self, unused_parameter, keyval, mask):
+        """Disables the accelerator in its previous use, set for this action."""
+        conflicting_accels = self.app.get_accels_for_action(self.conflicting_action)
+        if len(conflicting_accels) > 1:
+            self.app.shortcuts.set(self.conflicting_action,
+                                   conflicting_accels[1:])
+        else:
+            self.app.shortcuts.set(self.conflicting_action, [])
+        self.__apply_accel_setting_cb(unused_parameter, keyval, mask)
+        self.destroy()
+
+    def __apply_accel_setting_cb(self, unused_parameter, keyval, mask):
+        """Sets the user's preferred settings and closes the dialog."""
+        customised_action = self.customised_item.action_name
+        new_accelerator = Gtk.accelerator_name(keyval, mask)
+        self.app.shortcuts.set(customised_action, [new_accelerator])
+        self.app.shortcuts.save()
+        self.destroy()
diff --git a/pitivi/shortcuts.py b/pitivi/shortcuts.py
index 4007052..5950145 100644
--- a/pitivi/shortcuts.py
+++ b/pitivi/shortcuts.py
@@ -19,16 +19,22 @@
 """Accelerators info."""
 import os.path
 
+from gi.repository import GObject
 from gi.repository import Gtk
 
 from pitivi.settings import xdg_config_home
 from pitivi.utils.misc import show_user_manual
 
 
-class ShortcutsManager:
+class ShortcutsManager(GObject.Object):
     """Manager storing the shortcuts from all across the app."""
 
+    __gsignals__ = {
+        "accel-changed": (GObject.SIGNAL_RUN_LAST, None, (str,))
+    }
+
     def __init__(self, app):
+        GObject.Object.__init__(self)
         self.app = app
         self.groups = []
         self.group_titles = {}
@@ -88,6 +94,17 @@ class ShortcutsManager:
                 self.group_actions[action_prefix] = []
             self.group_actions[action_prefix].append((action, title))
 
+    def set(self, action, accelerators):
+        """Sets accelerators for a shortcut.
+
+        Args:
+            action (str): The name identifying the action, formatted like
+                "prefix.name".
+            accelerators ([str]): The array containing accelerators to be set.
+        """
+        self.app.set_accels_for_action(action, accelerators)
+        self.emit("accel-changed", action)
+
     def register_group(self, action_prefix, title):
         """Registers a group of shortcuts to be displayed.
 
@@ -117,6 +134,7 @@ class ShortcutsManager:
                 os.remove(self.config_path)
             except FileNotFoundError:
                 pass
+        self.emit("accel-changed", action)
 
 
 class ShortcutsWindow(Gtk.ShortcutsWindow):


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]