1# -*- coding: utf-8 -*-
2# Pitivi video editor
3# Copyright (c) 2010, Thibault Saunier <tsaunier@gnome.org>
4# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this program; if not, write to the
18# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19# Boston, MA 02110-1301, USA.
20"""Effects categorization and management.
21
22 There are different types of effects available:
23  _ Simple Audio/Video Effects
24     GStreamer elements that only apply to audio OR video
25     Only take the elements who have a straightforward meaning/action
26  _ Expanded Audio/Video Effects
27     These are the Gstreamer elements that don't have a easy meaning/action or
28     that are too cumbersome to use as such
29  _ Complex Audio/Video Effects
30"""
31import os
32import re
33from gettext import gettext as _
34
35from gi.repository import Gdk
36from gi.repository import GdkPixbuf
37from gi.repository import GES
38from gi.repository import GLib
39from gi.repository import Gst
40from gi.repository import Gtk
41from gi.repository import Pango
42
43from pitivi.configure import get_pixmap_dir
44from pitivi.configure import get_ui_dir
45from pitivi.settings import GlobalSettings
46from pitivi.utils.loggable import Loggable
47from pitivi.utils.ui import EFFECT_TARGET_ENTRY
48from pitivi.utils.ui import SPACING
49from pitivi.utils.widgets import FractionWidget
50from pitivi.utils.widgets import GstElementSettingsWidget
51
52
53(VIDEO_EFFECT, AUDIO_EFFECT) = list(range(1, 3))
54
55AUDIO_EFFECTS_CATEGORIES = ()
56
57ALLOWED_ONLY_ONCE_EFFECTS = ['videoflip']
58
59VIDEO_EFFECTS_CATEGORIES = (
60    (_("Colors"), (
61        # Mostly "serious" stuff that relates to correction/adjustments
62        # Fancier stuff goes into the "fancy" category
63        "cogcolorspace", "videobalance", "chromahold", "gamma",
64        "coloreffects", "exclusion", "burn", "dodge", "videomedian",
65        "frei0r-filter-color-distance", "frei0r-filter-threshold0r",
66        "frei0r-filter-contrast0r", "frei0r-filter-saturat0r",
67        "frei0r-filter-white-balance", "frei0r-filter-brightness",
68        "frei0r-filter-gamma", "frei0r-filter-invert0r",
69        "frei0r-filter-hueshift0r", "frei0r-filter-equaliz0r",
70        "frei0r-filter-bw0r", "frei0r-filter-glow",
71        "frei0r-filter-twolay0r", "frei0r-filter-3-point-color-balance",
72        "frei0r-filter-coloradj-rgb", "frei0r-filter-curves",
73        "frei0r-filter-levels", "frei0r-filter-primaries",
74        "frei0r-filter-sop-sat", "frei0r-filter-threelay0r",
75        "frei0r-filter-tint0r",
76    )),
77    (_("Compositing"), (
78        "alpha", "alphacolor", "gdkpixbufoverlay",
79        "frei0r-filter-transparency", "frei0r-filter-mask0mate",
80        "frei0r-filter-alpha0ps", "frei0r-filter-alphagrad",
81        "frei0r-filter-alphaspot", "frei0r-filter-bluescreen0r",
82        "frei0r-filter-select0r",
83    )),
84    (_("Noise & blur"), (
85        "gaussianblur", "diffuse", "dilate", "marble", "smooth",
86        "frei0r-filter-hqdn3d", "frei0r-filter-squareblur",
87        "frei0r-filter-sharpness", "frei0r-filter-edgeglow",
88        "frei0r-filter-facebl0r",
89    )),
90    (_("Analysis"), (
91        "videoanalyse", "videodetect", "videomark", "revtv",
92        "navigationtest", "frei0r-filter-rgb-parade",
93        "frei0r-filter-r", "frei0r-filter-g", "frei0r-filter-b",
94        "frei0r-filter-vectorscope", "frei0r-filter-luminance",
95        "frei0r-filter-opencvfacedetect", "frei0r-filter-pr0be",
96        "frei0r-filter-pr0file",
97    )),
98    (_("Geometry"), (
99        "cogscale", "aspectratiocrop", "cogdownsample", "videoscale",
100        "videocrop", "videoflip", "videobox", "gdkpixbufscale",
101        "kaleidoscope", "mirror", "pinch", "sphere", "square", "fisheye",
102        "stretch", "twirl", "waterriple", "rotate", "bulge", "circle",
103        "frei0r-filter-letterb0xed", "frei0r-filter-k-means-clustering",
104        "frei0r-filter-lens-correction", "frei0r-filter-defish0r",
105        "frei0r-filter-perspective", "frei0r-filter-c0rners",
106        "frei0r-filter-scale0tilt", "frei0r-filter-pixeliz0r",
107        "frei0r-filter-flippo", "frei0r-filter-3dflippo",
108    )),
109    (_("Fancy"), (
110        "rippletv", "streaktv", "radioactv", "optv", "solarize",
111        "quarktv", "vertigotv", "shagadelictv", "warptv", "dicetv",
112        "agingtv", "edgetv", "bulge", "circle", "fisheye", "tunnel",
113        "kaleidoscope", "mirror", "pinch", "sphere", "square",
114        "stretch", "twirl", "waterripple", "glfiltersobel", "chromium",
115        "frei0r-filter-sobel", "frei0r-filter-cartoon",
116        "frei0r-filter-water", "frei0r-filter-nosync0r",
117        "frei0r-filter-k-means-clustering", "frei0r-filter-delay0r",
118        "frei0r-filter-distort0r", "frei0r-filter-light-graffiti",
119        "frei0r-filter-tehroxx0r", "frei0r-filter-vertigo",
120    )),
121    (_("Time"), (
122        "videorate", "frei0r-filter-delay0r", "frei0r-filter-baltan",
123        "frei0r-filter-nervous",
124    )),
125)
126
127BLACKLISTED_EFFECTS = ["colorconvert", "coglogoinsert", "festival",
128                       "alphacolor", "cogcolorspace", "videodetect",
129                       "navigationtest", "videoanalyse", "volume"]
130
131BLACKLISTED_PLUGINS = []
132
133HIDDEN_EFFECTS = [
134    # Overlaying an image onto a video stream can already be done.
135    "gdkpixbufoverlay"]
136
137GlobalSettings.addConfigSection('effect-library')
138
139(COL_NAME_TEXT,
140 COL_DESC_TEXT,
141 COL_EFFECT_TYPE,
142 COL_EFFECT_CATEGORIES,
143 COL_ELEMENT_NAME,
144 COL_ICON) = list(range(6))
145
146ICON_WIDTH = 48 + 2 * 6  # 48 pixels, plus a margin on each side
147
148
149class EffectInfo(object):
150    """Info for displaying and using an effect.
151
152    Attributes:
153        effect_name (str): The bin_description identifying the effect.
154    """
155
156    def __init__(self, effect_name, media_type, categories,
157                 human_name, description):
158        object.__init__(self)
159        self.effect_name = effect_name
160        self.media_type = media_type
161        self.categories = categories
162        self.description = description
163        self.human_name = human_name
164
165    @property
166    def icon(self):
167        pixdir = os.path.join(get_pixmap_dir(), "effects")
168        try:
169            # We can afford to scale the images here, the impact is negligible
170            icon = GdkPixbuf.Pixbuf.new_from_file_at_size(
171                os.path.join(pixdir, self.effect_name + ".png"),
172                ICON_WIDTH, ICON_WIDTH)
173        # An empty except clause is bad, but "gi._glib.GError" is not helpful.
174        except:
175            icon = GdkPixbuf.Pixbuf.new_from_file(
176                os.path.join(pixdir, "defaultthumbnail.svg"))
177        return icon
178
179    @property
180    def bin_description(self):
181        """Gets the bin description which defines this effect."""
182        if self.effect_name.startswith("gl"):
183            return "glupload ! %s ! gldownload" % self.effect_name
184        else:
185            return self.effect_name
186
187    @staticmethod
188    def name_from_bin_description(bin_description):
189        """Gets the name of the effect defined by the `bin_description`."""
190        if bin_description.startswith("glupload"):
191            return bin_description.split("!")[1].strip()
192        else:
193            return bin_description
194
195    def good_for_track_element(self, track_element):
196        """Checks the effect is compatible with the specified track element.
197
198        Args:
199            track_element (GES.TrackElement): The track element to check against.
200
201        Returns:
202            bool: Whether it makes sense to apply the effect to the track element.
203        """
204        track_type = track_element.get_track_type()
205        if track_type == GES.TrackType.AUDIO:
206            return self.media_type == AUDIO_EFFECT
207        elif track_type == GES.TrackType.VIDEO:
208            return self.media_type == VIDEO_EFFECT
209        else:
210            return False
211
212
213class EffectsManager(Loggable):
214    """Keeps info about effects and their categories.
215
216    Attributes:
217        video_effects (List[Gst.ElementFactory]): The available video effects.
218        audio_effects (List[Gst.ElementFactory]): The available audio effects.
219    """
220
221    def __init__(self):
222        Loggable.__init__(self)
223        self.video_effects = []
224        self.audio_effects = []
225        self.gl_effects = []
226        self._effects = {}
227
228        useless_words = ["Video", "Audio", "audio", "effect",
229                         _("Video"), _("Audio"), _("Audio").lower(), _("effect")]
230        uselessRe = re.compile(" |".join(useless_words))
231
232        registry = Gst.Registry.get()
233        factories = registry.get_feature_list(Gst.ElementFactory)
234        longnames = set()
235        duplicate_longnames = set()
236        for factory in factories:
237            longname = factory.get_longname()
238            if longname in longnames:
239                duplicate_longnames.add(longname)
240            else:
241                longnames.add(longname)
242        for factory in factories:
243            klass = factory.get_klass()
244            name = factory.get_name()
245            if ("Effect" not in klass or
246                    any(black in name for black in BLACKLISTED_PLUGINS)):
247                continue
248
249            media_type = None
250            if "Audio" in klass:
251                self.audio_effects.append(factory)
252                media_type = AUDIO_EFFECT
253            elif "Video" in klass:
254                self.video_effects.append(factory)
255                media_type = VIDEO_EFFECT
256            if not media_type:
257                HIDDEN_EFFECTS.append(name)
258                continue
259
260            longname = factory.get_longname()
261            if longname in duplicate_longnames:
262                # Workaround https://bugzilla.gnome.org/show_bug.cgi?id=760566
263                # Add name which identifies the element and is unique.
264                longname = "%s %s" % (longname, name)
265            human_name = uselessRe.sub("", longname).title()
266            effect = EffectInfo(name,
267                                media_type,
268                                categories=self._getEffectCategories(name),
269                                human_name=human_name,
270                                description=factory.get_description())
271            self._effects[name] = effect
272
273        gl_element_factories = registry.get_feature_list_by_plugin("opengl")
274        self.gl_effects = [element_factory.get_name()
275                           for element_factory in gl_element_factories]
276        if self.gl_effects:
277            # Checking whether the GL effects can be used
278            # by setting a pipeline with "gleffects" to PAUSED.
279            pipeline = Gst.parse_launch("videotestsrc ! glupload ! gleffects ! fakesink")
280            bus = pipeline.get_bus()
281            bus.add_signal_watch()
282            bus.connect("message", self._gl_pipeline_message_cb, pipeline)
283            assert pipeline.set_state(Gst.State.PAUSED) == Gst.StateChangeReturn.ASYNC
284
285    def _gl_pipeline_message_cb(self, bus, message, pipeline):
286        """Handles a `message` event on the pipeline for checking gl effects."""
287        done = False
288        if message.type == Gst.MessageType.ASYNC_DONE:
289            self.debug("GL effects check pipeline successfully PAUSED")
290            done = True
291        elif message.type == Gst.MessageType.ERROR:
292            # The pipeline cannot be set to PAUSED.
293            error, detail = message.parse_error()
294            self.debug("Hiding the GL effects because: %s, %s", error, detail)
295            HIDDEN_EFFECTS.extend(self.gl_effects)
296            done = True
297
298        if done:
299            bus.remove_signal_watch()
300            bus.disconnect_by_func(self._gl_pipeline_message_cb)
301            pipeline.set_state(Gst.State.NULL)
302
303    def getInfo(self, bin_description):
304        """Gets the info for an effect which can be applied.
305
306        Args:
307            bin_description (str): The bin_description defining the effect.
308
309        Returns:
310            EffectInfo: The info corresponding to the name, or None.
311        """
312        name = EffectInfo.name_from_bin_description(bin_description)
313        return self._effects.get(name)
314
315    def _getEffectCategories(self, effect_name):
316        """Gets the categories to which the specified effect belongs.
317
318        Args:
319            effect_name (str): The bin_description identifying the effect.
320
321        Returns:
322            List[str]: The categories which contain the effect.
323        """
324        categories = []
325        for category_name, effects in AUDIO_EFFECTS_CATEGORIES:
326            if effect_name in effects:
327                categories.append(category_name)
328        for category_name, effects in VIDEO_EFFECTS_CATEGORIES:
329            if effect_name in effects:
330                categories.append(category_name)
331        if not categories:
332            categories.append(_("Uncategorized"))
333        categories.insert(0, _("All effects"))
334        return categories
335
336    @property
337    def video_categories(self):
338        """Gets all video effect categories names."""
339        return EffectsManager._getCategoriesNames(VIDEO_EFFECTS_CATEGORIES)
340
341    @property
342    def audio_categories(self):
343        """Gets all audio effect categories names."""
344        return EffectsManager._getCategoriesNames(AUDIO_EFFECTS_CATEGORIES)
345
346    @staticmethod
347    def _getCategoriesNames(categories):
348        ret = [category_name for category_name, unused_effects in categories]
349        ret.sort()
350        ret.insert(0, _("All effects"))
351        if categories:
352            # Add Uncategorized only if there are other categories defined.
353            ret.append(_("Uncategorized"))
354        return ret
355
356
357# ----------------------- UI classes to manage effects -------------------------#
358
359
360class EffectListWidget(Gtk.Box, Loggable):
361    """Widget for listing effects."""
362
363    def __init__(self, instance):
364        Gtk.Box.__init__(self)
365        Loggable.__init__(self)
366
367        self.app = instance
368
369        self._draggedItems = None
370        self._effectType = VIDEO_EFFECT
371
372        self.set_orientation(Gtk.Orientation.VERTICAL)
373        builder = Gtk.Builder()
374        builder.add_from_file(os.path.join(get_ui_dir(), "effectslibrary.ui"))
375        builder.connect_signals(self)
376        toolbar = builder.get_object("effectslibrary_toolbar")
377        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR)
378        self.video_togglebutton = builder.get_object("video_togglebutton")
379        self.audio_togglebutton = builder.get_object("audio_togglebutton")
380        self.categoriesWidget = builder.get_object("categories")
381        self.searchEntry = builder.get_object("search_entry")
382
383        # Store
384        self.storemodel = Gtk.ListStore(
385            str, str, int, object, str, GdkPixbuf.Pixbuf)
386        self.storemodel.set_sort_column_id(
387            COL_NAME_TEXT, Gtk.SortType.ASCENDING)
388
389        # Create the filter for searching the storemodel.
390        self.model_filter = self.storemodel.filter_new()
391        self.model_filter.set_visible_func(self._setRowVisible, data=None)
392
393        self.view = Gtk.TreeView(model=self.model_filter)
394        self.view.props.headers_visible = False
395        self.view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
396
397        icon_col = Gtk.TreeViewColumn()
398        icon_col.set_spacing(SPACING)
399        icon_col.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
400        icon_col.props.fixed_width = ICON_WIDTH
401        icon_cell = Gtk.CellRendererPixbuf()
402        icon_cell.props.xpad = 6
403        icon_col.pack_start(icon_cell, True)
404        icon_col.add_attribute(icon_cell, "pixbuf", COL_ICON)
405
406        text_col = Gtk.TreeViewColumn()
407        text_col.set_expand(True)
408        text_col.set_spacing(SPACING)
409        text_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
410        text_cell = Gtk.CellRendererText()
411        text_cell.props.yalign = 0.0
412        text_cell.props.xpad = 6
413        text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
414        text_col.pack_start(text_cell, True)
415        text_col.set_cell_data_func(
416            text_cell, self.viewDescriptionCellDataFunc, None)
417
418        self.view.append_column(icon_col)
419        self.view.append_column(text_col)
420
421        self.view.connect("query-tooltip", self._treeViewQueryTooltipCb)
422        self.view.props.has_tooltip = True
423
424        # Make the treeview a drag source which provides effects.
425        self.view.enable_model_drag_source(
426            Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
427
428        self.view.connect("button-press-event", self._buttonPressEventCb)
429        self.view.connect("select-cursor-row", self._enterPressEventCb)
430        self.view.connect("drag-data-get", self._dndDragDataGetCb)
431
432        scrollwin = Gtk.ScrolledWindow()
433        scrollwin.props.hscrollbar_policy = Gtk.PolicyType.NEVER
434        scrollwin.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC
435        scrollwin.add(self.view)
436
437        self.pack_start(toolbar, False, False, 0)
438        self.pack_start(scrollwin, True, True, 0)
439
440        # Delay the loading of the available effects so the application
441        # starts faster.
442        GLib.idle_add(self._loadAvailableEffectsCb)
443        self.populate_categories_widget()
444
445        # Individually show the tab's widgets.
446        # If you use self.show_all(), the tab will steal focus on startup.
447        scrollwin.show_all()
448        toolbar.show_all()
449
450    def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
451        is_row, x, y, model, path, tree_iter = view.get_tooltip_context(
452            x, y, keyboard_mode)
453        if not is_row:
454            return False
455
456        view.set_tooltip_row(tooltip, path)
457        tooltip.set_markup(self.formatDescription(model, tree_iter))
458        return True
459
460    def viewDescriptionCellDataFunc(self, unused_column, cell, model, iter_, unused_data):
461        cell.props.markup = self.formatDescription(model, iter_)
462
463    def formatDescription(self, model, iter_):
464        name, element_name, desc = model.get(iter_, COL_NAME_TEXT, COL_ELEMENT_NAME, COL_DESC_TEXT)
465        escape = GLib.markup_escape_text
466        return "<b>%s</b>\n%s" % (escape(name), escape(desc))
467
468    def _loadAvailableEffectsCb(self):
469        self._addFactories(self.app.effects.video_effects, VIDEO_EFFECT)
470        self._addFactories(self.app.effects.audio_effects, AUDIO_EFFECT)
471        return False
472
473    def _addFactories(self, elements, effectType):
474        for element in elements:
475            name = element.get_name()
476            if name in HIDDEN_EFFECTS:
477                continue
478            effect_info = self.app.effects.getInfo(name)
479            self.storemodel.append([effect_info.human_name,
480                                    effect_info.description,
481                                    effectType,
482                                    effect_info.categories,
483                                    name,
484                                    effect_info.icon])
485
486    def populate_categories_widget(self):
487        self.categoriesWidget.get_model().clear()
488        icon_column = self.view.get_column(0)
489
490        if self._effectType is VIDEO_EFFECT:
491            for category in self.app.effects.video_categories:
492                self.categoriesWidget.append_text(category)
493            icon_column.props.visible = True
494        else:
495            for category in self.app.effects.audio_categories:
496                self.categoriesWidget.append_text(category)
497            icon_column.props.visible = False
498
499        self.categoriesWidget.set_active(0)
500
501    def _dndDragDataGetCb(self, unused_view, drag_context, selection_data, unused_info, unused_timestamp):
502        data = bytes(self.getSelectedEffect(), "UTF-8")
503        selection_data.set(drag_context.list_targets()[0], 0, data)
504
505    def _rowUnderMouseSelected(self, view, event):
506        result = view.get_path_at_pos(int(event.x), int(event.y))
507        if result:
508            path = result[0]
509            selection = view.get_selection()
510            return selection.path_is_selected(path) and\
511                selection.count_selected_rows() > 0
512        return False
513
514    def _enterPressEventCb(self, unused_view, unused_event=None):
515        self._addSelectedEffect()
516
517    def _buttonPressEventCb(self, view, event):
518        chain_up = True
519
520        if event.button == 3:
521            chain_up = False
522        elif event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'):
523            self._addSelectedEffect()
524        else:
525            chain_up = not self._rowUnderMouseSelected(view, event)
526
527        if chain_up:
528            self._draggedItems = None
529        else:
530            self._draggedItems = self.getSelectedEffect()
531
532        Gtk.TreeView.do_button_press_event(view, event)
533        return True
534
535    def _addSelectedEffect(self):
536        """Adds the selected effect to the single selected clip, if any."""
537        effect = self.getSelectedEffect()
538        effect_info = self.app.effects.getInfo(effect)
539        if not effect_info:
540            return
541        timeline = self.app.gui.timeline_ui.timeline
542        clip = timeline.selection.getSingleClip()
543        if not clip:
544            return
545        pipeline = timeline.ges_timeline.get_parent()
546        from pitivi.undo.timeline import CommitTimelineFinalizingAction
547        with self.app.action_log.started("add effect",
548                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
549                                         toplevel=True):
550            clip.ui.add_effect(effect_info)
551
552    def getSelectedEffect(self):
553        if self._draggedItems:
554            return self._draggedItems
555        model, rows = self.view.get_selection().get_selected_rows()
556        path = self.model_filter.convert_path_to_child_path(rows[0])
557        return self.storemodel[path][COL_ELEMENT_NAME]
558
559    def _toggleViewTypeCb(self, widget):
560        """Switches the view mode between video and audio.
561
562        This makes the two togglebuttons behave like a group of radiobuttons.
563        """
564        if widget is self.video_togglebutton:
565            self.audio_togglebutton.set_active(not widget.get_active())
566        else:
567            assert widget is self.audio_togglebutton
568            self.video_togglebutton.set_active(not widget.get_active())
569
570        if self.video_togglebutton.get_active():
571            self._effectType = VIDEO_EFFECT
572        else:
573            self._effectType = AUDIO_EFFECT
574        self.populate_categories_widget()
575        self.model_filter.refilter()
576
577    def _categoryChangedCb(self, unused_combobox):
578        self.model_filter.refilter()
579
580    def _searchEntryChangedCb(self, unused_entry):
581        self.model_filter.refilter()
582
583    def _searchEntryIconClickedCb(self, entry, unused, unused1):
584        entry.set_text("")
585
586    def _setRowVisible(self, model, iter, unused_data):
587        if not self._effectType == model.get_value(iter, COL_EFFECT_TYPE):
588            return False
589        if model.get_value(iter, COL_EFFECT_CATEGORIES) is None:
590            return False
591        if self.categoriesWidget.get_active_text() not in model.get_value(iter, COL_EFFECT_CATEGORIES):
592            return False
593        text = self.searchEntry.get_text().lower()
594        return text in model.get_value(iter, COL_DESC_TEXT).lower() or\
595            text in model.get_value(iter, COL_NAME_TEXT).lower()
596
597
598PROPS_TO_IGNORE = ['name', 'qos', 'silent', 'message', 'parent']
599
600
601class EffectsPropertiesManager:
602    """Provides and caches UIs for editing effects.
603
604    Attributes:
605        app (Pitivi): The app.
606    """
607
608    def __init__(self, app):
609        self.cache_dict = {}
610        self._current_element_values = {}
611        self.app = app
612
613    def getEffectConfigurationUI(self, effect):
614        """Gets a configuration UI element for the effect.
615
616        Args:
617            effect (Gst.Element): The effect for which we want the UI.
618
619        Returns:
620            GstElementSettingsWidget: A container for configuring the effect.
621        """
622        if effect not in self.cache_dict:
623            # Here we should handle special effects configuration UI
624            effect_widget = GstElementSettingsWidget()
625            effect_widget.setElement(effect, ignore=PROPS_TO_IGNORE,
626                                     with_reset_button=True)
627            self.cache_dict[effect] = effect_widget
628            self._connectAllWidgetCallbacks(effect_widget, effect)
629            self._postConfiguration(effect, effect_widget)
630
631        for prop in effect.list_children_properties():
632            value = effect.get_child_property(prop.name)
633            self._current_element_values[prop.name] = value
634
635        return self.cache_dict[effect]
636
637    def cleanCache(self, effect):
638        if effect in self.cache_dict:
639            return self.cache_dict.pop(effect)
640
641    def _postConfiguration(self, effect, effect_set_ui):
642        if 'aspectratiocrop' in effect.get_property("bin-description"):
643            for widget in effect_set_ui.get_children()[0].get_children():
644                if isinstance(widget, FractionWidget):
645                    widget.addPresets(["4:3", "5:4", "9:3", "16:9", "16:10"])
646
647    def _connectAllWidgetCallbacks(self, effect_settings_widget, effect):
648        for prop, widget in effect_settings_widget.properties.items():
649            widget.connectValueChanged(self._onValueChangedCb, widget, prop, effect)
650
651    def _onSetDefaultCb(self, unused_widget, effect_widget):
652        effect_widget.setWidgetToDefault()
653
654    def _onValueChangedCb(self, unused_widget, effect_widget, prop, effect):
655        value = effect_widget.getWidgetValue()
656
657        # FIXME Workaround in order to make aspectratiocrop working
658        if isinstance(value, Gst.Fraction):
659            value = Gst.Fraction(int(value.num), int(value.denom))
660
661        if value != self._current_element_values.get(prop.name):
662            from pitivi.undo.timeline import CommitTimelineFinalizingAction
663
664            pipeline = self.app.project_manager.current_project.pipeline
665            with self.app.action_log.started("Effect property change",
666                                             finalizing_action=CommitTimelineFinalizingAction(pipeline),
667                                             toplevel=True):
668                effect.set_child_property(prop.name, value)
669            self._current_element_values[prop.name] = value
670