[pitivi] utils: Implement the infrastructure for custom UI widget for effect configuration
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] utils: Implement the infrastructure for custom UI widget for effect configuration
- Date: Tue, 5 Sep 2017 22:54:26 +0000 (UTC)
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]