[pitivi] utils: Implement the infrastructure for custom UI widget for effect configuration



commit 4f9f0b0795cb77488e26bef7a5b28c80836b91f7
Author: Suhas Nayak <suhas2go gmail com>
Date:   Wed Feb 27 20:28:31 2013 -0300

    utils: Implement the infrastructure for custom UI widget for effect configuration
    
    Co-authored-by: Jean-François Fortin Tam <nekohayo gmail com>
    Co-authored-by: Thibault Saunier <thibault saunier collabora com>
    
    Differential Revision: https://phabricator.freedesktop.org/D1744

 pitivi/clipproperties.py              |    2 +
 pitivi/effects.py                     |   36 +++++-
 pitivi/utils/custom_effect_widgets.py |   61 +++++++++
 pitivi/utils/widgets.py               |  224 ++++++++++++++++++++++++++------
 tests/plugins/test_alpha.ui           |  134 ++++++++++++++++++++
 tests/test_custom_effect_ui.py        |  130 +++++++++++++++++++
 6 files changed, 539 insertions(+), 48 deletions(-)
---
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index d109e58..f18bbba 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -31,6 +31,7 @@ from pitivi.configure import get_ui_dir
 from pitivi.effects import EffectsPropertiesManager
 from pitivi.effects import HIDDEN_EFFECTS
 from pitivi.undo.timeline import CommitTimelineFinalizingAction
+from pitivi.utils.custom_effect_widgets import setup_custom_effect_widgets
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.misc import disconnectAllByFunc
 from pitivi.utils.pipeline import PipelineError
@@ -124,6 +125,7 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.clip = None
         self._effect_config_ui = None
         self.effects_properties_manager = EffectsPropertiesManager(app)
+        setup_custom_effect_widgets(self.effects_properties_manager)
         self.clip_properties = clip_properties
 
         # The toolbar that will go between the list of effects and properties
diff --git a/pitivi/effects.py b/pitivi/effects.py
index 6c9413f..6897051 100644
--- a/pitivi/effects.py
+++ b/pitivi/effects.py
@@ -36,6 +36,7 @@ from gi.repository import Gdk
 from gi.repository import GdkPixbuf
 from gi.repository import GES
 from gi.repository import GLib
+from gi.repository import GObject
 from gi.repository import Gst
 from gi.repository import Gtk
 from gi.repository import Pango
@@ -49,7 +50,6 @@ from pitivi.utils.ui import SPACING
 from pitivi.utils.widgets import FractionWidget
 from pitivi.utils.widgets import GstElementSettingsWidget
 
-
 (VIDEO_EFFECT, AUDIO_EFFECT) = list(range(1, 3))
 
 AUDIO_EFFECTS_CATEGORIES = ()
@@ -566,14 +566,36 @@ class EffectListWidget(Gtk.Box, Loggable):
 PROPS_TO_IGNORE = ['name', 'qos', 'silent', 'message', 'parent']
 
 
-class EffectsPropertiesManager:
+class EffectsPropertiesManager(GObject.Object, Loggable):
     """Provides and caches UIs for editing effects.
 
     Attributes:
         app (Pitivi): The app.
     """
 
+    def create_widget_accumulator(*args):
+        """Aborts `create_widget` emission if we got a widget."""
+        handler_return = args[2]
+        if handler_return is None:
+            return True, handler_return
+        return False, handler_return
+
+    __gsignals__ = {
+        "create_widget": (GObject.SIGNAL_RUN_LAST, Gtk.Widget, (GstElementSettingsWidget, GES.Effect,),
+                          create_widget_accumulator),
+    }
+
+    def do_create_widget(self, effect_widget, effect):
+        """Creates a widget if the `create_widget` handlers did not."""
+        effect_name = effect.get_property("bin-description")
+        self.log('UI is being auto-generated for "%s"', effect_name)
+        effect_widget.add_widgets(with_reset_button=True)
+        self._postConfiguration(effect, effect_widget)
+        return None
+
     def __init__(self, app):
+        GObject.Object.__init__(self)
+        Loggable.__init__(self)
         self.cache_dict = {}
         self._current_element_values = {}
         self.app = app
@@ -588,13 +610,15 @@ class EffectsPropertiesManager:
             GstElementSettingsWidget: A container for configuring the effect.
         """
         if effect not in self.cache_dict:
-            # Here we should handle special effects configuration UI
             effect_widget = GstElementSettingsWidget()
-            effect_widget.setElement(effect, ignore=PROPS_TO_IGNORE,
-                                     with_reset_button=True)
+            effect_widget.setElement(effect, PROPS_TO_IGNORE)
+            widget = self.emit("create_widget", effect_widget, effect)
+            # The default handler of `create_widget` handles visibility
+            # itself and returns None
+            if widget is not None:
+                effect_widget.show_widget(widget)
             self.cache_dict[effect] = effect_widget
             self._connectAllWidgetCallbacks(effect_widget, effect)
-            self._postConfiguration(effect, effect_widget)
 
         for prop in effect.list_children_properties():
             value = effect.get_child_property(prop.name)
diff --git a/pitivi/utils/custom_effect_widgets.py b/pitivi/utils/custom_effect_widgets.py
new file mode 100644
index 0000000..3b34b5d
--- /dev/null
+++ b/pitivi/utils/custom_effect_widgets.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2013, Thibault Saunier <thibault saunier collabora 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., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Utility methods for custom effect UI."""
+import os
+
+from gi.repository import Gtk
+
+from pitivi import configure
+
+
+CUSTOM_WIDGETS_DIR = os.path.join(configure.get_ui_dir(), "customwidgets")
+
+
+def setup_custom_effect_widgets(effect_prop_manager):
+    """Sets up the specified effects manager to be able to create custom UI."""
+    effect_prop_manager.connect("create_widget", create_custom_widget_cb)
+
+
+def setup_from_ui_file(element_setting_widget, path):
+    """Creates and connects the UI for a widget."""
+    # Load the ui file using builder
+    builder = Gtk.Builder()
+    builder.add_from_file(path)
+    # Link ui widgets to the corresponding properties of the effect
+    element_setting_widget.mapBuilder(builder)
+    return builder
+
+
+def create_custom_widget_cb(unused_effect_prop_manager, effect_widget, effect):
+    """Creates custom effect UI."""
+    effect_name = effect.get_property("bin-description")
+    path = os.path.join(CUSTOM_WIDGETS_DIR, effect_name + ".ui")
+    if not os.path.isfile(path):
+        return None
+
+    # Check if there is a UI file available as a glade file
+    # Assuming a GtkGrid called base_table exists
+    builder = setup_from_ui_file(effect_widget, path)
+    widget = builder.get_object("base_table")
+    return widget
+
+
+def create_alpha_widget(unused_element_setting_widget, unused_element):
+    """Not implemented yet."""
+    return None
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index 3f079fb..09465e4 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -109,7 +109,7 @@ class TextWidget(Gtk.Box, DynamicWidget):
         "activate": (GObject.SignalFlags.RUN_LAST, None, (),)
     }
 
-    def __init__(self, matches=None, choices=None, default=None, combobox=False):
+    def __init__(self, matches=None, choices=None, default=None, combobox=False, widget=None):
         if not default:
             # In the case of text widgets, a blank default is an empty string
             default = ""
@@ -120,23 +120,27 @@ class TextWidget(Gtk.Box, DynamicWidget):
         self.set_orientation(Gtk.Orientation.HORIZONTAL)
         self.set_border_width(0)
         self.set_spacing(0)
-        if choices:
-            self.combo = Gtk.ComboBoxText.new_with_entry()
-            self.text = self.combo.get_child()
-            self.combo.show()
-            disable_scroll(self.combo)
-            self.pack_start(self.combo, expand=False, fill=False, padding=0)
-            for choice in choices:
-                self.combo.append_text(choice)
-        elif combobox:
-            self.combo = Gtk.ComboBox.new_with_entry()
-            self.text = self.combo.get_child()
-            self.combo.show()
-            self.pack_start(self.combo, expand=False, fill=False, padding=0)
+        if widget is None:
+            if choices:
+                self.combo = Gtk.ComboBoxText.new_with_entry()
+                self.text = self.combo.get_child()
+                self.combo.show()
+                disable_scroll(self.combo)
+                self.pack_start(self.combo, expand=False, fill=False, padding=0)
+                for choice in choices:
+                    self.combo.append_text(choice)
+            elif combobox:
+                self.combo = Gtk.ComboBox.new_with_entry()
+                self.text = self.combo.get_child()
+                self.combo.show()
+                self.pack_start(self.combo, expand=False, fill=False, padding=0)
+            else:
+                self.text = Gtk.Entry()
+                self.text.show()
+                self.pack_start(self.text, expand=False, fill=False, padding=0)
         else:
-            self.text = Gtk.Entry()
-            self.text.show()
-            self.pack_start(self.text, expand=False, fill=False, padding=0)
+            self.text = widget
+
         self.matches = None
         self.last_valid = None
         self.valid = False
@@ -212,18 +216,21 @@ class NumericWidget(Gtk.Box, DynamicWidget):
         lower (Optional[int]): The lower limit for this widget.
     """
 
-    def __init__(self, upper=None, lower=None, default=None):
+    def __init__(self, upper=None, lower=None, default=None, adjustment=None):
         Gtk.Box.__init__(self)
         DynamicWidget.__init__(self, default)
 
         self.set_orientation(Gtk.Orientation.HORIZONTAL)
         self.set_spacing(SPACING)
         self._type = None
+        self.spinner = None
 
+        if adjustment:
+            self.adjustment = adjustment
+            return
         reasonable_limit = 5000
         with_slider = (lower is not None and lower > -reasonable_limit and
-            upper is not None and upper < reasonable_limit)
-
+                       upper is not None and upper < reasonable_limit)
         self.adjustment = Gtk.Adjustment()
         # Limit the limits, otherwise the widget appears huge.
         # Workaround https://bugzilla.gnome.org/show_bug.cgi?id=727294
@@ -276,7 +283,8 @@ class NumericWidget(Gtk.Box, DynamicWidget):
         elif type_ == float:
             step = 0.01
             page = 0.1
-            self.spinner.props.digits = 2
+            if self.spinner:
+                self.spinner.props.digits = 2
         else:
             raise Exception('Unsupported property type: %s' % type_)
         lower = min(self.adjustment.props.lower, value)
@@ -437,21 +445,28 @@ class FractionWidget(TextWidget, DynamicWidget):
         return Gst.Fraction(num, denom)
 
 
-class ToggleWidget(Gtk.CheckButton, DynamicWidget):
+class ToggleWidget(Gtk.Box, DynamicWidget):
     """Widget for entering an on/off value."""
 
-    def __init__(self, default=None):
-        Gtk.CheckButton.__init__(self)
+    def __init__(self, default=None, check_button=None):
+        Gtk.Box.__init__(self)
         DynamicWidget.__init__(self, default)
+        if check_button is None:
+            self.check_button = Gtk.CheckButton()
+            self.pack_start(self.check_button, expand=False, fill=False, padding=0)
+            self.check_button.show()
+        else:
+            self.check_button = check_button
+            self.setWidgetToDefault()
 
     def connectValueChanged(self, callback, *args):
-        self.connect("toggled", callback, *args)
+        self.check_button.connect("toggled", callback, *args)
 
     def setWidgetValue(self, value):
-        self.set_active(value)
+        self.check_button.set_active(value)
 
     def getWidgetValue(self):
-        return self.get_active()
+        return self.check_button.get_active()
 
 
 class ChoiceWidget(Gtk.Box, DynamicWidget):
@@ -635,6 +650,27 @@ class InputValidationWidget(Gtk.Box, DynamicWidget):
             self._warning_sign.show()
 
 
+def make_widget_wrapper(prop, widget):
+    """Creates a wrapper child of DynamicWidget for @widget."""
+    # Respect Object hierarchy here
+    if isinstance(widget, Gtk.SpinButton):
+        widget_adjustment = widget.get_adjustment()
+        widget_lower = widget_adjustment.props.lower
+        widget_upper = widget_adjustment.props.upper
+        return NumericWidget(upper=widget_upper, lower=widget_lower, adjustment=widget_adjustment, 
default=prop.default_value)
+    elif isinstance(widget, Gtk.Entry):
+        return TextWidget(widget=widget)
+    elif isinstance(widget, Gtk.Range):
+        widget_adjustment = widget.get_adjustment()
+        widget_lower = widget_adjustment.props.lower
+        widget_upper = widget_adjustment.props.upper
+        return NumericWidget(upper=widget_upper, lower=widget_lower, adjustment=widget_adjustment, 
default=prop.default_value)
+    elif isinstance(widget, Gtk.CheckButton):
+        return ToggleWidget(prop.default_value, widget)
+    else:
+        Loggable().fixme("%s has not been wrapped into a Dynamic Widget", widget)
+
+
 class GstElementSettingsWidget(Gtk.Box, Loggable):
     """Widget to modify the properties of a Gst.Element.
 
@@ -659,6 +695,11 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
         self.properties = {}
         self.__controllable = controllable
         self.set_orientation(Gtk.Orientation.VERTICAL)
+        self.__bindings_by_keyframe_button = {}
+        self.__widgets_by_keyframe_button = {}
+        self.__widgets_by_reset_button = {}
+        self._unhandled_properties = []
+        self.uncontrolled_properties = {}
 
     def deactivate_keyframe_toggle_buttons(self):
         """Makes sure the keyframe togglebuttons are deactivated."""
@@ -671,29 +712,122 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
                 # There can be only one active keyframes button.
                 break
 
-    def setElement(self, element, values={}, ignore=['name'],
-                   with_reset_button=False):
-        """Sets the element to be edited.
-
-        Args:
-            values (dict): The current values of the element props, by name.
-                If empty, the default values will be used.
-            with_reset_button (bool): Whether to show a reset button for each
-                property.
-        """
-        self.info("element: %s, use values: %s", element, values)
+    def setElement(self, element, ignore=['name']):
+        """Sets the element to be edited."""
         self.element = element
         self.ignore = ignore
-        self.__add_widgets(values, with_reset_button)
 
-    def __add_widgets(self, values, with_reset_button):
+    def show_widget(self, widget):
+        self.pack_start(widget, True, True, 0)
+        self.show_all()
+
+    def mapBuilder(self, builder):
+        """Maps the GStreamer element's properties to corresponding widgets in @builder.
+
+        Prop control widgets should be named "element_name::prop_name", where:
+        - element_name is the gstreamer element (ex: the "alpha" effect)
+        - prop_name is the name of one of a particular property of the element
+        If present, a reset button corresponding to the property will be used
+        (the button must be named similarly, with "::reset" after the prop name)
+        A button named reset_all_button can also be provided and will be used as
+        a fallback for each property without an individual reset button.
+        Similarly, the keyframe control button corresponding to the property (if controllable)
+        can be used whose name is to be "element_name::prop_name::keyframe".
+        """
+        reset_all_button = builder.get_object("reset_all_button")
+        for prop in self._getProperties():
+            widget_name = prop.owner_type.name + "::" + prop.name
+            widget = builder.get_object(widget_name)
+            if widget is None:
+                self._unhandled_properties.append(prop)
+                self.warning("No custom widget found for %s property \"%s\"" %
+                             (prop.owner_type.name, prop.name))
+            else:
+                reset_name = widget_name + "::" + "reset"
+                reset_widget = builder.get_object(reset_name)
+                if not reset_widget:
+                    # If reset_all_button is not found, it will be None
+                    reset_widget = reset_all_button
+                keyframe_name = widget_name + "::" + "keyframe"
+                keyframe_widget = builder.get_object(keyframe_name)
+                self.addPropertyWidget(prop, widget, reset_widget, keyframe_widget)
+
+    def addPropertyWidget(self, prop, widget, to_default_btn=None, keyframe_btn=None):
+        """Connects an element property to a GTK Widget.
+
+        Optionally, a reset button widget can also be provided.
+        Unless you want to connect each widget individually, you should be using
+        the "mapBuilder" method instead.
+        """
+        if isinstance(widget, DynamicWidget):
+            # if the widget is already a DynamicWidget we use it as is
+            dynamic_widget = widget
+        else:
+            # if the widget is not dynamic we try to create a wrapper around it
+            # so we can control it with the standardized DynamicWidget API
+            dynamic_widget = make_widget_wrapper(prop, widget)
+
+        if dynamic_widget:
+            self.properties[prop] = dynamic_widget
+
+            self.element.connect("notify::" + prop.name, self._propertyChangedCb,
+                                 dynamic_widget)
+            # The "reset to default" button associated with this property
+            if isinstance(to_default_btn, Gtk.Button):
+                self.__widgets_by_reset_button[to_default_btn] = widget
+                to_default_btn.connect("clicked", self.__reset_to_default_clicked_cb, dynamic_widget, 
keyframe_btn)
+            elif to_default_btn is not None:
+                self.warning("to_default_btn should be Gtk.Button or None, got %s", to_default_btn)
+
+            # The "keyframe toggle" button associated with this property
+            if not isinstance(widget, (ToggleWidget, ChoiceWidget)):
+                res, element, pspec = self.element.lookup_child(prop.name)
+                assert res
+                binding = GstController.DirectControlBinding.new(
+                    element, prop.name,
+                    GstController.InterpolationControlSource())
+                if binding.pspec:
+                    # The prop can be controlled (keyframed).
+                    if isinstance(keyframe_btn, Gtk.ToggleButton):
+                        keyframe_btn.connect("toggled", self.__keyframes_toggled_cb, prop)
+                        self.__widgets_by_keyframe_button[keyframe_btn] = widget
+                        prop_binding = self.element.get_control_binding(prop.name)
+                        self.__bindings_by_keyframe_button[keyframe_btn] = prop_binding
+                        self.__display_controlled(keyframe_btn, bool(prop_binding))
+                    elif keyframe_btn is not None:
+                        self.warning("keyframe_btn should be Gtk.ToggleButton or None, got %s", 
to_default_btn)
+        else:
+            # If we add a non-standard widget, the creator of the widget is
+            # responsible for handling its behaviour "by hand"
+            self.info("Can not wrap widget %s for property %s" % (widget, prop))
+            # We still keep a ref to that widget, "just in case"
+            self.uncontrolled_properties[prop] = widget
+
+        if hasattr(prop, "blurb"):
+            widget.set_tooltip_text(prop.blurb)
+
+    def _getProperties(self):
+        if isinstance(self.element, GES.BaseEffect):
+            props = self.element.list_children_properties()
+        else:
+            props = GObject.list_properties(self.element)
+        return [prop for prop in props if prop.name not in self.ignore]
+
+    def add_widgets(self, values={}, with_reset_button=False):
         """Prepares a Gtk.Grid containing the property widgets of an element.
 
         Each property is on a separate row.
         A row is typically a label followed by the widget and a reset button.
 
         If there are no properties, returns a "No properties" label.
+
+        Args:
+            values (dict): The current values of the element props, by name.
+                If empty, the default values will be used.
+            with_reset_button (bool): Whether to show a reset button for each
+                property.
         """
+        self.info("element: %s, use values: %s", self.element, values)
         self.properties.clear()
         self.__bindings_by_keyframe_button = {}
         self.__widgets_by_keyframe_button = {}
@@ -747,7 +881,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
                 widget = prop_widget
 
             if isinstance(prop_widget, ToggleWidget):
-                prop_widget.set_label(prop.nick)
+                prop_widget.check_button.set_label(prop.nick)
                 grid.attach(widget, 0, y, 2, 1)
             else:
                 text = _("%(preference_label)s:") % {"preference_label": prop.nick}
@@ -818,6 +952,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
         button.set_relief(Gtk.ReliefStyle.NONE)
         button.connect('clicked', self.__reset_to_default_clicked_cb, widget,
                        keyframe_button)
+        self.__widgets_by_reset_button[button] = widget
         return button
 
     def __set_keyframe_active(self, toggle_button, active):
@@ -866,7 +1001,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
             track_element.ui_element.showDefaultKeyframes()
 
     def __reset_to_default_clicked_cb(self, unused_button, widget,
-                                      keyframe_button):
+                                      keyframe_button=None):
         if keyframe_button:
             # The prop is controllable (keyframmable).
             binding = self.__bindings_by_keyframe_button.get(keyframe_button)
@@ -938,6 +1073,11 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
 
         return widget
 
+    def get_widget_of_prop(self, prop_name):
+        for prop in self.properties:
+            if prop.name == prop_name:
+                return self.properties[prop]
+
 
 class GstElementSettingsDialog(Loggable):
     """Dialog window for viewing/modifying properties of a Gst.Element."""
diff --git a/tests/plugins/test_alpha.ui b/tests/plugins/test_alpha.ui
new file mode 100644
index 0000000..e231be7
--- /dev/null
+++ b/tests/plugins/test_alpha.ui
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.0"/>
+  <object class="GtkAdjustment" id="alpha_adjustment">
+    <property name="upper">1</property>
+    <property name="value">1</property>
+    <property name="step_increment">0.01</property>
+    <property name="page_increment">0.10000000000000001</property>
+  </object>
+  <object class="GtkAdjustment" id="black_sens_adjustment">
+    <property name="upper">128</property>
+    <property name="value">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
+  <object class="GtkImage" id="image1">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">edit-clear-all-symbolic</property>
+  </object>
+  <object class="GtkImage" id="image2">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">edit-clear-all-symbolic</property>
+  </object>
+  <object class="GtkGrid" id="base_table">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="border_width">6</property>
+    <property name="row_spacing">6</property>
+    <property name="column_spacing">6</property>
+    <child>
+      <object class="GtkLabel" id="label2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">Alpha:</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScale" id="GstAlpha::alpha">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="adjustment">alpha_adjustment</property>
+        <property name="restrict_to_fill_level">False</property>
+        <property name="fill_level">1</property>
+        <property name="round_digits">1</property>
+        <property name="digits">2</property>
+        <property name="value_pos">left</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">Black sensitivity:</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScale" id="GstAlpha::black-sensitivity">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="adjustment">black_sens_adjustment</property>
+        <property name="restrict_to_fill_level">False</property>
+        <property name="fill_level">1</property>
+        <property name="round_digits">2</property>
+        <property name="digits">0</property>
+        <property name="value_pos">left</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkToggleButton" id="GstAlpha::black-sensitivity::keyframe">
+        <property name="label" translatable="yes">◇</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="relief">none</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="GstAlpha::black-sensitivity::reset">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="image">image1</property>
+        <property name="relief">none</property>
+        <property name="image_position">top</property>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="GstAlpha::alpha::reset">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="image">image2</property>
+        <property name="relief">none</property>
+        <property name="image_position">top</property>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+</interface>
diff --git a/tests/test_custom_effect_ui.py b/tests/test_custom_effect_ui.py
new file mode 100644
index 0000000..d03bfb2
--- /dev/null
+++ b/tests/test_custom_effect_ui.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2017, Suhas Nayak <suhas2go gmail 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., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Tests for the custom effect UI."""
+# pylint: disable=attribute-defined-outside-init,protected-access
+import os
+
+from gi.repository import GES
+from gi.repository import Gtk
+
+from pitivi.effects import EffectsPropertiesManager
+from pitivi.effects import PROPS_TO_IGNORE
+from pitivi.utils.widgets import DynamicWidget
+from pitivi.utils.widgets import GstElementSettingsWidget
+from pitivi.utils.widgets import NumericWidget
+from tests import common
+
+
+class TestCustomEffectUI(common.TestCase):
+    """Tests for the custom effect UI create mechanism."""
+
+    def create_alpha_widget_cb(self, unused_manager, unused_container, unused_effect, widgets):
+        """Handles the request to create an effect widget."""
+        self.builder = Gtk.Builder()
+        path = os.path.join(os.path.dirname(__file__), "plugins", "test_alpha.ui")
+        self.builder.add_objects_from_file(path, widgets)
+        self.element_settings_widget.mapBuilder(self.builder)
+        return self.builder.get_object("GstAlpha::black-sensitivity")
+
+    def _register_alpha_widget(self, widgets):
+        """Sets up an EffectsPropertiesManager instance to create custom effect UI."""
+        self.alpha_effect = GES.Effect.new("alpha")
+        self.prop_name = "black-sensitivity"
+        _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name)
+
+        self.effects_prop_manager = EffectsPropertiesManager(self.app)
+        self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets)
+        self.element_settings_widget = GstElementSettingsWidget()
+        self.element_settings_widget.setElement(self.alpha_effect, PROPS_TO_IGNORE)
+
+        self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect)
+        self.effects_prop_manager._connectAllWidgetCallbacks(self.element_settings_widget, self.alpha_effect)
+        self.effects_prop_manager._postConfiguration(self.alpha_effect, self.element_settings_widget)
+
+    def test_wrapping(self):
+        """Checks UI updating results in updating the effect."""
+        self.app = common.create_pitivi_mock()
+        self._register_alpha_widget(("black_sens_adjustment", "GstAlpha::black-sensitivity"))
+
+        # Check if the widget is wrapped correctly
+        wrapped_spin_button = self.element_settings_widget.properties[self.prop]
+        self.assertTrue(isinstance(wrapped_spin_button, DynamicWidget))
+        self.assertTrue(isinstance(wrapped_spin_button, NumericWidget))
+
+        # Check if the wrapper has the correct default value
+        self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetDefault())
+
+        # Check if the callbacks are functioning
+        value = (1 + self.prop.default_value) % self.prop.maximum
+        wrapped_spin_button.setWidgetValue(value)
+        self.assertEqual(wrapped_spin_button.getWidgetValue(), value)
+        _, prop_value = self.alpha_effect.get_child_property(self.prop_name)
+        self.assertEqual(prop_value, value)
+
+    def test_prop_keyframe(self):
+        """Checks the keyframe button effect."""
+        uri = common.get_sample_uri("tears_of_steel.webm")
+        asset = GES.UriClipAsset.request_sync(uri)
+        ges_clip = asset.extract()
+
+        # Add the clip to a timeline so it gets tracks.
+        timeline = common.create_timeline_container()
+        self.app = timeline.app
+        ges_timeline = timeline.ges_timeline
+        ges_timeline.append_layer()
+        ges_layer, = ges_timeline.get_layers()
+        ges_layer.add_clip(ges_clip)
+
+        self._register_alpha_widget(
+            ("black_sens_adjustment", "GstAlpha::black-sensitivity", 
"GstAlpha::black-sensitivity::keyframe"))
+        ges_clip.add(self.alpha_effect)
+        track_element = 
self.element_settings_widget._GstElementSettingsWidget__get_track_element_of_same_type(
+            self.alpha_effect)
+        prop_keyframe_button = \
+            
list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_keyframe_button.keys())[0]
+
+        # Control the self.prop property on the timeline
+        prop_keyframe_button.set_active(True)
+        self.assertEqual(track_element.ui_element._TimelineElement__controlledProperty, self.prop)
+        # Revert to controlling the default property
+        prop_keyframe_button.set_active(False)
+        self.assertNotEqual(track_element.ui_element._TimelineElement__controlledProperty, self.prop)
+
+    def test_prop_reset(self):
+        """Checks the reset button resets the property."""
+        self.app = common.create_pitivi_mock()
+        self._register_alpha_widget(
+            ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::reset", 
"image1"))
+        wrapped_spin_button = self.element_settings_widget.properties[self.prop]
+        _, prop_value = self.alpha_effect.get_child_property(self.prop_name)
+        self.assertEqual(self.prop.default_value, prop_value)
+        self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue())
+
+        # Set the property value to a different value than the default
+        wrapped_spin_button.setWidgetValue((1 + self.prop.default_value) % self.prop.maximum)
+        _, prop_value = self.alpha_effect.get_child_property(self.prop_name)
+        self.assertEqual(prop_value, (1 + self.prop.default_value) % self.prop.maximum)
+
+        # Reset the value of the property to default
+        prop_reset_button = \
+            list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_reset_button.keys())[0]
+        prop_reset_button.clicked()
+        _, prop_value = self.alpha_effect.get_child_property(self.prop_name)
+        self.assertEqual(self.prop.default_value, prop_value)
+        self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue())


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