Created
August 24, 2017 08:24
-
-
Save stefanzzz22/2a814a8797b7921e689e8a725bd1d593 to your computer and use it in GitHub Desktop.
Ken Burns patch compilation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 003be0aa43ffa9db9eb0a3136205347856bdd0bb Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Sun, 28 May 2017 22:39:49 +0300 | |
Subject: [PATCH 1/9] Keyframes for transformation properties. | |
Added the possibility to add/remove keyframes on transformation | |
properties + visual keyframe curve. | |
When adding or moving a keyframe, we don't use the matplotlib position | |
anymore. Instead, we compute the position the same way we do for the seek | |
logic, to make sure the playhead seeks precisely on the added/moved keyframe. | |
This change led to some other changes in the unit test which tested the | |
keyframe curve. More precisely, we needed to compute the position in pixels | |
for the click events, as that is how the seek logic received it. | |
Differential Revision: https://phabricator.freedesktop.org/D1766 | |
--- | |
data/ui/cliptransformation.ui | 147 ++++++++++++++++------- | |
pitivi/clipproperties.py | 255 ++++++++++++++++++++++++++++++++++++---- | |
pitivi/timeline/elements.py | 249 ++++++++++++++++++++++++++++----------- | |
pitivi/timeline/timeline.py | 1 - | |
pitivi/undo/timeline.py | 72 +++++++++--- | |
tests/test_timeline_elements.py | 87 ++++++++------ | |
6 files changed, 618 insertions(+), 193 deletions(-) | |
diff --git a/data/ui/cliptransformation.ui b/data/ui/cliptransformation.ui | |
index 3be718b..1609258 100644 | |
--- a/data/ui/cliptransformation.ui | |
+++ b/data/ui/cliptransformation.ui | |
@@ -1,5 +1,5 @@ | |
<?xml version="1.0" encoding="UTF-8"?> | |
-<!-- Generated with glade 3.19.0 --> | |
+<!-- Generated with glade 3.20.0 --> | |
<interface> | |
<requires lib="gtk+" version="3.10"/> | |
<object class="GtkActionGroup" id="actiongroup1"/> | |
@@ -9,6 +9,11 @@ | |
<property name="step_increment">1</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="GtkAdjustment" id="position_x_adjustment"> | |
<property name="lower">-9999999999</property> | |
<property name="upper">9999999999</property> | |
@@ -37,18 +42,7 @@ | |
<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="label" translatable="yes">X:</property> | |
- <property name="xalign">0</property> | |
- </object> | |
- <packing> | |
- <property name="left_attach">0</property> | |
- <property name="top_attach">0</property> | |
- </packing> | |
- </child> | |
+ <property name="column_homogeneous">True</property> | |
<child> | |
<object class="GtkLabel" id="label11"> | |
<property name="visible">True</property> | |
@@ -65,7 +59,7 @@ | |
<object class="GtkSpinButton" id="xpos_spinbtn"> | |
<property name="visible">True</property> | |
<property name="can_focus">True</property> | |
- <property name="halign">start</property> | |
+ <property name="halign">end</property> | |
<property name="invisible_char">•</property> | |
<property name="progress_pulse_step">1</property> | |
<property name="adjustment">position_x_adjustment</property> | |
@@ -74,13 +68,14 @@ | |
<packing> | |
<property name="left_attach">1</property> | |
<property name="top_attach">0</property> | |
+ <property name="width">3</property> | |
</packing> | |
</child> | |
<child> | |
<object class="GtkSpinButton" id="ypos_spinbtn"> | |
<property name="visible">True</property> | |
<property name="can_focus">True</property> | |
- <property name="halign">start</property> | |
+ <property name="halign">end</property> | |
<property name="invisible_char">•</property> | |
<property name="progress_pulse_step">1</property> | |
<property name="adjustment">position_y_adjustment</property> | |
@@ -89,34 +84,7 @@ | |
<packing> | |
<property name="left_attach">1</property> | |
<property name="top_attach">1</property> | |
- </packing> | |
- </child> | |
- <child> | |
- <object class="GtkButtonBox" id="buttonbox1"> | |
- <property name="visible">True</property> | |
- <property name="can_focus">False</property> | |
- <property name="margin_top">6</property> | |
- <property name="spacing">6</property> | |
- <property name="layout_style">end</property> | |
- <child> | |
- <object class="GtkButton" id="clear_button"> | |
- <property name="label">Reset all</property> | |
- <property name="use_action_appearance">False</property> | |
- <property name="visible">True</property> | |
- <property name="can_focus">True</property> | |
- <property name="receives_default">True</property> | |
- </object> | |
- <packing> | |
- <property name="expand">False</property> | |
- <property name="fill">True</property> | |
- <property name="position">0</property> | |
- </packing> | |
- </child> | |
- </object> | |
- <packing> | |
- <property name="left_attach">0</property> | |
- <property name="top_attach">4</property> | |
- <property name="width">2</property> | |
+ <property name="width">3</property> | |
</packing> | |
</child> | |
<child> | |
@@ -147,7 +115,7 @@ | |
<object class="GtkSpinButton" id="width_spinbtn"> | |
<property name="visible">True</property> | |
<property name="can_focus">True</property> | |
- <property name="halign">start</property> | |
+ <property name="halign">end</property> | |
<property name="invisible_char">•</property> | |
<property name="input_purpose">digits</property> | |
<property name="adjustment">width_adjustment</property> | |
@@ -156,13 +124,14 @@ | |
<packing> | |
<property name="left_attach">1</property> | |
<property name="top_attach">2</property> | |
+ <property name="width">3</property> | |
</packing> | |
</child> | |
<child> | |
<object class="GtkSpinButton" id="height_spinbtn"> | |
<property name="visible">True</property> | |
<property name="can_focus">True</property> | |
- <property name="halign">start</property> | |
+ <property name="halign">end</property> | |
<property name="invisible_char">•</property> | |
<property name="adjustment">height_adjustment</property> | |
<property name="numeric">True</property> | |
@@ -170,6 +139,94 @@ | |
<packing> | |
<property name="left_attach">1</property> | |
<property name="top_attach">3</property> | |
+ <property name="width">3</property> | |
+ </packing> | |
+ </child> | |
+ <child> | |
+ <object class="GtkLabel" id="label10"> | |
+ <property name="visible">True</property> | |
+ <property name="can_focus">False</property> | |
+ <property name="label" translatable="yes">X:</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="GtkButtonBox"> | |
+ <property name="visible">True</property> | |
+ <property name="can_focus">False</property> | |
+ <property name="layout_style">expand</property> | |
+ <child> | |
+ <object class="GtkButton" id="prev_keyframe_button"> | |
+ <property name="label" translatable="yes"><</property> | |
+ <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">Previous keyframe</property> | |
+ <property name="relief">none</property> | |
+ </object> | |
+ <packing> | |
+ <property name="expand">True</property> | |
+ <property name="fill">True</property> | |
+ <property name="position">0</property> | |
+ </packing> | |
+ </child> | |
+ <child> | |
+ <object class="GtkButton" id="next_keyframe_button"> | |
+ <property name="label" translatable="yes">></property> | |
+ <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">Next keyframe</property> | |
+ <property name="relief">none</property> | |
+ </object> | |
+ <packing> | |
+ <property name="expand">True</property> | |
+ <property name="fill">True</property> | |
+ <property name="position">1</property> | |
+ </packing> | |
+ </child> | |
+ <child> | |
+ <object class="GtkToggleButton" id="activate_keyframes_button"> | |
+ <property name="label" translatable="yes">◇</property> | |
+ <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="relief">none</property> | |
+ </object> | |
+ <packing> | |
+ <property name="expand">True</property> | |
+ <property name="fill">True</property> | |
+ <property name="position">2</property> | |
+ </packing> | |
+ </child> | |
+ <child> | |
+ <object class="GtkButton" id="clear_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 to default values</property> | |
+ <property name="image">icon_reset1</property> | |
+ <property name="relief">none</property> | |
+ </object> | |
+ <packing> | |
+ <property name="expand">True</property> | |
+ <property name="fill">True</property> | |
+ <property name="position">3</property> | |
+ </packing> | |
+ </child> | |
+ </object> | |
+ <packing> | |
+ <property name="left_attach">0</property> | |
+ <property name="top_attach">4</property> | |
+ <property name="width">4</property> | |
</packing> | |
</child> | |
</object> | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index 7e22dc1..c2557da 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -23,6 +23,8 @@ from gettext import gettext as _ | |
from gi.repository import Gdk | |
from gi.repository import GES | |
from gi.repository import Gio | |
+from gi.repository import Gst | |
+from gi.repository import GstController | |
from gi.repository import Gtk | |
from gi.repository import Pango | |
@@ -30,7 +32,9 @@ from pitivi.configure import get_ui_dir | |
from pitivi.effects import EffectsPropertiesManager | |
from pitivi.effects import HIDDEN_EFFECTS | |
from pitivi.undo.timeline import CommitTimelineFinalizingAction | |
+from pitivi.utils import pipeline | |
from pitivi.utils.loggable import Loggable | |
+from pitivi.utils.misc import disconnectAllByFunc | |
from pitivi.utils.ui import disable_scroll | |
from pitivi.utils.ui import EFFECT_TARGET_ENTRY | |
from pitivi.utils.ui import fix_infobar | |
@@ -533,10 +537,13 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
self.builder = Gtk.Builder() | |
self.builder.add_from_file(os.path.join(get_ui_dir(), | |
"cliptransformation.ui")) | |
- | |
+ self.__control_bindings = {} | |
+ # Used to make sure self.__control_bindings_changed doesn't get called | |
+ # when bindings are changed from this class | |
+ self.__own_bindings_change = False | |
self.add(self.builder.get_object("transform_box")) | |
- self.show_all() | |
self._initButtons() | |
+ self.show_all() | |
self.hide() | |
self.app.project_manager.connect_after( | |
@@ -555,37 +562,229 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
clear_button = self.builder.get_object("clear_button") | |
clear_button.connect("clicked", self._defaultValuesCb) | |
- self.__setupSpinButton("xpos_spinbtn", "posx") | |
- self.__setupSpinButton("ypos_spinbtn", "posy") | |
+ 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, 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, False) | |
+ self.__prev_keyframe_btn.set_sensitive(False) | |
+ | |
+ self.__setup_spin_button("xpos_spinbtn", "posx") | |
+ self.__setup_spin_button("ypos_spinbtn", "posy") | |
- self.__setupSpinButton("width_spinbtn", "width") | |
- self.__setupSpinButton("height_spinbtn", "height") | |
+ self.__setup_spin_button("width_spinbtn", "width") | |
+ self.__setup_spin_button("height_spinbtn", "height") | |
+ | |
+ 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]) | |
+ | |
+ return sorted(set(keyframes_ts)) | |
+ | |
+ def __go_to_keyframe(self, unused_button, next_keyframe): | |
+ assert self.__control_bindings | |
+ start = self.source.props.start | |
+ duration = self.source.props.duration | |
+ in_point = self.source.props.in_point | |
+ pipeline = self._project.pipeline | |
+ position = pipeline.getPosition() - start + in_point | |
+ seekval = start | |
+ | |
+ if in_point <= position <= in_point + duration: | |
+ keyframes_ts = self.__get_keyframes_timestamps() | |
+ | |
+ for i in range(1, len(keyframes_ts)): | |
+ if keyframes_ts[i - 1] <= position <= keyframes_ts[i]: | |
+ prev_kf_ts = keyframes_ts[i - 1] | |
+ kf_ts = keyframes_ts[i] | |
+ if next_keyframe: | |
+ if kf_ts == position: | |
+ try: | |
+ kf_ts = keyframes_ts[i + 1] | |
+ except IndexError: | |
+ pass | |
+ seekval = kf_ts + start - in_point | |
+ else: | |
+ seekval = prev_kf_ts + start - in_point | |
+ break | |
+ if position > in_point + duration: | |
+ seekval = start + duration | |
+ pipeline.simple_seek(seekval) | |
+ | |
+ def __show_keyframes_toggled_cb(self, unused_button): | |
+ if self.__activate_keyframes_btn.props.active: | |
+ self.__set_control_bindings() | |
+ self.__update_keyframes_ui() | |
+ | |
+ def __update_keyframes_ui(self): | |
+ if self.__source_uses_keyframes(): | |
+ self.__activate_keyframes_btn.props.label = "◆" | |
+ else: | |
+ self.__activate_keyframes_btn.props.label = "◇" | |
+ self.__activate_keyframes_btn.props.active = False | |
+ | |
+ if not self.__activate_keyframes_btn.props.active: | |
+ 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")) | |
+ else: | |
+ self.__activate_keyframes_btn.set_tooltip_text(_("Activate keyframes")) | |
+ self.source.ui_element.showDefaultKeyframes() | |
+ else: | |
+ self.__prev_keyframe_btn.set_sensitive(True) | |
+ self.__next_keyframe_btn.set_sensitive(True) | |
+ self.__activate_keyframes_btn.set_tooltip_text(_("Hide keyframes")) | |
+ self.source.ui_element.showMultipleKeyframes( | |
+ list(self.__control_bindings.values())) | |
+ | |
+ def __update_control_bindings(self): | |
+ self.__control_bindings = {} | |
+ if self.__source_uses_keyframes(): | |
+ self.__set_control_bindings() | |
+ | |
+ def __source_uses_keyframes(self): | |
+ if self.source is None: | |
+ return False | |
+ | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ binding = self.source.get_control_binding(prop) | |
+ if binding is None: | |
+ return False | |
+ | |
+ return True | |
+ | |
+ def __remove_control_bindings(self): | |
+ for propname, binding in self.__control_bindings.items(): | |
+ control_source = binding.props.control_source | |
+ # 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()] | |
+ for ts in keyframes_ts: | |
+ control_source.unset(ts) | |
+ self.__own_bindings_change = True | |
+ self.source.remove_control_binding(propname) | |
+ self.__own_bindings_change = False | |
+ self.__control_bindings = {} | |
+ | |
+ def __set_control_bindings(self): | |
+ adding_kfs = not self.__source_uses_keyframes() | |
+ | |
+ if adding_kfs: | |
+ self.app.action_log.begin("Transformation properties keyframes activate", | |
+ toplevel=True) | |
+ | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ binding = self.source.get_control_binding(prop) | |
+ | |
+ if not binding: | |
+ 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 | |
+ self.__set_default_keyframes_values(control_source, prop) | |
+ | |
+ binding = self.source.get_control_binding(prop) | |
+ self.__control_bindings[prop] = binding | |
+ | |
+ if adding_kfs: | |
+ 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) | |
def _defaultValuesCb(self, unused_widget): | |
- for name, spinbtn in list(self.spin_buttons.items()): | |
- spinbtn.set_value(self.source.ui.default_position[name]) | |
+ with self.app.action_log.started("Transformation properties reset default", | |
+ 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.__update_keyframes_ui() | |
- def __sourcePropertyChangedCb(self, unused_source, unused_element, param): | |
+ def __get_source_property(self, prop): | |
+ if self.__source_uses_keyframes(): | |
+ try: | |
+ position = self._project.pipeline.getPosition() | |
+ source_position = position - self.source.props.start + self.source.props.in_point | |
+ value = self.__control_bindings[prop].get_value(source_position) | |
+ res = value is not None | |
+ return res, value | |
+ except pipeline.PipelineError: | |
+ pass | |
+ | |
+ return self.source.get_child_property(prop) | |
+ | |
+ def __source_property_changed_cb(self, unused_source, unused_element, param): | |
try: | |
spin = self.spin_buttons[param.name] | |
except KeyError: | |
return | |
- res, value = self.source.get_child_property(param.name) | |
+ res, value = self.__get_source_property(param.name) | |
assert res | |
if spin.get_value() != value: | |
spin.set_value(value) | |
- def _updateSpinButtons(self): | |
+ def _control_bindings_changed(self, unused_track_element, unused_binding): | |
+ if self.__own_bindings_change: | |
+ # Do nothing if the change occurred from this class | |
+ return | |
+ | |
+ self.__update_control_bindings() | |
+ self.__update_keyframes_ui() | |
+ | |
+ def _update_spin_buttons(self): | |
for name, spinbtn in list(self.spin_buttons.items()): | |
res, value = self.source.get_child_property(name) | |
assert res | |
spinbtn.set_value(value) | |
- def __setupSpinButton(self, widget_name, property_name): | |
+ def __set_prop(self, prop, value): | |
+ assert self.source | |
+ | |
+ if self.__source_uses_keyframes(): | |
+ try: | |
+ position = self._project.pipeline.getPosition() | |
+ start = self.source.props.start | |
+ in_point = self.source.props.in_point | |
+ duration = self.source.props.duration | |
+ if position < start or position > start + duration: | |
+ return | |
+ source_position = position - start + in_point | |
+ | |
+ with self.app.action_log.started( | |
+ "Transformation property change", | |
+ finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline), | |
+ toplevel=True): | |
+ self.__control_bindings[prop].props.control_source.set(source_position, value) | |
+ except pipeline.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), | |
+ 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) | |
- spinbtn.connect("output", self._onValueChangedCb, property_name) | |
+ spinbtn.connect("value-changed", self._onValueChangedCb, property_name) | |
disable_scroll(spinbtn) | |
self.spin_buttons[property_name] = spinbtn | |
@@ -595,25 +794,29 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
value = spinbtn.get_value() | |
- res, cvalue = self.source.get_child_property(prop) | |
- assert res | |
+ res, cvalue = self.__get_source_property(prop) | |
+ if not res: | |
+ return | |
+ | |
if value != cvalue: | |
- with self.app.action_log.started("Transformation property change", | |
- finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline), | |
- toplevel=True): | |
- self.source.set_child_property(prop, value) | |
+ self.__set_prop(prop, value) | |
self.app.gui.viewer.overlay_stack.update(self.source) | |
- def __setSource(self, source): | |
+ def __set_source(self, source): | |
if self.source: | |
try: | |
- self.source.disconnect_by_func(self.__sourcePropertyChangedCb) | |
+ self.source.disconnect_by_func(self.__source_property_changed_cb) | |
+ disconnectAllByFunc(self.source, self._control_bindings_changed) | |
except TypeError: | |
pass | |
self.source = source | |
if self.source: | |
- self._updateSpinButtons() | |
- self.source.connect("deep-notify", self.__sourcePropertyChangedCb) | |
+ self._update_spin_buttons() | |
+ self.__update_control_bindings() | |
+ self.__update_keyframes_ui() | |
+ self.source.connect("deep-notify", self.__source_property_changed_cb) | |
+ self.source.connect("control-binding-added", self._control_bindings_changed) | |
+ self.source.connect("control-binding-removed", self._control_bindings_changed) | |
def _selectionChangedCb(self, unused_timeline): | |
if len(self._selection) == 1: | |
@@ -621,7 +824,7 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
source = clip.find_track_element(None, GES.VideoSource) | |
if source: | |
self._selected_clip = clip | |
- self.__setSource(source) | |
+ self.__set_source(source) | |
self.app.gui.viewer.overlay_stack.select(source) | |
self.show() | |
return | |
@@ -629,6 +832,6 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
# Deselect | |
if self._selected_clip: | |
self._selected_clip = None | |
- self._project.pipeline.flushSeek() | |
- self.__setSource(None) | |
+ self._project.pipeline.commit_timeline() | |
+ self.__set_source(None) | |
self.hide() | |
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py | |
index be108e1..e06a18f 100644 | |
--- a/pitivi/timeline/elements.py | |
+++ b/pitivi/timeline/elements.py | |
@@ -94,11 +94,9 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
FigureCanvas.__init__(self, figure) | |
Loggable.__init__(self) | |
- self.__timeline = timeline | |
+ self._timeline = timeline | |
self.__source = binding.props.control_source | |
- self.__source.connect("value-added", self.__controlSourceChangedCb) | |
- self.__source.connect("value-removed", self.__controlSourceChangedCb) | |
- self.__source.connect("value-changed", self.__controlSourceChangedCb) | |
+ self._connect_sources() | |
self.__propertyName = binding.props.name | |
self.__paramspec = binding.pspec | |
self.get_style_context().add_class("KeyframeCurve") | |
@@ -108,8 +106,8 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# Curve values, basically separating source.get_values() timestamps | |
# and values. | |
- self.__line_xs = [] | |
- self.__line_ys = [] | |
+ self._line_xs = [] | |
+ self._line_ys = [] | |
# facecolor to None for transparency | |
self._ax = figure.add_axes([0, 0, 1, 1], facecolor='None') | |
@@ -143,7 +141,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
alpha=KEYFRAME_LINE_ALPHA, | |
c=KEYFRAME_LINE_COLOR, | |
linewidth=KEYFRAME_LINE_HEIGHT, zorder=1)[0] | |
- self.__updatePlots() | |
+ self._update_plots() | |
# Drag and drop logic | |
# Whether the clicked keyframe or line has been dragged. | |
@@ -168,54 +166,81 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
def release(self): | |
disconnectAllByFunc(self, self.__heightRequestCb) | |
disconnectAllByFunc(self, self.__gtkMotionEventCb) | |
- disconnectAllByFunc(self, self.__controlSourceChangedCb) | |
+ disconnectAllByFunc(self, self._controlSourceChangedCb) | |
- # Private methods | |
- def __computeYlim(self): | |
- height = self.props.height_request | |
- | |
- if height <= 0: | |
- return | |
+ def _connect_sources(self): | |
+ self.__source.connect("value-added", self._controlSourceChangedCb) | |
+ self.__source.connect("value-removed", self._controlSourceChangedCb) | |
+ self.__source.connect("value-changed", self._controlSourceChangedCb) | |
- ylim_min = -(KEYFRAME_LINE_HEIGHT / height) | |
- ylim_max = (self.__ylim_max * height) / (height - KEYFRAME_LINE_HEIGHT) | |
- self._ax.set_ylim(ylim_min, ylim_max) | |
- | |
- def __heightRequestCb(self, unused_self, unused_pspec): | |
- self.__computeYlim() | |
- | |
- def __updatePlots(self): | |
+ def _update_plots(self): | |
values = self.__source.get_all() | |
if len(values) < 2: | |
# No plot for less than two points. | |
return | |
- self.__line_xs = [] | |
- self.__line_ys = [] | |
+ self._line_xs = [] | |
+ self._line_ys = [] | |
for value in values: | |
- self.__line_xs.append(value.timestamp) | |
- self.__line_ys.append(value.value) | |
+ self._line_xs.append(value.timestamp) | |
+ self._line_ys.append(value.value) | |
+ | |
+ self._populate_lines() | |
- self._ax.set_xlim(self.__line_xs[0], self.__line_xs[-1]) | |
+ def _populate_lines(self): | |
+ self._ax.set_xlim(self._line_xs[0], self._line_xs[-1]) | |
self.__computeYlim() | |
- arr = numpy.array((self.__line_xs, self.__line_ys)) | |
+ arr = numpy.array((self._line_xs, self._line_ys)) | |
arr = arr.transpose() | |
self.__keyframes.set_offsets(arr) | |
- self.__line.set_xdata(self.__line_xs) | |
- self.__line.set_ydata(self.__line_ys) | |
+ self.__line.set_xdata(self._line_xs) | |
+ self.__line.set_ydata(self._line_ys) | |
self.queue_draw() | |
+ # Private methods | |
+ def __computeYlim(self): | |
+ height = self.props.height_request | |
+ | |
+ if height <= 0: | |
+ return | |
+ | |
+ ylim_min = -(KEYFRAME_LINE_HEIGHT / height) | |
+ ylim_max = (self.__ylim_max * height) / (height - KEYFRAME_LINE_HEIGHT) | |
+ self._ax.set_ylim(ylim_min, ylim_max) | |
+ | |
+ def __heightRequestCb(self, unused_self, unused_pspec): | |
+ self.__computeYlim() | |
+ | |
def __maybeCreateKeyframe(self, event): | |
line_contains = self.__line.contains(event)[0] | |
keyframe_existed = self.__keyframes.contains(event)[0] | |
if line_contains and not keyframe_existed: | |
- res, value = self.__source.control_source_get_value(event.xdata) | |
- assert res | |
- self.debug("Create keyframe at (%lf, %lf)", event.xdata, value) | |
- with self.__timeline.app.action_log.started("Keyframe added", | |
- toplevel=True): | |
- self.__source.set(event.xdata, value) | |
+ self._create_keyframe(event.xdata) | |
+ | |
+ def _create_keyframe(self, timestamp): | |
+ res, value = self.__source.control_source_get_value(timestamp) | |
+ assert res | |
+ self.debug("Create keyframe at (%lf, %lf)", timestamp, value) | |
+ with self._timeline.app.action_log.started("Keyframe added", | |
+ toplevel=True): | |
+ self.__source.set(timestamp, value) | |
+ | |
+ def _remove_keyframe(self, timestamp): | |
+ self.debug("Removing keyframe at timestamp %lf", timestamp) | |
+ with self._timeline.app.action_log.started("Remove keyframe", | |
+ toplevel=True): | |
+ self.__source.unset(timestamp) | |
+ | |
+ def _move_keyframe(self, source_timestamp, dest_timestamp, dest_value): | |
+ self.__source.unset(source_timestamp) | |
+ self.__source.set(dest_timestamp, dest_value) | |
+ | |
+ def _move_keyframe_line(self, line, y_dest_value, y_start_value): | |
+ delta = y_dest_value - y_start_value | |
+ for offset, value in line: | |
+ value = max(self.__ylim_min, min(value + delta, self.__ylim_max)) | |
+ self.__source.set(offset, value) | |
def toggle_keyframe(self, offset): | |
"""Sets or unsets the keyframe at the specified offset.""" | |
@@ -231,9 +256,9 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
self.__source.set(offset, value) | |
# Callbacks | |
- def __controlSourceChangedCb(self, unused_control_source, unused_timed_value): | |
- self.__updatePlots() | |
- self.__timeline.ges_timeline.get_parent().commit_timeline() | |
+ def _controlSourceChangedCb(self, unused_control_source, unused_timed_value): | |
+ self._update_plots() | |
+ self._timeline.ges_timeline.get_parent().commit_timeline() | |
def __gtkMotionEventCb(self, unused_widget, unused_event): | |
# We need to do this here, because Matplotlib's callbacks can't stop | |
@@ -245,7 +270,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
def _eventCb(self, unused_element, event): | |
if event.type == Gdk.EventType.LEAVE_NOTIFY: | |
cursor = NORMAL_CURSOR | |
- self.__timeline.get_window().set_cursor(cursor) | |
+ self._timeline.get_window().set_cursor(cursor) | |
return False | |
def _mpl_button_press_event_cb(self, event): | |
@@ -268,18 +293,15 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# Rollback the last operation if it is "Move keyframe". | |
# This is needed because a double-click also triggers a | |
# BUTTON_PRESS event which starts a "Move keyframe" operation | |
- self.__timeline.app.action_log.try_rollback("Move keyframe") | |
+ self._timeline.app.action_log.try_rollback("Move keyframe") | |
self.__offset = None | |
# A keyframe has been double-clicked, remove it. | |
- self.debug("Removing keyframe at timestamp %lf", offset) | |
- with self.__timeline.app.action_log.started("Remove keyframe", | |
- toplevel=True): | |
- self.__source.unset(offset) | |
+ self._remove_keyframe(offset) | |
else: | |
# Remember the clicked frame for drag&drop. | |
- self.__timeline.app.action_log.begin("Move keyframe", | |
- toplevel=True) | |
+ self._timeline.app.action_log.begin("Move keyframe", | |
+ toplevel=True) | |
self.__offset = offset | |
self.handling_motion = True | |
return | |
@@ -288,8 +310,8 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if result[0]: | |
# The line has been clicked. | |
self.debug("The keyframe curve has been clicked") | |
- self.__timeline.app.action_log.begin("Move keyframe curve segment", | |
- toplevel=True) | |
+ self._timeline.app.action_log.begin("Move keyframe curve segment", | |
+ toplevel=True) | |
x = event.xdata | |
offsets = self.__keyframes.get_offsets() | |
keyframes = offsets[:, 0] | |
@@ -305,20 +327,16 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if self.__offset is not None: | |
self.__dragged = True | |
keyframe_ts = self.__computeKeyframeNewTimestamp(event) | |
- self.__source.unset(int(self.__offset)) | |
- | |
ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
- self.__source.set(keyframe_ts, ydata) | |
+ | |
+ self._move_keyframe(int(self.__offset), keyframe_ts, ydata) | |
self.__offset = keyframe_ts | |
- self.__update_tooltip(event) | |
+ self._update_tooltip(event) | |
hovering = True | |
elif self.__clicked_line: | |
self.__dragged = True | |
ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
- delta = ydata - self.__ydata_drag_start | |
- for offset, value in self.__clicked_line: | |
- value = max(self.__ylim_min, min(value + delta, self.__ylim_max)) | |
- self.__source.set(offset, value) | |
+ self._move_keyframe_line(self.__clicked_line, ydata, self.__ydata_drag_start) | |
hovering = True | |
else: | |
hovering = self.__line.contains(event)[0] | |
@@ -327,7 +345,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if hovering: | |
cursor = DRAG_CURSOR | |
- self.__update_tooltip(event) | |
+ self._update_tooltip(event) | |
if not self.__hovered: | |
self.emit("enter") | |
self.__hovered = True | |
@@ -335,21 +353,38 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
cursor = NORMAL_CURSOR | |
if self.__hovered: | |
self.emit("leave") | |
- self.__update_tooltip(None) | |
+ self._update_tooltip(None) | |
self.__hovered = False | |
- self.__timeline.get_window().set_cursor(cursor) | |
+ self._timeline.get_window().set_cursor(cursor) | |
def _mpl_button_release_event_cb(self, event): | |
if event.button != 1: | |
return | |
+ # In order to make sure we seek to the exact position where we added a | |
+ # new keyframe, we don't use matplotlib's event.xdata, but rather | |
+ # compute it the same way we do for the seek logic. | |
+ event_widget = Gtk.get_event_widget(event.guiEvent) | |
+ x, unused_y = event_widget.translate_coordinates(self._timeline.layout.layers_vbox, | |
+ event.x, event.y) | |
+ ges_clip = self._timeline.selection.getSingleClip(GES.Clip) | |
+ event.xdata = Zoomable.pixelToNs(x) - ges_clip.props.start + ges_clip.props.in_point | |
+ | |
if self.__offset is not None: | |
+ # If dragging a keyframe, make sure the keyframe ends up exactly | |
+ # where the mouse was released. Otherwise, the playhead will not | |
+ # seek exactly on the keyframe. | |
+ if self.__dragged: | |
+ if event.ydata is not None: | |
+ keyframe_ts = self.__computeKeyframeNewTimestamp(event) | |
+ ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
+ self._move_keyframe(int(self.__offset), keyframe_ts, ydata) | |
self.debug("Keyframe released") | |
- self.__timeline.app.action_log.commit("Move keyframe") | |
+ self._timeline.app.action_log.commit("Move keyframe") | |
elif self.__clicked_line: | |
self.debug("Line released") | |
- self.__timeline.app.action_log.commit("Move keyframe curve segment") | |
+ self._timeline.app.action_log.commit("Move keyframe curve segment") | |
if not self.__dragged: | |
# The keyframe line was clicked, but not dragged | |
@@ -361,7 +396,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
self.__clicked_line = () | |
self.__dragged = False | |
- def __update_tooltip(self, event): | |
+ def _update_tooltip(self, event): | |
"""Sets or clears the tooltip showing info about the hovered line.""" | |
markup = None | |
if event: | |
@@ -370,7 +405,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if self.__offset is not None: | |
xdata = self.__offset | |
else: | |
- xdata = max(self.__line_xs[0], min(event.xdata, self.__line_xs[-1])) | |
+ xdata = max(self._line_xs[0], min(event.xdata, self._line_xs[-1])) | |
res, value = self.__source.control_source_get_value(xdata) | |
assert res | |
pmin = self.__paramspec.minimum | |
@@ -409,6 +444,72 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
return event.xdata | |
+class MultipleKeyframeCurve(KeyframeCurve): | |
+ """Keyframe curve which controls multiple properties at once.""" | |
+ | |
+ def __init__(self, timeline, bindings): | |
+ self.__bindings = bindings | |
+ | |
+ super().__init__(timeline, bindings[0]) | |
+ | |
+ def _connect_sources(self): | |
+ for binding in self.__bindings: | |
+ source = binding.props.control_source | |
+ source.connect("value-added", self._controlSourceChangedCb) | |
+ source.connect("value-removed", self._controlSourceChangedCb) | |
+ source.connect("value-changed", self._controlSourceChangedCb) | |
+ | |
+ def _update_plots(self): | |
+ timestamps = [] | |
+ for binding in self.__bindings: | |
+ ts = [value.timestamp for value in binding.props.control_source.get_all()] | |
+ timestamps.extend(ts) | |
+ timestamps = sorted(list(set(timestamps))) | |
+ | |
+ if len(timestamps) < 2: | |
+ # No plot for less than two points. | |
+ return | |
+ | |
+ self._line_xs = [] | |
+ self._line_ys = [] | |
+ for timestamp in timestamps: | |
+ self._line_xs.append(timestamp) | |
+ self._line_ys.append(0.5) | |
+ | |
+ self._populate_lines() | |
+ | |
+ def _create_keyframe(self, timestamp): | |
+ with self._timeline.app.action_log.started("Add keyframe", | |
+ toplevel=True): | |
+ for binding in self.__bindings: | |
+ binding.props.control_source.set(timestamp, binding.get_value(timestamp)) | |
+ | |
+ def _remove_keyframe(self, timestamp): | |
+ with self._timeline.app.action_log.started("Remove keyframe", | |
+ toplevel=True): | |
+ for binding in self.__bindings: | |
+ binding.props.control_source.unset(timestamp) | |
+ | |
+ def _move_keyframe(self, source_timestamp, dest_timestamp, unused_dest_value): | |
+ if source_timestamp == dest_timestamp: | |
+ return | |
+ | |
+ for binding in self.__bindings: | |
+ dest_value = binding.get_value(source_timestamp) | |
+ binding.props.control_source.set(dest_timestamp, dest_value) | |
+ binding.props.control_source.unset(source_timestamp) | |
+ | |
+ def _move_keyframe_line(self, line, y_dest_value, y_start_value): | |
+ pass | |
+ | |
+ def _update_tooltip(self, event): | |
+ markup = None | |
+ if event: | |
+ if not event.xdata: | |
+ return | |
+ markup = _("Timestamp: %s") % Gst.TIME_ARGS(event.xdata) | |
+ self.set_tooltip_markup(markup) | |
+ | |
class TimelineElement(Gtk.Layout, Zoomable, Loggable): | |
__gsignals__ = { | |
# Signal the keyframes curve are being hovered | |
@@ -477,13 +578,18 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable): | |
def showKeyframes(self, ges_elem, prop): | |
self.__setKeyframes(ges_elem, prop) | |
- self.__create_keyframe_curve(ges_elem) | |
+ binding = ges_elem.get_control_binding(prop.name) | |
+ self.__create_keyframe_curve([binding]) | |
def showDefaultKeyframes(self, lazy_render=False): | |
self.__setKeyframes(self._ges_elem, self._getDefaultMixingProperty()) | |
if not lazy_render: | |
self.__create_keyframe_curve() | |
+ def showMultipleKeyframes(self, bindings): | |
+ self.__controlledProperty = None | |
+ self.__create_keyframe_curve(bindings) | |
+ | |
def __setKeyframes(self, ges_elem, prop): | |
self.__removeKeyframes() | |
self.__controlledProperty = prop | |
@@ -523,14 +629,17 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable): | |
assert source.set(inpoint, val) | |
assert source.set(inpoint + self._ges_elem.props.duration, val) | |
- def __create_keyframe_curve(self, ges_elem=None): | |
+ def __create_keyframe_curve(self, bindings=[]): | |
"""Creates required keyframe curve.""" | |
self.__removeKeyframes() | |
- if not ges_elem: | |
- ges_elem = self._ges_elem | |
+ if not bindings: | |
+ bindings = [self._ges_elem.get_control_binding(self.__controlledProperty.name)] | |
+ | |
+ if len(bindings) == 1: | |
+ self.keyframe_curve = KeyframeCurve(self.timeline, bindings[0]) | |
+ else: | |
+ self.keyframe_curve = MultipleKeyframeCurve(self.timeline, bindings) | |
- binding = ges_elem.get_control_binding(self.__controlledProperty.name) | |
- self.keyframe_curve = KeyframeCurve(self.timeline, binding) | |
self.keyframe_curve.connect("enter", self.__curveEnterCb) | |
self.keyframe_curve.connect("leave", self.__curveLeaveCb) | |
self.keyframe_curve.set_size_request(self.__width, self.__height) | |
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py | |
index 6d02d1a..67077dc 100644 | |
--- a/pitivi/timeline/timeline.py | |
+++ b/pitivi/timeline/timeline.py | |
@@ -864,7 +864,6 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): | |
# The preview clips have not been created yet. | |
self.__create_clips(x, y) | |
self.__drag_update(x, y) | |
- | |
Gdk.drag_status(context, Gdk.DragAction.COPY, timestamp) | |
return True | |
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py | |
index 2e986b3..12d46a6 100644 | |
--- a/pitivi/undo/timeline.py | |
+++ b/pitivi/undo/timeline.py | |
@@ -19,6 +19,7 @@ | |
from gi.repository import GES | |
from gi.repository import GObject | |
from gi.repository import Gst | |
+from gi.repository import GstController | |
from pitivi.effects import PROPS_TO_IGNORE | |
from pitivi.undo.undo import Action | |
@@ -584,22 +585,54 @@ class KeyframeChangedAction(UndoableAction): | |
self.control_source.set(time, value) | |
-class ControlSourceSetAction(Action): | |
+class ControlSourceSetAction(UndoableAction): | |
- def __init__(self, action_info): | |
- Action.__init__(self) | |
- self.action_info = action_info | |
+ def __init__(self, track_element, binding): | |
+ UndoableAction.__init__(self) | |
+ self.track_element = track_element | |
+ self.control_source = binding.props.control_source | |
+ self.property_name = binding.props.name | |
+ self.binding_type = "direct-absolute" if binding.props.absolute else "direct" | |
+ | |
+ def do(self): | |
+ self.track_element.set_control_source(self.control_source, | |
+ self.property_name, self.binding_type) | |
+ | |
+ def undo(self): | |
+ assert self.track_element.remove_control_binding(self.property_name) | |
def asScenarioAction(self): | |
st = Gst.Structure.new_empty("set-control-source") | |
- for key, value in self.action_info.items(): | |
- st.set_value(key, value) | |
- st.set_value("binding-type", "direct") | |
+ st.set_value("element-name", self.track_element.get_name()) | |
+ st.set_value("property-name", self.property_name) | |
+ st.set_value("binding-type", self.binding_type) | |
st.set_value("source-type", "interpolation") | |
st.set_value("interpolation-mode", "linear") | |
return st | |
+class ControlSourceRemoveAction(UndoableAction): | |
+ | |
+ def __init__(self, track_element, binding): | |
+ UndoableAction.__init__(self) | |
+ self.track_element = track_element | |
+ self.control_source = binding.props.control_source | |
+ self.property_name = binding.props.name | |
+ self.binding_type = "direct-absolute" if binding.props.absolute else "direct" | |
+ | |
+ def do(self): | |
+ assert self.track_element.remove_control_binding(self.property_name) | |
+ | |
+ def undo(self): | |
+ self.track_element.set_control_source(self.control_source, | |
+ self.property_name, self.binding_type) | |
+ | |
+ def asScenarioAction(self): | |
+ st = Gst.Structure.new_empty("remove-control-source") | |
+ st.set_value("element-name", self.track_element.get_name()) | |
+ st.set_value("property-name", self.property_name) | |
+ return st | |
+ | |
class LayerObserver(MetaContainerObserver, Loggable): | |
"""Monitors a Layer and reports UndoableActions. | |
@@ -654,11 +687,14 @@ class LayerObserver(MetaContainerObserver, Loggable): | |
clip_observer = self.clip_observers.pop(ges_clip) | |
clip_observer.release() | |
- def _controlBindingAddedCb(self, track_element, binding): | |
+ def _control_binding_added_cb(self, track_element, binding): | |
self._connectToControlSource(track_element, binding) | |
- action_info = {"element-name": track_element.get_name(), | |
- "property-name": binding.props.name} | |
- action = ControlSourceSetAction(action_info) | |
+ action = ControlSourceSetAction(track_element, binding) | |
+ self.action_log.push(action) | |
+ | |
+ def _control_binding_removed_cb(self, track_element, binding): | |
+ self._disconnectFromControlSource(binding) | |
+ action = ControlSourceRemoveAction(track_element, binding) | |
self.action_log.push(action) | |
def _connectToTrackElement(self, track_element): | |
@@ -677,13 +713,18 @@ class LayerObserver(MetaContainerObserver, Loggable): | |
for prop, binding in track_element.get_all_control_bindings().items(): | |
self._connectToControlSource(track_element, binding) | |
track_element.connect("control-binding-added", | |
- self._controlBindingAddedCb) | |
+ self._control_binding_added_cb) | |
+ track_element.connect("control-binding-removed", | |
+ self._control_binding_removed_cb) | |
if isinstance(track_element, GES.BaseEffect) or \ | |
isinstance(track_element, GES.VideoSource): | |
observer = TrackElementObserver(track_element, self.action_log) | |
self.track_element_observers[track_element] = observer | |
def _disconnectFromTrackElement(self, track_element): | |
+ if not isinstance(track_element, GES.VideoTransition): | |
+ track_element.disconnect_by_func(self._control_binding_added_cb) | |
+ track_element.disconnect_by_func(self._control_binding_removed_cb) | |
for prop, binding in track_element.get_all_control_bindings().items(): | |
self._disconnectFromControlSource(binding) | |
observer = self.track_element_observers.pop(track_element, None) | |
@@ -695,9 +736,10 @@ class LayerObserver(MetaContainerObserver, Loggable): | |
control_source = binding.props.control_source | |
action_info = {"element-name": track_element.get_name(), | |
"property-name": binding.props.name} | |
- observer = ControlSourceObserver(control_source, self.action_log, | |
- action_info) | |
- self.keyframe_observers[control_source] = observer | |
+ if control_source not in self.keyframe_observers: | |
+ observer = ControlSourceObserver(control_source, self.action_log, | |
+ action_info) | |
+ self.keyframe_observers[control_source] = observer | |
def _disconnectFromControlSource(self, binding): | |
control_source = binding.props.control_source | |
diff --git a/tests/test_timeline_elements.py b/tests/test_timeline_elements.py | |
index 3b954f7..dfa848c 100644 | |
--- a/tests/test_timeline_elements.py | |
+++ b/tests/test_timeline_elements.py | |
@@ -24,10 +24,13 @@ from unittest import TestCase | |
from gi.overrides import GObject | |
from gi.repository import Gdk | |
from gi.repository import GES | |
+from gi.repository import Gst | |
+from gi.repository import Gtk | |
from matplotlib.backend_bases import MouseEvent | |
from pitivi.timeline.elements import GES_TYPE_UI_TYPE | |
from pitivi.undo.undo import UndoableActionLog | |
+from pitivi.utils.timeline import Zoomable | |
from tests.common import create_test_clip | |
from tests.common import create_timeline_container | |
from tests.test_timeline_timeline import BaseTestTimeline | |
@@ -42,13 +45,13 @@ class TestKeyframeCurve(BaseTestTimeline): | |
timeline_container.app.action_log = UndoableActionLog() | |
timeline = timeline_container.timeline | |
ges_layer = timeline.ges_timeline.append_layer() | |
- ges_clip1 = self.add_clip(ges_layer, 0) | |
- ges_clip2 = self.add_clip(ges_layer, 10) | |
- ges_clip3 = self.add_clip(ges_layer, 20, inpoint=100) | |
+ ges_clip1 = self.add_clip(ges_layer, 0, duration=2*Gst.SECOND) | |
+ ges_clip2 = self.add_clip(ges_layer, 10, duration=2*Gst.SECOND) | |
+ ges_clip3 = self.add_clip(ges_layer, 20, inpoint=100, duration=2*Gst.SECOND) | |
# For variety, add TitleClip to the list of clips. | |
ges_clip4 = create_test_clip(GES.TitleClip) | |
ges_clip4.props.start = 30 | |
- ges_clip4.props.duration = 4.5 | |
+ ges_clip4.props.duration = int(0.9 * Gst.SECOND) | |
ges_layer.add_clip(ges_clip4) | |
self.check_keyframe_toggle(ges_clip1, timeline_container) | |
@@ -56,10 +59,8 @@ class TestKeyframeCurve(BaseTestTimeline): | |
self.check_keyframe_toggle(ges_clip3, timeline_container) | |
self.check_keyframe_toggle(ges_clip4, timeline_container) | |
- self.check_keyframe_ui_toggle(ges_clip1, timeline_container) | |
- self.check_keyframe_ui_toggle(ges_clip2, timeline_container) | |
- self.check_keyframe_ui_toggle(ges_clip3, timeline_container) | |
- self.check_keyframe_ui_toggle(ges_clip4, timeline_container) | |
+ for ges_clip in [ges_clip1, ges_clip2, ges_clip3, ges_clip4]: | |
+ self.check_keyframe_ui_toggle(ges_clip, timeline_container) | |
def check_keyframe_toggle(self, ges_clip, timeline_container): | |
"""Checks keyframes toggling on the specified clip.""" | |
@@ -122,9 +123,12 @@ class TestKeyframeCurve(BaseTestTimeline): | |
"""Checks keyframes toggling by click events.""" | |
timeline = timeline_container.timeline | |
+ start = ges_clip.props.start | |
+ start_px = Zoomable.nsToPixel(start) | |
inpoint = ges_clip.props.in_point | |
duration = ges_clip.props.duration | |
- offsets = (1, int(duration / 2), int(duration) - 1) | |
+ duration_px = Zoomable.nsToPixel(duration) | |
+ offsets_px = (1, int(duration_px / 2), int(duration_px) - 1) | |
timeline.selection.select([ges_clip]) | |
ges_video_source = ges_clip.find_track_element(None, GES.VideoSource) | |
@@ -135,29 +139,36 @@ class TestKeyframeCurve(BaseTestTimeline): | |
values = [item.timestamp for item in control_source.get_all()] | |
self.assertEqual(values, [inpoint, inpoint + duration]) | |
- # Add keyframes. | |
- for offset in offsets: | |
+ # Add keyframes by simulating mouse clicks. | |
+ for offset_px in offsets_px: | |
+ offset = Zoomable.pixelToNs(start_px + offset_px) - start | |
xdata, ydata = inpoint + offset, 1 | |
x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) | |
event = MouseEvent( | |
- name = "button_press_event", | |
- canvas = keyframe_curve, | |
- x = x, | |
- y = y, | |
- button = 1 | |
+ name="button_press_event", | |
+ canvas=keyframe_curve, | |
+ x=x, | |
+ y=y, | |
+ button=1 | |
) | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
- keyframe_curve._mpl_button_press_event_cb(event) | |
- event.name = "button_release_event" | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
- keyframe_curve._mpl_button_release_event_cb(event) | |
+ keyframe_curve.translate_coordinates = \ | |
+ mock.Mock(return_value=(start_px+offset_px, None)) | |
+ | |
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: | |
+ get_event_widget.return_value = keyframe_curve | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
+ keyframe_curve._mpl_button_press_event_cb(event) | |
+ event.name = "button_release_event" | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
+ keyframe_curve._mpl_button_release_event_cb(event) | |
values = [item.timestamp for item in control_source.get_all()] | |
self.assertIn(inpoint + offset, values) | |
- # Remove keyframes. | |
- for offset in offsets: | |
+ # Remove keyframes by simulating mouse double-clicks. | |
+ for offset_px in offsets_px: | |
+ offset = Zoomable.pixelToNs(start_px + offset_px) - start | |
xdata, ydata = inpoint + offset, 1 | |
x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) | |
@@ -168,19 +179,23 @@ class TestKeyframeCurve(BaseTestTimeline): | |
y=y, | |
button=1 | |
) | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
- keyframe_curve._mpl_button_press_event_cb(event) | |
- event.name = "button_release_event" | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
- keyframe_curve._mpl_button_release_event_cb(event) | |
- event.name = "button_press_event" | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
- keyframe_curve._mpl_button_press_event_cb(event) | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType._2BUTTON_PRESS) | |
- keyframe_curve._mpl_button_press_event_cb(event) | |
- event.name = "button_release_event" | |
- event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
- keyframe_curve._mpl_button_release_event_cb(event) | |
+ keyframe_curve.translate_coordinates = \ | |
+ mock.Mock(return_value=(start_px + offset_px, None)) | |
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: | |
+ get_event_widget.return_value = keyframe_curve | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
+ keyframe_curve._mpl_button_press_event_cb(event) | |
+ event.name = "button_release_event" | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
+ keyframe_curve._mpl_button_release_event_cb(event) | |
+ event.name = "button_press_event" | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) | |
+ keyframe_curve._mpl_button_press_event_cb(event) | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType._2BUTTON_PRESS) | |
+ keyframe_curve._mpl_button_press_event_cb(event) | |
+ event.name = "button_release_event" | |
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) | |
+ keyframe_curve._mpl_button_release_event_cb(event) | |
values = [item.timestamp for item in control_source.get_all()] | |
self.assertNotIn(inpoint + offset, values) | |
-- | |
2.9.3 | |
From eb495e112fad344e9bc5f078a5bef5d4c1b01fcf Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Fri, 23 Jun 2017 15:43:20 +0300 | |
Subject: [PATCH 2/9] tests: Added tests for | |
clipproperties.TransformationProperties | |
Differential Revision: https://phabricator.freedesktop.org/D1767 | |
--- | |
pitivi/clipproperties.py | 40 +++---- | |
tests/test_clipproperties.py | 272 +++++++++++++++++++++++++++++++++++++++++++ | |
2 files changed, 292 insertions(+), 20 deletions(-) | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index c2557da..3e08d3c 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -562,16 +562,16 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
clear_button = self.builder.get_object("clear_button") | |
clear_button.connect("clicked", self._defaultValuesCb) | |
- 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, True) | |
- self.__next_keyframe_btn.set_sensitive(False) | |
+ self._next_keyframe_btn = self.builder.get_object("next_keyframe_button") | |
+ self._next_keyframe_btn.connect("clicked", self.__go_to_keyframe, 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, False) | |
- self.__prev_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, False) | |
+ self._prev_keyframe_btn.set_sensitive(False) | |
self.__setup_spin_button("xpos_spinbtn", "posx") | |
self.__setup_spin_button("ypos_spinbtn", "posy") | |
@@ -618,29 +618,29 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
pipeline.simple_seek(seekval) | |
def __show_keyframes_toggled_cb(self, unused_button): | |
- if self.__activate_keyframes_btn.props.active: | |
+ if self._activate_keyframes_btn.props.active: | |
self.__set_control_bindings() | |
self.__update_keyframes_ui() | |
def __update_keyframes_ui(self): | |
if self.__source_uses_keyframes(): | |
- self.__activate_keyframes_btn.props.label = "◆" | |
+ self._activate_keyframes_btn.props.label = "◆" | |
else: | |
- self.__activate_keyframes_btn.props.label = "◇" | |
- self.__activate_keyframes_btn.props.active = False | |
+ self._activate_keyframes_btn.props.label = "◇" | |
+ self._activate_keyframes_btn.props.active = False | |
- if not self.__activate_keyframes_btn.props.active: | |
- self.__prev_keyframe_btn.set_sensitive(False) | |
- self.__next_keyframe_btn.set_sensitive(False) | |
+ if not self._activate_keyframes_btn.props.active: | |
+ 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.showDefaultKeyframes() | |
else: | |
- self.__prev_keyframe_btn.set_sensitive(True) | |
- self.__next_keyframe_btn.set_sensitive(True) | |
- self.__activate_keyframes_btn.set_tooltip_text(_("Hide keyframes")) | |
+ self._prev_keyframe_btn.set_sensitive(True) | |
+ self._next_keyframe_btn.set_sensitive(True) | |
+ self._activate_keyframes_btn.set_tooltip_text(_("Hide keyframes")) | |
self.source.ui_element.showMultipleKeyframes( | |
list(self.__control_bindings.values())) | |
diff --git a/tests/test_clipproperties.py b/tests/test_clipproperties.py | |
index 2a8f189..7d9dde5 100644 | |
--- a/tests/test_clipproperties.py | |
+++ b/tests/test_clipproperties.py | |
@@ -17,10 +17,14 @@ | |
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, | |
# Boston, MA 02110-1301, USA. | |
import unittest | |
+from unittest import mock | |
from gi.repository import Gtk | |
from pitivi.clipproperties import EffectProperties | |
+from pitivi.clipproperties import TransformationProperties | |
+from tests import common | |
+from tests.test_timeline_timeline import BaseTestTimeline | |
class EffectPropertiesTest(unittest.TestCase): | |
@@ -59,3 +63,271 @@ class EffectPropertiesTest(unittest.TestCase): | |
2, 1, Gtk.TreeViewDropPosition.INTO_OR_BEFORE)) | |
self.assertEqual(1, EffectProperties.calculateEffectPriority( | |
2, 1, Gtk.TreeViewDropPosition.INTO_OR_AFTER)) | |
+ | |
+ | |
+class TransformationPropertiesTest(BaseTestTimeline): | |
+ """Tests for the TransformationProperties widget.""" | |
+ | |
+ def setup_transformation_box(self): | |
+ """Creates a TransformationProperties widget.""" | |
+ timeline_container = common.create_timeline_container() | |
+ app = timeline_container.app | |
+ transformation_box = TransformationProperties(app) | |
+ project = timeline_container._project | |
+ transformation_box._newProjectLoadedCb(app, project) | |
+ | |
+ return transformation_box | |
+ | |
+ def test_spin_buttons_read(self): | |
+ """ | |
+ Tests that the transformation properties spin buttons display | |
+ the correct values of the source properties. | |
+ """ | |
+ # Create tranformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ spin_buttons = transformation_box.spin_buttons | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ | |
+ # Check that spin buttons display the correct values by default | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ self.assertIn(prop, spin_buttons) | |
+ ret, source_value = source.get_child_property(prop) | |
+ self.assertTrue(ret) | |
+ spin_btn_value = spin_buttons[prop].get_value_as_int() | |
+ self.assertEqual(spin_btn_value, source_value) | |
+ | |
+ # Change the source properties and check the spin buttons update | |
+ # correctly. | |
+ new_values = {"posx": 20, "posy": -50, "width": 70, "height": 450} | |
+ for prop, new_val in new_values.items(): | |
+ self.assertTrue(source.set_child_property(prop, new_val)) | |
+ spin_btn_value = spin_buttons[prop].get_value_as_int() | |
+ self.assertEqual(new_val, spin_btn_value) | |
+ | |
+ def test_spin_buttons_write(self): | |
+ """ | |
+ Tests that changes in spin buttons values are reflected in source | |
+ properties. | |
+ """ | |
+ # Create tranformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ spin_buttons = transformation_box.spin_buttons | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ | |
+ # Get current spin buttons values | |
+ current_spin_values = {} | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ current_spin_values[prop] = spin_buttons[prop].get_value_as_int() | |
+ | |
+ changes = [ | |
+ ("posx", -300), ("posy", 450), ("width", 1), ("height", 320), | |
+ ("posx", 230), ("posx", 520), ("posy", -10), ("posy", -1000), | |
+ ("width", 600), ("width", 1000), ("height", 1), ("height", 1000) | |
+ ] | |
+ | |
+ # Change the spin buttons values and check the source properties are | |
+ # updated correctly. | |
+ for prop, new_value in changes: | |
+ spin_buttons[prop].set_value(new_value) | |
+ current_spin_values[prop] = new_value | |
+ for source_prop in ["posx", "posy", "width", "height"]: | |
+ ret, source_value = source.get_child_property(source_prop) | |
+ self.assertTrue(ret) | |
+ self.assertEqual(current_spin_values[source_prop], source_value) | |
+ | |
+ def test_spin_buttons_source_change(self): | |
+ """ | |
+ Check that spin buttons update correctly when changing the selected | |
+ clip. | |
+ """ | |
+ # Create tranformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ spin_buttons = transformation_box.spin_buttons | |
+ | |
+ # Add two clips and select the first one | |
+ clips = self.addClipsSimple(timeline, 2) | |
+ timeline.selection.select([clips[0]]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ | |
+ # Change the spin buttons values | |
+ new_values = {"posx": 45, "posy": 10, "width": 450, "height": 25} | |
+ for prop, new_val in new_values.items(): | |
+ spin_buttons[prop].set_value(new_val) | |
+ | |
+ # Select the second clip and check the spin buttons values update | |
+ # correctly | |
+ timeline.selection.select([clips[1]]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ ret, source_value = source.get_child_property(prop) | |
+ self.assertTrue(ret) | |
+ self.assertEqual(spin_buttons[prop].get_value_as_int(), source_value) | |
+ | |
+ # Select the first clip again and check spin buttons values | |
+ timeline.selection.select([clips[0]]) | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ self.assertEqual(spin_buttons[prop].get_value_as_int(), new_values[prop]) | |
+ | |
+ def test_keyframes_activate(self): | |
+ """Tests transformation properties keyframes activation.""" | |
+ # Create transformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ inpoint = source.props.in_point | |
+ duration = source.props.duration | |
+ | |
+ # Check keyframes are deactivated by default | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ self.assertIsNone(source.get_control_binding(prop)) | |
+ | |
+ # Get current source properties | |
+ initial_values = {} | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ ret, value = source.get_child_property(prop) | |
+ self.assertTrue(ret) | |
+ initial_values[prop] = value | |
+ | |
+ # Activate keyframes and check the default keyframes are created | |
+ transformation_box._activate_keyframes_btn.set_active(True) | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ control_binding = source.get_control_binding(prop) | |
+ self.assertIsNotNone(control_binding) | |
+ control_source = control_binding.props.control_source | |
+ keyframes = [(item.timestamp, item.value) for item in control_source.get_all()] | |
+ self.assertEqual(keyframes, [(inpoint, initial_values[prop]), | |
+ (inpoint + duration, initial_values[prop])]) | |
+ | |
+ def test_keyframes_add(self): | |
+ """Tests keyframe creation.""" | |
+ # Create transformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ pipeline = timeline._project.pipeline | |
+ spin_buttons = transformation_box.spin_buttons | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ start = source.props.start | |
+ inpoint = source.props.in_point | |
+ duration = source.props.duration | |
+ | |
+ # Activate keyframes | |
+ transformation_box._activate_keyframes_btn.set_active(True) | |
+ | |
+ # Add some more keyframes | |
+ offsets = [1, int(duration / 2), duration - 1] | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ for index, offset in enumerate(offsets): | |
+ timestamp, value = inpoint + offset, offset * 10 | |
+ with mock.patch.object(pipeline, "getPosition") as get_position: | |
+ get_position.return_value = start + offset | |
+ spin_buttons[prop].set_value(value) | |
+ | |
+ control_source = source.get_control_binding(prop).props.control_source | |
+ keyframes = [(item.timestamp, item.value) for item in control_source.get_all()] | |
+ self.assertEqual((timestamp, value), keyframes[index + 1]) | |
+ | |
+ def test_keyframes_navigation(self): | |
+ """Tests keyframe navigation.""" | |
+ # Create transformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ pipeline = timeline._project.pipeline | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ start = source.props.start | |
+ inpoint = source.props.in_point | |
+ duration = source.props.duration | |
+ | |
+ # Activate keyframes and add some more keyframes | |
+ transformation_box._activate_keyframes_btn.set_active(True) | |
+ offsets = [1, int(duration / 2), duration - 1] | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ for offset in offsets: | |
+ timestamp, value = inpoint + offset, offset * 10 | |
+ control_source = source.get_control_binding(prop).props.control_source | |
+ control_source.set(timestamp, value) | |
+ | |
+ # Add edge keyframes in the offsets array | |
+ offsets.insert(0, 0) | |
+ offsets.append(duration) | |
+ | |
+ # Test keyframe navigation | |
+ prev_index = 0 | |
+ next_index = 1 | |
+ for position in range(duration + 1): | |
+ prev_keyframe_ts = offsets[prev_index] + inpoint | |
+ next_keyframe_ts = offsets[next_index] + inpoint | |
+ | |
+ with mock.patch.object(pipeline, "getPosition") as get_position: | |
+ get_position.return_value = start + position | |
+ with mock.patch.object(pipeline, "simple_seek") as simple_seek: | |
+ transformation_box._prev_keyframe_btn.clicked() | |
+ simple_seek.assert_called_with(prev_keyframe_ts) | |
+ transformation_box._next_keyframe_btn.clicked() | |
+ simple_seek.assert_called_with(next_keyframe_ts) | |
+ | |
+ if position + 1 == next_keyframe_ts and next_index + 1 < len(offsets): | |
+ next_index += 1 | |
+ if position in offsets and position != 0: | |
+ prev_index += 1 | |
+ | |
+ def test_reset_to_default(self): | |
+ """Tests "reset to default" button.""" | |
+ # Create transformation box | |
+ transformation_box = self.setup_transformation_box() | |
+ timeline = transformation_box.app.gui.timeline_ui.timeline | |
+ | |
+ # Add a clip and select it | |
+ clip = self.addClipsSimple(timeline, 1)[0] | |
+ timeline.selection.select([clip]) | |
+ source = transformation_box.source | |
+ self.assertIsNotNone(source) | |
+ | |
+ # Change source properties | |
+ new_values = {"posx": 20, "posy": -50, "width": 70, "height": 450} | |
+ for prop, new_val in new_values.items(): | |
+ self.assertTrue(source.set_child_property(prop, new_val)) | |
+ | |
+ # Activate keyframes | |
+ transformation_box._activate_keyframes_btn.set_active(True) | |
+ | |
+ # Press "reset to default" button | |
+ clear_button = transformation_box.builder.get_object("clear_button") | |
+ clear_button.clicked() | |
+ | |
+ # Check that control bindings were erased and the properties were | |
+ # reset to their default values | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ self.assertIsNone(source.get_control_binding(prop)) | |
+ ret, value = source.get_child_property(prop) | |
+ self.assertTrue(ret) | |
+ self.assertEqual(value, source.ui.default_position[prop]) | |
-- | |
2.9.3 | |
From 640b5acfbc207d9023e180c61edff8a2ecf1ae35 Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Mon, 26 Jun 2017 10:15:43 +0300 | |
Subject: [PATCH 3/9] clipproperties: Fixed a bug where keyframes were added on | |
selection change | |
When changing the selected clip, a new keyframe was added incorrectly. This | |
happened because the transformation spin buttons values were updated | |
before clearing the previous control bindings, which was interpreted as | |
a new keyframe being added on the old selected clip. The fix consists of | |
updating the spin buttons values after clearing the previous control | |
bindings. | |
Differential Revision: https://phabricator.freedesktop.org/D1769 | |
--- | |
pitivi/clipproperties.py | 2 +- | |
1 file changed, 1 insertion(+), 1 deletion(-) | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index 3e08d3c..a3dce83 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -811,8 +811,8 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
pass | |
self.source = source | |
if self.source: | |
- self._update_spin_buttons() | |
self.__update_control_bindings() | |
+ self._update_spin_buttons() | |
self.__update_keyframes_ui() | |
self.source.connect("deep-notify", self.__source_property_changed_cb) | |
self.source.connect("control-binding-added", self._control_bindings_changed) | |
-- | |
2.9.3 | |
From e42be25f137e390bc11499bbf1e0b5f745aa3160 Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Mon, 26 Jun 2017 11:42:49 +0300 | |
Subject: [PATCH 4/9] clipproperties: Keyframes added on play bug fixed | |
When playing the preview, unintended keyframes were sometimes added. This | |
happened because changes in the source properties led to changes in spin | |
button values, which were misinterpreted as user inputs. | |
Differential Revision: https://phabricator.freedesktop.org/D1770 | |
--- | |
pitivi/clipproperties.py | 15 +++++++++++---- | |
1 file changed, 11 insertions(+), 4 deletions(-) | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index a3dce83..63f42a2 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -23,7 +23,6 @@ from gettext import gettext as _ | |
from gi.repository import Gdk | |
from gi.repository import GES | |
from gi.repository import Gio | |
-from gi.repository import Gst | |
from gi.repository import GstController | |
from gi.repository import Gtk | |
from gi.repository import Pango | |
@@ -532,6 +531,7 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
self.source = None | |
self._selected_clip = None | |
self.spin_buttons = {} | |
+ self.spin_buttons_handler_ids = {} | |
self.set_label(_("Transformation")) | |
self.builder = Gtk.Builder() | |
@@ -732,13 +732,17 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
def __source_property_changed_cb(self, unused_source, unused_element, param): | |
try: | |
spin = self.spin_buttons[param.name] | |
+ spin_handler_id = self.spin_buttons_handler_ids[param.name] | |
except KeyError: | |
return | |
res, value = self.__get_source_property(param.name) | |
assert res | |
if spin.get_value() != value: | |
- spin.set_value(value) | |
+ # Make sure self._onValueChangedCb doesn't get called here. If that | |
+ # happens, we might have unintended keyframes added. | |
+ with spin.handler_block(spin_handler_id): | |
+ spin.set_value(value) | |
def _control_bindings_changed(self, unused_track_element, unused_binding): | |
if self.__own_bindings_change: | |
@@ -750,9 +754,11 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
def _update_spin_buttons(self): | |
for name, spinbtn in list(self.spin_buttons.items()): | |
+ spin_handler_id = self.spin_buttons_handler_ids[name] | |
res, value = self.source.get_child_property(name) | |
assert res | |
- spinbtn.set_value(value) | |
+ with spinbtn.handler_block(spin_handler_id): | |
+ spinbtn.set_value(value) | |
def __set_prop(self, prop, value): | |
assert self.source | |
@@ -784,9 +790,10 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
def __setup_spin_button(self, widget_name, property_name): | |
"""Creates a SpinButton for editing a property value.""" | |
spinbtn = self.builder.get_object(widget_name) | |
- spinbtn.connect("value-changed", self._onValueChangedCb, property_name) | |
+ handler_id = spinbtn.connect("value-changed", self._onValueChangedCb, property_name) | |
disable_scroll(spinbtn) | |
self.spin_buttons[property_name] = spinbtn | |
+ self.spin_buttons_handler_ids[property_name] = handler_id | |
def _onValueChangedCb(self, spinbtn, prop): | |
if not self.source: | |
-- | |
2.9.3 | |
From f8a90839724bcc6a0f33fd1fbc394b32132c777a Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Tue, 4 Jul 2017 11:14:06 +0300 | |
Subject: [PATCH 5/9] viewer: Added support for keyframable transformation | |
properties | |
Differential Revision: https://phabricator.freedesktop.org/D1771 | |
--- | |
pitivi/viewer/move_scale_overlay.py | 52 +++++++++++++++++++++++++++++++------ | |
1 file changed, 44 insertions(+), 8 deletions(-) | |
diff --git a/pitivi/viewer/move_scale_overlay.py b/pitivi/viewer/move_scale_overlay.py | |
index 6d73f4b..889c009 100644 | |
--- a/pitivi/viewer/move_scale_overlay.py | |
+++ b/pitivi/viewer/move_scale_overlay.py | |
@@ -24,6 +24,7 @@ import numpy | |
from pitivi.undo.timeline import CommitTimelineFinalizingAction | |
from pitivi.utils.misc import disconnectAllByFunc | |
+from pitivi.utils.pipeline import PipelineError | |
from pitivi.viewer.overlay import Overlay | |
@@ -327,15 +328,50 @@ class MoveScaleOverlay(Overlay): | |
self._source.connect("deep-notify", self.__source_property_changed_cb) | |
self.update_from_source() | |
+ def __get_source_property(self, prop): | |
+ if self.__source_property_keyframed(prop): | |
+ binding = self._source.get_control_binding(prop) | |
+ res, timestamp = self.__get_pipeline_position() | |
+ if res: | |
+ source_timestamp = timestamp - self._source.props.start + self._source.props.in_point | |
+ value = binding.get_value(source_timestamp) | |
+ res = value is not None | |
+ return res, value | |
+ | |
+ return self._source.get_child_property(prop) | |
+ | |
+ def __set_source_property(self, prop, value): | |
+ if self.__source_property_keyframed(prop): | |
+ control_source = self._source.get_control_binding(prop).props.control_source | |
+ res, timestamp = self.__get_pipeline_position() | |
+ if not res: | |
+ return | |
+ source_timestamp = timestamp - self._source.props.start + self._source.props.in_point | |
+ control_source.set(source_timestamp, value) | |
+ else: | |
+ self._source.set_child_property(prop, value) | |
+ | |
+ def __source_property_keyframed(self, prop): | |
+ binding = self._source.get_control_binding(prop) | |
+ return binding is not None | |
+ | |
+ def __get_pipeline_position(self): | |
+ pipeline = self.stack.app.project_manager.current_project.pipeline | |
+ try: | |
+ position = pipeline.getPosition() | |
+ return True, position | |
+ except PipelineError: | |
+ return False, None | |
+ | |
def __get_source_position(self): | |
- res_x, x = self._source.get_child_property("posx") | |
- res_y, y = self._source.get_child_property("posy") | |
+ res_x, x = self.__get_source_property("posx") | |
+ res_y, y = self.__get_source_property("posy") | |
assert res_x and res_y | |
return numpy.array([x, y]) | |
def __get_source_size(self): | |
- res_x, x = self._source.get_child_property("width") | |
- res_y, y = self._source.get_child_property("height") | |
+ res_x, x = self.__get_source_property("width") | |
+ res_y, y = self.__get_source_property("height") | |
assert res_x and res_y | |
return numpy.array([x, y]) | |
@@ -343,12 +379,12 @@ class MoveScaleOverlay(Overlay): | |
return self.__get_source_position() / self.project_size | |
def __set_source_position(self, position): | |
- self._source.set_child_property("posx", int(position[0])) | |
- self._source.set_child_property("posy", int(position[1])) | |
+ self.__set_source_property("posx", int(position[0])) | |
+ self.__set_source_property("posy", int(position[1])) | |
def __set_source_size(self, size): | |
- self._source.set_child_property("width", int(size[0])) | |
- self._source.set_child_property("height", int(size[1])) | |
+ self.__set_source_property("width", int(size[0])) | |
+ self.__set_source_property("height", int(size[1])) | |
def __get_size(self): | |
return numpy.array([self.__get_width(), self.__get_height()]) | |
-- | |
2.9.3 | |
From 6702b1708f207b3ae0c0c5ac85dab0c6adcf3586 Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Tue, 18 Jul 2017 18:24:02 +0300 | |
Subject: [PATCH 6/9] elements: Select MultipleKeyframeCurve keyframes. | |
Added the possibility to select MultipleKeyframeCurve keyframes by clicking | |
on them. | |
Differential Revision: https://phabricator.freedesktop.org/D1784 | |
--- | |
pitivi/timeline/elements.py | 145 +++++++++++++++++++++++++++++++++++--------- | |
pitivi/timeline/timeline.py | 15 ++++- | |
2 files changed, 129 insertions(+), 31 deletions(-) | |
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py | |
index e06a18f..b2d9227 100644 | |
--- a/pitivi/timeline/elements.py | |
+++ b/pitivi/timeline/elements.py | |
@@ -38,6 +38,7 @@ from pitivi.effects import VIDEO_EFFECT | |
from pitivi.timeline.previewers import AudioPreviewer | |
from pitivi.timeline.previewers import VideoPreviewer | |
from pitivi.undo.timeline import CommitTimelineFinalizingAction | |
+from pitivi.utils import pipeline | |
from pitivi.utils.loggable import Loggable | |
from pitivi.utils.misc import disconnectAllByFunc | |
from pitivi.utils.misc import filename_from_uri | |
@@ -54,6 +55,8 @@ KEYFRAME_LINE_HEIGHT = 2 | |
KEYFRAME_LINE_ALPHA = 0.5 | |
KEYFRAME_LINE_COLOR = "#EDD400" # "Tango" medium yellow | |
KEYFRAME_NODE_COLOR = "#F57900" # "Tango" medium orange | |
+SELECTED_KEYFRAME_NODE_COLOR = "#204A87" # "Tango" dark sky blue | |
+HOVERED_KEYFRAME_NODE_COLOR = "#3465A4" # "Tango" medium sky blue | |
CURSORS = { | |
GES.Edge.EDGE_START: Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE), | |
@@ -130,11 +133,11 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# The PathCollection object holding the keyframes dots. | |
sizes = [50] | |
- self.__keyframes = self._ax.scatter([], [], marker='D', s=sizes, | |
- c=KEYFRAME_NODE_COLOR, zorder=2) | |
+ self._keyframes = self._ax.scatter([], [], marker='D', s=sizes, | |
+ c=KEYFRAME_NODE_COLOR, zorder=2) | |
# matplotlib weirdness, simply here to avoid a warning .. | |
- self.__keyframes.set_picker(True) | |
+ self._keyframes.set_picker(True) | |
# The Line2D object holding the lines between keyframes. | |
self.__line = self._ax.plot([], [], | |
@@ -145,9 +148,9 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# Drag and drop logic | |
# Whether the clicked keyframe or line has been dragged. | |
- self.__dragged = False | |
+ self._dragged = False | |
# The inpoint of the clicked keyframe. | |
- self.__offset = None | |
+ self._offset = None | |
# The (offset, value) of both keyframes of the clicked keyframe line. | |
self.__clicked_line = () | |
# Whether the mouse events go to the keyframes logic. | |
@@ -193,7 +196,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
arr = numpy.array((self._line_xs, self._line_ys)) | |
arr = arr.transpose() | |
- self.__keyframes.set_offsets(arr) | |
+ self._keyframes.set_offsets(arr) | |
self.__line.set_xdata(self._line_xs) | |
self.__line.set_ydata(self._line_ys) | |
self.queue_draw() | |
@@ -214,7 +217,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
def __maybeCreateKeyframe(self, event): | |
line_contains = self.__line.contains(event)[0] | |
- keyframe_existed = self.__keyframes.contains(event)[0] | |
+ keyframe_existed = self._keyframes.contains(event)[0] | |
if line_contains and not keyframe_existed: | |
self._create_keyframe(event.xdata) | |
@@ -277,11 +280,11 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if event.button != 1: | |
return | |
- result = self.__keyframes.contains(event) | |
+ result = self._keyframes.contains(event) | |
if result[0]: | |
# A keyframe has been clicked. | |
keyframe_index = result[1]['ind'][0] | |
- offsets = self.__keyframes.get_offsets() | |
+ offsets = self._keyframes.get_offsets() | |
offset = offsets[keyframe_index][0] | |
if event.guiEvent.type == Gdk.EventType._2BUTTON_PRESS: | |
@@ -294,7 +297,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# This is needed because a double-click also triggers a | |
# BUTTON_PRESS event which starts a "Move keyframe" operation | |
self._timeline.app.action_log.try_rollback("Move keyframe") | |
- self.__offset = None | |
+ self._offset = None | |
# A keyframe has been double-clicked, remove it. | |
self._remove_keyframe(offset) | |
@@ -302,7 +305,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# Remember the clicked frame for drag&drop. | |
self._timeline.app.action_log.begin("Move keyframe", | |
toplevel=True) | |
- self.__offset = offset | |
+ self._offset = offset | |
self.handling_motion = True | |
return | |
@@ -313,7 +316,7 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
self._timeline.app.action_log.begin("Move keyframe curve segment", | |
toplevel=True) | |
x = event.xdata | |
- offsets = self.__keyframes.get_offsets() | |
+ offsets = self._keyframes.get_offsets() | |
keyframes = offsets[:, 0] | |
right = numpy.searchsorted(keyframes, x) | |
# Remember the clicked line for drag&drop. | |
@@ -324,17 +327,17 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
def _mpl_motion_event_cb(self, event): | |
if event.ydata is not None and event.xdata is not None: | |
# The mouse event is in the figure boundaries. | |
- if self.__offset is not None: | |
- self.__dragged = True | |
+ if self._offset is not None: | |
+ self._dragged = True | |
keyframe_ts = self.__computeKeyframeNewTimestamp(event) | |
ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
- self._move_keyframe(int(self.__offset), keyframe_ts, ydata) | |
- self.__offset = keyframe_ts | |
+ self._move_keyframe(int(self._offset), keyframe_ts, ydata) | |
+ self._offset = keyframe_ts | |
self._update_tooltip(event) | |
hovering = True | |
elif self.__clicked_line: | |
- self.__dragged = True | |
+ self._dragged = True | |
ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
self._move_keyframe_line(self.__clicked_line, ydata, self.__ydata_drag_start) | |
hovering = True | |
@@ -371,30 +374,30 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
ges_clip = self._timeline.selection.getSingleClip(GES.Clip) | |
event.xdata = Zoomable.pixelToNs(x) - ges_clip.props.start + ges_clip.props.in_point | |
- if self.__offset is not None: | |
+ if self._offset is not None: | |
# If dragging a keyframe, make sure the keyframe ends up exactly | |
# where the mouse was released. Otherwise, the playhead will not | |
# seek exactly on the keyframe. | |
- if self.__dragged: | |
+ if self._dragged: | |
if event.ydata is not None: | |
keyframe_ts = self.__computeKeyframeNewTimestamp(event) | |
ydata = max(self.__ylim_min, min(event.ydata, self.__ylim_max)) | |
- self._move_keyframe(int(self.__offset), keyframe_ts, ydata) | |
+ self._move_keyframe(int(self._offset), keyframe_ts, ydata) | |
self.debug("Keyframe released") | |
self._timeline.app.action_log.commit("Move keyframe") | |
elif self.__clicked_line: | |
self.debug("Line released") | |
self._timeline.app.action_log.commit("Move keyframe curve segment") | |
- if not self.__dragged: | |
+ if not self._dragged: | |
# The keyframe line was clicked, but not dragged | |
assert event.guiEvent.type == Gdk.EventType.BUTTON_RELEASE | |
self.__maybeCreateKeyframe(event) | |
self.handling_motion = False | |
- self.__offset = None | |
+ self._offset = None | |
self.__clicked_line = () | |
- self.__dragged = False | |
+ self._dragged = False | |
def _update_tooltip(self, event): | |
"""Sets or clears the tooltip showing info about the hovered line.""" | |
@@ -402,8 +405,8 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
if event: | |
if not event.xdata: | |
return | |
- if self.__offset is not None: | |
- xdata = self.__offset | |
+ if self._offset is not None: | |
+ xdata = self._offset | |
else: | |
xdata = max(self._line_xs[0], min(event.xdata, self._line_xs[-1])) | |
res, value = self.__source.control_source_get_value(xdata) | |
@@ -424,12 +427,12 @@ class KeyframeCurve(FigureCanvas, Loggable): | |
# The user can not change the timestamp of the first | |
# and last keyframes. | |
values = self.__source.get_all() | |
- if self.__offset in (values[0].timestamp, values[-1].timestamp): | |
- return self.__offset | |
+ if self._offset in (values[0].timestamp, values[-1].timestamp): | |
+ return self._offset | |
- if event.xdata != self.__offset: | |
+ if event.xdata != self._offset: | |
try: | |
- kf = next(kf for kf in values if kf.timestamp == int(self.__offset)) | |
+ kf = next(kf for kf in values if kf.timestamp == int(self._offset)) | |
except StopIteration: | |
return event.xdata | |
@@ -449,9 +452,24 @@ class MultipleKeyframeCurve(KeyframeCurve): | |
def __init__(self, timeline, bindings): | |
self.__bindings = bindings | |
- | |
super().__init__(timeline, bindings[0]) | |
+ self._timeline = timeline | |
+ self._project = timeline.app.project_manager.current_project | |
+ self._project.pipeline.connect("position", self._position_cb) | |
+ | |
+ sizes = [80] | |
+ self.__selected_keyframe = self._ax.scatter([0], [0.5], marker='D', s=sizes, | |
+ c=SELECTED_KEYFRAME_NODE_COLOR, zorder=3) | |
+ self.__hovered_keyframe = self._ax.scatter([0], [0.5], marker='D', s=sizes, | |
+ c=HOVERED_KEYFRAME_NODE_COLOR, zorder=3) | |
+ self.__update_selected_keyframe() | |
+ self.__hovered_keyframe.set_visible(False) | |
+ | |
+ def release(self): | |
+ super().release() | |
+ self._project.pipeline.disconnect_by_func(self._position_cb) | |
+ | |
def _connect_sources(self): | |
for binding in self.__bindings: | |
source = binding.props.control_source | |
@@ -502,6 +520,73 @@ class MultipleKeyframeCurve(KeyframeCurve): | |
def _move_keyframe_line(self, line, y_dest_value, y_start_value): | |
pass | |
+ def _mpl_button_release_event_cb(self, event): | |
+ if event.button == 1: | |
+ if self._offset is not None and not self._dragged: | |
+ # A keyframe was clicked but not dragged, so we | |
+ # should select it by seeking to its position. | |
+ source = self._timeline.selection.getSingleClip() | |
+ assert source | |
+ position = int(self._offset) - source.props.in_point + source.props.start | |
+ | |
+ if self._timeline.app.settings.leftClickAlsoSeeks: | |
+ self._timeline.set_next_seek_position(position) | |
+ else: | |
+ self._project.pipeline.simple_seek(position) | |
+ | |
+ super()._mpl_button_release_event_cb(event) | |
+ | |
+ def _mpl_motion_event_cb(self, event): | |
+ super()._mpl_motion_event_cb(event) | |
+ | |
+ result = self._keyframes.contains(event) | |
+ if result[0]: | |
+ # A keyframe is hovered | |
+ keyframe_index = result[1]['ind'][0] | |
+ offset = self._keyframes.get_offsets()[keyframe_index][0] | |
+ self.__show_special_keyframe(self.__hovered_keyframe, offset) | |
+ else: | |
+ self.__hide_special_keyframe(self.__hovered_keyframe) | |
+ | |
+ def __show_special_keyframe(self, keyframe, offset): | |
+ offsets = numpy.array([[offset, 0.5]]) | |
+ keyframe.set_offsets(offsets) | |
+ keyframe.set_visible(True) | |
+ self.queue_draw() | |
+ | |
+ def __hide_special_keyframe(self, keyframe): | |
+ keyframe.set_visible(False) | |
+ self.queue_draw() | |
+ | |
+ def _controlSourceChangedCb(self, control_source, timed_value): | |
+ super()._controlSourceChangedCb(control_source, timed_value) | |
+ self.__update_selected_keyframe() | |
+ self.__hide_special_keyframe(self.__hovered_keyframe) | |
+ | |
+ def _position_cb(self, unused_pipeline, unused_position): | |
+ self.__update_selected_keyframe() | |
+ | |
+ def __update_selected_keyframe(self): | |
+ try: | |
+ position = self._project.pipeline.getPosition() | |
+ except pipeline.PipelineError: | |
+ self.warning("Could not get pipeline position") | |
+ return | |
+ | |
+ source = self._timeline.selection.getSingleClip() | |
+ if source is None: | |
+ return | |
+ source_position = position - source.props.start + source.props.in_point | |
+ | |
+ offsets = self._keyframes.get_offsets() | |
+ keyframes = offsets[:, 0] | |
+ | |
+ index = numpy.searchsorted(keyframes, source_position) | |
+ if 0 <= index < len(keyframes) and keyframes[index] == source_position: | |
+ self.__show_special_keyframe(self.__selected_keyframe, source_position) | |
+ else: | |
+ self.__hide_special_keyframe(self.__selected_keyframe) | |
+ | |
def _update_tooltip(self, event): | |
markup = None | |
if event: | |
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py | |
index 67077dc..fafc069 100644 | |
--- a/pitivi/timeline/timeline.py | |
+++ b/pitivi/timeline/timeline.py | |
@@ -375,6 +375,7 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): | |
self.__last_position = 0 | |
self._scrubbing = False | |
self._scrolling = False | |
+ self.__next_seek_position = None | |
# Clip selection. | |
self.selection = Selection() | |
@@ -670,6 +671,14 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): | |
sources = self.get_sources_at_position(self.__last_position) | |
self.app.gui.viewer.overlay_stack.set_current_sources(sources) | |
+ def set_next_seek_position(self, next_seek_position): | |
+ """Sets the position the playhead seeks to on the next button-release. | |
+ | |
+ Args: | |
+ next_seek_position (int): the position to seek to | |
+ """ | |
+ self.__next_seek_position = next_seek_position | |
+ | |
def _button_press_event_cb(self, unused_widget, event): | |
self.debug("PRESSED %s", event) | |
self.app.gui.focusTimeline() | |
@@ -727,7 +736,11 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): | |
self._scrolling = False | |
if allow_seek and res and (button == 1 and self.app.settings.leftClickAlsoSeeks): | |
- self._seek(event) | |
+ if self.__next_seek_position is not None: | |
+ self._project.pipeline.simple_seek(self.__next_seek_position) | |
+ self.__next_seek_position = None | |
+ else: | |
+ self._seek(event) | |
self._snapEndedCb() | |
self.update_visible_overlays() | |
-- | |
2.9.3 | |
From 04a30077173186080451c1061e9d5191ed386cc2 Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Fri, 21 Jul 2017 17:22:17 +0300 | |
Subject: [PATCH 7/9] clipproperties+viewer: Clip out of bounds position | |
When getting the value of a control binded source property, clip the | |
pipeline position if it is outside of the source range. | |
Differential Revision: https://phabricator.freedesktop.org/D1799 | |
--- | |
pitivi/clipproperties.py | 8 +++++++- | |
pitivi/viewer/move_scale_overlay.py | 11 ++++++++--- | |
2 files changed, 15 insertions(+), 4 deletions(-) | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index 63f42a2..e274c9e 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -720,7 +720,13 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
if self.__source_uses_keyframes(): | |
try: | |
position = self._project.pipeline.getPosition() | |
- source_position = position - self.source.props.start + self.source.props.in_point | |
+ start = self.source.props.start | |
+ in_point = self.source.props.in_point | |
+ duration = self.source.props.duration | |
+ | |
+ # 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) | |
res = value is not None | |
return res, value | |
diff --git a/pitivi/viewer/move_scale_overlay.py b/pitivi/viewer/move_scale_overlay.py | |
index 889c009..75d93ec 100644 | |
--- a/pitivi/viewer/move_scale_overlay.py | |
+++ b/pitivi/viewer/move_scale_overlay.py | |
@@ -331,10 +331,15 @@ class MoveScaleOverlay(Overlay): | |
def __get_source_property(self, prop): | |
if self.__source_property_keyframed(prop): | |
binding = self._source.get_control_binding(prop) | |
- res, timestamp = self.__get_pipeline_position() | |
+ res, position = self.__get_pipeline_position() | |
if res: | |
- source_timestamp = timestamp - self._source.props.start + self._source.props.in_point | |
- value = binding.get_value(source_timestamp) | |
+ start = self._source.props.start | |
+ in_point = self._source.props.in_point | |
+ duration = self._source.props.duration | |
+ # 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 = binding.get_value(source_position) | |
res = value is not None | |
return res, value | |
-- | |
2.9.3 | |
From 4b744947b107ec84e0f8ba3595b5f5d75681b59a Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Thu, 27 Jul 2017 10:04:14 +0300 | |
Subject: [PATCH 8/9] clipproperties: Update displayed properties on position | |
change | |
When using control bindings, update the displayed values of the | |
transformation properties, as well as the overlay of the selected clip | |
on each position change. This will produce more accurate results. | |
Differential Revision: https://phabricator.freedesktop.org/D1810 | |
--- | |
pitivi/clipproperties.py | 34 ++++++++++++++++++++++------------ | |
1 file changed, 22 insertions(+), 12 deletions(-) | |
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py | |
index e274c9e..0af8ba6 100644 | |
--- a/pitivi/clipproperties.py | |
+++ b/pitivi/clipproperties.py | |
@@ -553,10 +553,14 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
if self._selection is not None: | |
self._selection.disconnect_by_func(self._selectionChangedCb) | |
self._selection = None | |
+ if self._project: | |
+ self._project.pipeline.disconnect_by_func(self._position_cb) | |
+ | |
self._project = project | |
if project: | |
self._selection = project.ges_timeline.ui.selection | |
self._selection.connect('selection-changed', self._selectionChangedCb) | |
+ self._project.pipeline.connect("position", self._position_cb) | |
def _initButtons(self): | |
clear_button = self.builder.get_object("clear_button") | |
@@ -735,14 +739,27 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
return self.source.get_child_property(prop) | |
+ def _position_cb(self, unused_pipeline, unused_position): | |
+ if not self.__source_uses_keyframes(): | |
+ return | |
+ for prop in ["posx", "posy", "width", "height"]: | |
+ self.__update_spin_btn(prop) | |
+ # Keep the overlay stack in sync with the spin buttons values | |
+ self.app.gui.viewer.overlay_stack.update(self.source) | |
+ | |
def __source_property_changed_cb(self, unused_source, unused_element, param): | |
+ self.__update_spin_btn(param.name) | |
+ | |
+ def __update_spin_btn(self, prop): | |
+ assert self.source | |
+ | |
try: | |
- spin = self.spin_buttons[param.name] | |
- spin_handler_id = self.spin_buttons_handler_ids[param.name] | |
+ spin = self.spin_buttons[prop] | |
+ spin_handler_id = self.spin_buttons_handler_ids[prop] | |
except KeyError: | |
return | |
- res, value = self.__get_source_property(param.name) | |
+ res, value = self.__get_source_property(prop) | |
assert res | |
if spin.get_value() != value: | |
# Make sure self._onValueChangedCb doesn't get called here. If that | |
@@ -758,14 +775,6 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
self.__update_control_bindings() | |
self.__update_keyframes_ui() | |
- def _update_spin_buttons(self): | |
- for name, spinbtn in list(self.spin_buttons.items()): | |
- spin_handler_id = self.spin_buttons_handler_ids[name] | |
- res, value = self.source.get_child_property(name) | |
- assert res | |
- with spinbtn.handler_block(spin_handler_id): | |
- spinbtn.set_value(value) | |
- | |
def __set_prop(self, prop, value): | |
assert self.source | |
@@ -825,7 +834,8 @@ class TransformationProperties(Gtk.Expander, Loggable): | |
self.source = source | |
if self.source: | |
self.__update_control_bindings() | |
- self._update_spin_buttons() | |
+ for prop in self.spin_buttons: | |
+ self.__update_spin_btn(prop) | |
self.__update_keyframes_ui() | |
self.source.connect("deep-notify", self.__source_property_changed_cb) | |
self.source.connect("control-binding-added", self._control_bindings_changed) | |
-- | |
2.9.3 | |
From 8d703c11823eab569627e424e8863e15adbd8af8 Mon Sep 17 00:00:00 2001 | |
From: Stefan Popa <stefanpopa2209@gmail.com> | |
Date: Thu, 27 Jul 2017 10:15:47 +0300 | |
Subject: [PATCH 9/9] viewer: Hide overlays when playing | |
Make sure only the sink widget remains visible when playing and all | |
the overlays are hidden. When using keyframes with the transformation | |
properties, the properties of the overlay are updated with a delay, | |
which causes the sink widget and the overlay to get out of sync. Hiding | |
the overlay will hide this problem. | |
Differential Revision: https://phabricator.freedesktop.org/D1811 | |
--- | |
pitivi/viewer/overlay_stack.py | 24 +++++++++++++++++++----- | |
pitivi/viewer/viewer.py | 2 ++ | |
2 files changed, 21 insertions(+), 5 deletions(-) | |
diff --git a/pitivi/viewer/overlay_stack.py b/pitivi/viewer/overlay_stack.py | |
index 9e8abde..2da6a7f 100644 | |
--- a/pitivi/viewer/overlay_stack.py | |
+++ b/pitivi/viewer/overlay_stack.py | |
@@ -34,6 +34,7 @@ class OverlayStack(Gtk.Overlay, Loggable): | |
Loggable.__init__(self) | |
self.__overlays = {} | |
self.__visible_overlays = [] | |
+ self.__hide_all_overlays = False | |
self.app = app | |
self.window_size = numpy.array([1, 1]) | |
self.click_position = None | |
@@ -122,11 +123,12 @@ class OverlayStack(Gtk.Overlay, Loggable): | |
overlay = self.__overlay_for_source(source) | |
self.__visible_overlays.append(overlay) | |
# check if viewer should be visible | |
- for source, overlay in self.__overlays.items(): | |
- if source in sources: | |
- overlay.show() | |
- else: | |
- overlay.hide() | |
+ if not self.__hide_all_overlays: | |
+ for source, overlay in self.__overlays.items(): | |
+ if source in sources: | |
+ overlay.show() | |
+ else: | |
+ overlay.hide() | |
def update(self, source): | |
self.__overlays[source].update_from_source() | |
@@ -135,6 +137,18 @@ class OverlayStack(Gtk.Overlay, Loggable): | |
self.selected_overlay = self.__overlay_for_source(source) | |
self.selected_overlay.queue_draw() | |
+ def hide_overlays(self): | |
+ if not self.__hide_all_overlays: | |
+ for overlay in self.__visible_overlays: | |
+ overlay.hide() | |
+ self.__hide_all_overlays = True | |
+ | |
+ def show_overlays(self): | |
+ if self.__hide_all_overlays: | |
+ for overlay in self.__visible_overlays: | |
+ overlay.show() | |
+ self.__hide_all_overlays = False | |
+ | |
def set_cursor(self, name): | |
cursor = None | |
display = Gdk.Display.get_default() | |
diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py | |
index 4d3968a..645ca44 100644 | |
--- a/pitivi/viewer/viewer.py | |
+++ b/pitivi/viewer/viewer.py | |
@@ -460,6 +460,7 @@ class ViewerContainer(Gtk.Box, Loggable): | |
self.playpause_button.setPause() | |
self.app.simple_inhibit(ViewerContainer.INHIBIT_REASON, | |
Gtk.ApplicationInhibitFlags.IDLE) | |
+ self.overlay_stack.hide_overlays() | |
else: | |
if state == Gst.State.PAUSED: | |
if old_state != Gst.State.PAUSED: | |
@@ -470,6 +471,7 @@ class ViewerContainer(Gtk.Box, Loggable): | |
self.app.write_action(st) | |
self.playpause_button.setPlay() | |
+ self.overlay_stack.show_overlays() | |
self.app.simple_uninhibit(ViewerContainer.INHIBIT_REASON) | |
-- | |
2.9.3 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment