[pitivi] ui.prefs: test cases for UI prefs and dummy implementation for all dynamic widgets
- From: Edward Hervey <edwardrv src gnome org>
- To: svn-commits-list gnome org
- Subject: [pitivi] ui.prefs: test cases for UI prefs and dummy implementation for all dynamic widgets
- Date: Thu, 30 Apr 2009 12:19:20 -0400 (EDT)
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]