[pitivi] trackerperspective: Allow covering tracked object to hide it



commit c6c41c31285d123b90934d4b29514f934217cd4a
Author: Vivek R <123vivekr gmail com>
Date:   Sat Aug 8 10:50:53 2020 +0530

    trackerperspective: Allow covering tracked object to hide it
    
    This adds a feature to apply a cover effect to a tracked object inside a
    video clip. The effect is a videotestsrc with a solid foreground color.
    
    Fixes #1942

 data/ui/customwidgets/pitivi:object_effect.ui |  48 ++++++++++
 pitivi/clipproperties.py                      |  65 +++++++-------
 pitivi/dialogs/prefs.py                       |   2 +-
 pitivi/effects.py                             |  23 ++++-
 pitivi/greeterperspective.py                  |   2 +-
 pitivi/trackerperspective.py                  | 123 +++++++++++++++++++++++++-
 pitivi/utils/custom_effect_widgets.py         |  73 +++++++++++++--
 pitivi/utils/ui.py                            |  26 +++---
 tests/test_trackerperspective.py              |  45 ++++++++++
 9 files changed, 349 insertions(+), 58 deletions(-)
---
diff --git a/data/ui/customwidgets/pitivi:object_effect.ui b/data/ui/customwidgets/pitivi:object_effect.ui
new file mode 100644
index 000000000..8888a91a6
--- /dev/null
+++ b/data/ui/customwidgets/pitivi:object_effect.ui
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <object class="GtkBox" id="base_table">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <property name="halign">start</property>
+    <property name="margin-start">12</property>
+    <property name="margin-end">12</property>
+    <property name="margin-top">12</property>
+    <property name="margin-bottom">12</property>
+    <property name="spacing">10</property>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Color:</property>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkColorButton" id="color_button">
+        <property name="use-action-appearance">False</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="receives-default">True</property>
+        <property name="halign">start</property>
+        <property name="margin-left">2</property>
+        <property name="hexpand">True</property>
+        <property name="use-alpha">True</property>
+        <property name="title" translatable="yes">Pick the clip color</property>
+        <property name="rgba">rgb(255,255,255)</property>
+        <signal name="color-set" handler="color_button_color_set_cb" swapped="no"/>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 05e5a8f53..cb906bc3d 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -45,9 +45,10 @@ from pitivi.configure import in_devel
 from pitivi.effects import EffectsPopover
 from pitivi.effects import EffectsPropertiesManager
 from pitivi.effects import HIDDEN_EFFECTS
-from pitivi.trackerperspective import TrackerPerspective
+from pitivi.trackerperspective import CoverObjectPopover
 from pitivi.undo.timeline import CommitTimelineFinalizingAction
-from pitivi.utils.custom_effect_widgets import setup_custom_effect_widgets
+from pitivi.utils.custom_effect_widgets import create_custom_prop_widget_cb
+from pitivi.utils.custom_effect_widgets import create_custom_widget_cb
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.misc import disconnect_all_by_func
 from pitivi.utils.pipeline import PipelineError
@@ -573,7 +574,9 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.clip = None
 
         self.effects_properties_manager = EffectsPropertiesManager(app)
-        setup_custom_effect_widgets(self.effects_properties_manager)
+        # Set up the effects manager to be able to create custom UI.
+        self.effects_properties_manager.connect("create_widget", create_custom_widget_cb)
+        self.effects_properties_manager.connect("create_property_widget", create_custom_prop_widget_cb)
 
         self.drag_lines_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
             os.path.join(get_pixmap_dir(), "grip-lines-solid.svg"),
@@ -598,11 +601,11 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.object_tracker_box = Gtk.ButtonBox()
         self.object_tracker_box.props.halign = Gtk.Align.CENTER
 
+        self.cover_popover: Optional[Gtk.Popover] = None
+        self.cover_object_button: Optional[Gtk.MenuButton] = None
         if "cvtracker" not in MISSING_SOFT_DEPS:
-            self.track_object_button = Gtk.Button(_("Track Object"))
-            self.track_object_button.connect("clicked", self.__track_object_button_clicked_cb)
-            self.track_object_button.props.halign = Gtk.Align.CENTER
-            self.object_tracker_box.pack_start(self.track_object_button, False, False, 0)
+            self.cover_object_button = Gtk.MenuButton(_("Cover Object"))
+            self.object_tracker_box.pack_start(self.cover_object_button, False, False, 0)
 
         self.drag_dest_set(Gtk.DestDefaults.DROP, [EFFECT_TARGET_ENTRY],
                            Gdk.DragAction.COPY)
@@ -618,40 +621,36 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.connect("drag-leave", self._drag_leave_cb)
         self.connect("drag-data-received", self._drag_data_received_cb)
 
-        self.add_effect_button.connect("toggled", self._add_effect_button_cb)
+        self.add_effect_button.connect("toggled", self._add_effect_button_toggled_cb)
+        if self.cover_object_button:
+            self.cover_object_button.connect("toggled", self._cover_object_button_toggled_cb)
 
         self.show_all()
 
-    def __track_object_button_clicked_cb(self, button):
-        tracker = TrackerPerspective(self.app, self.clip.asset)
-        self.app.project_manager.current_project.pipeline.pause()
-        tracker.setup_ui()
-        self.app.gui.show_perspective(tracker)
-
-    def _add_effect_button_cb(self, button):
+    def _add_effect_button_toggled_cb(self, button):
         # MenuButton interacts directly with the popover, bypassing our subclassed method
         if button.props.active:
             self.effect_popover.search_entry.set_text("")
 
+    def _cover_object_button_toggled_cb(self, button):
+        if button.props.active:
+            self.cover_popover.update_object_list()
+
     def _create_effect_row(self, effect):
         if is_time_effect(effect):
             return None
 
-        effect_info = self.app.effects.get_info(effect.props.bin_description)
-
         vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 
-        row_drag_icon = Gtk.Image.new_from_pixbuf(self.drag_lines_pixbuf)
-
         toggle = Gtk.CheckButton()
         toggle.props.active = effect.props.active
 
+        effect_info = self.app.effects.get_info(effect)
         effect_label = Gtk.Label(effect_info.human_name)
         effect_label.set_tooltip_text(effect_info.description)
 
         # Set up revealer + expander
-        effect_config_ui = self.effects_properties_manager.get_effect_configuration_ui(
-            effect)
+        effect_config_ui = self.effects_properties_manager.get_effect_configuration_ui(effect)
         config_ui_revealer = Gtk.Revealer()
         config_ui_revealer.add(effect_config_ui)
 
@@ -668,6 +667,7 @@ class EffectProperties(Gtk.Expander, Loggable):
         remove_effect_button.props.margin_right = PADDING
 
         row_widgets_box = Gtk.Box()
+        row_drag_icon = Gtk.Image.new_from_pixbuf(self.drag_lines_pixbuf)
         row_widgets_box.pack_start(row_drag_icon, False, False, PADDING)
         row_widgets_box.pack_start(toggle, False, False, PADDING)
         row_widgets_box.pack_start(expander, True, True, PADDING)
@@ -769,6 +769,7 @@ class EffectProperties(Gtk.Expander, Loggable):
 
         self.clip = clip
         if self.clip:
+            cover_object_button_show = False
             self.clip.connect("child-added", self._track_element_added_cb)
             self.clip.connect("child-removed", self._track_element_removed_cb)
             for track_element in self.clip.get_children(recursive=True):
@@ -777,12 +778,15 @@ class EffectProperties(Gtk.Expander, Loggable):
                         continue
                     self._connect_to_track_element(track_element)
                 if isinstance(track_element, GES.VideoUriSource) and not clip.asset.is_image():
-                    self.track_object_button.show()
-
+                    cover_object_button_show = True
+            if self.cover_object_button:
+                self.cover_object_button.set_visible(cover_object_button_show)
+                if cover_object_button_show:
+                    self.cover_popover = CoverObjectPopover(self.app, self.clip)
+                    self.cover_object_button.set_popover(self.cover_popover)
             self._update_listbox()
-            self.show()
-        else:
-            self.hide()
+
+        self.props.visible = bool(self.clip)
 
     def _track_element_added_cb(self, unused_clip, track_element):
         if isinstance(track_element, GES.BaseEffect):
@@ -831,7 +835,7 @@ class EffectProperties(Gtk.Expander, Loggable):
 
     def _drag_data_get_cb(self, eventbox, drag_context, selection_data, unused_info, unused_timestamp):
         row = eventbox.get_parent()
-        effect_info = self.app.effects.get_info(row.effect.props.bin_description)
+        effect_info = self.app.effects.get_info(row.effect)
         effect_name = effect_info.human_name
 
         data = bytes(effect_name, "UTF-8")
@@ -1116,9 +1120,10 @@ class TransformationProperties(Gtk.Expander, Loggable):
                 control_source = GstController.InterpolationControlSource()
                 control_source.props.mode = GstController.InterpolationMode.LINEAR
                 self.__own_bindings_change = True
-                self.source.set_control_source(
-                    control_source, prop, "direct-absolute")
-                self.__own_bindings_change = False
+                try:
+                    self.source.set_control_source(control_source, prop, "direct-absolute")
+                finally:
+                    self.__own_bindings_change = False
                 self.__set_default_keyframes_values(control_source, prop)
 
                 binding = self.source.get_control_binding(prop)
diff --git a/pitivi/dialogs/prefs.py b/pitivi/dialogs/prefs.py
index de7217656..2f673e2ac 100644
--- a/pitivi/dialogs/prefs.py
+++ b/pitivi/dialogs/prefs.py
@@ -402,7 +402,7 @@ class PreferencesDialog(Loggable):
 
         self.content_box.bind_model(self.list_store, self._create_widget_func, None)
         self.content_box.set_header_func(self._add_header_func, None)
-        self.content_box.connect("row_activated", self.__row_activated_cb)
+        self.content_box.connect("row-activated", self.__row_activated_cb)
         self.content_box.set_selection_mode(Gtk.SelectionMode.NONE)
         self.content_box.props.margin = PADDING * 3
         self.content_box.props.halign = Gtk.Align.CENTER
diff --git a/pitivi/effects.py b/pitivi/effects.py
index 7c71def48..d61ae68b1 100644
--- a/pitivi/effects.py
+++ b/pitivi/effects.py
@@ -32,6 +32,8 @@ import subprocess
 import sys
 import threading
 from gettext import gettext as _
+from typing import Optional
+from typing import Union
 
 import cairo
 from gi.repository import Gdk
@@ -45,6 +47,8 @@ from gi.repository import Gtk
 from pitivi.configure import get_pixmap_dir
 from pitivi.configure import get_ui_dir
 from pitivi.settings import GlobalSettings
+from pitivi.trackerperspective import EFFECT_TRACKED_OBJECT_ID_META
+from pitivi.trackerperspective import EFFECT_TRACKED_OBJECT_NAME_META
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.ui import disable_scroll
 from pitivi.utils.ui import EFFECT_TARGET_ENTRY
@@ -172,7 +176,6 @@ class EffectInfo:
 
     def __init__(self, effect_name, media_type, categories,
                  human_name, description):
-        object.__init__(self)
         self.effect_name = effect_name
         self.media_type = media_type
         self.categories = categories
@@ -308,15 +311,29 @@ class EffectsManager(Loggable):
             self.error("Can not use GL effects: %s", e)
             HIDDEN_EFFECTS.extend(self.gl_effects)
 
-    def get_info(self, bin_description):
+    def get_info(self, effect: Union[str, GES.Effect]) -> Optional[EffectInfo]:
         """Gets the info for an effect which can be applied.
 
         Args:
-            bin_description (str): The bin_description defining the effect.
+            effect: The effect itself or the bin_description defining it.
 
         Returns:
             EffectInfo: The info corresponding to the name, or None.
         """
+        if isinstance(effect, GES.Effect):
+            tracked_object_id = effect.get_string(EFFECT_TRACKED_OBJECT_ID_META)
+            if tracked_object_id:
+                tracked_object_name = effect.get_string(EFFECT_TRACKED_OBJECT_NAME_META)
+                # Translators: How the video effect which covers/hides a
+                # tracked object is listed. The {} is entered by the user
+                # and denotes the tracked object.
+                human_name = _("{} cover").format(tracked_object_name)
+                description = _("Object cover effect")
+                return EffectInfo(None, None, None, human_name, description)
+            bin_description = effect.props.bin_description
+        else:
+            bin_description = effect
+
         name = EffectInfo.name_from_bin_description(bin_description)
         return self._effects.get(name)
 
diff --git a/pitivi/greeterperspective.py b/pitivi/greeterperspective.py
index cb1b63d72..b822460d8 100644
--- a/pitivi/greeterperspective.py
+++ b/pitivi/greeterperspective.py
@@ -145,7 +145,7 @@ class GreeterPerspective(Perspective):
         self.__recent_projects_listbox = builder.get_object("recent_projects_listbox")
         self.__recent_projects_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
         self.__recent_projects_listbox.connect(
-            "row_activated", self.__projects_row_activated_cb)
+            "row-activated", self.__projects_row_activated_cb)
         self.__recent_projects_listbox.connect(
             "button-press-event", self.__projects_button_press_cb)
 
diff --git a/pitivi/trackerperspective.py b/pitivi/trackerperspective.py
index 39c18dd3d..b12bcaf23 100644
--- a/pitivi/trackerperspective.py
+++ b/pitivi/trackerperspective.py
@@ -32,6 +32,7 @@ from gi.repository import GES
 from gi.repository import Gio
 from gi.repository import GObject
 from gi.repository import Gst
+from gi.repository import GstController
 from gi.repository import GstVideo
 from gi.repository import Gtk
 
@@ -48,6 +49,11 @@ from pitivi.utils.ui import SPACING
 # The meta of an Asset holding all the tracked objects data version 1.
 ASSET_TRACKED_OBJECTS_META = "pitivi::tracker_data::1"
 
+# The meta of an Effect holding the object_id of the tracked object.
+EFFECT_TRACKED_OBJECT_ID_META = "pitivi:tracked_object_id"
+# The meta of an Effect holding the name of the tracked object.
+EFFECT_TRACKED_OBJECT_NAME_META = "pitivi:tracked_object_name"
+
 
 # TODO: Replace with bisect.bisect_left when we use Python 3.10.
 def bisect_left(values, val, key):
@@ -475,7 +481,7 @@ class ToplevelWidget(Gtk.Box, Loggable):
     def _remove_object_button_clicked_cb(self, button):
         row = self.object_listbox.get_selected_row()
         index = row.get_index()
-        tracked_object = self.tracked_objects_store.get_item(index)
+        tracked_object: TrackedObjectItem = self.tracked_objects_store.get_item(index)
         self.tracked_objects_store.remove(index)
 
         self.remove_object_button.props.sensitive = False
@@ -677,3 +683,118 @@ class TrackerPerspective(Perspective):
     def refresh(self):
         """Refreshes the perspective."""
         self.toplevel_widget.play_pause_button.grab_focus()
+
+
+class CoverObjectPopover(Gtk.Popover, Loggable):
+    """Popover for selecting an object to cover."""
+
+    # The representation of the effect providing the cover.
+    _EFFECT_PIPELINE = "video videotestsrc pattern=solid-color foreground-color=0xff000000 ! framepositioner 
name=positioner ! gescompositor"
+
+    def __init__(self, app, clip: GES.Clip):
+        Gtk.Popover.__init__(self)
+        Loggable.__init__(self)
+
+        self.app = app
+
+        self.clip: GES.Clip = clip
+        self.object_manager: Optional[ObjectManager] = None
+
+        self.listbox = Gtk.ListBox()
+        self.listbox.connect("row-activated", self.__row_activated_cb)
+
+        self.scroll_window = Gtk.ScrolledWindow()
+        self.scroll_window.add(self.listbox)
+        self.scroll_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
+        self.scroll_window.props.max_content_height = 350
+        self.scroll_window.props.propagate_natural_height = True
+
+        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, margin=PADDING)
+        vbox.pack_start(self.scroll_window, True, True, 0)
+        vbox.show_all()
+
+        self.add(vbox)
+
+    def update_object_list(self):
+        """Updates the list of not yet covered objects."""
+        self.object_manager = ObjectManager(self.clip.asset)
+
+        for row in self.listbox.get_children():
+            self.listbox.remove(row)
+
+        # Check which tracked objects have already been covered.
+        covered_objects = []
+        for effect in self.clip.get_top_effects():
+            tracked_object_id = effect.get_string(EFFECT_TRACKED_OBJECT_ID_META)
+            if tracked_object_id:
+                covered_objects.append(tracked_object_id)
+
+        # Allow selecting the not-yet-covered objects.
+        for _index, object_id, name in self.object_manager.objects:
+            if object_id not in covered_objects:
+                self.listbox.add(TrackedObjectRow(object_id, name))
+
+        # Allow tracking new objects.
+        button_row = Gtk.ListBoxRow(selectable=False)
+        track_objects_button = Gtk.Button(_("Track objects"))
+        track_objects_button.connect("clicked", self.__track_objects_button_clicked_cb)
+        button_row.add(track_objects_button)
+        self.listbox.add(button_row)
+
+        self.listbox.show_all()
+
+    def __row_activated_cb(self, listbox: Gtk.ListBox, row: TrackedObjectRow):
+        self._create_effect(row.object_id, row.name)
+
+        self.popdown()
+
+    def __effect_control_binding_added_cb(self, track_element, binding, object_id):
+        control_source = binding.props.control_source
+        timed_data = self.object_manager.values[object_id]
+        for timestamp, (x, y, w, h) in timed_data:
+            if binding.name == "posx":
+                value = x
+            elif binding.name == "posy":
+                value = y
+            elif binding.name == "width":
+                value = w
+            elif binding.name == "height":
+                value = h
+            else:
+                break
+
+            control_source.set(timestamp, value)
+
+    def __clip_child_added_cb(self, clip, track_element, object_id):
+        if not isinstance(track_element, GES.Effect):
+            return
+
+        clip.disconnect_by_func(self.__clip_child_added_cb)
+
+        track_element.connect("control-binding-added",
+                              self.__effect_control_binding_added_cb,
+                              object_id)
+        try:
+            for prop in ("posx", "posy", "width", "height"):
+                control_source = GstController.InterpolationControlSource()
+                control_source.props.mode = GstController.InterpolationMode.NONE
+                track_element.set_control_source(control_source, prop, "direct-absolute")
+        finally:
+            track_element.disconnect_by_func(self.__effect_control_binding_added_cb)
+
+    def _create_effect(self, object_id: str, name: str):
+        effect = GES.Effect.new(self._EFFECT_PIPELINE)
+        effect.register_meta_string(GES.MetaFlag.READABLE, EFFECT_TRACKED_OBJECT_ID_META, object_id)
+        effect.register_meta_string(GES.MetaFlag.READABLE, EFFECT_TRACKED_OBJECT_NAME_META, name)
+
+        self.log("Waiting for effect to be added to the clip")
+        self.clip.connect("child-added", self.__clip_child_added_cb, object_id)
+        self.clip.add_top_effect(effect, 0)
+
+        self.app.project_manager.current_project.pipeline.commit_timeline()
+
+    def __track_objects_button_clicked_cb(self, button):
+        tracker = TrackerPerspective(self.app, self.clip.asset)
+        self.app.project_manager.current_project.pipeline.pause()
+        tracker.setup_ui()
+        self.app.gui.show_perspective(tracker)
diff --git a/pitivi/utils/custom_effect_widgets.py b/pitivi/utils/custom_effect_widgets.py
index 44b5a60ed..5c2e27157 100644
--- a/pitivi/utils/custom_effect_widgets.py
+++ b/pitivi/utils/custom_effect_widgets.py
@@ -23,6 +23,7 @@ from gi.repository import Gdk
 from gi.repository import Gtk
 
 from pitivi import configure
+from pitivi.trackerperspective import EFFECT_TRACKED_OBJECT_ID_META
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.ui import create_model
 from pitivi.utils.widgets import ColorPickerButton
@@ -31,12 +32,6 @@ from pitivi.utils.widgets import ColorPickerButton
 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)
-    effect_prop_manager.connect("create_property_widget", create_custom_prop_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
@@ -59,6 +54,11 @@ def create_custom_prop_widget_cb(unused_effect_prop_manager, effect_widget, effe
 
 def create_custom_widget_cb(effect_prop_manager, effect_widget, effect):
     """Creates custom effect UI."""
+    tracked_object_id = effect.get_string(EFFECT_TRACKED_OBJECT_ID_META)
+    if tracked_object_id:
+        widget = object_cover_effect_widget(effect_prop_manager, effect_widget, effect)
+        return widget
+
     effect_name = effect.get_property("bin-description")
     path = os.path.join(CUSTOM_WIDGETS_DIR, effect_name + ".ui")
 
@@ -83,6 +83,67 @@ def create_custom_widget_cb(effect_prop_manager, effect_widget, effect):
     return widget
 
 
+def object_cover_effect_widget(effect_prop_manager, element_setting_widget, element):
+    """Creates the UI for the `Object cover` effect."""
+    builder = setup_from_ui_file(element_setting_widget, os.path.join(CUSTOM_WIDGETS_DIR, 
"pitivi:object_effect.ui"))
+    base_table = builder.get_object("base_table")
+
+    def set_foreground_color(color):
+        from pitivi.undo.timeline import CommitTimelineFinalizingAction
+        pipeline = effect_prop_manager.app.project_manager.current_project.pipeline
+        action_log = effect_prop_manager.app.action_log
+        with action_log.started("Effect property change",
+                                finalizing_action=CommitTimelineFinalizingAction(pipeline),
+                                toplevel=True):
+            element.set_child_property("foreground-color", color)
+
+    def color_picker_value_changed_cb(widget: ColorPickerButton):
+        """Handles the selection of a color with the color picker."""
+        argb = widget.calculate_argb()
+        set_foreground_color(argb)
+
+    color_picker_button = ColorPickerButton()
+    base_table.add(color_picker_button)
+    handler_value_changed = color_picker_button.connect("value-changed", color_picker_value_changed_cb)
+
+    def color_button_color_set_cb(button: Gtk.ColorButton):
+        """Handles the selection of a color with the color button."""
+        color = button.get_rgba()
+        red = int(color.red * 255)
+        green = int(color.green * 255)
+        blue = int(color.blue * 255)
+        argb = (0xFF << 24) + (red << 16) + (green << 8) + blue
+        set_foreground_color(argb)
+
+    color_button = builder.get_object("color_button")
+    handler_color_set = color_button.connect("color-set", color_button_color_set_cb)
+
+    def update_ui():
+        res, argb = element.get_child_property("foreground-color")
+        assert res
+        color = Gdk.RGBA()
+        color.red = ((argb >> 16) & 0xFF) / 255
+        color.green = ((argb >> 8) & 0xFF) / 255
+        color.blue = ((argb >> 0) & 0xFF) / 255
+        color.alpha = ((argb >> 24) & 0xFF) / 255
+        color_button.set_rgba(color)
+
+    update_ui()
+
+    def notify_foreground_color_cb(self, element, param_spec):
+        color_picker_button.handler_block(handler_value_changed)
+        color_button.handler_block(handler_color_set)
+        try:
+            update_ui()
+        finally:
+            color_picker_button.handler_unblock(handler_value_changed)
+            color_button.handler_block(handler_color_set)
+
+    element.connect("notify::foreground-color", notify_foreground_color_cb)
+
+    return base_table
+
+
 def create_alpha_widget(effect_prop_manager, element_setting_widget, element):
     """Creates the UI for the `alpha` effect."""
     builder = setup_from_ui_file(element_setting_widget, os.path.join(CUSTOM_WIDGETS_DIR, "alpha.ui"))
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 4303cf793..cf591ddbe 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -437,20 +437,18 @@ def gtk_style_context_get_color(context, state):
     return color
 
 
-def argb_to_gdk_rgba(color_int):
-    return Gdk.RGBA(color_int / 256 ** 2 % 256 / 255.,
-                    color_int / 256 ** 1 % 256 / 255.,
-                    color_int / 256 ** 0 % 256 / 255.,
-                    color_int / 256 ** 3 % 256 / 255.)
+def argb_to_gdk_rgba(argb: int) -> Gdk.RGBA:
+    return Gdk.RGBA(((argb >> 16) & 0xFF) / 255,
+                    ((argb >> 8) & 0xFF) / 255,
+                    ((argb >> 0) & 0xFF) / 255,
+                    ((argb >> 24) & 0xFF) / 255)
 
 
-def gdk_rgba_to_argb(color):
-    color_int = 0
-    color_int += int(color.alpha * 255) * 256 ** 3
-    color_int += int(color.red * 255) * 256 ** 2
-    color_int += int(color.green * 255) * 256 ** 1
-    color_int += int(color.blue * 255) * 256 ** 0
-    return color_int
+def gdk_rgba_to_argb(color: Gdk.RGBA) -> int:
+    return ((int(color.alpha * 255) << 24) +
+            (int(color.red * 255) << 16) +
+            (int(color.green * 255) << 8) +
+            int(color.blue * 255))
 
 
 def pack_color_32(red, green, blue, alpha=0xFFFF):
@@ -497,10 +495,6 @@ def unpack_color_64(value):
     return red, green, blue, alpha
 
 
-def hex_to_rgb(value):
-    return tuple(float(int(value[i:i + 2], 16)) / 255.0 for i in range(0, 6, 2))
-
-
 def set_cairo_color(context, color):
     if isinstance(color, Gdk.RGBA):
         cairo_color = (float(color.red), float(color.green), float(color.blue))
diff --git a/tests/test_trackerperspective.py b/tests/test_trackerperspective.py
index 2d5e8905a..2a0af20a6 100644
--- a/tests/test_trackerperspective.py
+++ b/tests/test_trackerperspective.py
@@ -16,12 +16,57 @@
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 """Tests for the pitivi.trackerperspective module."""
 # pylint: disable=protected-access
+from unittest import skipUnless
+
 from gi.repository import GES
 
+from pitivi.check import MISSING_SOFT_DEPS
 from pitivi.trackerperspective import ObjectManager
 from tests import common
 
 
+class TestCoverObjectPopover(common.TestCase):
+    """Tests for the CoverObjectPopover class."""
+
+    @skipUnless("cvtracker" not in MISSING_SOFT_DEPS, "cvtracker element missing")
+    @common.setup_project_with_clips(assets_names=["tears_of_steel.webm"])
+    @common.setup_clipproperties
+    def test_cover(self):
+        clip, = self.layer.get_clips()
+        self.click_clip(clip, expect_selected=True)
+
+        expander = self.clipproperties.effect_expander
+        expander.cover_object_button.clicked()
+        self.assertTrue(expander.cover_popover.props.visible)
+        # Only one row containing the Track Objects button should exist.
+        self.assertEqual(len(expander.cover_popover.listbox.get_children()), 1)
+
+        expander.cover_object_button.clicked()
+        self.assertFalse(expander.cover_popover.props.visible)
+
+        object_manager = ObjectManager(clip.asset)
+        object_manager.add_object(1, "object1", "Object 1")
+        object_manager.update_object_position("object1", 0, (10, 20, 30, 40))
+        object_manager.add_object(2, "object2", "Object 2")
+        object_manager.update_object_position("object2", 1, (20, 30, 40, 50))
+        object_manager.save()
+
+        expander.cover_object_button.clicked()
+        self.assertTrue(expander.cover_popover.props.visible)
+        # Two rows for two object and one for Track Objects.
+        self.assertEqual(len(expander.cover_popover.listbox.get_children()), 3)
+
+        self.assertEqual(len(clip.get_top_effects()), 0)
+        expander.cover_popover.listbox.get_row_at_index(0).emit("activate")
+        self.assertFalse(expander.cover_popover.props.visible)
+        self.assertEqual(len(clip.get_top_effects()), 1)
+
+        expander.cover_object_button.clicked()
+        self.assertTrue(expander.cover_popover.props.visible)
+        # One row for the uncovered object and one for the Track Objects button.
+        self.assertEqual(len(expander.cover_popover.listbox.get_children()), 2)
+
+
 class TestObjectManager(common.TestCase):
     """Tests for the ObjectManager class."""
 


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