[pitivi] preferences: Add Plugin Manager
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] preferences: Add Plugin Manager
- Date: Mon, 28 Aug 2017 22:07:46 +0000 (UTC)
commit 4279582a759f4c14c6313c5a776d8d82a320beb1
Author: Fabian Orccon <cfoch fabian gmail com>
Date: Wed Aug 16 16:25:28 2017 -0500
preferences: Add Plugin Manager
Differential Revision: https://phabricator.freedesktop.org/D1789
pitivi/dialogs/prefs.py | 393 +++++++++++++++++++++++++++++++++++++++--------
tests/test_prefs.py | 23 ++--
2 files changed, 336 insertions(+), 80 deletions(-)
---
diff --git a/pitivi/dialogs/prefs.py b/pitivi/dialogs/prefs.py
index 3e1cfa7..6bcccd1 100644
--- a/pitivi/dialogs/prefs.py
+++ b/pitivi/dialogs/prefs.py
@@ -17,19 +17,24 @@
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
"""User preferences."""
+import itertools
import os
from gettext import gettext as _
+from threading import Timer
from gi.repository import Gdk
from gi.repository import Gio
+from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
+from gi.repository import Peas
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 alter_style_class
+from pitivi.utils.ui import fix_infobar
from pitivi.utils.ui import PADDING
from pitivi.utils.ui import SPACING
@@ -50,11 +55,16 @@ GlobalSettings.addConfigOption('prefsDialogHeight',
class PreferencesDialog(Loggable):
"""Preferences for how the app works."""
prefs = {}
- section_names = {"timeline": _("Timeline")}
+ section_names = {
+ "timeline": _("Timeline"),
+ "_plugins": _("Plugins"),
+ "_shortcuts": _("Shortcuts")
+ }
def __init__(self, app):
Loggable.__init__(self)
self.app = app
+
self.app.shortcuts.connect("accel-changed", self.__accel_changed_cb)
self.settings = app.settings
@@ -74,8 +84,12 @@ class PreferencesDialog(Loggable):
self.factory_settings = builder.get_object("resetButton")
self.restart_warning = builder.get_object("restartWarning")
- self.__add_settings_sections()
+ for section_id in self.settings_sections:
+ self.add_settings_page(section_id)
+ self.factory_settings.set_sensitive(self._canReset())
+
self.__add_shortcuts_section()
+ self.__add_plugin_manager_section()
self.dialog.set_transient_for(app.gui)
def run(self):
@@ -83,6 +97,19 @@ class PreferencesDialog(Loggable):
self.dialog.run()
# Public API
+ @property
+ def settings_sections(self):
+ return [section for section in PreferencesDialog.section_names if not section.startswith("_")]
+
+ @classmethod
+ def add_section(cls, section, title):
+ """Adds a new valid section.
+
+ Args:
+ section (str): The id of a preferences category.
+ title (str): The title of the new `section`.
+ """
+ cls.section_names[section] = title
@classmethod
def _add_preference(cls, attrname, label, description, section,
@@ -101,6 +128,8 @@ class PreferencesDialog(Loggable):
"""
if section not in cls.section_names:
raise Exception("%s is not a valid section id" % section)
+ if section.startswith("_"):
+ raise Exception("Cannot add preferences to reserved sections")
if section not in cls.prefs:
cls.prefs[section] = {}
cls.prefs[section][attrname] = (label, description, widget_class, args)
@@ -152,74 +181,85 @@ class PreferencesDialog(Loggable):
cls._add_preference(attrname, label, description, section,
widgets.FontWidget)
- 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)
- grid.props.column_spacing = SPACING
- grid.props.row_spacing = SPACING / 2
-
- prefs = []
- for attrname in options:
- label, description, widget_class, args = options[attrname]
- widget = widget_class(**args)
- widget.setWidgetValue(getattr(self.settings, attrname))
- widget.connectValueChanged(
- self._valueChangedCb, widget, attrname)
- widget.set_tooltip_text(description)
- self.widgets[attrname] = widget
- # Add a semicolon, except for checkbuttons.
- if isinstance(widget, widgets.ToggleWidget):
- widget.set_label(label)
- label_widget = None
- else:
- # Translators: This adds a semicolon to an already
- # translated name of a preference.
- label = _("%(preference_label)s:") % {"preference_label": label}
- label_widget = Gtk.Label(label=label)
- label_widget.set_tooltip_text(description)
- label_widget.set_alignment(1.0, 0.5)
- label_widget.show()
- icon = Gtk.Image()
- icon.set_from_icon_name(
- "edit-clear-all-symbolic", Gtk.IconSize.MENU)
- revert = Gtk.Button()
- revert.add(icon)
- revert.set_tooltip_text(_("Reset to default value"))
- revert.set_relief(Gtk.ReliefStyle.NONE)
- revert.set_sensitive(not self.settings.isDefault(attrname))
- revert.connect("clicked", self._resetOptionCb, attrname)
- revert.show_all()
- self.resets[attrname] = revert
- row_widgets = (label_widget, widget, revert)
- # Construct the prefs list so that it can be sorted.
- # Make sure the L{ToggleWidget}s appear at the end.
- prefs.append((label_widget is None, label, row_widgets))
-
- # Sort widgets: I think we only want to sort by the non-localized
- # names, so options appear in the same place across locales ...
- # but then I may be wrong
- for y, (_1, _2, row_widgets) in enumerate(sorted(prefs)):
- label, widget, revert = row_widgets
- if not label:
- grid.attach(widget, 0, y, 2, 1)
- grid.attach(revert, 2, y, 1, 1)
- else:
- grid.attach(label, 0, y, 1, 1)
- grid.attach(widget, 1, y, 1, 1)
- grid.attach(revert, 2, y, 1, 1)
- widget.show()
- revert.show()
- grid.show()
- self.stack.add_titled(grid, section_id, self.section_names[section_id])
- self.factory_settings.set_sensitive(self._canReset())
+ def _add_page(self, section, widget):
+ """Adds a widget to the internal stack."""
+ if section not in self.section_names:
+ raise Exception("%s is not a valid section id" % section)
+ self.stack.add_titled(widget, section, self.section_names[section])
+
+ def add_settings_page(self, section_id):
+ """Adds a page for the preferences in the specified section."""
+ options = self.prefs[section_id]
+
+ grid = Gtk.Grid()
+ grid.set_border_width(SPACING)
+ grid.props.column_spacing = SPACING
+ grid.props.row_spacing = SPACING / 2
+
+ prefs = []
+ for attrname in options:
+ label, description, widget_class, args = options[attrname]
+ widget = widget_class(**args)
+ widget.setWidgetValue(getattr(self.settings, attrname))
+ widget.connectValueChanged(
+ self._valueChangedCb, widget, attrname)
+ widget.set_tooltip_text(description)
+ self.widgets[attrname] = widget
+ # Add a semicolon, except for checkbuttons.
+ if isinstance(widget, widgets.ToggleWidget):
+ widget.set_label(label)
+ label_widget = None
+ else:
+ # Translators: This adds a semicolon to an already
+ # translated name of a preference.
+ label = _("%(preference_label)s:") % {"preference_label": label}
+ label_widget = Gtk.Label(label=label)
+ label_widget.set_tooltip_text(description)
+ label_widget.set_alignment(1.0, 0.5)
+ label_widget.show()
+ icon = Gtk.Image()
+ icon.set_from_icon_name(
+ "edit-clear-all-symbolic", Gtk.IconSize.MENU)
+ revert = Gtk.Button()
+ revert.add(icon)
+ revert.set_tooltip_text(_("Reset to default value"))
+ revert.set_relief(Gtk.ReliefStyle.NONE)
+ revert.set_sensitive(not self.settings.isDefault(attrname))
+ revert.connect("clicked", self._resetOptionCb, attrname)
+ revert.show_all()
+ self.resets[attrname] = revert
+ row_widgets = (label_widget, widget, revert)
+ # Construct the prefs list so that it can be sorted.
+ # Make sure the L{ToggleWidget}s appear at the end.
+ prefs.append((label_widget is None, label, row_widgets))
+
+ # Sort widgets: I think we only want to sort by the non-localized
+ # names, so options appear in the same place across locales ...
+ # but then I may be wrong
+ for y, (_1, _2, row_widgets) in enumerate(sorted(prefs)):
+ label, widget, revert = row_widgets
+ if not label:
+ grid.attach(widget, 0, y, 2, 1)
+ grid.attach(revert, 2, y, 1, 1)
+ else:
+ grid.attach(label, 0, y, 1, 1)
+ grid.attach(widget, 1, y, 1, 1)
+ grid.attach(revert, 2, y, 1, 1)
+ widget.show()
+ revert.show()
+ grid.show()
+ self._add_page(section_id, grid)
+
+ def __add_plugin_manager_section(self):
+ page = PluginPreferencesPage(self.app, self)
+ page.show_all()
+ self._add_page("_plugins", page)
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.description_size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
+ self.accel_size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
self.content_box = Gtk.ListBox()
self.list_store = Gio.ListStore.new(ModelItem)
index = 0
@@ -247,7 +287,7 @@ class PreferencesDialog(Loggable):
outside_box.add(scrolled_window)
outside_box.show_all()
- self.stack.add_titled(outside_box, "shortcuts", _("Shortcuts"))
+ self._add_page("_shortcuts", outside_box)
def __row_activated_cb(self, list_box, row):
index = row.get_index()
@@ -532,3 +572,222 @@ class CustomShortcutDialog(Gtk.Dialog):
self.app.shortcuts.set(action, [self.accelerator])
self.app.shortcuts.save()
self.destroy()
+
+
+class PluginPreferencesRow(Gtk.ListBoxRow):
+ """A row in the plugins list allowing activating and deactivating a plugin."""
+
+ def __init__(self, model):
+ Gtk.Bin.__init__(self)
+ self.plugin_info = model.plugin_info
+
+ self._container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ self.add(self._container)
+
+ self._title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ self._title_label = Gtk.Label(self.plugin_info.get_name())
+ self._description_label = Gtk.Label()
+
+ description = self.plugin_info.get_description()
+ if not description:
+ description = _("No description available.")
+ self._description_label.set_markup(
+ "<span style=\"italic\">%s</span>" % description)
+ else:
+ self._description_label.set_text(description)
+
+ self.switch = Gtk.Switch()
+ self.switch_handler_id = None
+
+ # Pack widgets.
+ self._title_box.pack_start(self._title_label, True, True, 0)
+ self._title_box.pack_start(self._description_label, True, True, 0)
+ self._container.pack_start(self._title_box, True, True, 0)
+ self._container.pack_end(self.switch, False, False, 0)
+
+ # Widgets' design.
+ self._container.props.margin_left = PADDING * 2
+ self._container.props.margin_right = PADDING * 2
+ self._container.props.margin_top = PADDING
+ self._container.props.margin_bottom = PADDING
+ self._title_label.props.xalign = 0
+ self._description_label.props.xalign = 0
+ self._description_label.get_style_context().add_class("dim-label")
+ self.switch.props.margin_left = PADDING
+
+
+class PluginItem(GObject.Object):
+ """Holds the data of a plugin info for a Gio.ListStore."""
+
+ def __init__(self, app, plugin_info):
+ GObject.Object.__init__(self)
+ self.app = app
+ self.plugin_info = plugin_info
+
+
+class PluginManagerStore(Gio.ListStore):
+ """Stores the models for available plugins."""
+
+ def __init__(self):
+ Gio.ListStore.__init__(self)
+ self.app = None
+ self.preferences_dialog = None
+
+ @classmethod
+ def new(cls, app, preferences_dialog):
+ obj = PluginManagerStore()
+ obj.app = app
+ obj.preferences_dialog = preferences_dialog
+ # FIXME
+ # For some reason this property cannot be set at construct time
+ # with GObject.Object.new.
+ obj.set_property("item-type", PluginItem)
+ obj.reload()
+ return obj
+
+ def reload(self):
+ self.remove_all()
+ for plugin_info in self.app.plugin_manager.engine.get_plugin_list():
+ item = PluginItem(self.app, plugin_info)
+ self.append(item)
+
+
+class PluginsBox(Gtk.ListBox):
+
+ def __init__(self, list_store):
+ Gtk.ListBox.__init__(self)
+ self.app = list_store.app
+ self.list_store = list_store
+ self.title_size_group = None
+ self.switch_size_group = None
+
+ self.set_header_func(self._add_header_func, None)
+ self.bind_model(self.list_store, self._create_widget_func, None)
+
+ self.props.margin = PADDING * 3
+
+ # Activate the plugins' switches for plugins that are already loaded.
+ loaded_plugins = self.app.plugin_manager.engine.get_loaded_plugins()
+ for row in self.get_children():
+ if row.plugin_info.get_module_name() in loaded_plugins:
+ row.switch.set_active(True)
+
+ self.app.plugin_manager.engine.connect_after("load-plugin",
+ self.__load_plugin_cb)
+ self.app.plugin_manager.engine.connect_after("unload-plugin",
+ self.__unload_plugin_cb)
+
+ def get_row(self, module_name):
+ """Gets the PluginPreferencesRow linked to a given module name."""
+ for row in self.get_children():
+ if row.plugin_info.get_module_name() == module_name:
+ return row
+ return None
+
+ def _create_widget_func(self, item, unused_user_data):
+ row = PluginPreferencesRow(item)
+ row.switch_handler_id = row.switch.connect("notify::active",
+ self.__switch_active_cb,
+ row.plugin_info)
+ return row
+
+ def __switch_active_cb(self, switch, unused_pspec, plugin_info):
+ engine = self.app.plugin_manager.engine
+ if switch.get_active():
+ if not engine.load_plugin(plugin_info):
+ stack = self.list_store.preferences_dialog.stack
+ preferences_page = stack.get_child_by_name("_plugins")
+ msg = _("Unable to load the plugin '{module_name}'").format(
+ module_name=plugin_info.get_module_name())
+ preferences_page.show_infobar(msg, Gtk.MessageType.WARNING)
+ switch.set_active(False)
+ else:
+ self.app.plugin_manager.engine.unload_plugin(plugin_info)
+
+ def __load_plugin_cb(self, engine, plugin_info):
+ """Handles the activation of one of the available plugins."""
+ row = self.get_row(plugin_info.get_module_name())
+ if row is not None and row.switch_handler_id is not None:
+ with row.switch.handler_block(row.switch_handler_id):
+ row.switch.set_active(True)
+
+ def __unload_plugin_cb(self, engine, plugin_info):
+ """Handles the deactivation of one of the available plugins."""
+ row = self.get_row(plugin_info.get_module_name())
+ if row is not None and row.switch_handler_id is not None:
+ with row.switch.handler_block(row.switch_handler_id):
+ row.switch.set_active(False)
+
+ def _add_header_func(self, row, before, unused_user_data):
+ """Adds a header for a new section in the model."""
+ if row.get_index() == 0:
+ header = Gtk.Label()
+ header.set_use_markup(True)
+
+ group_title = _("Plugins")
+ header.set_markup("<b>%s</b>" % group_title)
+ header.props.margin_top = PADDING * 3
+ header.props.margin_bottom = PADDING
+ header.props.margin_left = PADDING * 2
+ header.props.margin_right = PADDING * 2
+ header.props.xalign = 0
+ alter_style_class("group_title", header, "font-size: small;")
+ header.get_style_context().add_class("group_title")
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ box.add(header)
+ box.get_style_context().add_class("background")
+ box.show_all()
+ row.set_header(box)
+
+
+class PluginPreferencesPage(Gtk.ScrolledWindow):
+ """The page that displays the list of available plugins."""
+
+ INFOBAR_TIMEOUT_SECONDS = 5
+
+ def __init__(self, app, preferences_dialog):
+ Gtk.ScrolledWindow.__init__(self)
+ list_store = PluginManagerStore.new(app, preferences_dialog)
+
+ viewport = Gtk.Viewport()
+ self._wrapper_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ plugins_box = PluginsBox(list_store)
+ viewport.add(self._wrapper_box)
+
+ self._infobar_revealer = Gtk.Revealer()
+ self._infobar = Gtk.InfoBar()
+ fix_infobar(self._infobar)
+ self._infobar_label = Gtk.Label()
+ self._setup_infobar()
+
+ self.add_with_viewport(viewport)
+ self.set_min_content_height(500)
+ self.set_min_content_width(600)
+
+ self._wrapper_box.pack_start(self._infobar_revealer, False, False, 0)
+ self._wrapper_box.pack_start(plugins_box, False, False, 0)
+
+ # Helpers
+ self._infobar_timer = None
+
+ def _setup_infobar(self):
+ self._infobar_revealer.add(self._infobar)
+ self._infobar_label.set_line_wrap(True)
+ self._infobar.get_content_area().add(self._infobar_label)
+
+ def show_infobar(self, text, message_type, auto_hide=True):
+ """Sets a text and a message type to the infobar to display it."""
+ self._infobar.set_message_type(message_type)
+ self._infobar_label.set_text(text)
+ self._infobar_revealer.set_reveal_child(True)
+ if auto_hide:
+ if self._infobar_timer is not None:
+ self._infobar_timer.cancel()
+ self._infobar_timer = Timer(self.INFOBAR_TIMEOUT_SECONDS,
+ self.hide_infobar)
+ self._infobar_timer.start()
+
+ def hide_infobar(self):
+ """Hides the info bar."""
+ self._infobar_revealer.set_reveal_child(False)
+ self._infobar_timer = None
diff --git a/tests/test_prefs.py b/tests/test_prefs.py
index 8cc5430..2caa93d 100644
--- a/tests/test_prefs.py
+++ b/tests/test_prefs.py
@@ -24,45 +24,42 @@ from pitivi.dialogs.prefs import PreferencesDialog
class PreferencesDialogTest(unittest.TestCase):
def testNumeric(self):
- section = list(PreferencesDialog.section_names.keys())[0]
PreferencesDialog.addNumericPreference('numericPreference1',
label="Open Range",
- section=section,
+ section="timeline",
description="This option has no upper bound",
lower=-10)
self.assertTrue(
- 'numericPreference1' in PreferencesDialog.prefs[section])
+ 'numericPreference1' in PreferencesDialog.prefs["timeline"])
PreferencesDialog.addNumericPreference('numericPreference2',
label="Closed Range",
- section=section,
+ section="timeline",
description="This option has both upper and lower bounds",
lower=-10,
upper=10000)
def testText(self):
- section = list(PreferencesDialog.section_names.keys())[0]
PreferencesDialog.addTextPreference('textPreference1',
label="Unfiltered",
- section=section,
+ section="timeline",
description="Anything can go in this box")
PreferencesDialog.addTextPreference('textPreference2',
label="Numbers only",
- section=section,
+ section="timeline",
description="This input validates its input with a regex",
matches=r"^-?\d+(\.\d+)?$")
def testOther(self):
- section = list(PreferencesDialog.section_names.keys())[0]
PreferencesDialog.addPathPreference('aPathPreference',
label="Test Path",
- section=section,
+ section="timeline",
description="Test the path widget")
PreferencesDialog.addChoicePreference('aChoicePreference',
label="Swallow Velocity",
- section=section,
+ section="timeline",
description="What is the airspeed velocity of a coconut-laden
swallow?",
choices=(
("42 Knots", 32),
@@ -71,7 +68,7 @@ class PreferencesDialogTest(unittest.TestCase):
PreferencesDialog.addChoicePreference('aLongChoicePreference',
label="Favorite Color",
- section=section,
+ section="timeline",
description="What is the color of the parrot's plumage?",
choices=(
("Mauve", "Mauve"),
@@ -83,10 +80,10 @@ class PreferencesDialogTest(unittest.TestCase):
PreferencesDialog.addTogglePreference('aTogglePreference',
label="Test Toggle",
- section=section,
+ section="timeline",
description="Test the toggle widget")
PreferencesDialog.addFontPreference('aFontPreference',
label="Foo Font",
- section=section,
+ section="timeline",
description="Test the font widget")
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]