[pitivi] timeline: Add clip markers
- From: Mathieu Duponchelle <mathieudu src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] timeline: Add clip markers
- Date: Wed, 25 Aug 2021 12:28:29 +0000 (UTC)
commit 474e8e2260188d9886bb7201b379b4a12f17a904
Author: Piotrek BrzeziĆski <thewildtree outlook com>
Date: Sat Aug 7 20:22:12 2021 +0200
timeline: Add clip markers
Uses the existing MarkerBox as a base,
adds the ability to display and manage markers
on each source visible on the timeline,
moves marker-related actions to the TimelineContainer,
as well as extends tests coverage.
data/pixmaps/clip-marker-hover.png | Bin 0 -> 629 bytes
data/pixmaps/clip-marker-select.png | Bin 0 -> 630 bytes
data/pixmaps/clip-marker.png | Bin 0 -> 616 bytes
pitivi/check.py | 2 +-
pitivi/clipproperties.py | 4 +-
pitivi/timeline/elements.py | 86 +++++++++----
pitivi/timeline/markers.py | 235 ++++++++++++++++++++++++------------
pitivi/timeline/timeline.py | 105 +++++++++++++++-
pitivi/utils/ui.py | 44 +++++++
pitivi/utils/widgets.py | 10 +-
tests/test_effects.py | 4 +-
tests/test_timeline_markers.py | 137 +++++++++++++++++++--
12 files changed, 505 insertions(+), 122 deletions(-)
---
diff --git a/data/pixmaps/clip-marker-hover.png b/data/pixmaps/clip-marker-hover.png
new file mode 100644
index 000000000..eb9216a8a
Binary files /dev/null and b/data/pixmaps/clip-marker-hover.png differ
diff --git a/data/pixmaps/clip-marker-select.png b/data/pixmaps/clip-marker-select.png
new file mode 100644
index 000000000..1a36bb9ed
Binary files /dev/null and b/data/pixmaps/clip-marker-select.png differ
diff --git a/data/pixmaps/clip-marker.png b/data/pixmaps/clip-marker.png
new file mode 100644
index 000000000..f061dc8f5
Binary files /dev/null and b/data/pixmaps/clip-marker.png differ
diff --git a/pitivi/check.py b/pitivi/check.py
index 9c429b26d..74abbc0ae 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -426,7 +426,7 @@ def initialize_modules():
# a specific version requirement, they have the "None" value.
GST_API_VERSION = "1.0"
-GST_VERSION = "1.17.90" # FIXME Remove check in proxy.py once we bump to > 1.19
+GST_VERSION = "1.17.90" # FIXME Remove checks in proxy.py and utils/markers.py once we bump to >= 1.19.2
GTK_API_VERSION = "3.0"
GLIB_API_VERSION = "2.0"
HARD_DEPENDENCIES = [GICheck(version_required="3.20.0"),
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 59bb166c9..d847f4e9e 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -1036,12 +1036,12 @@ class TransformationProperties(Gtk.Expander, Loggable):
else:
self._activate_keyframes_btn.set_tooltip_text(
_("Activate keyframes"))
- self.source.ui_element.show_default_keyframes()
+ self.source.ui.show_default_keyframes()
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.show_multiple_keyframes(
+ self.source.ui.show_multiple_keyframes(
list(self.__control_bindings.values()))
def __update_control_bindings(self):
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index ad0c8076a..b6f8c1940 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -35,6 +35,8 @@ from matplotlib.lines import Line2D
from pitivi.configure import get_pixmap_dir
from pitivi.effects import ALLOWED_ONLY_ONCE_EFFECTS
+from pitivi.timeline.markers import ClipMarkersBox
+from pitivi.timeline.markers import Marker
from pitivi.timeline.previewers import AudioPreviewer
from pitivi.timeline.previewers import ImagePreviewer
from pitivi.timeline.previewers import TitlePreviewer
@@ -50,7 +52,7 @@ from pitivi.utils.timeline import Selected
from pitivi.utils.timeline import UNSELECT
from pitivi.utils.timeline import Zoomable
from pitivi.utils.ui import EFFECT_TARGET_ENTRY
-from pitivi.utils.ui import set_children_state_recurse
+from pitivi.utils.ui import set_children_state_except
from pitivi.utils.ui import unset_children_state_recurse
KEYFRAME_LINE_HEIGHT = 2
@@ -654,9 +656,6 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
self.__width = 0
self.__height = 0
- # Needed for effect's keyframe toggling
- self._ges_elem.ui_element = self
-
self.props.vexpand = True
self.__previewer = self._get_previewer()
@@ -667,6 +666,9 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
if self.__background:
self.add(self.__background)
+ self.markers = ClipMarkersBox(self.app, self._ges_elem)
+ self.add(self.markers)
+
self.keyframe_curve = None
self.__controlled_property = None
self.show_all()
@@ -685,22 +687,18 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
if self.__previewer:
self.__previewer.release()
+ if self.markers:
+ self.markers.release()
+
# Public API
def set_size(self, width, height):
width = max(0, width)
self.set_size_request(width, height)
- if self.__previewer:
- self.__previewer.set_size_request(width, height)
-
- if self.__background:
- self.__background.set_size_request(width, height)
-
- if self.keyframe_curve:
- self.keyframe_curve.set_size_request(width, height)
-
- self.__width = width
- self.__height = height
+ if self.__width != width or self.__height != height:
+ self.__width = width
+ self.__height = height
+ self.update_sizes_and_positions()
def show_keyframes(self, ges_elem, prop):
self.__set_keyframes(ges_elem, prop)
@@ -771,7 +769,7 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
self.keyframe_curve.connect("leave", self.__curve_leave_cb)
self.keyframe_curve.set_size_request(self.__width, self.__height)
self.keyframe_curve.show()
- self.__update_keyframe_curve_visibility()
+ self.__update_keyframe_curve()
def __create_control_binding(self, element):
"""Creates the required ControlBinding and keyframes."""
@@ -806,23 +804,47 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
if project.pipeline.get_simple_state() != Gst.State.PLAYING:
self.propagate_draw(self.keyframe_curve, cr)
+ if self.markers and self.markers.is_drawable():
+ self.propagate_draw(self.markers, cr)
+
# Callbacks
def __selected_changed_cb(self, unused_selected, selected):
if not self.keyframe_curve and self.__controlled_property and \
selected and len(self.timeline.selection) == 1:
self.__create_keyframe_curve()
- if self.keyframe_curve:
- self.__update_keyframe_curve_visibility()
-
if self.__previewer:
self.__previewer.set_selected(selected)
- def __update_keyframe_curve_visibility(self):
+ self.update_sizes_and_positions()
+
+ def update_sizes_and_positions(self):
+ markers_height = self.markers.props.height_request
+ width = self.__width
+ height = self.__height
+
+ if self.__background:
+ self.__background.set_size_request(width, height)
+
+ if self.__previewer:
+ self.__previewer.set_size_request(width, height)
+
+ if self.markers:
+ self.markers.set_size_request(width, markers_height)
+
+ # Prevent keyframe curve from overlapping onto markers.
+ if self.keyframe_curve:
+ self.keyframe_curve.set_size_request(self.__width, self.__height - markers_height)
+ self.__update_keyframe_curve()
+
+ def __update_keyframe_curve(self):
"""Updates the keyframes widget visibility by adding or removing it."""
if self._ges_elem.selected and len(self.timeline.selection) == 1:
+ markers_height = self.markers.props.height_request
if not self.keyframe_curve.get_parent():
- self.add(self.keyframe_curve)
+ self.put(self.keyframe_curve, 0, markers_height)
+ else:
+ self.move(self.keyframe_curve, 0, markers_height)
else:
self.remove(self.keyframe_curve)
@@ -1277,6 +1299,15 @@ class Clip(Gtk.EventBox, Zoomable, Loggable):
parent_height != self._current_parent_height or \
layer != self._current_parent:
+ offset_px = self.ns_to_pixel(self.ges_clip.props.in_point)
+
+ for ges_timeline_element in self.ges_clip.get_children(False):
+ if not ges_timeline_element.ui:
+ continue
+
+ if ges_timeline_element.ui.markers:
+ ges_timeline_element.ui.markers.offset = offset_px
+
layer.move(self, x, y)
self.set_size_request(width, height)
@@ -1374,7 +1405,7 @@ class Clip(Gtk.EventBox, Zoomable, Loggable):
if (event.type == Gdk.EventType.ENTER_NOTIFY and
event.mode == Gdk.CrossingMode.NORMAL and
not self.timeline.scrubbing):
- set_children_state_recurse(self, Gtk.StateFlags.PRELIGHT)
+ set_children_state_except(self, Gtk.StateFlags.PRELIGHT, Marker)
for handle in self.handles:
handle.enlarge()
elif (event.type == Gdk.EventType.LEAVE_NOTIFY and
@@ -1440,6 +1471,17 @@ class SourceClip(Clip):
self.get_style_context().add_class("Clip")
+ def _add_child(self, ges_timeline_element):
+ super()._add_child(ges_timeline_element)
+
+ # In some cases a GESEffect is added here,
+ # so we have to limit the markers initialization to GESSources.
+ if not isinstance(ges_timeline_element, GES.Source):
+ return
+
+ if not hasattr(ges_timeline_element, "markers_manager"):
+ ges_timeline_element.markers_manager = MarkerListManager(self.app, ges_timeline_element)
+
def _remove_child(self, ges_timeline_element):
if ges_timeline_element.ui:
self._elements_container.remove(ges_timeline_element.ui)
diff --git a/pitivi/timeline/markers.py b/pitivi/timeline/markers.py
index ae6c0f6dc..024c2a4a4 100644
--- a/pitivi/timeline/markers.py
+++ b/pitivi/timeline/markers.py
@@ -15,26 +15,24 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, see <http://www.gnu.org/licenses/>.
"""Markers display and management."""
-from gettext import gettext as _
from typing import Optional
from gi.repository import Gdk
-from gi.repository import Gio
-from gi.repository import GLib
from gi.repository import Gtk
from pitivi.utils.loggable import Loggable
-from pitivi.utils.pipeline import PipelineError
from pitivi.utils.timeline import Zoomable
from pitivi.utils.ui import SPACING
-MARKER_WIDTH = 10
+TIMELINE_MARKER_SIZE = 10
+CLIP_MARKER_HEIGHT = 12
+CLIP_MARKER_WIDTH = 10
class Marker(Gtk.EventBox, Loggable):
"""Widget representing a marker."""
- def __init__(self, ges_marker):
+ def __init__(self, ges_marker, class_name, width, height):
Gtk.EventBox.__init__(self)
Loggable.__init__(self)
@@ -44,8 +42,10 @@ class Marker(Gtk.EventBox, Loggable):
self.ges_marker = ges_marker
self.ges_marker.ui = self
self.position_ns = self.ges_marker.props.position
+ self.width = width
+ self.height = height
- self.get_style_context().add_class("Marker")
+ self.get_style_context().add_class(class_name)
self.ges_marker.connect("notify-meta", self._notify_meta_cb)
self._selected = False
@@ -54,10 +54,10 @@ class Marker(Gtk.EventBox, Loggable):
return Gtk.SizeRequestMode.CONSTANT_SIZE
def do_get_preferred_height(self):
- return MARKER_WIDTH, MARKER_WIDTH
+ return self.height, self.height
def do_get_preferred_width(self):
- return MARKER_WIDTH, MARKER_WIDTH
+ return self.width, self.width
def do_enter_notify_event(self, unused_event):
self.set_state_flags(Gtk.StateFlags.PRELIGHT, clear=False)
@@ -117,8 +117,8 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
self.props.hexpand = True
self.props.valign = Gtk.Align.START
- self.offset = 0
- self.props.height_request = MARKER_WIDTH
+ self._offset = 0
+ self.props.height_request = TIMELINE_MARKER_SIZE
self.__markers_container = None
self.marker_moving = None
@@ -128,55 +128,16 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK)
- self._create_actions()
-
- def _create_actions(self):
- self.action_group = Gio.SimpleActionGroup()
- self.insert_action_group("markers", self.action_group)
- self.app.shortcuts.register_group("markers", _("Markers"), position=70)
-
- self.add_marker_action = Gio.SimpleAction.new("marker-add", GLib.VariantType("mx"))
- self.add_marker_action.connect("activate", self._add_marker_cb)
- self.action_group.add_action(self.add_marker_action)
- self.app.shortcuts.add("markers.marker-add(@mx nothing)", ["<Primary><Shift>m"],
- self.add_marker_action,
- _("Add a marker"))
-
- self.seek_backward_marker_action = Gio.SimpleAction.new("seek-backward-marker", None)
- self.seek_backward_marker_action.connect("activate", self._seek_backward_marker_cb)
- self.action_group.add_action(self.seek_backward_marker_action)
- self.app.shortcuts.add("markers.seek-backward-marker", ["<Alt>Left"],
- self.seek_backward_marker_action,
- _("Seek to the first marker before the playhead"))
-
- self.seek_forward_marker_action = Gio.SimpleAction.new("seek-forward-marker", None)
- self.seek_forward_marker_action.connect("activate", self._seek_forward_marker_cb)
- self.action_group.add_action(self.seek_forward_marker_action)
- self.app.shortcuts.add("markers.seek-forward-marker", ["<Alt>Right"],
- self.seek_forward_marker_action,
- _("Seek to the first marker after the playhead"))
-
- def _seek_backward_marker_cb(self, action, param):
- current_position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
- position = self.first_marker(before=current_position)
- if position is None:
- return
-
- self.app.project_manager.current_project.pipeline.simple_seek(position)
- self.app.gui.editor.timeline_ui.timeline.scroll_to_playhead(align=Gtk.Align.CENTER,
when_not_in_view=True)
-
- def _seek_forward_marker_cb(self, action, param):
- current_position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
- position = self.first_marker(after=current_position)
- if position is None:
- return
-
- self.app.project_manager.current_project.pipeline.simple_seek(position)
- self.app.gui.editor.timeline_ui.timeline.scroll_to_playhead(align=Gtk.Align.CENTER,
when_not_in_view=True)
-
def first_marker(self, before: Optional[int] = None, after: Optional[int] = None) -> Optional[int]:
+ """Returns position of the closest marker found before or after the given timestamp.
+
+ None is returned if no such marker is found.
+ """
assert (after is not None) != (before is not None)
+ if not self.markers_container:
+ return None
+
if after is not None:
start = after + 1
end = self.app.project_manager.current_project.ges_timeline.props.duration
@@ -188,7 +149,7 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
return None
markers_positions = list([ges_marker.props.position
- for ges_marker in self.__markers_container.get_markers()
+ for ges_marker in self.markers_container.get_markers()
if start <= ges_marker.props.position < end])
if not markers_positions:
return None
@@ -198,19 +159,12 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
else:
return max(markers_positions)
- def _add_marker_cb(self, action, param):
- maybe = param.get_maybe()
- if maybe:
- position = maybe.get_int64()
- else:
- try:
- position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
- except PipelineError:
- self.warning("Could not get pipeline position")
- return
+ def add_at_timeline_time(self, position):
+ """Adds a marker at the given timeline position."""
+ if not self.markers_container:
+ return
- with self.app.action_log.started("Added marker", toplevel=True):
- self.__markers_container.add(position)
+ self.markers_container.add(position)
@property
def markers_container(self):
@@ -223,6 +177,8 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
for marker in self.layout.get_children():
self.layout.remove(marker)
self.__markers_container.disconnect_by_func(self._marker_added_cb)
+ self.__markers_container.disconnect_by_func(self._marker_removed_cb)
+ self.__markers_container.disconnect_by_func(self._marker_moved_cb)
self.__markers_container = ges_markers_container
if self.__markers_container:
@@ -231,6 +187,24 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
self.__markers_container.connect("marker-removed", self._marker_removed_cb)
self.__markers_container.connect("marker-moved", self._marker_moved_cb)
+ def release(self):
+ if self.__markers_container:
+ self.__markers_container.disconnect_by_func(self._marker_added_cb)
+ self.__markers_container.disconnect_by_func(self._marker_removed_cb)
+ self.__markers_container.disconnect_by_func(self._marker_moved_cb)
+
+ @property
+ def offset(self):
+ return self._offset
+
+ @offset.setter
+ def offset(self, value):
+ if self.offset == value:
+ return
+
+ self._offset = value
+ self._update_position()
+
def __create_marker_widgets(self):
markers = self.__markers_container.get_markers()
@@ -241,17 +215,19 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
def _hadj_value_changed_cb(self, hadj):
"""Handles the adjustment value change."""
self.offset = hadj.get_value()
- self._update_position()
def zoom_changed(self):
self._update_position()
def _update_position(self):
for marker in self.layout.get_children():
- position = self.ns_to_pixel(marker.position) - self.offset - MARKER_WIDTH / 2
+ position = self.ns_to_pixel(marker.position) - self.offset - marker.width / 2
self.layout.move(marker, position, 0)
def do_button_press_event(self, event):
+ if not self.markers_container:
+ return False
+
event_widget = Gtk.get_event_widget(event)
button = event.button
if button == Gdk.BUTTON_PRIMARY:
@@ -269,11 +245,15 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
else:
position = self.pixel_to_ns(event.x + self.offset)
- param = GLib.Variant.new_maybe(GLib.VariantType("x"), GLib.Variant.new_int64(position))
- self.add_marker_action.activate(param)
- self.marker_new.selected = True
+ with self.app.action_log.started("Added marker", toplevel=True):
+ self.__markers_container.add(position)
+ self.marker_new.selected = True
+ return True
def do_button_release_event(self, event):
+ if not self.markers_container:
+ return False
+
button = event.button
event_widget = Gtk.get_event_widget(event)
if button == Gdk.BUTTON_PRIMARY:
@@ -281,28 +261,42 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
self.marker_moving.selected = False
self.marker_moving = None
self.app.action_log.commit("Move marker")
+ return True
elif self.marker_new:
self.marker_new.selected = False
self.marker_new = None
+ return True
elif button == Gdk.BUTTON_SECONDARY and isinstance(event_widget, Marker):
with self.app.action_log.started("Removed marker", toplevel=True):
self.__markers_container.remove(event_widget.ges_marker)
+ return True
+
+ return False
def do_motion_notify_event(self, event):
+ if not self.markers_container:
+ return False
+
event_widget = Gtk.get_event_widget(event)
if event_widget is self.marker_moving:
event_x, unused_y = event_widget.translate_coordinates(self, event.x, event.y)
event_x = max(0, event_x)
position_ns = self.pixel_to_ns(event_x + self.offset)
self.__markers_container.move(self.marker_moving.ges_marker, position_ns)
+ return True
+
+ return False
def _marker_added_cb(self, unused_markers, position, ges_marker):
self._add_marker(position, ges_marker)
+ def _create_marker(self, ges_marker):
+ return Marker(ges_marker, "Marker", TIMELINE_MARKER_SIZE, TIMELINE_MARKER_SIZE)
+
def _add_marker(self, position, ges_marker):
- marker = Marker(ges_marker)
- x = self.ns_to_pixel(position) - self.offset - MARKER_WIDTH / 2
+ marker = self._create_marker(ges_marker)
+ x = self.ns_to_pixel(position) - self.offset - marker.width / 2
self.layout.put(marker, x, 0)
marker.show()
self.marker_new = marker
@@ -322,7 +316,7 @@ class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
self._move_marker(position, ges_marker)
def _move_marker(self, position, ges_marker):
- x = self.ns_to_pixel(position) - self.offset - MARKER_WIDTH / 2
+ x = self.ns_to_pixel(position) - self.offset - ges_marker.ui.width / 2
self.layout.move(ges_marker.ui, x, 0)
@@ -359,3 +353,88 @@ class MarkerPopover(Gtk.Popover):
with self.app.action_log.started("marker comment", toplevel=True):
self.marker.comment = buffer.props.text
self.marker.selected = False
+
+
+class ClipMarkersBox(MarkersBox):
+ def __init__(self, app, ges_elem, hadj=None):
+ super().__init__(app, hadj=hadj)
+ self.ges_elem = ges_elem
+ # ges_elem is a GESSource, but we need a GESClip to convert timestamps
+ self.ges_clip = self.ges_elem.get_parent()
+ # Initially hide the box - only show once a marker container is set
+ self.props.height_request = 0
+ self.get_style_context().add_class("ClipMarkersBox")
+
+ def _create_marker(self, ges_marker):
+ return Marker(ges_marker, "ClipMarker", CLIP_MARKER_WIDTH, CLIP_MARKER_HEIGHT)
+
+ def __internal_to_timeline(self, timestamp):
+ return self.ges_clip.get_timeline_time_from_internal_time(self.ges_elem, timestamp)
+
+ def __timeline_to_internal(self, timestamp):
+ return self.ges_clip.get_internal_time_from_timeline_time(self.ges_elem, timestamp)
+
+ def first_marker(self, before: Optional[int] = None, after: Optional[int] = None) -> Optional[int]:
+ assert (after is not None) != (before is not None)
+
+ if not self.markers_container:
+ return None
+
+ # Limit search to visible markers
+ clip_start = self.ges_clip.props.start
+ clip_end = self.ges_clip.props.start + self.ges_clip.props.duration
+
+ if after is not None:
+ start = max(after + 1, clip_start)
+ end = clip_end
+ else:
+ start = clip_start
+ end = min(before, clip_end)
+
+ if start >= end:
+ return None
+
+ markers_positions = [self.__internal_to_timeline(marker.props.position)
+ for marker in self.markers_container.get_markers()
+ if start <= self.__internal_to_timeline(marker.props.position) < end]
+ if not markers_positions:
+ return None
+
+ if after is not None:
+ return min(markers_positions)
+ else:
+ return max(markers_positions)
+
+ def add_at_timeline_time(self, position):
+ if not self.markers_container:
+ return
+
+ start = self.ges_clip.props.start
+ inpoint = self.ges_clip.props.in_point
+ duration = self.ges_clip.props.duration
+
+ # Prevent timestamp conversion failing due to negative result.
+ if position < start:
+ return
+
+ internal_end = self.__timeline_to_internal(start + duration)
+ timestamp = self.__timeline_to_internal(position)
+
+ # Check if marker would land in the 'visible' part of the clip.
+ if not inpoint <= timestamp <= internal_end:
+ return
+
+ self.markers_container.add(timestamp)
+
+ @MarkersBox.markers_container.setter
+ def markers_container(self, ges_markers_container):
+ MarkersBox.markers_container.fset(self, ges_markers_container)
+
+ # Hide the box when no list is selected.
+ height = CLIP_MARKER_HEIGHT if ges_markers_container else 0
+ self.props.height_request = height
+ # Let the parent TimelineElement know to update sizes accordingly.
+ parent_el = self.get_parent()
+ if parent_el is not None:
+ parent_el.update_sizes_and_positions()
+ parent_el.queue_draw()
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index b92845339..d94dc118f 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -45,7 +45,9 @@ from pitivi.timeline.previewers import Previewer
from pitivi.timeline.ruler import TimelineScaleRuler
from pitivi.undo.timeline import CommitTimelineFinalizingAction
from pitivi.utils.loggable import Loggable
+from pitivi.utils.markers import GES_MARKERS_SNAPPABLE
from pitivi.utils.misc import asset_get_duration
+from pitivi.utils.pipeline import PipelineError
from pitivi.utils.timeline import EditingContext
from pitivi.utils.timeline import SELECT
from pitivi.utils.timeline import Selection
@@ -72,6 +74,19 @@ from pitivi.utils.widgets import ZoomBox
SEPARATOR_ACCEPTING_DROP_INTERVAL_MS = 1000
+GlobalSettings.add_config_option('markersSnappableByDefault',
+ section="user-interface",
+ key="markers-snappable-default",
+ default=False,
+ notify=False)
+
+if GES_MARKERS_SNAPPABLE:
+ PreferencesDialog.add_toggle_preference('markersSnappableByDefault',
+ section="timeline",
+ label=_("Markers magnetic by default"),
+ description=_(
+ "Whether markers created on new clips will be snapping
targets by default."))
+
GlobalSettings.add_config_option('edgeSnapDeadband',
section="user-interface",
key="edge-snap-deadband",
@@ -1909,7 +1924,26 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
_("Seek forward one second"))
# Markers actions.
- self.timeline.layout.insert_action_group("markers", self.markers.action_group)
+ self.add_marker_action = Gio.SimpleAction.new("marker-add", None)
+ self.add_marker_action.connect("activate", self._add_marker_cb)
+ navigation_group.add_action(self.add_marker_action)
+ self.app.shortcuts.add("navigation.marker-add", ["<Primary><Shift>m"],
+ self.add_marker_action,
+ _("Add a marker"))
+
+ self.seek_backward_marker_action = Gio.SimpleAction.new("seek-backward-marker", None)
+ self.seek_backward_marker_action.connect("activate", self._seek_backward_marker_cb)
+ navigation_group.add_action(self.seek_backward_marker_action)
+ self.app.shortcuts.add("navigation.seek-backward-marker", ["<Alt>Left"],
+ self.seek_backward_marker_action,
+ _("Seek to the first marker before the playhead"))
+
+ self.seek_forward_marker_action = Gio.SimpleAction.new("seek-forward-marker", None)
+ self.seek_forward_marker_action.connect("activate", self._seek_forward_marker_cb)
+ navigation_group.add_action(self.seek_forward_marker_action)
+ self.app.shortcuts.add("navigation.seek-forward-marker", ["<Alt>Right"],
+ self.seek_forward_marker_action,
+ _("Seek to the first marker after the playhead"))
# Viewer actions.
self.timeline.layout.insert_action_group("viewer", self.app.gui.editor.viewer.action_group)
@@ -2320,3 +2354,72 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
win.set_transient_for(self.app.gui)
win.show_all()
+
+ def __get_current_marker_boxes(self):
+ # Return a list of the selected elements' marker boxes
+ sources = self.timeline.selection.get_selected_track_elements()
+ if sources:
+ return [source.ui.markers for source in sources]
+
+ # Else focus on timeline markers
+ return [self.markers]
+
+ def __find_closest_marker(self, containers, before=None, after=None):
+ if not containers:
+ return None
+
+ position = before if before else after
+ timestamps = []
+ for container in containers:
+ timestamp = container.first_marker(before, after)
+ if timestamp is not None:
+ timestamps.append(timestamp)
+
+ if not timestamps:
+ return None
+
+ closest_timestamp = min(timestamps, key=lambda timestamp: abs(position - timestamp))
+ return closest_timestamp
+
+ def _add_marker_cb(self, action, param):
+ try:
+ position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
+ except PipelineError:
+ self.warning("Could not get pipeline position")
+ return
+
+ containers = self.__get_current_marker_boxes()
+
+ with self.app.action_log.started("Added marker", toplevel=True):
+ for marker_container in containers:
+ marker_container.add_at_timeline_time(position)
+
+ def _seek_backward_marker_cb(self, action, param):
+ try:
+ timeline_position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
+ except PipelineError:
+ self.warning("Could not get pipeline position")
+ return
+
+ containers = self.__get_current_marker_boxes()
+ position = self.__find_closest_marker(containers, before=timeline_position)
+ if position is None:
+ return
+
+ self.app.project_manager.current_project.pipeline.simple_seek(position)
+ self.app.gui.editor.timeline_ui.timeline.scroll_to_playhead(align=Gtk.Align.CENTER,
when_not_in_view=True)
+
+ def _seek_forward_marker_cb(self, action, param):
+ try:
+ timeline_position = self.app.project_manager.current_project.pipeline.get_position(fails=False)
+ except PipelineError:
+ self.warning("Could not get pipeline position")
+ return
+
+ containers = self.__get_current_marker_boxes()
+ position = self.__find_closest_marker(containers, after=timeline_position)
+ if position is None:
+ return
+
+ self.app.project_manager.current_project.pipeline.simple_seek(position)
+ self.app.gui.editor.timeline_ui.timeline.scroll_to_playhead(align=Gtk.Align.CENTER,
when_not_in_view=True)
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 129b31d90..56094139e 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -294,10 +294,40 @@ EDITOR_PERSPECTIVE_CSS = """
background-image: url('%(marker_hovered)s');
}
+ .ClipMarkersBox {
+ transition: 0.15s ease-out;
+ opacity: 0.7;
+ }
+
+ .ClipMarkersBox:hover {
+ background-color: rgba(0, 0, 0, 0.15);
+ opacity: 0.85;
+ }
+
+ .ClipMarkersBox:selected {
+ background-color: rgb(0, 0, 0);
+ opacity: 1;
+ }
+
+ .ClipMarker {
+ background-image: url('%(clip_marker_unselected)s');
+ }
+
+ .ClipMarker:hover {
+ background-image: url('%(clip_marker_hovered)s');
+ }
+
+ .ClipMarker:selected {
+ background-image: url('%(clip_marker_selected)s');
+ }
+
""" % ({
'clip_border_width': CLIP_BORDER_WIDTH,
'marker_hovered': os.path.join(get_pixmap_dir(), "marker-hover.png"),
'marker_unselected': os.path.join(get_pixmap_dir(), "marker-unselect.png"),
+ 'clip_marker_unselected': os.path.join(get_pixmap_dir(), "clip-marker.png"),
+ 'clip_marker_hovered': os.path.join(get_pixmap_dir(), "clip-marker-hover.png"),
+ 'clip_marker_selected': os.path.join(get_pixmap_dir(), "clip-marker-select.png"),
'trimbar_focused': os.path.join(get_pixmap_dir(), "trimbar-focused.png"),
'trimbar_normal': os.path.join(get_pixmap_dir(), "trimbar-normal.png")})
@@ -840,6 +870,7 @@ def alter_style_class(style_class, target_widget, css_style):
def set_children_state_recurse(widget, state):
+ """Sets the provided state on all children of the given widget."""
widget.set_state_flags(state, False)
for child in widget.get_children():
child.set_state_flags(state, False)
@@ -847,7 +878,20 @@ def set_children_state_recurse(widget, state):
set_children_state_recurse(child, state)
+def set_children_state_except(widget, state, *ignored_types):
+ """Sets the provided state on all children of the widget, except those of given types."""
+ widget.set_state_flags(state, False)
+ for child in widget.get_children():
+ if any(isinstance(child, klass) for klass in ignored_types):
+ continue
+
+ child.set_state_flags(state, False)
+ if isinstance(child, Gtk.Container):
+ set_children_state_except(child, state, ignored_types)
+
+
def unset_children_state_recurse(widget, state):
+ """Unsets the provided state on all children of the given widget."""
widget.unset_state_flags(state)
for child in widget.get_children():
child.unset_state_flags(state)
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index 7aec460dd..9a92d1453 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -1019,11 +1019,11 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
return
if active:
- track_element.ui_element.show_keyframes(self.element, prop)
+ track_element.ui.show_keyframes(self.element, prop)
binding = self.element.get_control_binding(prop.name)
self.__bindings_by_keyframe_button[keyframe_button] = binding
else:
- track_element.ui_element.show_default_keyframes()
+ track_element.ui.show_default_keyframes()
def __reset_to_default_clicked_cb(self, unused_button, widget,
keyframe_button=None):
@@ -1037,7 +1037,7 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
track_element = self.__get_track_element_of_same_type(
self.element)
if track_element:
- track_element.ui_element.show_default_keyframes()
+ track_element.ui.show_default_keyframes()
self.__set_keyframe_active(keyframe_button, False)
self.__display_controlled(keyframe_button, False)
@@ -1046,8 +1046,8 @@ class GstElementSettingsWidget(Gtk.Box, Loggable):
def __get_track_element_of_same_type(self, effect):
track_type = effect.get_track_type()
for track_element in effect.get_parent().get_children(False):
- if hasattr(track_element, "ui_element") and \
- track_element.get_track_type() == track_type:
+ if hasattr(track_element, "ui") and \
+ track_element.get_track_type() == track_type and track_element != effect:
return track_element
self.warning("Failed to find track element of type %s", track_type)
return None
diff --git a/tests/test_effects.py b/tests/test_effects.py
index 72dba9955..a27c464a8 100644
--- a/tests/test_effects.py
+++ b/tests/test_effects.py
@@ -144,10 +144,10 @@ class EffectsPropertiesManagerTest(common.TestCase):
# Control the self.prop property on the timeline
prop_keyframe_button.set_active(True)
- self.assertEqual(track_element.ui_element._TimelineElement__controlled_property, self.prop)
+ self.assertEqual(track_element.ui._TimelineElement__controlled_property, self.prop)
# Revert to controlling the default property
prop_keyframe_button.set_active(False)
- self.assertNotEqual(track_element.ui_element._TimelineElement__controlled_property, self.prop)
+ self.assertNotEqual(track_element.ui._TimelineElement__controlled_property, self.prop)
def test_prop_reset(self):
"""Checks the reset button resets the property."""
diff --git a/tests/test_timeline_markers.py b/tests/test_timeline_markers.py
index eb65451b5..87d3ef95d 100644
--- a/tests/test_timeline_markers.py
+++ b/tests/test_timeline_markers.py
@@ -19,6 +19,7 @@
from unittest import mock
from gi.repository import Gdk
+from gi.repository import GES
from gi.repository import Gtk
from pitivi.utils.timeline import Zoomable
@@ -173,14 +174,128 @@ class TestMarkers(common.TestCase):
marker_box.markers_container.add(12)
self.assert_markers(markers, [(10, None), (12, None)])
- self.check_seek(marker_box.seek_forward_marker_action, 9, 10)
- self.check_seek(marker_box.seek_forward_marker_action, 10, 12)
- self.check_seek(marker_box.seek_forward_marker_action, 11, 12)
- self.check_seek(marker_box.seek_forward_marker_action, 12, None)
- self.check_seek(marker_box.seek_forward_marker_action, 13, None)
-
- self.check_seek(marker_box.seek_backward_marker_action, 9, None)
- self.check_seek(marker_box.seek_backward_marker_action, 10, None)
- self.check_seek(marker_box.seek_backward_marker_action, 11, 10)
- self.check_seek(marker_box.seek_backward_marker_action, 12, 10)
- self.check_seek(marker_box.seek_backward_marker_action, 13, 12)
+ self.check_seek(self.timeline_container.seek_forward_marker_action, 9, 10)
+ self.check_seek(self.timeline_container.seek_forward_marker_action, 10, 12)
+ self.check_seek(self.timeline_container.seek_forward_marker_action, 11, 12)
+ self.check_seek(self.timeline_container.seek_forward_marker_action, 12, None)
+ self.check_seek(self.timeline_container.seek_forward_marker_action, 13, None)
+
+ self.check_seek(self.timeline_container.seek_backward_marker_action, 9, None)
+ self.check_seek(self.timeline_container.seek_backward_marker_action, 10, None)
+ self.check_seek(self.timeline_container.seek_backward_marker_action, 11, 10)
+ self.check_seek(self.timeline_container.seek_backward_marker_action, 12, 10)
+ self.check_seek(self.timeline_container.seek_backward_marker_action, 13, 12)
+
+ @common.setup_timeline
+ def test_seeking_with_clips(self):
+ """Checks the seeking actions with clip markers present."""
+ self.timeline.append_layer()
+ timeline = self.timeline_container.timeline
+ clip1 = self.add_clip(self.timeline.layers[0], start=0, duration=30)
+ clip2 = self.add_clip(self.timeline.layers[1], start=10, duration=20)
+
+ markers1 = next(common.get_clip_children(
+ clip1, GES.TrackType.VIDEO)).ui.markers.markers_container
+ markers2 = next(common.get_clip_children(
+ clip2, GES.TrackType.VIDEO)).ui.markers.markers_container
+
+ markers1.add(5)
+ markers1.add(25)
+ markers2.add(5)
+ markers2.add(15)
+
+ forward_seek = self.timeline_container.seek_forward_marker_action
+ backward_seek = self.timeline_container.seek_backward_marker_action
+
+ timeline.selection.select([clip1])
+ self.check_seek(forward_seek, 0, 5)
+ self.check_seek(forward_seek, 5, 25)
+ self.check_seek(forward_seek, 25, None)
+
+ timeline.selection.select([clip2])
+ self.check_seek(forward_seek, 25, None)
+ self.check_seek(backward_seek, 25, 15)
+ self.check_seek(backward_seek, 15, None)
+
+ timeline.selection.select([])
+ self.check_seek(forward_seek, 15, None)
+ self.check_seek(backward_seek, 15, None)
+
+ # When multiple clips are selected, take all their markers into consideration.
+ timeline.selection.select([clip1, clip2])
+ self.check_seek(forward_seek, 15, 25)
+ self.check_seek(backward_seek, 25, 15)
+ self.check_seek(backward_seek, 15, 5)
+
+ # Trim first 10 seconds of clip2, 'cutting off' its first marker.
+ clip2.trim(20)
+ self.check_seek(forward_seek, 5, 25)
+
+ # Add a marker "outside" clip1 and check if it's correctly ignored.
+ markers1.add(50)
+ self.check_seek(forward_seek, 25, None)
+
+ # Add a timeline marker which should be ignored while clips are selected.
+ timeline_markers = self.timeline_container.markers.markers_container
+ timeline_markers.add(28)
+ self.check_seek(forward_seek, 25, None)
+
+ # Unselect clips and seek again.
+ timeline.selection.select([])
+ self.check_seek(forward_seek, 25, 28)
+
+ def perform_at_timeline_position(self, action, position):
+ pipeline = self.project.pipeline
+ with mock.patch.object(pipeline, "get_position") as get_position:
+ get_position.return_value = position
+ action.activate()
+
+ @common.setup_timeline
+ def test_add_marker_action(self):
+ """Checks marker adding shortcut behaviour."""
+ self.timeline.append_layer()
+ timeline = self.timeline_container.timeline
+ add_action = self.timeline_container.add_marker_action
+ clip1 = self.add_clip(self.timeline.layers[0], start=0, duration=20)
+ clip2 = self.add_clip(self.timeline.layers[1], start=10, duration=20)
+
+ timeline_markers = self.timeline_container.markers.markers_container
+ markers1 = next(common.get_clip_children(
+ clip1, GES.TrackType.VIDEO)).ui.markers.markers_container
+ markers2 = next(common.get_clip_children(
+ clip2, GES.TrackType.VIDEO)).ui.markers.markers_container
+
+ # No clips selected - should add a marker to the timeline.
+ self.perform_at_timeline_position(add_action, 15)
+ self.assert_markers(timeline_markers, [(15, None)])
+
+ # Multiple clips selected - add marker to all of them.
+ timeline.selection.select([clip1, clip2])
+ self.perform_at_timeline_position(add_action, 15)
+ self.assert_markers(markers1, [(15, None)])
+ self.assert_markers(markers2, [(5, None)])
+
+ # Adding a marker 'outside' of one clip should fail, but still add to other selected clips.
+ self.perform_at_timeline_position(add_action, 5)
+ self.assert_markers(markers1, [(5, None), (15, None)])
+ self.assert_markers(markers2, [(5, None)])
+
+ self.perform_at_timeline_position(add_action, 25)
+ self.assert_markers(markers1, [(5, None), (15, None)])
+ self.assert_markers(markers2, [(5, None), (15, None)])
+
+ # Make sure nothing was added to the timeline.
+ self.assert_markers(timeline_markers, [(15, None)])
+
+ # One clip selected - make sure no other clips are affected.
+ timeline.selection.select([clip1])
+ self.perform_at_timeline_position(add_action, 10)
+ self.assert_markers(markers1, [(5, None), (10, None), (15, None)])
+ self.assert_markers(markers2, [(5, None), (15, None)])
+
+ timeline.selection.select([clip2])
+ self.perform_at_timeline_position(add_action, 20)
+ self.assert_markers(markers1, [(5, None), (10, None), (15, None)])
+ self.assert_markers(markers2, [(5, None), (10, None), (15, None)])
+
+ self.assert_markers(timeline_markers, [(15, None)])
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]