[pitivi] clipproperties: Add a compositing expander



commit b3f916187f929f9b341f9978ac316e1880f32843
Author: Aaron Friesen <maugrift maugrift com>
Date:   Fri Apr 1 04:11:11 2022 -0500

    clipproperties: Add a compositing expander
    
    Pitivi allows users to manually keyframe the opacity of a clip to create
    fade-in and fade-out transitions.
    
    However, this is relatively complex for basic use cases, and can be
    tedious to do on a large scale.
    
    This commit adds a compositing expander to the clip properties panel, in
    which the user can easily set fade-in and fade-out durations for a clip.
    
    Fixes #1472

 data/ui/clipcompositing.ui               | 176 ++++++++++++++++++++++++
 help/C/transitions.page                  |   2 +-
 pitivi/clip_properties/compositing.py    | 221 +++++++++++++++++++++++++++++++
 pitivi/clipproperties.py                 |   7 +
 tests/common.py                          |  11 +-
 tests/test_clipproperties_compositing.py | 139 +++++++++++++++++++
 6 files changed, 553 insertions(+), 3 deletions(-)
---
diff --git a/data/ui/clipcompositing.ui b/data/ui/clipcompositing.ui
new file mode 100644
index 000000000..64062c41e
--- /dev/null
+++ b/data/ui/clipcompositing.ui
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <object class="GtkActionGroup" id="actiongroup1"/>
+  <object class="GtkAdjustment" id="fade_in_adjustment">
+    <property name="upper">1</property>
+    <property name="step-increment">0.01</property>
+    <property name="page-increment">10</property>
+  </object>
+  <object class="GtkAdjustment" id="fade_out_adjustment">
+    <property name="upper">1</property>
+    <property name="step-increment">0.01</property>
+    <property name="page-increment">10</property>
+  </object>
+  <object class="GtkImage" id="icon_reset1">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <property name="icon-name">edit-clear-all-symbolic</property>
+  </object>
+  <object class="GtkImage" id="icon_reset2">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <property name="icon-name">edit-clear-all-symbolic</property>
+  </object>
+  <!-- n-columns=5 n-rows=2 -->
+  <object class="GtkGrid" id="compositing_box">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <property name="halign">start</property>
+    <property name="valign">start</property>
+    <property name="margin-left">12</property>
+    <property name="margin-top">6</property>
+    <property name="margin-bottom">6</property>
+    <property name="row-spacing">6</property>
+    <property name="column-spacing">6</property>
+    <child>
+      <object class="GtkLabel" id="label10">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Fade-in:</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left-attach">0</property>
+        <property name="top-attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="label" translatable="yes">seconds</property>
+      </object>
+      <packing>
+        <property name="left-attach">2</property>
+        <property name="top-attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkSpinButton">
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="adjustment">fade_in_adjustment</property>
+        <property name="climb-rate">0.10</property>
+        <property name="digits">2</property>
+      </object>
+      <packing>
+        <property name="left-attach">1</property>
+        <property name="top-attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScale">
+        <property name="width-request">150</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="margin-top">1</property>
+        <property name="hexpand">True</property>
+        <property name="adjustment">fade_in_adjustment</property>
+        <property name="round-digits">1</property>
+        <property name="draw-value">False</property>
+        <property name="value-pos">left</property>
+      </object>
+      <packing>
+        <property name="left-attach">3</property>
+        <property name="top-attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label11">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Fade-out:</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left-attach">0</property>
+        <property name="top-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkSpinButton">
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="adjustment">fade_out_adjustment</property>
+        <property name="climb-rate">0.10</property>
+        <property name="digits">2</property>
+      </object>
+      <packing>
+        <property name="left-attach">1</property>
+        <property name="top-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="label" translatable="yes">seconds</property>
+      </object>
+      <packing>
+        <property name="left-attach">2</property>
+        <property name="top-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScale">
+        <property name="width-request">150</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="hexpand">True</property>
+        <property name="adjustment">fade_out_adjustment</property>
+        <property name="round-digits">1</property>
+        <property name="draw-value">False</property>
+      </object>
+      <packing>
+        <property name="left-attach">3</property>
+        <property name="top-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="reset_fade_in_button">
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="focus-on-click">False</property>
+        <property name="receives-default">True</property>
+        <property name="tooltip-text" translatable="yes">Reset fade-in</property>
+        <property name="image">icon_reset1</property>
+        <property name="relief">none</property>
+      </object>
+      <packing>
+        <property name="left-attach">4</property>
+        <property name="top-attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="reset_fade_out_button">
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="focus-on-click">False</property>
+        <property name="receives-default">True</property>
+        <property name="tooltip-text" translatable="yes">Reset fade-out</property>
+        <property name="image">icon_reset2</property>
+        <property name="relief">none</property>
+      </object>
+      <packing>
+        <property name="left-attach">4</property>
+        <property name="top-attach">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/help/C/transitions.page b/help/C/transitions.page
index ccf8aed50..5a5cf8179 100644
--- a/help/C/transitions.page
+++ b/help/C/transitions.page
@@ -62,7 +62,7 @@
   </section>
   <section id="fades">
     <title>Fade-ins and fade-outs</title>
-    <p>You can create fade-in and fade-out transitions in single clips by using keyframes controlling the 
clip's opacity. For more information on keyframes, see <link xref="keyframecurves">Keyframe curves</link>. To 
understand opacity, see <link xref="layers">Understanding layers</link>. The following images illustrate how 
to fade a clip to black:</p>
+    <p>You can create fade-in and fade-out transitions in single clips by specifying their duration in the 
<gui>Compositing</gui> section of the <gui>Clip Properties</gui> panel. These can be fine-tuned by 
interacting with the <link xref="keyframecurves">Keyframe curve</link> controlling the clip's opacity. To 
understand opacity, see <link xref="layers">Understanding layers</link>. The following images illustrate how 
to fade a clip to black by editing the clip's keyframes:</p>
     <figure>
       <title>Keyframe curves controlling the video opacity</title>
       <desc>The default curve is flat indicating the same opacity at any position within the clip.</desc>
diff --git a/pitivi/clip_properties/compositing.py b/pitivi/clip_properties/compositing.py
new file mode 100644
index 000000000..77e15eb93
--- /dev/null
+++ b/pitivi/clip_properties/compositing.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2021, Tyler Senne <tsenne2 huskers unl edu>
+# Copyright (c) 2021, Michael Ervin <michael ervin huskers unl edu>
+# Copyright (c) 2021, Aaron Friesen <afriesen4 huskers unl edu>
+#
+# 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/>.
+import os
+from gettext import gettext as _
+from typing import Iterable
+from typing import Optional
+
+from gi.repository import GES
+from gi.repository import Gst
+from gi.repository import GstController
+from gi.repository import Gtk
+
+from pitivi.configure import get_ui_dir
+from pitivi.undo.timeline import CommitTimelineFinalizingAction
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.misc import disconnect_all_by_func
+
+
+# The threshold for the lower value of a keyframe segment to consider it a fade.
+FADE_OPACITY_THRESHOLD = 0.9
+
+
+class CompositingProperties(Gtk.Expander, Loggable):
+    """Widget for setting the opacity-related properties of a clip.
+
+    Attributes:
+        app (Pitivi): The app.
+    """
+
+    def __init__(self, app: Gtk.Application) -> None:
+        Gtk.Expander.__init__(self)
+        Loggable.__init__(self)
+
+        self.app: Gtk.Application = app
+
+        self.set_expanded(True)
+        self.set_label(_("Compositing"))
+
+        builder = Gtk.Builder()
+        builder.add_from_file(os.path.join(get_ui_dir(), "clipcompositing.ui"))
+
+        self.add(builder.get_object("compositing_box"))
+        self._fade_in_adjustment = builder.get_object("fade_in_adjustment")
+        self._fade_out_adjustment = builder.get_object("fade_out_adjustment")
+        reset_fade_in_button = builder.get_object("reset_fade_in_button")
+        reset_fade_out_button = builder.get_object("reset_fade_out_button")
+
+        self._video_source: Optional[GES.VideoSource] = None
+        self._control_source: Optional[Gst.ControlSource] = None
+
+        self._fade_in: int = 0
+        self._fade_out: int = 0
+
+        self._applying_fade: bool = False
+        self._updating_adjustments: bool = False
+
+        self._fade_in_adjustment.connect("value-changed", self.__fade_in_adjustment_changed_cb)
+        self._fade_out_adjustment.connect("value-changed", self.__fade_out_adjustment_changed_cb)
+        reset_fade_in_button.connect("pressed", self.__reset_fade_in_cb)
+        reset_fade_out_button.connect("pressed", self.__reset_fade_out_cb)
+
+    def set_source(self, video_source: GES.VideoSource) -> None:
+        if self._control_source:
+            disconnect_all_by_func(self._control_source, self.__keyframe_changed_cb)
+            self._video_source.disconnect_by_func(self.__keyframe_changed_cb)
+            self._control_source = None
+
+        self._video_source = video_source
+
+        if self._video_source:
+            assert isinstance(self._video_source, GES.VideoSource)
+
+            control_binding = self._video_source.get_control_binding("alpha")
+            assert control_binding
+            self._control_source = control_binding.props.control_source
+
+            self._control_source.connect("value-added", self.__keyframe_changed_cb)
+            self._control_source.connect("value-changed", self.__keyframe_changed_cb)
+            self._control_source.connect("value-removed", self.__keyframe_changed_cb)
+            self._video_source.connect("notify::duration", self.__keyframe_changed_cb)
+
+            self._update_adjustments()
+            self.show_all()
+        else:
+            self.hide()
+
+    @property
+    def _duration(self) -> int:
+        return self._video_source.duration
+
+    def _update_adjustments(self) -> None:
+        """Updates the UI to reflect the current opacity keyframes."""
+        assert self._video_source
+        assert self._control_source
+
+        keyframes = self._control_source.get_all()
+        if len(keyframes) < 2:
+            self._fade_in = 0
+            self._fade_out = 0
+            return
+
+        start_opacity = keyframes[0].value
+        end_opacity = keyframes[-1].value
+
+        fade_in = keyframes[-1].timestamp
+        fade_out = keyframes[0].timestamp
+
+        if len(keyframes) >= 3:
+            fade_in = keyframes[1].timestamp
+            fade_out = keyframes[-2].timestamp
+
+        self._fade_in = fade_in if start_opacity <= FADE_OPACITY_THRESHOLD else 0
+        self._fade_out = self._duration - fade_out if end_opacity <= FADE_OPACITY_THRESHOLD else 0
+
+        self._updating_adjustments = True
+        try:
+            self._fade_in_adjustment.props.value = self._fade_in / Gst.SECOND
+            self._fade_out_adjustment.props.value = self._fade_out / Gst.SECOND
+        finally:
+            self._updating_adjustments = False
+
+        self._fade_in_adjustment.props.upper = (self._duration - self._fade_out) / Gst.SECOND
+        self._fade_out_adjustment.props.upper = (self._duration - self._fade_in) / Gst.SECOND
+
+    def __fade_in_adjustment_changed_cb(self, adjustment: Gtk.Adjustment) -> None:
+        if not self._updating_adjustments:
+            fade_timestamp: int = int(self._fade_in_adjustment.props.value * Gst.SECOND)
+            self._move_keyframe(self._fade_in, fade_timestamp, 0, self._duration - self._fade_out)
+            self._update_adjustments()
+
+    def __fade_out_adjustment_changed_cb(self, adjustment: Gtk.Adjustment) -> None:
+        if not self._updating_adjustments:
+            fade_timestamp: int = self._duration - int(self._fade_out_adjustment.props.value * Gst.SECOND)
+            self._move_keyframe(self._duration - self._fade_out, fade_timestamp, self._duration, 
self._fade_in)
+            self._update_adjustments()
+
+    def __reset_fade_in_cb(self, button: Gtk.Button) -> None:
+        self._fade_in_adjustment.props.value = 0
+
+    def __reset_fade_out_cb(self, button: Gtk.Button) -> None:
+        self._fade_out_adjustment.props.value = 0
+
+    def __keyframe_changed_cb(self, control_source: GstController.TimedValueControlSource, timed_value: 
GstController.ControlPoint) -> None:
+        if not self._applying_fade:
+            self._update_adjustments()
+
+    def _get_keyframe(self, timestamp: int) -> Gst.TimedValue:
+        assert self._control_source
+        for keyframe in self._control_source.get_all():
+            if keyframe.timestamp == timestamp:
+                return keyframe
+        return None
+
+    def _get_keyframes_in_range(self, start: int, end: int) -> Iterable[Gst.TimedValue]:
+        assert self._control_source
+        for keyframe in self._control_source.get_all():
+            if start <= keyframe.timestamp <= end:
+                yield keyframe
+            elif keyframe.timestamp > end:
+                break
+
+    def _move_keyframe(self, current_fade_timestamp: int, fade_timestamp: int, edge_timestamp: int, 
middle_timestamp: int) -> None:
+        """Moves a fade keyframe.
+
+        Args:
+            current_fade_timestamp: The current position of the keyframe.
+            fade_timestamp: The new position of the keyframe.
+            edge_timestamp: 0 for fade-in, duration for fade-out.
+            middle_timestamp: The position of the keyframe of the other fade.
+        """
+        if current_fade_timestamp == fade_timestamp:
+            return
+
+        assert self._video_source
+
+        pipeline = self.app.project_manager.current_project.pipeline
+        with self.app.action_log.started("apply fade",
+                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
+                                         toplevel=True, mergeable=True):
+            self._applying_fade = True
+            try:
+                keyframe = self._get_keyframe(current_fade_timestamp)
+                assert keyframe
+                opacity = keyframe.value
+
+                # Unset the keyframes in the delta interval.
+                start = min(current_fade_timestamp, fade_timestamp)
+                end = max(current_fade_timestamp, fade_timestamp)
+                for keyframe in self._get_keyframes_in_range(start, end):
+                    timestamp = keyframe.timestamp
+                    if 0 < timestamp < self._duration:
+                        self._control_source.unset(timestamp)
+
+                self._control_source.set(fade_timestamp, opacity)
+
+                if current_fade_timestamp == middle_timestamp:
+                    # The keyframe at current_fade_timestamp was being used for
+                    # both fade-in and fade-out. Make sure it persists.
+                    self._control_source.set(middle_timestamp, opacity)
+                elif current_fade_timestamp == edge_timestamp:
+                    # The fade has just been created.
+                    # Make sure the edge keyframe is transparent.
+                    self._control_source.set(edge_timestamp, 0)
+            finally:
+                self._applying_fade = False
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 39431fd9a..ef5ed92fa 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -35,6 +35,7 @@ from gi.repository import Gtk
 
 from pitivi.clip_properties.alignment import AlignmentEditor
 from pitivi.clip_properties.color import ColorProperties
+from pitivi.clip_properties.compositing import CompositingProperties
 from pitivi.clip_properties.title import TitleProperties
 from pitivi.configure import get_pixmap_dir
 from pitivi.configure import get_ui_dir
@@ -121,6 +122,10 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.color_expander.set_vexpand(False)
         vbox.pack_start(self.color_expander, False, False, 0)
 
+        self.compositing_expander = CompositingProperties(app)
+        self.compositing_expander.set_vexpand(False)
+        vbox.pack_start(self.compositing_expander, False, False, 0)
+
         self.effect_expander = EffectProperties(app)
         self.effect_expander.set_vexpand(False)
         vbox.pack_start(self.effect_expander, False, False, 0)
@@ -138,6 +143,7 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.speed_expander.set_clip(None)
         self.title_expander.set_source(None)
         self.color_expander.set_source(None)
+        self.compositing_expander.set_source(None)
         self.effect_expander.set_clip(None)
         self.marker_expander.set_clip(None)
 
@@ -234,6 +240,7 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
         self.speed_expander.set_clip(ges_clip if (not title_source and not color_clip_source) else None)
         self.title_expander.set_source(title_source)
         self.color_expander.set_source(color_clip_source)
+        self.compositing_expander.set_source(video_source)
         self.effect_expander.set_clip(ges_clip)
         self.marker_expander.set_clip(ges_clip)
 
diff --git a/tests/common.py b/tests/common.py
index 3129b7c0b..500f5e5ae 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -305,6 +305,7 @@ def setup_timeline(func):
 
 
 def setup_clipproperties(func):
+    """Wraps a test, providing a usable ClipProperties."""
     def wrapped(self):
         app = self.timeline_container.app
 
@@ -315,10 +316,14 @@ def setup_clipproperties(func):
         self.transformation_box._new_project_loaded_cb(None, self.project)
 
         self.speed_box = self.clipproperties.speed_expander
+        self.compositing_box = self.clipproperties.compositing_expander
         self.markers_box = self.clipproperties.marker_expander
 
         func(self)
 
+        del self.markers_box
+        del self.compositing_box
+        del self.speed_box
         del self.transformation_box
         del self.clipproperties
 
@@ -539,10 +544,12 @@ class TestCase(unittest.TestCase, Loggable):
         self.assertEqual(len(effects), count)
 
     def assert_control_source_values(self, control_source, expected_values, expected_timestamps):
-        values = [timed_value.value for timed_value in control_source.get_all()]
+        keyframes = control_source.get_all()
+
+        values = [timed_value.value for timed_value in keyframes]
         self.assertListEqual(values, expected_values)
 
-        timestamps = [timed_value.timestamp for timed_value in control_source.get_all()]
+        timestamps = [timed_value.timestamp for timed_value in keyframes]
         self.assertListEqual(timestamps, expected_timestamps)
 
     def get_timeline_clips(self):
diff --git a/tests/test_clipproperties_compositing.py b/tests/test_clipproperties_compositing.py
new file mode 100644
index 000000000..8dd73b387
--- /dev/null
+++ b/tests/test_clipproperties_compositing.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2021, Tyler Senne <tsenne2 huskers unl edu>
+# Copyright (c) 2021, Michael Ervin <michael ervin huskers unl edu>
+# Copyright (c) 2021, Aaron Friesen <afriesen4 huskers unl edu>
+#
+# 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.clipproperties module."""
+# pylint: disable=protected-access,no-self-use,import-outside-toplevel,no-member
+from gi.repository import Gst
+
+from tests import common
+
+
+class CompositingPropertiesTest(common.TestCase):
+
+    @common.setup_project_with_clips(assets_names=["1sec_simpsons_trailer.mp4"])
+    @common.setup_clipproperties
+    def test_max_fade_duration(self):
+        fade_in_adjustment = self.compositing_box._fade_in_adjustment
+        fade_out_adjustment = self.compositing_box._fade_out_adjustment
+
+        clip, = self.layer.get_clips()
+        self.timeline_container.timeline.selection.select([clip])
+
+        self.assertEqual(fade_in_adjustment.props.upper * Gst.SECOND, clip.duration)
+        self.assertEqual(fade_out_adjustment.props.upper * Gst.SECOND, clip.duration)
+
+        fade_in_adjustment.props.value = 0.5
+        self.assertEqual(fade_out_adjustment.props.upper * Gst.SECOND, clip.duration - (0.5 * Gst.SECOND))
+
+        fade_out_adjustment.props.value = 0.3
+        self.assertEqual(fade_in_adjustment.props.upper * Gst.SECOND, clip.duration - (0.3 * Gst.SECOND))
+
+    def _get_control_source(self, clip):
+        source = self.get_clip_element(clip)
+        control_binding = source.get_control_binding("alpha")
+        self.assertIsNotNone(control_binding)
+        control_source = control_binding.props.control_source
+        self.assertIsNotNone(control_source)
+        return control_source
+
+    @common.setup_project_with_clips(assets_names=["1sec_simpsons_trailer.mp4"])
+    @common.setup_clipproperties
+    def test_apply_keyframes(self):
+        clip, = self.layer.get_clips()
+        self.timeline_container.timeline.selection.select([clip])
+
+        fade_in_adjustment = self.compositing_box._fade_in_adjustment
+        fade_out_adjustment = self.compositing_box._fade_out_adjustment
+        fade_in_adjustment.props.value = 0.5
+        fade_out_adjustment.props.value = 0.3
+
+        control_source = self._get_control_source(clip)
+        self.assert_control_source_values(control_source,
+                                          [0, 1, 1, 0],
+                                          [0, 0.5 * Gst.SECOND, clip.duration - 0.3 * Gst.SECOND, 
clip.duration])
+
+    @common.setup_project_with_clips(assets_names=["1sec_simpsons_trailer.mp4"])
+    @common.setup_clipproperties
+    def test_move_keyframes(self):
+        clip, = self.layer.get_clips()
+        self.timeline_container.timeline.selection.select([clip])
+
+        control_source = self._get_control_source(clip)
+        control_source.set(0, 0)  # Necessary in order for fade-in to be recognized
+        control_source.set(clip.duration, 0)  # Necessary in order for fade-out to be recognized
+        control_source.set(0.6 * Gst.SECOND, 0.5)  # This keyframe should be unaffected
+        control_source.set(0.1 * Gst.SECOND, 1)  # This keyframe should be the fade-in
+        control_source.set(clip.duration - (0.2 * Gst.SECOND), 1)  # This keyframe should be the fade-out
+
+        fade_in_adjustment = self.compositing_box._fade_in_adjustment
+        fade_out_adjustment = self.compositing_box._fade_out_adjustment
+        self.assertEqual(fade_in_adjustment.props.value, 0.1)
+        self.assertEqual(fade_out_adjustment.props.value, 0.2)
+
+        fade_in_adjustment.props.value = 0.5
+        fade_out_adjustment.props.value = 0.3
+
+        self.assert_control_source_values(control_source,
+                                          [0, 1, 0.5, 1, 0],
+                                          [0, 0.5 * Gst.SECOND, 0.6 * Gst.SECOND, clip.duration - 0.3 * 
Gst.SECOND, clip.duration])
+
+    @common.setup_project_with_clips(assets_names=["1sec_simpsons_trailer.mp4", 
"30fps_numeroted_frames_blue.webm"])
+    @common.setup_clipproperties
+    def test_adjustments_updated_when_switching_clips(self):
+        clip1, clip2 = self.layer.get_clips()
+        self.timeline_container.timeline.selection.select([clip2])
+
+        fade_in_adjustment = self.compositing_box._fade_in_adjustment
+        fade_out_adjustment = self.compositing_box._fade_out_adjustment
+        self.assertEqual(fade_in_adjustment.props.upper * Gst.SECOND, clip2.duration)
+        self.assertEqual(fade_out_adjustment.props.upper * Gst.SECOND, clip2.duration)
+
+        fade_in_adjustment.props.value = 1.3
+        mainloop = common.create_main_loop()
+        mainloop.run(until_empty=True)
+        self.assertEqual(int(fade_out_adjustment.props.upper * Gst.SECOND), clip2.duration - int(1.3 * 
Gst.SECOND))
+
+        fade_out_adjustment.props.value = 0.5
+        self.assertEqual(int(fade_in_adjustment.props.upper * Gst.SECOND), clip2.duration - int(0.5 * 
Gst.SECOND))
+
+        self.timeline_container.timeline.selection.select([clip1])
+        self.assertEqual(fade_in_adjustment.props.value, 0)
+        self.assertEqual(fade_out_adjustment.props.value, 0)
+        self.assertEqual(fade_in_adjustment.props.upper * Gst.SECOND, clip1.duration)
+        self.assertEqual(fade_out_adjustment.props.upper * Gst.SECOND, clip1.duration)
+
+    @common.setup_project_with_clips(assets_names=["1sec_simpsons_trailer.mp4"])
+    @common.setup_clipproperties
+    def test_adjustments_updated_when_keyframes_updated(self):
+        clip, = self.layer.get_clips()
+        self.timeline_container.timeline.selection.select([clip])
+
+        fade_in_adjustment = self.compositing_box._fade_in_adjustment
+        fade_out_adjustment = self.compositing_box._fade_out_adjustment
+        fade_in_adjustment.props.value = 0.5
+        fade_out_adjustment.props.value = 0.3
+
+        # Move the keyframes
+        control_source = self._get_control_source(clip)
+        control_source.unset(0.5 * Gst.SECOND)
+        control_source.set(0.4 * Gst.SECOND, 1)
+        control_source.unset(clip.duration - (0.3 * Gst.SECOND))
+        control_source.set(clip.duration - (0.2 * Gst.SECOND), 1)
+
+        self.assertEqual(fade_in_adjustment.props.value, 0.4)
+        self.assertEqual(fade_out_adjustment.props.value, 0.2)


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