[pitivi] Added functionality for Color Clips



commit 81c3d4b81aca7a758153d83f1507c08271c1f195
Author: Andrew Hazel <aceknifes gmail com>
Date:   Wed Apr 29 20:20:47 2020 -0500

    Added functionality for Color Clips
    
    This allows filmmakers to create and edit color clips
    by using the Clip properties tab.
    
    Closes issue #2296

 data/ui/clipcolor.ui               |  32 ++++++++
 help/C/usingclips.page             |  11 ++-
 pitivi/clip_properties/color.py    | 118 ++++++++++++++++++++++++++++
 pitivi/clip_properties/title.py    |   7 +-
 pitivi/clipproperties.py           | 153 ++++++++++++++++++++++++++-----------
 pitivi/editorperspective.py        |   2 +
 pitivi/timeline/elements.py        |  46 ++++++++++-
 pitivi/timeline/previewers.py      |  42 +++++++---
 pitivi/utils/widgets.py            |   9 +++
 pitivi/viewer/overlay.py           |   2 +-
 tests/test_clipproperties.py       |   2 +-
 tests/test_clipproperties_color.py |  77 +++++++++++++++++++
 12 files changed, 437 insertions(+), 64 deletions(-)
---
diff --git a/data/ui/clipcolor.ui b/data/ui/clipcolor.ui
new file mode 100644
index 00000000..c994f936
--- /dev/null
+++ b/data/ui/clipcolor.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.2 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <object class="GtkBox" id="color_box">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">start</property>
+    <property name="margin_left">12</property>
+    <property name="margin_right">12</property>
+    <property name="margin_top">12</property>
+    <property name="margin_bottom">12</property>
+    <property name="spacing">5</property>
+    <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="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_cb" swapped="no"/>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/help/C/usingclips.page b/help/C/usingclips.page
index 56b82c6c..50df2218 100644
--- a/help/C/usingclips.page
+++ b/help/C/usingclips.page
@@ -2,7 +2,7 @@
 <page xmlns="http://projectmallard.org/1.0/"; xmlns:e="http://projectmallard.org/experimental/"; type="topic" 
id="usingclips">
   <info>
     <link type="guide" xref="index#gettingstarted"/>
-    <revision pkgversion="0.96" version="0.2" date="2016-02-17" status="complete"/>
+    <revision pkgversion="0.96" version="0.2" date="2020-04-21" status="complete"/>
     <credit type="author">
       <name>Jean-François Fortin Tam</name>
       <email>nekohayo gmail com</email>
@@ -15,6 +15,10 @@
       <name>Tomáš Karger</name>
       <email>tomkarger gmail com</email>
     </credit>
+    <credit type="contributor">
+      <name>UNL SOFT261 - Team 3</name>
+      <email>troy ogden huskers unl edu</email>
+    </credit>
     <desc>
       Learn the difference between clips and files and how to do basic operations on clips in the timeline.
     </desc>
@@ -70,4 +74,9 @@
       <p>You can release and press <key>Shift</key> at any time during the drag operation to disable or 
enable ripple editing.</p>
     </section>
   </section>
+  <section id="color">
+    <title>Using Color Clips</title>
+    <p>You can create custom color clips using the <gui>Create a color clip</gui> button located in the 
<guiseq><gui>middle pane</gui><gui>Clip</gui></guiseq> tab. The button is available when no clip is <link 
xref="selectiongrouping#selection">selected</link>.</p>
+    <p>Select a color clip to change its color. When a new color clip is created, it is automatically 
selected. The color can be changed using the <guiseq><gui>middle 
pane</gui><gui>Clip</gui><gui>Color</gui></guiseq> expander. The <gui>color</gui> button opens a dialog for 
selecting a color or entering a hexadecimal color value. Alternatively, any color on the screen can be picked 
by using the <gui>color picker</gui> button.</p>
+  </section>
 </page>
diff --git a/pitivi/clip_properties/color.py b/pitivi/clip_properties/color.py
new file mode 100644
index 00000000..56a445c8
--- /dev/null
+++ b/pitivi/clip_properties/color.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (C) 2020 Andrew Hazel, Thomas Braccia, Troy Ogden, Robert Kirkpatrick
+#
+# 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, see <http://www.gnu.org/licenses/>.
+"""Widgets to control clips properties."""
+import os
+from gettext import gettext as _
+
+from gi.repository import GES
+from gi.repository import Gtk
+
+from pitivi.configure import get_ui_dir
+from pitivi.settings import GlobalSettings
+from pitivi.undo.timeline import CommitTimelineFinalizingAction
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.ui import argb_to_gdk_rgba
+from pitivi.utils.ui import gdk_rgba_to_argb
+from pitivi.utils.widgets import ColorPickerButton
+
+
+GlobalSettings.add_config_section("user-interface")
+
+GlobalSettings.add_config_option("ColorClipLength",
+                                 section="user-interface",
+                                 key="color-clip-length",
+                                 default=5000,
+                                 notify=True)
+
+
+class ColorProperties(Gtk.Expander, Loggable):
+    """Widget for configuring the properties of a color clip."""
+
+    def __init__(self, app):
+        Gtk.Expander.__init__(self)
+        Loggable.__init__(self)
+
+        self.app = app
+        self.source = None
+        self._children_props_handler = None
+
+        self.set_label(_("Color"))
+        self.set_expanded(True)
+
+        self._create_ui()
+
+    def _create_ui(self):
+        self.builder = Gtk.Builder()
+        self.builder.add_from_file(os.path.join(get_ui_dir(), "clipcolor.ui"))
+        self.builder.connect_signals(self)
+
+        box = self.builder.get_object("color_box")
+        self.add(box)
+
+        self.color_button = self.builder.get_object("color_button")
+
+        self.color_picker_button = ColorPickerButton()
+        box.add(self.color_picker_button)
+        self.color_picker_button.connect(
+            "value-changed", self._color_picker_value_changed_cb)
+
+        self.show_all()
+
+    def _set_child_property(self, name, value):
+        with self.app.action_log.started("Color change property",
+                                         
finalizing_action=CommitTimelineFinalizingAction(self.app.project_manager.current_project.pipeline),
+                                         toplevel=True):
+            res = self.source.set_child_property(name, value)
+            assert res
+
+    def _color_picker_value_changed_cb(self, widget):
+        argb = widget.calculate_argb()
+        self._set_child_property("foreground-color", argb)
+
+    def _color_button_cb(self, widget):
+        argb = gdk_rgba_to_argb(widget.get_rgba())
+        self._set_child_property("foreground-color", argb)
+
+    def set_source(self, source):
+        """Sets the clip source to be edited with this editor.
+
+        Args:
+            source (GES.VideoTestSource): The source of the clip.
+        """
+        self.debug("Source set to %s", source)
+        if self._children_props_handler is not None:
+            self.source.disconnect(self._children_props_handler)
+            self._children_props_handler = None
+        self.source = None
+        if source:
+            assert isinstance(source, GES.VideoTestSource)
+            self.source = source
+            self._update_color_button()
+            self._children_props_handler = self.source.connect("deep-notify",
+                                                               self._source_deep_notify_cb)
+        self.set_visible(bool(self.source))
+
+    def _source_deep_notify_cb(self, source, unused_gstelement, pspec):
+        """Handles updates in the VideoTestSource backing the current TestClip."""
+        if pspec.name == "foreground-color":
+            self._update_color_button()
+
+    def _update_color_button(self):
+        res, argb = self.source.get_child_property("foreground-color")
+        assert res
+        color = argb_to_gdk_rgba(argb)
+        self.color_button.set_rgba(color)
diff --git a/pitivi/clip_properties/title.py b/pitivi/clip_properties/title.py
index 91031b7a..9f375ce9 100644
--- a/pitivi/clip_properties/title.py
+++ b/pitivi/clip_properties/title.py
@@ -112,7 +112,6 @@ class TitleProperties(Gtk.Expander, Loggable):
             self.settings["halignment"].append(value_id, text)
 
         self.show_all()
-        self.hide()
 
     def _set_child_property(self, name, value):
         with self.app.action_log.started("Title change property",
@@ -125,11 +124,7 @@ class TitleProperties(Gtk.Expander, Loggable):
                 self._setting_props = False
 
     def _color_picker_value_changed_cb(self, widget, color_button, color_layer):
-        argb = 0
-        argb += (1 * 255) * 256 ** 3
-        argb += float(widget.color_r) * 256 ** 2
-        argb += float(widget.color_g) * 256 ** 1
-        argb += float(widget.color_b) * 256 ** 0
+        argb = widget.calculate_argb()
         self.debug("Setting text %s to %x", color_layer, argb)
         self._set_child_property(color_layer, argb)
         rgba = argb_to_gdk_rgba(argb)
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index d4452840..c3c6e181 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -27,6 +27,7 @@ from gi.repository import GstController
 from gi.repository import Gtk
 from gi.repository import Pango
 
+from pitivi.clip_properties.color import ColorProperties
 from pitivi.clip_properties.title import TitleProperties
 from pitivi.configure import get_ui_dir
 from pitivi.effects import EffectsPropertiesManager
@@ -93,6 +94,10 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.title_expander.set_vexpand(False)
         vbox.pack_start(self.title_expander, False, False, 0)
 
+        self.color_expander = ColorProperties(app)
+        self.color_expander.set_vexpand(False)
+        vbox.pack_start(self.color_expander, False, False, 0)
+
         self.effect_expander = EffectProperties(app, self)
         self.effect_expander.set_vexpand(False)
         vbox.pack_start(self.effect_expander, False, False, 0)
@@ -100,6 +105,9 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.helper_box = self.create_helper_box()
         self.clips_box.pack_start(self.helper_box, False, False, 0)
 
+        self.title_expander.set_source(None)
+        self.color_expander.set_source(None)
+
         self._project = None
         self._selection = None
         self.app.project_manager.connect_after(
@@ -119,13 +127,18 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
 
         title_button = Gtk.Button()
         title_button.set_label(_("Create a title clip"))
-        title_button.connect("clicked", self.create_cb)
+        title_button.connect("clicked", self.create_title_clip_cb)
         box.pack_start(title_button, False, False, SPACING)
 
+        color_button = Gtk.Button()
+        color_button.set_label(_("Create a color clip"))
+        color_button.connect("clicked", self.create_color_clip_cb)
+        box.pack_start(color_button, False, False, SPACING)
+
         box.show_all()
         return box
 
-    def create_cb(self, unused_button):
+    def create_title_clip_cb(self, unused_button):
         title_clip = GES.TitleClip()
         duration = self.app.settings.titleClipLength * Gst.MSECOND
         title_clip.set_duration(duration)
@@ -146,6 +159,18 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
                 assert res, prop
         self._selection.set_selection([title_clip], SELECT)
 
+    def create_color_clip_cb(self, unused_widget):
+        color_clip = GES.TestClip.new()
+        duration = self.app.settings.ColorClipLength * Gst.MSECOND
+        color_clip.set_duration(duration)
+        color_clip.set_vpattern(GES.VideoTestPattern.SOLID_COLOR)
+        color_clip.set_supported_formats(GES.TrackType.VIDEO)
+        with self.app.action_log.started("add color clip",
+                                         
finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                                         toplevel=True):
+            self.app.gui.editor.timeline_ui.insert_clips_on_first_layer([color_clip])
+        self._selection.set_selection([color_clip], SELECT)
+
     def new_project_loaded_cb(self, unused_project_manager, project):
         if self._selection is not None:
             self._selection.disconnect_by_func(self._selection_changed_cb)
@@ -161,12 +186,17 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.helper_box.set_visible(not single_clip_selected)
 
         title_source = None
+        color_clip_source = None
         if single_clip_selected:
             for child in list(selected_clips)[0].get_children(False):
                 if isinstance(child, GES.TitleSource):
                     title_source = child
                     break
+                if isinstance(child, GES.VideoTestSource):
+                    color_clip_source = child
+                    break
         self.title_expander.set_source(title_source)
+        self.color_expander.set_source(color_clip_source)
 
     def __project_closed_cb(self, unused_project_manager, unused_project):
         self._project = None
@@ -217,8 +247,7 @@ class EffectProperties(Gtk.Expander, Loggable):
         remove_effect_button.set_image(remove_icon)
         remove_effect_button.set_always_show_image(True)
         remove_effect_button.set_label(_("Remove effect"))
-        buttons_box.pack_start(remove_effect_button,
-                               expand=False, fill=False, padding=0)
+        buttons_box.pack_start(remove_effect_button, expand=False, fill=False, padding=0)
 
         # We need to specify Gtk.TreeDragSource because otherwise we are hitting
         # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740.
@@ -309,25 +338,32 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.hide()
 
         effects_actions_group = Gio.SimpleActionGroup()
-        self.treeview.insert_action_group("clipproperties-effects", effects_actions_group)
-        buttons_box.insert_action_group("clipproperties-effects", effects_actions_group)
+        self.treeview.insert_action_group(
+            "clipproperties-effects", effects_actions_group)
+        buttons_box.insert_action_group(
+            "clipproperties-effects", effects_actions_group)
 
         self.remove_effect_action = Gio.SimpleAction.new("remove-effect", None)
         self.remove_effect_action.connect("activate", self._remove_effect_cb)
         effects_actions_group.add_action(self.remove_effect_action)
-        self.app.set_accels_for_action("clipproperties-effects.remove-effect", ["Delete"])
+        self.app.set_accels_for_action(
+            "clipproperties-effects.remove-effect", ["Delete"])
         self.remove_effect_action.set_enabled(False)
-        remove_effect_button.set_action_name("clipproperties-effects.remove-effect")
+        remove_effect_button.set_action_name(
+            "clipproperties-effects.remove-effect")
 
         # Connect all the widget signals
-        self.treeview_selection.connect("changed", self._treeview_selection_changed_cb)
+        self.treeview_selection.connect(
+            "changed", self._treeview_selection_changed_cb)
         self.connect("drag-motion", self._drag_motion_cb)
         self.connect("drag-leave", self._drag_leave_cb)
         self.connect("drag-data-received", self._drag_data_received_cb)
         self.treeview.connect("drag-motion", self._drag_motion_cb)
         self.treeview.connect("drag-leave", self._drag_leave_cb)
-        self.treeview.connect("drag-data-received", self._drag_data_received_cb)
-        self.treeview.connect("query-tooltip", self._tree_view_query_tooltip_cb)
+        self.treeview.connect("drag-data-received",
+                              self._drag_data_received_cb)
+        self.treeview.connect(
+            "query-tooltip", self._tree_view_query_tooltip_cb)
         self.app.project_manager.connect_after(
             "new-project-loaded", self._new_project_loaded_cb)
         self.connect('notify::expanded', self._expanded_cb)
@@ -339,7 +375,8 @@ class EffectProperties(Gtk.Expander, Loggable):
         self._project = project
         if project:
             self._selection = project.ges_timeline.ui.selection
-            self._selection.connect('selection-changed', self._selection_changed_cb)
+            self._selection.connect(
+                'selection-changed', self._selection_changed_cb)
         self.__update_all()
 
     def _selection_changed_cb(self, selection):
@@ -406,9 +443,7 @@ class EffectProperties(Gtk.Expander, Loggable):
 
     def _remove_effect(self, effect):
         pipeline = self._project.pipeline
-        with self.app.action_log.started("remove effect",
-                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
-                                         toplevel=True):
+        with self.app.action_log.started("remove effect", 
finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True):
             self.__remove_configuration_widget()
             self.effects_properties_manager.clean_cache(effect)
             effect.get_parent().remove(effect)
@@ -447,7 +482,8 @@ class EffectProperties(Gtk.Expander, Loggable):
             effect_info = self.app.effects.get_info(factory_name)
             pipeline = self._project.pipeline
             with self.app.action_log.started("add effect",
-                                             finalizing_action=CommitTimelineFinalizingAction(pipeline),
+                                             finalizing_action=CommitTimelineFinalizingAction(
+                                                 pipeline),
                                              toplevel=True):
                 effect = self.clip.ui.add_effect(effect_info)
                 if effect:
@@ -500,7 +536,8 @@ class EffectProperties(Gtk.Expander, Loggable):
         effect = effects[source_index]
         pipeline = self._project.ges_timeline.get_parent()
         with self.app.action_log.started("move effect",
-                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
+                                         finalizing_action=CommitTimelineFinalizingAction(
+                                             pipeline),
                                          toplevel=True):
             clip.set_top_effect_index(effect, drop_index)
 
@@ -526,7 +563,8 @@ class EffectProperties(Gtk.Expander, Loggable):
         effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT)
         pipeline = self._project.ges_timeline.get_parent()
         with self.app.action_log.started("change active state",
-                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
+                                         finalizing_action=CommitTimelineFinalizingAction(
+                                             pipeline),
                                          toplevel=True):
             effect.props.active = not effect.props.active
         # This is not strictly necessary, but makes sure
@@ -552,6 +590,7 @@ class EffectProperties(Gtk.Expander, Loggable):
     def __update_all(self, path=None):
         if self.clip:
             self.show()
+            self.no_effect_infobar.hide()
             self._update_treeview()
             if path:
                 self.treeview_selection.select_path(path)
@@ -565,7 +604,8 @@ class EffectProperties(Gtk.Expander, Loggable):
         for effect in self.clip.get_top_effects():
             if effect.props.bin_description in HIDDEN_EFFECTS:
                 continue
-            effect_info = self.app.effects.get_info(effect.props.bin_description)
+            effect_info = self.app.effects.get_info(
+                effect.props.bin_description)
             to_append = [effect.props.active]
             track_type = effect.get_track_type()
             if track_type == GES.TrackType.AUDIO:
@@ -660,7 +700,8 @@ class TransformationProperties(Gtk.Expander, Loggable):
         self._project = project
         if project:
             self._selection = project.ges_timeline.ui.selection
-            self._selection.connect('selection-changed', self._selection_changed_cb)
+            self._selection.connect(
+                'selection-changed', self._selection_changed_cb)
             self._project.pipeline.connect("position", self._position_cb)
 
     def __project_closed_cb(self, unused_project_manager, unused_project):
@@ -670,15 +711,21 @@ class TransformationProperties(Gtk.Expander, Loggable):
         clear_button = self.builder.get_object("clear_button")
         clear_button.connect("clicked", self._default_values_cb)
 
-        self._activate_keyframes_btn = self.builder.get_object("activate_keyframes_button")
-        self._activate_keyframes_btn.connect("toggled", self.__show_keyframes_toggled_cb)
+        self._activate_keyframes_btn = self.builder.get_object(
+            "activate_keyframes_button")
+        self._activate_keyframes_btn.connect(
+            "toggled", self.__show_keyframes_toggled_cb)
 
-        self._next_keyframe_btn = self.builder.get_object("next_keyframe_button")
-        self._next_keyframe_btn.connect("clicked", self.__go_to_keyframe_cb, True)
+        self._next_keyframe_btn = self.builder.get_object(
+            "next_keyframe_button")
+        self._next_keyframe_btn.connect(
+            "clicked", self.__go_to_keyframe_cb, True)
         self._next_keyframe_btn.set_sensitive(False)
 
-        self._prev_keyframe_btn = self.builder.get_object("prev_keyframe_button")
-        self._prev_keyframe_btn.connect("clicked", self.__go_to_keyframe_cb, False)
+        self._prev_keyframe_btn = self.builder.get_object(
+            "prev_keyframe_button")
+        self._prev_keyframe_btn.connect(
+            "clicked", self.__go_to_keyframe_cb, False)
         self._prev_keyframe_btn.set_sensitive(False)
 
         self.__setup_spin_button("xpos_spinbtn", "posx")
@@ -690,8 +737,10 @@ class TransformationProperties(Gtk.Expander, Loggable):
     def __get_keyframes_timestamps(self):
         keyframes_ts = []
         for prop in ["posx", "posy", "width", "height"]:
-            prop_keyframes = self.__control_bindings[prop].props.control_source.get_all()
-            keyframes_ts.extend([keyframe.timestamp for keyframe in prop_keyframes])
+            prop_keyframes = self.__control_bindings[prop].props.control_source.get_all(
+            )
+            keyframes_ts.extend(
+                [keyframe.timestamp for keyframe in prop_keyframes])
 
         return sorted(set(keyframes_ts))
 
@@ -726,9 +775,11 @@ class TransformationProperties(Gtk.Expander, Loggable):
             self._prev_keyframe_btn.set_sensitive(False)
             self._next_keyframe_btn.set_sensitive(False)
             if self.__source_uses_keyframes():
-                self._activate_keyframes_btn.set_tooltip_text(_("Show keyframes"))
+                self._activate_keyframes_btn.set_tooltip_text(
+                    _("Show keyframes"))
             else:
-                self._activate_keyframes_btn.set_tooltip_text(_("Activate keyframes"))
+                self._activate_keyframes_btn.set_tooltip_text(
+                    _("Activate keyframes"))
             self.source.ui_element.show_default_keyframes()
         else:
             self._prev_keyframe_btn.set_sensitive(True)
@@ -759,7 +810,8 @@ class TransformationProperties(Gtk.Expander, Loggable):
             # control_source.unset_all() can't be used here as it doesn't emit
             # the 'value-removed' signal, so the undo system wouldn't notice
             # the removed keyframes
-            keyframes_ts = [keyframe.timestamp for keyframe in control_source.get_all()]
+            keyframes_ts = [
+                keyframe.timestamp for keyframe in control_source.get_all()]
             for ts in keyframes_ts:
                 control_source.unset(ts)
             self.__own_bindings_change = True
@@ -781,7 +833,8 @@ 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.source.set_control_source(
+                    control_source, prop, "direct-absolute")
                 self.__own_bindings_change = False
                 self.__set_default_keyframes_values(control_source, prop)
 
@@ -789,23 +842,27 @@ class TransformationProperties(Gtk.Expander, Loggable):
             self.__control_bindings[prop] = binding
 
         if adding_kfs:
-            self.app.action_log.commit("Transformation properties keyframes activate")
+            self.app.action_log.commit(
+                "Transformation properties keyframes activate")
 
     def __set_default_keyframes_values(self, control_source, prop):
         res, val = self.source.get_child_property(prop)
         assert res
         control_source.set(self.source.props.in_point, val)
-        control_source.set(self.source.props.in_point + self.source.props.duration, val)
+        control_source.set(self.source.props.in_point +
+                           self.source.props.duration, val)
 
     def _default_values_cb(self, unused_widget):
         with self.app.action_log.started("Transformation properties reset default",
-                                         
finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                                         finalizing_action=CommitTimelineFinalizingAction(
+                                             self._project.pipeline),
                                          toplevel=True):
             if self.__source_uses_keyframes():
                 self.__remove_control_bindings()
 
             for prop in ["posx", "posy", "width", "height"]:
-                self.source.set_child_property(prop, self.source.ui.default_position[prop])
+                self.source.set_child_property(
+                    prop, self.source.ui.default_position[prop])
 
         self.__update_keyframes_ui()
 
@@ -819,8 +876,10 @@ class TransformationProperties(Gtk.Expander, Loggable):
 
                 # If the position is outside of the clip, take the property
                 # value at the start/end (whichever is closer) of the clip.
-                source_position = max(0, min(position - start, duration - 1)) + in_point
-                value = self.__control_bindings[prop].get_value(source_position)
+                source_position = max(
+                    0, min(position - start, duration - 1)) + in_point
+                value = self.__control_bindings[prop].get_value(
+                    source_position)
                 res = value is not None
                 return res, value
             except PipelineError:
@@ -879,22 +938,26 @@ class TransformationProperties(Gtk.Expander, Loggable):
 
                 with self.app.action_log.started(
                         "Transformation property change",
-                        finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                        finalizing_action=CommitTimelineFinalizingAction(
+                            self._project.pipeline),
                         toplevel=True):
-                    self.__control_bindings[prop].props.control_source.set(source_position, value)
+                    self.__control_bindings[prop].props.control_source.set(
+                        source_position, value)
             except PipelineError:
                 self.warning("Could not get pipeline position")
                 return
         else:
             with self.app.action_log.started("Transformation property change",
-                                             
finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                                             finalizing_action=CommitTimelineFinalizingAction(
+                                                 self._project.pipeline),
                                              toplevel=True):
                 self.source.set_child_property(prop, value)
 
     def __setup_spin_button(self, widget_name, property_name):
         """Creates a SpinButton for editing a property value."""
         spinbtn = self.builder.get_object(widget_name)
-        handler_id = spinbtn.connect("value-changed", self._on_value_changed_cb, property_name)
+        handler_id = spinbtn.connect(
+            "value-changed", self._on_value_changed_cb, property_name)
         disable_scroll(spinbtn)
         self.spin_buttons[property_name] = spinbtn
         self.spin_buttons_handler_ids[property_name] = handler_id
@@ -916,8 +979,10 @@ class TransformationProperties(Gtk.Expander, Loggable):
     def __set_source(self, source):
         if self.source:
             try:
-                self.source.disconnect_by_func(self.__source_property_changed_cb)
-                disconnect_all_by_func(self.source, self._control_bindings_changed)
+                self.source.disconnect_by_func(
+                    self.__source_property_changed_cb)
+                disconnect_all_by_func(
+                    self.source, self._control_bindings_changed)
             except TypeError:
                 pass
         self.source = source
diff --git a/pitivi/editorperspective.py b/pitivi/editorperspective.py
index 881b4867..66468713 100644
--- a/pitivi/editorperspective.py
+++ b/pitivi/editorperspective.py
@@ -283,6 +283,8 @@ class EditorPerspective(Perspective, Loggable):
             page = 0
         elif isinstance(ges_clip, GES.TransitionClip):
             page = 1
+        elif isinstance(ges_clip, GES.TestClip):
+            page = 0
         else:
             self.warning("Unknown clip type: %s", ges_clip)
             return
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 6f70041d..24beb6bb 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -963,7 +963,7 @@ class TitleSource(VideoSource):
         for spec in self._ges_elem.list_children_properties():
             if spec.name == "alpha":
                 return spec
-            return None
+        return None
 
     def _get_previewer(self):
         previewer = TitlePreviewer(self._ges_elem)
@@ -977,6 +977,27 @@ class TitleSource(VideoSource):
                 "height": self._project_height}
 
 
+class VideoTestSource(VideoSource):
+
+    __gtype_name__ = "PitiviVideoTestSource"
+
+    def _get_default_mixing_property(self):
+        for spec in self._ges_elem.list_children_properties():
+            if spec.name == "alpha":
+                return spec
+        return None
+
+    def _get_previewer(self):
+        previewer = ImagePreviewer(self._ges_elem, self.timeline.app.settings.previewers_max_cpu)
+        return previewer
+
+    def _get_default_position(self):
+        return {"posx": 0,
+                "posy": 0,
+                "width": self._project_width,
+                "height": self._project_height}
+
+
 class VideoUriSource(VideoSource):
 
     __gtype_name__ = "PitiviUriVideoSource"
@@ -1415,6 +1436,26 @@ class UriClip(SourceClip):
             self.video_widget.set_visible(True)
 
 
+class TestClip(SourceClip):
+    __gtype_name__ = "PitiviTestClip"
+
+    def __init__(self, layer, ges_clip):
+        SourceClip.__init__(self, layer, ges_clip)
+        self.get_style_context().add_class("TestClip")
+
+    def _add_child(self, ges_timeline_element):
+        SourceClip._add_child(self, ges_timeline_element)
+
+        if not isinstance(ges_timeline_element, GES.Source):
+            return
+
+        if ges_timeline_element.get_track_type() == GES.TrackType.VIDEO:
+            self.video_widget = VideoTestSource(ges_timeline_element, self.timeline)
+            ges_timeline_element.ui = self.video_widget
+            self._elements_container.pack_start(self.video_widget, True, False, 0)
+            self.video_widget.set_visible(True)
+
+
 class TitleClip(SourceClip):
     __gtype_name__ = "PitiviTitleClip"
 
@@ -1486,5 +1527,6 @@ class TransitionClip(Clip):
 GES_TYPE_UI_TYPE = {
     GES.UriClip.__gtype__: UriClip,
     GES.TitleClip.__gtype__: TitleClip,
-    GES.TransitionClip.__gtype__: TransitionClip
+    GES.TransitionClip.__gtype__: TransitionClip,
+    GES.TestClip.__gtype__: TestClip
 }
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index fcf71c9d..98ab9a89 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -404,7 +404,10 @@ class Previewer(GObject.Object):
 
 
 class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
-    """An image previewer widget, drawing thumbnails."""
+    """A previewer widget drawing the same thumbnail repeatedly.
+
+    Can be used for Image clips or Color clips.
+    """
 
     # We could define them in Previewer, but for some reason they are ignored.
     __gsignals__ = PREVIEW_GENERATOR_SIGNALS
@@ -418,9 +421,7 @@ class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
         self.get_style_context().add_class("VideoPreviewer")
 
         self.ges_elem = ges_elem
-
-        # Guard against malformed URIs
-        self.uri = quote_uri(get_proxy_target(ges_elem).props.id)
+        self.uri = get_proxy_target(ges_elem).props.id
 
         self.__start_id = 0
 
@@ -432,6 +433,9 @@ class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
 
         self.ges_elem.connect("notify::duration", self._duration_changed_cb)
 
+        if isinstance(self.ges_elem, GES.VideoTestSource):
+            self.ges_elem.connect("deep-notify", self._source_deep_notify_cb)
+
         self.become_controlled()
 
         self.connect("notify::height-request", self._height_changed_cb)
@@ -445,16 +449,32 @@ class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
 
         self.__start_id = None
 
-        self.debug("Generating thumbnail for image: %s", path_from_uri(self.uri))
-        self.__image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
-            Gst.uri_get_location(self.uri), -1, self.thumb_height, True)
+        self.__image_pixbuf = self._generate_thumbnail()
         self.thumb_width = self.__image_pixbuf.props.width
+
         self._update_thumbnails()
         self.emit("done")
 
         # Stop calling me, I started already.
         return False
 
+    def _generate_thumbnail(self):
+        if isinstance(self.ges_elem, GES.ImageSource):
+            self.debug("Generating thumbnail for image: %s", path_from_uri(self.uri))
+            return GdkPixbuf.Pixbuf.new_from_file_at_scale(
+                Gst.uri_get_location(self.uri), -1, self.thumb_height, True)
+
+        if isinstance(self.ges_elem, GES.VideoTestSource):
+            self.debug("Generating thumbnail for color")
+            pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, THUMB_HEIGHT, THUMB_HEIGHT)
+            res, argb = self.ges_elem.get_child_property("foreground-color")
+            assert res
+            rgba = ((argb & 0xffffff) << 8) | ((argb & 0xff000000) >> 24)
+            pixbuf.fill(rgba)
+            return pixbuf
+
+        raise Exception("Unsupported ges_source type: %s" % type(self.ges_elem))
+
     def _update_thumbnails(self):
         """Updates the thumbnail widgets for the clip at the current zoom."""
         if not self.thumb_width:
@@ -493,6 +513,11 @@ class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
         """Handles the changing of the duration of the clip."""
         self._update_thumbnails()
 
+    def _source_deep_notify_cb(self, source, unused_gstelement, pspec):
+        """Handles updates in the VideoTestSource."""
+        if pspec.name == "foreground-color":
+            self.become_controlled()
+
     def set_selected(self, selected):
         if selected:
             opacity = 0.5
@@ -503,8 +528,7 @@ class ImagePreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
             thumb.props.opacity = opacity
 
     def start_generation(self):
-        self.debug("Waiting for UI to become idle for: %s",
-                   path_from_uri(self.uri))
+        self.debug("Waiting for UI to become idle for: %s", self.uri)
         self.__start_id = GLib.idle_add(self._start_thumbnailing_cb,
                                         priority=GLib.PRIORITY_LOW)
 
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index 1d33bb93..8247b8fb 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -1454,3 +1454,12 @@ class ColorPickerButton(Gtk.Button):
         self.dropper_grab_widget.grab_remove()
         self.pointer_device = None
         self.dropper_grab_widget = None
+
+    def calculate_argb(self):
+        argb = 0
+        argb += (1 * 255) * 256 ** 3
+        argb += float(self.color_r) * 256 ** 2
+        argb += float(self.color_g) * 256 ** 1
+        argb += float(self.color_b) * 256 ** 0
+        argb = int(argb)
+        return argb
diff --git a/pitivi/viewer/overlay.py b/pitivi/viewer/overlay.py
index fbb79e6a..5d30f1a8 100644
--- a/pitivi/viewer/overlay.py
+++ b/pitivi/viewer/overlay.py
@@ -54,7 +54,7 @@ class Overlay(Gtk.DrawingArea, Loggable):
     def _select(self):
         self.stack.selected_overlay = self
         self.stack.app.gui.editor.timeline_ui.timeline.selection.set_selection([self._source], SELECT)
-        if isinstance(self._source, (GES.TitleSource, GES.VideoUriSource)):
+        if isinstance(self._source, (GES.TitleSource, GES.VideoUriSource, GES.VideoTestSource)):
             page = 0
         else:
             self.warning("Unknown clip type: %s", self._source)
diff --git a/tests/test_clipproperties.py b/tests/test_clipproperties.py
index 203a1397..472b6885 100644
--- a/tests/test_clipproperties.py
+++ b/tests/test_clipproperties.py
@@ -357,7 +357,7 @@ class ClipPropertiesTest(BaseTestUndoTimeline):
         clipproperties.new_project_loaded_cb(None, self.project)
         self.project.pipeline.get_position = mock.Mock(return_value=0)
 
-        clipproperties.create_cb(None)
+        clipproperties.create_title_clip_cb(None)
         ps1 = self._get_title_source_child_props()
 
         self.action_log.undo()
diff --git a/tests/test_clipproperties_color.py b/tests/test_clipproperties_color.py
new file mode 100644
index 00000000..17c75ff2
--- /dev/null
+++ b/tests/test_clipproperties_color.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (C) 2020 Andrew Hazel, Thomas Braccia, Troy Ogden, Robert Kirkpatrick
+#
+# 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, see <http://www.gnu.org/licenses/>.
+"""Tests for the pitivi.clip_properties.color module."""
+# pylint: disable=protected-access
+from unittest import mock
+
+from gi.repository import GES
+
+from pitivi.clipproperties import ClipProperties
+from tests import common
+from tests.test_undo_timeline import BaseTestUndoTimeline
+
+
+class ColorPropertiesTest(BaseTestUndoTimeline):
+    """Tests for the ColorProperties class."""
+
+    def test_create_hard_coded(self):
+        """Exercise creation of a color test clip."""
+        # Wait until the project creates a layer in the timeline.
+        common.create_main_loop().run(until_empty=True)
+
+        from pitivi.timeline.timeline import TimelineContainer
+        timeline_container = TimelineContainer(self.app)
+        timeline_container.set_project(self.project)
+        self.app.gui.editor.timeline_ui = timeline_container
+
+        clipproperties = ClipProperties(self.app)
+        clipproperties.new_project_loaded_cb(None, self.project)
+        self.project.pipeline.get_position = mock.Mock(return_value=0)
+
+        clipproperties.create_color_clip_cb(None)
+        clips = self.layer.get_clips()
+        pattern = clips[0].get_vpattern()
+        self.assertEqual(pattern, GES.VideoTestPattern.SOLID_COLOR)
+
+        self.action_log.undo()
+        self.assertListEqual(self.layer.get_clips(), [])
+
+        self.action_log.redo()
+        self.assertListEqual(self.layer.get_clips(), clips)
+
+    def test_color_change(self):
+        """Exercise the changing of colors for color clip."""
+        # Wait until the project creates a layer in the timeline.
+        common.create_main_loop().run(until_empty=True)
+
+        from pitivi.timeline.timeline import TimelineContainer
+        timeline_container = TimelineContainer(self.app)
+        timeline_container.set_project(self.project)
+        self.app.gui.editor.timeline_ui = timeline_container
+
+        clipproperties = ClipProperties(self.app)
+        clipproperties.new_project_loaded_cb(None, self.project)
+        self.project.pipeline.get_position = mock.Mock(return_value=0)
+
+        clipproperties.create_color_clip_cb(None)
+
+        color_expander = clipproperties.color_expander
+        color_picker_mock = mock.Mock()
+        color_picker_mock.calculate_argb.return_value = 1 << 24 | 2 << 16 | 3 << 8 | 4
+        color_expander._color_picker_value_changed_cb(color_picker_mock)
+        color = color_expander.source.get_child_property("foreground-color")[1]
+        self.assertEqual(color, 0x1020304)



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