Skip to content

Instantly share code, notes, and snippets.

@stefanzzz22
Created August 24, 2017 08:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stefanzzz22/2a814a8797b7921e689e8a725bd1d593 to your computer and use it in GitHub Desktop.
Save stefanzzz22/2a814a8797b7921e689e8a725bd1d593 to your computer and use it in GitHub Desktop.
Ken Burns patch compilation
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">&lt;</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">&gt;</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