[pitivi] ui.prefs: test cases for UI prefs and dummy implementation for all dynamic widgets



commit df13eabaf4db618276438d9658772669972ee8ba
Author: Brandon Lewis <brandon_lewis berkeley edu>
Date:   Wed Apr 15 17:32:44 2009 -0700

    ui.prefs: test cases for UI prefs and dummy implementation for all dynamic widgets
---
 pitivi/settings.py      |    4 +-
 pitivi/ui/dynamic.py    |  259 ++++++++++++++++++++++++++++++++++++++++++++
 pitivi/ui/mainwindow.py |    5 +-
 pitivi/ui/prefs.py      |  276 ++++++++++++++++++++++++++++++++++++++++++++---
 pitivi/ui/previewer.py  |    2 +
 5 files changed, 524 insertions(+), 22 deletions(-)

diff --git a/pitivi/settings.py b/pitivi/settings.py
index c16917d..d877707 100644
--- a/pitivi/settings.py
+++ b/pitivi/settings.py
@@ -275,7 +275,7 @@ class GlobalSettings(object, Signallable):
 
     @classmethod
     def addConfigOption(cls, attrname, type_=None, section=None, key=None,
-        environment=None, default=None, notify=False, prefs_group=None):
+        environment=None, default=None, notify=False,):
         """
         Add a configuration option.
 
@@ -300,8 +300,6 @@ class GlobalSettings(object, Signallable):
         @param notify: whether or not this attribute should emit notification
         signals when modified (default is False).
         @type notify: C{boolean}
-        @param prefs_group: use this if you would like a widget to change this
-        option to be automatically created in the user preferences panel
         """
         if section and not section in cls.options:
             raise ConfigError("You must add the section \"%s\" first." %
diff --git a/pitivi/ui/dynamic.py b/pitivi/ui/dynamic.py
new file mode 100644
index 0000000..d29451e
--- /dev/null
+++ b/pitivi/ui/dynamic.py
@@ -0,0 +1,259 @@
+# PiTiVi , Non-linear video editor
+#
+#       ui/dynamic.py
+#
+# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+A collection of helper classes and routines for dynamically creating user
+interfaces
+"""
+import gobject
+import gtk
+import re
+from gettext import gettext as _
+
+class DynamicWidget(object):
+
+    """An interface which provides a uniform way to get, set, and observe
+    widget properties"""
+
+    def connectValueChanged(self, callback, *args):
+        raise NotImplementedError
+
+    def setWidgetValue(self, value):
+        raise NotImplementedError
+
+    def getWidgetValue(self, value):
+        raise NotImplementedError
+
+class DefaultWidget(gtk.Label):
+
+    """When all hope fails...."""
+
+    def __init__(self, *unused, **kw_unused):
+        gtk.Label.__init__(self, _("Implement Me"))
+
+    def connectValueChanged(self, callback, *args):
+        pass
+
+    def setWidgetValue(self, value):
+        self.set_text(value)
+
+    def getWidgetValue(self):
+        return self.get_text()
+
+
+class TextWidget(gtk.HBox):
+
+    """A gtk.Entry which emits a value-changed signal only when its input is
+    valid (matches the provided regex)"""
+
+    __gtype_name__ = 'TextWidget'
+    __gsignals__ = {
+        "value-changed" : (
+            gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (),)
+    }
+
+    __INVALID__ = gtk.gdk.Color(0xFFFF, 0, 0)
+    __NORMAL__ = gtk.gdk.Color(0, 0, 0)
+
+    def __init__(self, matches = None):
+        gtk.HBox.__init__(self)
+        self.text = gtk.Entry()
+        self.text.show()
+        self.pack_start(self.text)
+        self.matches = None
+        self.last_valid = None
+        self.valid = True
+        self.image = gtk.Image()
+        self.image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 
+            gtk.ICON_SIZE_BUTTON)
+        self.pack_start(self.image)
+        if matches:
+            self.text.connect("changed", self._filter)
+            self.matches = re.compile(matches)
+            self._filter(None)
+
+    def connectValueChanged(self, callback, *args):
+        return self.connect("value-changed", callback, *args)
+
+    def setWidgetValue(self, value):
+        self.text.set_text(value)
+
+    def getWidgetValue(self):
+        if self.matches:
+            return self.last_valid
+        return self.text.get_text()
+
+    def _filter(self, unused_widget):
+        text = self.text.get_text()
+        if self.matches:
+            if self.matches.match(text):
+                self.last_valid = text
+                self.emit("value-changed")
+                if not self.valid:
+                    self.image.hide()
+                self.valid = True
+            else:
+                if self.valid:
+                    self.image.show()
+                self.valid = False
+        else:
+            self.emit("value-changed")
+
+class NumericWidget(gtk.HBox):
+
+    def __init__(self, upper = None, lower = None):
+        gtk.HBox.__init__(self)
+
+        self.adjustment = gtk.Adjustment()
+        if (upper != None) and (lower != None):
+            self.slider = gtk.HScale(self.adjustment)
+            self.pack_end(self.slider)
+            self.slider.show()
+
+        if not upper:
+            upper = float("Infinity")
+        if not lower:
+            lower = float("-Infinity")
+        self.adjustment.props.lower = lower
+        self.adjustment.props.upper = upper
+        self.spinner = gtk.SpinButton(self.adjustment)
+        self.pack_start(self.spinner, False, False)
+        self.spinner.show()
+
+
+    def connectValueChanged(self, callback, *args):
+        self.adjustment.connect("value-changed", callback, *args)
+
+    def getWidgetValue(self):
+        return self.adjustment.get_value()
+
+    def setWidgetValue(self, value):
+        return self.adjustment.set_value(value)
+
+class ToggleWidget(gtk.CheckButton):
+
+    def __init__(self):
+        gtk.CheckButton.__init__(self)
+
+    def connectValueChanged(self, callback, *args):
+        self.connect("toggled", callback, *args)
+
+    def setWidgetValue(self, value):
+        self.set_active(value)
+
+    def getWidgetValue(self):
+        return self.get_active()
+
+class ChoiceWidget(gtk.VBox):
+
+    def __init__(self, choices):
+        gtk.VBox.__init__(self)
+        self.choices = [choice[0] for choice in choices]
+        self.values = [choice[1] for choice in choices]
+        self.contents = gtk.combo_box_new_text()
+        for choice, value in choices:
+            self.contents.append_text(_(choice))
+        self.pack_start(self.contents)
+        self.contents.show()
+
+    def connectValueChanged(self, callback, *args):
+        return self.contents.connect("changed", callback, *args)
+
+    def setWidgetValue(self, value):
+        self.contents.set_active(self.values.index(value))
+
+    def getWidgetValue(self):
+        return self.values[self.contents.get_active()]
+
+class PathWidget(gtk.FileChooserButton):
+
+    __gtype_name__ = 'PathWidget'
+
+    __gsignals__ = {
+        "value-changed" : (gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            ()),
+    }
+
+    def __init__(self, action = gtk.FILE_CHOOSER_ACTION_OPEN):
+        self.dialog = gtk.FileChooserDialog(
+            action = action,
+            buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_CLOSE,
+            gtk.RESPONSE_CLOSE))
+        self.dialog.set_default_response(gtk.RESPONSE_OK)
+        gtk.FileChooserButton.__init__(self, self.dialog)
+        self.set_title(_("Choose..."))
+        self.dialog.connect("response", self._responseCb)
+        self.uri = ""
+
+    def connectValueChanged(self, callback, *args):
+        return self.connect("value-changed", callback, *args)
+
+    def setWidgetValue(self, value):
+        self.set_uri(value)
+        self.uri = value
+
+    def getWidgetValue(self):
+        return self.uri
+
+    def _responseCb(self, unused_dialog, response):
+        if response == gtk.RESPONSE_CLOSE:
+            self.uri = self.get_uri()
+            self.emit("value-changed")
+            self.dialog.hide()
+
+if __name__ == '__main__':
+
+    def valueChanged(unused_widget, widget, target):
+        target.set_text(str(widget.getWidgetValue()))
+
+    widgets = (
+        (PathWidget, "file:///home/", ()),
+        (TextWidget, "banana", ()),
+        (TextWidget, "words only", ("^([a-zA-Z]+\s*)+$",)),
+        (NumericWidget, 42, (100, 1)),
+        (ToggleWidget, True, ()),
+        (ChoiceWidget, "banana", ((
+            ("banana", "banana"),
+            ("apple", "apple"),
+            ("pear", "pear")),)),
+    )
+
+    W = gtk.Window()
+    v = gtk.VBox()
+    t = gtk.Table()
+
+    for y, (klass, default, args) in enumerate(widgets):
+        w = klass(*args)
+        l = gtk.Label(str(default))
+        w.setWidgetValue(default)
+        w.connectValueChanged(valueChanged, w, l)
+        w.show()
+        l.show()
+        t.attach(w, 0, 1, y, y + 1)
+        t.attach(l, 1, 2, y, y + 1)
+    t.show()
+
+    W.add(t)
+    W.show()
+    gtk.main()
diff --git a/pitivi/ui/mainwindow.py b/pitivi/ui/mainwindow.py
index 02729e8..ef0353c 100644
--- a/pitivi/ui/mainwindow.py
+++ b/pitivi/ui/mainwindow.py
@@ -163,8 +163,7 @@ class PitiviMainWindow(gtk.Window, Loggable):
         if len(self.app.deviceprobe.getVideoSourceDevices()) < 1:
             self.webcam_button.set_sensitive(False)
 
-        # connect to timeline
-        self.show_all()
+        self.show()
 
     def showEncodingDialog(self, project, pause=True):
         """
@@ -658,7 +657,7 @@ class PitiviMainWindow(gtk.Window, Loggable):
             self.prefsdialog.set_transient_for(self)
             self.prefsdialog.connect("delete-event", self._hideChildWindow)
             self.prefsdialog.set_default_size(400, 300)
-        self.prefsdialog.show_all()
+        self.prefsdialog.show()
 
     def rewind(self, unused_action):
         pass
diff --git a/pitivi/ui/prefs.py b/pitivi/ui/prefs.py
index 34f7631..c4634d9 100644
--- a/pitivi/ui/prefs.py
+++ b/pitivi/ui/prefs.py
@@ -20,26 +20,30 @@
 # Boston, MA 02111-1307, USA.
 
 """
-Dialog box for project settings
+Dialog box for user preferences.
 """
 
 import gtk
 from gettext import gettext as _
+import pitivi.ui.dynamic as dynamic
 
 class PreferencesDialog(gtk.Window):
 
+    prefs = {}
+
     def __init__(self, instance):
         gtk.Window.__init__(self)
         self.app = instance
         self.settings = instance.settings
+        self._current = None
         self._createUi()
         self._fillContents()
-        self._current = None
-        self.set_border_width(12)
+
 
     def _createUi(self):
         self.set_title(_("Preferences"))
         self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+        self.set_border_width(12)
 
         # basic layout
         vbox = gtk.VBox()
@@ -50,7 +54,9 @@ class PreferencesDialog(gtk.Window):
         pane = gtk.HPaned()
         vbox.pack_start(pane, True, True)
         vbox.pack_end(button_box, False, False)
+        pane.show()
         self.add(vbox)
+        vbox.show()
 
         # left-side list view
         self.model = gtk.ListStore(str, str)
@@ -63,47 +69,199 @@ class PreferencesDialog(gtk.Window):
         scrolled = gtk.ScrolledWindow()
         scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
         scrolled.add(self.treeview)
+        self.treeview.show()
         pane.pack1(scrolled)
+        scrolled.show()
 
         # preferences content region
-        self.contents = gtk.ScrolledWindow()
-        self.contents.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
-        pane.pack2(self.contents)
+        self.contents = gtk.VBox()
+        scrolled = gtk.ScrolledWindow()
+        scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        scrolled.add_with_viewport(self.contents)
+        pane.pack2(scrolled)
+        scrolled.show()
+        self.contents.show()
 
         # revert, close buttons
         factory_settings = gtk.Button(label=_("Restore Factory Settings"))
         factory_settings.connect("clicked", self._factorySettingsButtonCb)
         factory_settings.set_sensitive(False)
+        factory_settings.show()
         revert_button = gtk.Button(_("Revert"))
         revert_button.connect("clicked", self._revertButtonCb)
         revert_button.set_sensitive(False)
+        revert_button.show()
         accept_button = gtk.Button(stock=gtk.STOCK_CLOSE)
         accept_button.connect("clicked", self._acceptButtonCb)
+        accept_button.show()
         button_box.pack_start(factory_settings, False, True)
         button_box.pack_end(accept_button, False, True)
         button_box.pack_end(revert_button, False, True)
+        button_box.show()
+
+## Public API
+
+    @classmethod
+    def addPreference(cls, attrname, label, description, section=None, 
+        widget_klass=None, **args):
+        """
+        Add a user preference. The preferences dialog will try
+        to guess the appropriate widget to use based on the type of the
+        option, but you can override this by specifying a ustom class.
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param : user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        @param widget_klass: overrides auto-detected widget
+        @type widget_klass: C{class}
+        """
+        if not section:
+            section = "General"
+        if not section in cls.prefs:
+            cls.prefs[section] = {}
+        cls.prefs[section][attrname] = (label, description, widget_klass, args)
+
+    @classmethod
+    def addPathPreference(cls, attrname, label, description, section=None):
+        """
+        Add an auto-generated user preference that will show up as a
+        gtk.FileChooserButton.
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param section: user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        """
+        cls.addPreference(attrname, label, description, section,
+            dynamic.PathWidget)
+
+    @classmethod
+    def addNumericPreference(cls, attrname, label, description, section=None,
+        upper = None, lower = None):
+        """
+        Add an auto-generated user preference that will show up as either a
+        gtk.SpinButton or a gtk.HScale, depending whether both the upper and lower
+        limits are set.
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param section: user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        @param upper: upper limit for this widget, or None
+        @type upper: C{number}
+        @param lower: lower limit for this widget, or None
+        @type lower: C{number}
+        """
+        cls.addPreference(attrname, label, description, section,
+            dynamic.NumericWidget, upper=upper, lower=lower)
+
+    @classmethod
+    def addTextPreference(cls, attrname, label, description, section=None,
+        matches = None):
+        """
+        Add an auto-generated user preference that will show up as either a
+        gtk.SpinButton or a gtk.HScale, depending on the upper and lower
+        limits
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param section: user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        """
+        cls.addPreference(attrname, label, description, section,
+            dynamic.TextWidget, matches=matches)
+
+    @classmethod
+    def addChoicePreference(cls, attrname, label, description, choices,
+        section=None):
+        """
+        Add an auto-generated user preference that will show up as either a
+        gtk.ComboBox or a group of radio buttons, depending on the number of
+        choices.
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param choices: a sequence of (<label>, <value>) pairs
+        @type choices: C{[(str, pyobject), ...]}
+        @param section: user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        """
+        cls.addPreference(attrname, label, description, section,
+            dynamic.ChoiceWidget, choices=choices)
+
+    @classmethod
+    def addTogglePreference(cls, attrname, label, description, section=None):
+        """
+        Add an auto-generated user preference that will show up as a
+        gtk.CheckButton.
+
+        @param label: user-visible name for this option
+        @type label: C{str}
+        @param desc: a user-visible description documenting this option
+        (ignored unless prefs_label is non-null)
+        @type desc: C{str}
+        @param section: user-visible category to which this option
+        belongs (ignored unless prefs_label is non-null)
+        @type section: C{str}
+        """
+        cls.addPreference(attrname, label, description, section,
+            dynamic.ToggleWidget)
+
+## Implementation
 
     def _fillContents(self):
         self.sections = {}
-        for section, options in self.settings.prefs.iteritems():
+        for section, options in self.prefs.iteritems():
             self.model.append((_(section), section))
             widgets = gtk.Table()
-            vp = gtk.Viewport()
-            vp.add(widgets)
-            self.sections[section] = vp
-            for y, (attrname, (label, description)) in enumerate(options.iteritems()):
-                widgets.attach(gtk.Label(_(label)), 0, 1, y, y + 1,
-                    xoptions=0, yoptions=0)
+            widgets.set_border_width(6)
+            widgets.props.column_spacing = 6
+            widgets.props.row_spacing = 3
+            self.sections[section] = widgets
+            for y, (attrname, (label, description, klass, args)) in enumerate(
+                options.iteritems()):
+                label = gtk.Label(_(label))
+                label.set_justify(gtk.JUSTIFY_RIGHT)
+                widget = klass(**args)
+                widgets.attach(label, 0, 1, y, y + 1, xoptions=0, yoptions=0)
+                widgets.attach(widget, 1, 2, y, y + 1, yoptions=0)
+                widget.setWidgetValue(getattr(self.settings, attrname))
+                widget.connectValueChanged(self._valueChanged, widget,
+                    attrname)
+                label.show()
+                widget.show()
+            self.contents.pack_start(widgets, True, True)
+        self.treeview.get_selection().select_path((0,))
 
     def _treeSelectionChangedCb(self, selection):
         model, iter = selection.get_selected()
         new = self.sections[model[iter][1]]
         if self._current != new:
             if self._current:
-                self.contents.remove(self._current)
-            self.contents.add(new)
+                self._current.hide()
+            new.show()
             self._current = new
-            new.show_all()
 
     def _clearHistory(self):
         pass
@@ -117,3 +275,89 @@ class PreferencesDialog(gtk.Window):
     def _acceptButtonCb(self, unused_button):
         self._clearHistory()
         self.hide()
+
+    def _valueChanged(self, fake_widget, real_widget, attrname):
+        setattr(self.settings, attrname, real_widget.getWidgetValue())
+
+## Preference Test Cases
+
+if True:
+
+    from pitivi.settings import GlobalSettings
+
+    options = (
+        ('numericPreference1', 10),
+        ('numericPreference2', 2.4),
+        ('textPreference1', "banana"),
+        ('textPreference2', "42"),
+        ('aPathPreference', "file:///etc/"),
+        ('aChoicePreference', 42),
+        ('aLongChoicePreference', "Mauve"),
+        ('aTogglePreference', True),
+    )
+
+    for attrname, default in options:
+        GlobalSettings.addConfigOption(attrname, default=default)
+
+## Numeric
+
+    PreferencesDialog.addNumericPreference('numericPreference1',
+        label = "Open Range",
+        section = "Test",
+        description = "This option has no upper bound",
+        lower = -10)
+
+    PreferencesDialog.addNumericPreference('numericPreference2',
+        label = "Closed Range",
+        section = "Test",
+        description = "This option has both upper and lower bounds",
+        lower = -10,
+        upper = 10000)
+
+## Text
+
+    PreferencesDialog.addTextPreference('textPreference1',
+        label = "Unfiltered",
+        section = "Test",
+        description = "Anything can go in this box")
+
+    PreferencesDialog.addTextPreference('textPreference2',
+        label = "Numbers only",
+        section = "Test",
+        description = "This input validates its input with a regex",
+        matches = "^-?\d+(\.\d+)?$")
+
+## other
+
+    PreferencesDialog.addPathPreference('aPathPreference',
+        label = "Test Path",
+        section = "Test",
+        description = "Test the path widget")
+
+    PreferencesDialog.addChoicePreference('aChoicePreference',
+        label = "Swallow Velocity",
+        section = "Test",
+        description = "What is the velocity of an african swollow laden " \
+            "a coconut?",
+        choices = (
+            ("42 Knots", 32),
+            ("9 furlongs per fortnight", 42),
+            ("I don't know that!", None)))
+
+    PreferencesDialog.addChoicePreference('aLongChoicePreference',
+        label = "Favorite Color",
+        section = "Test",
+        description = "What is the velocity of an african swollow laden " \
+            "a coconut?",
+        choices = (
+            ("Mauve", "Mauve"),
+            ("Chartreuse", "Chartreuse"),
+            ("Magenta", "Magenta"),
+            ("Pink", "Pink"),
+            ("Orange", "Orange"),
+            ("Yellow Ochre", "Yellow Ochre")))
+
+    PreferencesDialog.addTogglePreference('aTogglePreference',
+        label = "Test Toggle",
+        section = "Test",
+        description = "Test the toggle widget")
diff --git a/pitivi/ui/previewer.py b/pitivi/ui/previewer.py
index 56fcb03..63f0cda 100644
--- a/pitivi/ui/previewer.py
+++ b/pitivi/ui/previewer.py
@@ -41,12 +41,14 @@ from pitivi.ui.zoominterface import Zoomable
 from pitivi.log.loggable import Loggable
 from pitivi.factories.file import PictureFileSourceFactory
 from pitivi.thumbnailcache import ThumbnailCache
+from pitivi.ui.prefs import PreferencesDialog
 
 GlobalSettings.addConfigSection("thumbnailing")
 GlobalSettings.addConfigOption("thumbnailSpacingHint",
     section="thumbnailing",
     key="spacing-hint",
     default=2.0)
+
 # this default works out to a maximum of ~ 1.78 MiB per factory, assuming:
 # 4:3 aspect ratio
 # 4 bytes per pixel



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