[pitivi] timeline: Add a markers bar above the ruler
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] timeline: Add a markers bar above the ruler
- Date: Fri, 30 Aug 2019 06:23:53 +0000 (UTC)
commit 4fc254539f261cb1928db18327a7334220e303f3
Author: Millan Castro <m castrovilarino gmail com>
Date: Fri Jun 14 09:08:48 2019 +0200
timeline: Add a markers bar above the ruler
Fixes #739
data/pixmaps/marker-hover.png | Bin 0 -> 1003 bytes
data/pixmaps/marker-unselect.png | Bin 0 -> 1016 bytes
data/pixmaps/marker.png | Bin 0 -> 1003 bytes
pitivi/project.py | 3 +
pitivi/timeline/markers.py | 276 +++++++++++++++++++++++++++++++++++++++
pitivi/timeline/timeline.py | 18 ++-
pitivi/undo/markers.py | 152 +++++++++++++++++++++
pitivi/undo/timeline.py | 4 +-
pitivi/undo/undo.py | 23 +++-
pitivi/utils/ui.py | 21 ++-
po/POTFILES.in | 1 +
tests/common.py | 12 ++
tests/test_project.py | 6 +
tests/test_timeline_markers.py | 156 ++++++++++++++++++++++
tests/test_undo_markers.py | 238 +++++++++++++++++++++++++++++++++
15 files changed, 895 insertions(+), 15 deletions(-)
---
diff --git a/data/pixmaps/marker-hover.png b/data/pixmaps/marker-hover.png
new file mode 100644
index 00000000..e9e59165
Binary files /dev/null and b/data/pixmaps/marker-hover.png differ
diff --git a/data/pixmaps/marker-unselect.png b/data/pixmaps/marker-unselect.png
new file mode 100644
index 00000000..60e827ed
Binary files /dev/null and b/data/pixmaps/marker-unselect.png differ
diff --git a/data/pixmaps/marker.png b/data/pixmaps/marker.png
new file mode 100644
index 00000000..e9e59165
Binary files /dev/null and b/data/pixmaps/marker.png differ
diff --git a/pitivi/project.py b/pitivi/project.py
index e6b7993f..b7da243a 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -1580,6 +1580,9 @@ class Project(Loggable, GES.Project):
self.warning("Failed to set the pipeline's timeline: %s", self.ges_timeline)
return False
+ if self.ges_timeline.get_marker_list("markers") is None:
+ self.ges_timeline.set_marker_list("markers", GES.MarkerList.new())
+
return True
def update_restriction_caps(self):
diff --git a/pitivi/timeline/markers.py b/pitivi/timeline/markers.py
new file mode 100644
index 00000000..0d22003b
--- /dev/null
+++ b/pitivi/timeline/markers.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2019, Millan Castro <m castrovilarino gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Markers display and management."""
+from gi.repository import Gdk
+from gi.repository import Gtk
+
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.timeline import Zoomable
+from pitivi.utils.ui import SPACING
+
+MARKER_WIDTH = 10
+
+
+# pylint: disable=too-many-instance-attributes
+class Marker(Gtk.EventBox, Loggable):
+ """Widget representing a marker"""
+
+ def __init__(self, ges_marker):
+ Gtk.EventBox.__init__(self)
+ Loggable.__init__(self)
+
+ self.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK |
+ Gdk.EventMask.LEAVE_NOTIFY_MASK)
+
+ self.ges_marker = ges_marker
+ self.ges_marker.ui = self
+ self.position_ns = self.ges_marker.props.position
+
+ self.get_style_context().add_class("Marker")
+ self.ges_marker.connect("notify-meta", self._notify_meta_cb)
+
+ self._selected = False
+
+ # pylint: disable=arguments-differ
+ def do_get_request_mode(self):
+ return Gtk.SizeRequestMode.CONSTANT_SIZE
+
+ def do_get_preferred_height(self):
+ return MARKER_WIDTH, MARKER_WIDTH
+
+ def do_get_preferred_width(self):
+ return MARKER_WIDTH, MARKER_WIDTH
+
+ def do_enter_notify_event(self, unused_event):
+ self.set_state_flags(Gtk.StateFlags.PRELIGHT, clear=False)
+
+ def do_leave_notify_event(self, unused_event):
+ self.unset_state_flags(Gtk.StateFlags.PRELIGHT)
+
+ def _notify_meta_cb(self, unused_ges_marker, item, value):
+ self.set_tooltip_text(self.comment)
+
+ @property
+ def position(self):
+ """Returns the position of the marker, in nanoseconds."""
+ return self.ges_marker.props.position
+
+ @property
+ def comment(self):
+ """Returns a comment from ges_marker."""
+ return self.ges_marker.get_string("comment")
+
+ @comment.setter
+ def comment(self, text):
+ if text == self.comment:
+ return
+ self.ges_marker.set_string("comment", text)
+
+ @property
+ def selected(self):
+ """Returns whether the marker is selected."""
+ return self._selected
+
+ @selected.setter
+ def selected(self, selected):
+ self._selected = selected
+ if self._selected:
+ self.set_state_flags(Gtk.StateFlags.SELECTED, clear=False)
+ else:
+ self.unset_state_flags(Gtk.StateFlags.SELECTED)
+
+
+class MarkersBox(Gtk.EventBox, Zoomable, Loggable):
+ """Container for displaying and managing markers."""
+
+ def __init__(self, app, hadj=None):
+ Gtk.EventBox.__init__(self)
+ Zoomable.__init__(self)
+ Loggable.__init__(self)
+
+ self.layout = Gtk.Layout()
+ self.add(self.layout)
+ self.get_style_context().add_class("MarkersBox")
+
+ self.app = app
+
+ if hadj:
+ hadj.connect("value-changed", self._hadj_value_changed_cb)
+ self.props.hexpand = True
+ self.props.valign = Gtk.Align.START
+
+ self.offset = 0
+ self.props.height_request = MARKER_WIDTH
+
+ self.__markers_container = None
+ self.marker_moving = None
+ self.marker_new = None
+
+ self.add_events(Gdk.EventMask.POINTER_MOTION_MASK |
+ Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK)
+
+ @property
+ def markers_container(self):
+ """Gets the GESMarkerContainer."""
+ return self.__markers_container
+
+ @markers_container.setter
+ def markers_container(self, ges_markers_container):
+ if self.__markers_container:
+ for marker in self.layout.get_children():
+ self.layout.remove(marker)
+ self.__markers_container.disconnect_by_func(self._marker_added_cb)
+
+ self.__markers_container = ges_markers_container
+ if self.__markers_container:
+ self.__create_marker_widgets()
+ self.__markers_container.connect("marker-added", self._marker_added_cb)
+ self.__markers_container.connect("marker-removed", self._marker_removed_cb)
+ self.__markers_container.connect("marker-moved", self._marker_moved_cb)
+
+ def __create_marker_widgets(self):
+ markers = self.__markers_container.get_markers()
+
+ for ges_marker in markers:
+ position = ges_marker.props.position
+ self._add_marker(position, ges_marker)
+
+ def _hadj_value_changed_cb(self, hadj):
+ """Handles the adjustment value change."""
+ self.offset = hadj.get_value()
+ self._update_position()
+
+ def zoomChanged(self):
+ self._update_position()
+
+ def _update_position(self):
+ for marker in self.layout.get_children():
+ position = self.nsToPixel(marker.position) - self.offset - MARKER_WIDTH / 2
+ self.layout.move(marker, position, 0)
+
+ # pylint: disable=arguments-differ
+ def do_button_press_event(self, event):
+ event_widget = Gtk.get_event_widget(event)
+ button = event.button
+ if button == Gdk.BUTTON_PRIMARY:
+ if isinstance(event_widget, Marker):
+ if event.type == Gdk.EventType.BUTTON_PRESS:
+ self.marker_moving = event_widget
+ self.marker_moving.selected = True
+ self.app.action_log.begin("Move marker", toplevel=True)
+
+ elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
+ self.marker_moving = None
+ self.app.action_log.rollback()
+ marker_popover = MarkerPopover(self.app, event_widget)
+ marker_popover.popup()
+
+ else:
+ position = self.pixelToNs(event.x + self.offset)
+ with self.app.action_log.started("Added marker", toplevel=True):
+ self.__markers_container.add(position)
+ self.marker_new.selected = True
+
+ def do_button_release_event(self, event):
+ button = event.button
+ event_widget = Gtk.get_event_widget(event)
+ if button == Gdk.BUTTON_PRIMARY:
+ if self.marker_moving:
+ self.marker_moving.selected = False
+ self.marker_moving = None
+ self.app.action_log.commit("Move marker")
+ elif self.marker_new:
+ self.marker_new.selected = False
+ self.marker_new = None
+
+ 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)
+
+ def do_motion_notify_event(self, event):
+ 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.pixelToNs(event_x + self.offset)
+ self.__markers_container.move(self.marker_moving.ges_marker, position_ns)
+
+ def _marker_added_cb(self, unused_markers, position, ges_marker):
+ self._add_marker(position, ges_marker)
+
+ def _add_marker(self, position, ges_marker):
+ marker = Marker(ges_marker)
+ x = self.nsToPixel(position) - self.offset - MARKER_WIDTH / 2
+ self.layout.put(marker, x, 0)
+ marker.show()
+ self.marker_new = marker
+
+ def _marker_removed_cb(self, unused_markers, ges_marker):
+ self._remove_marker(ges_marker)
+
+ def _remove_marker(self, ges_marker):
+ if not ges_marker.ui:
+ return
+
+ self.layout.remove(ges_marker.ui)
+ ges_marker.ui = None
+
+ def _marker_moved_cb(self, unused_markers, position, ges_marker):
+ self._move_marker(position, ges_marker)
+
+ def _move_marker(self, position, ges_marker):
+ x = self.nsToPixel(position) - self.offset - MARKER_WIDTH / 2
+ self.layout.move(ges_marker.ui, x, 0)
+
+
+class MarkerPopover(Gtk.Popover):
+ """A popover to edit a marker's metadata."""
+
+ def __init__(self, app, marker):
+ Gtk.Popover.__init__(self)
+
+ self.app = app
+
+ self.text_view = Gtk.TextView()
+ self.text_view.set_size_request(100, -1)
+
+ self.marker = marker
+
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ vbox.props.margin = SPACING
+ vbox.pack_start(self.text_view, False, True, 0)
+ self.add(vbox)
+
+ text = self.marker.comment
+ if text:
+ text_buffer = self.text_view.get_buffer()
+ text_buffer.set_text(text)
+
+ self.set_position(Gtk.PositionType.TOP)
+ self.set_relative_to(self.marker)
+ self.show_all()
+
+ # pylint: disable=arguments-differ
+ def do_closed(self):
+ buffer = self.text_view.get_buffer()
+ if buffer.props.text != self.marker.comment:
+ with self.app.action_log.started("marker comment", toplevel=True):
+ self.marker.comment = buffer.props.text
+ self.marker.selected = False
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 7ee765c5..01e0d47b 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -38,6 +38,7 @@ from pitivi.timeline.elements import TrimHandle
from pitivi.timeline.layer import Layer
from pitivi.timeline.layer import LayerControls
from pitivi.timeline.layer import SpacedSeparator
+from pitivi.timeline.markers import MarkersBox
from pitivi.timeline.previewers import Previewer
from pitivi.timeline.ruler import ScaleRuler
from pitivi.undo.timeline import CommitTimelineFinalizingAction
@@ -1539,6 +1540,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
pass # We were not connected no problem
self.timeline._pipeline = None
+ self.markers.markers_container = None
self._project = project
@@ -1558,6 +1560,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.timeline.set_best_zoom_ratio(allow_zoom_in=True)
self.timeline.update_snapping_distance()
+ self.markers.markers_container = project.ges_timeline.get_marker_list("markers")
def updateActions(self):
selection = self.timeline.selection
@@ -1607,12 +1610,15 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.gapless_button = builder.get_object("gapless_button")
self.gapless_button.set_active(self._settings.timelineAutoRipple)
- self.attach(zoom_box, 0, 0, 1, 1)
- self.attach(self.ruler, 1, 0, 1, 1)
- self.attach(self.timeline, 0, 1, 2, 1)
- self.attach(self.vscrollbar, 2, 1, 1, 1)
- self.attach(hscrollbar, 1, 2, 1, 1)
- self.attach(self.toolbar, 3, 1, 1, 1)
+ self.markers = MarkersBox(self.app, hadj=self.timeline.hadj)
+
+ self.attach(self.markers, 1, 0, 1, 1)
+ self.attach(zoom_box, 0, 1, 1, 1)
+ self.attach(self.ruler, 1, 1, 1, 1)
+ self.attach(self.timeline, 0, 2, 2, 1)
+ self.attach(self.vscrollbar, 2, 2, 1, 1)
+ self.attach(hscrollbar, 1, 3, 1, 1)
+ self.attach(self.toolbar, 3, 2, 1, 1)
self.set_margin_top(SPACING)
diff --git a/pitivi/undo/markers.py b/pitivi/undo/markers.py
new file mode 100644
index 00000000..0ec7b55b
--- /dev/null
+++ b/pitivi/undo/markers.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2019, Millan Castro <m castrovilarino gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Undo/redo markers"""
+from gi.repository import Gst
+
+from pitivi.undo.undo import MetaContainerObserver
+from pitivi.undo.undo import UndoableAutomaticObjectAction
+from pitivi.utils.loggable import Loggable
+
+
+class MarkerListObserver(Loggable):
+ """Monitors a MarkerList and reports UndoableActions.
+
+ Args:
+ ges_marker_list (GES.MarkerList): The markerlist to observe.
+
+ Attributes:
+ action_log (UndoableActionLog): The action log where to report actions.
+ """
+
+ def __init__(self, ges_marker_list, action_log):
+ Loggable.__init__(self)
+
+ self.action_log = action_log
+
+ self.markers_position = {}
+ self.marker_observers = {}
+
+ ges_marker_list.connect("marker-added", self._marker_added_cb)
+ ges_marker_list.connect("marker-removed", self._marker_removed_cb)
+ ges_marker_list.connect("marker-moved", self._marker_moved_cb)
+
+ ges_markers = ges_marker_list.get_markers()
+ for ges_marker in ges_markers:
+ self._connect(ges_marker)
+
+ def _connect(self, ges_marker):
+ marker_observer = MetaContainerObserver(ges_marker, self.action_log)
+ self.marker_observers[ges_marker] = marker_observer
+ self.markers_position[ges_marker] = ges_marker.props.position
+
+ def _marker_added_cb(self, ges_marker_list, position, ges_marker):
+ action = MarkerAdded(ges_marker_list, ges_marker)
+ self.action_log.push(action)
+ self._connect(ges_marker)
+
+ def _marker_removed_cb(self, ges_marker_list, ges_marker):
+ action = MarkerRemoved(ges_marker_list, ges_marker)
+ self.action_log.push(action)
+ marker_observer = self.marker_observers.pop(ges_marker)
+ marker_observer.release()
+ del self.markers_position[ges_marker]
+
+ def _marker_moved_cb(self, ges_marker_list, position, ges_marker):
+ old_position = self.markers_position[ges_marker]
+ action = MarkerMoved(ges_marker_list, ges_marker, old_position)
+ self.action_log.push(action)
+ self.markers_position[ges_marker] = ges_marker.props.position
+
+
+# pylint: disable=abstract-method, too-many-ancestors
+class MarkerAction(UndoableAutomaticObjectAction):
+ """Base class for add and remove marker actions"""
+
+ def __init__(self, ges_marker_list, ges_marker):
+ UndoableAutomaticObjectAction.__init__(self, ges_marker)
+ self.ges_marker_list = ges_marker_list
+ self.position = ges_marker.props.position
+ self.ges_marker = ges_marker
+
+ def add(self):
+ """Adds a marker and updates the auto-object."""
+ ges_marker = self.ges_marker_list.add(self.position)
+ comment = self.auto_object.get_string("comment")
+ if comment:
+ ges_marker.set_string("comment", comment)
+ UndoableAutomaticObjectAction.update_object(self.auto_object, ges_marker)
+
+ def remove(self):
+ """Removes the marker represented by the auto_object."""
+ self.ges_marker_list.remove(self.auto_object)
+
+
+class MarkerAdded(MarkerAction):
+ """Action for added markers."""
+
+ def do(self):
+ self.add()
+
+ def undo(self):
+ self.remove()
+
+ def asScenarioAction(self):
+ st = Gst.Structure.new_empty("add-marker")
+ return st
+
+
+class MarkerRemoved(MarkerAction):
+ """Action for removed markers."""
+
+ def do(self):
+ self.remove()
+
+ def undo(self):
+ self.add()
+
+ def asScenarioAction(self):
+ st = Gst.Structure.new_empty("remove-marker")
+ return st
+
+
+class MarkerMoved(UndoableAutomaticObjectAction):
+ """Action for moved markers."""
+
+ def __init__(self, ges_marker_list, ges_marker, old_position):
+ UndoableAutomaticObjectAction.__init__(self, ges_marker)
+ self.ges_marker_list = ges_marker_list
+ self.new_position = ges_marker.props.position
+ self.old_position = old_position
+
+ def do(self):
+ self.ges_marker_list.move(self.auto_object, self.new_position)
+
+ def undo(self):
+ self.ges_marker_list.move(self.auto_object, self.old_position)
+
+ def asScenarioAction(self):
+ st = Gst.Structure.new_empty("move-marker")
+ return st
+
+ def expand(self, action):
+ if not isinstance(action, MarkerMoved):
+ return False
+
+ self.new_position = action.new_position
+ return True
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index 67d139fc..a2d27e34 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -860,7 +860,7 @@ class GroupObserver(Loggable):
self.action_log.push(action)
-class TimelineObserver(Loggable):
+class TimelineObserver(MetaContainerObserver, Loggable):
"""Monitors a project's timeline and reports UndoableActions.
Attributes:
@@ -869,12 +869,14 @@ class TimelineObserver(Loggable):
"""
def __init__(self, ges_timeline, action_log):
+ MetaContainerObserver.__init__(self, ges_timeline, action_log)
Loggable.__init__(self)
self.ges_timeline = ges_timeline
self.action_log = action_log
self.layer_observers = {}
self.group_observers = {}
+
for ges_layer in ges_timeline.get_layers():
self._connect_to_layer(ges_layer)
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index 3fade910..b088e271 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -19,6 +19,7 @@
"""Undo/redo."""
import contextlib
+from gi.repository import GES
from gi.repository import GObject
from pitivi.utils.loggable import Loggable
@@ -366,20 +367,19 @@ class UndoableActionLog(GObject.Object, Loggable):
return False
-class MetaChangedAction(UndoableAction):
+class MetaChangedAction(UndoableAutomaticObjectAction):
def __init__(self, meta_container, item, current_value, new_value):
- UndoableAction.__init__(self)
- self.meta_container = meta_container
+ UndoableAutomaticObjectAction.__init__(self, meta_container)
self.item = item
self.old_value = current_value
self.new_value = new_value
def do(self):
- self.meta_container.set_meta(self.item, self.new_value)
+ self.auto_object.set_meta(self.item, self.new_value)
def undo(self):
- self.meta_container.set_meta(self.item, self.old_value)
+ self.auto_object.set_meta(self.item, self.old_value)
class MetaContainerObserver(GObject.Object):
@@ -396,8 +396,10 @@ class MetaContainerObserver(GObject.Object):
self.metas = {}
+ self.marker_list_observers = {}
+
def set_meta(unused_meta_container, item, value):
- self.metas[item] = value
+ self.__update_meta(item, value)
meta_container.foreach(set_meta)
meta_container.connect("notify-meta", self._notify_meta_cb)
@@ -405,13 +407,20 @@ class MetaContainerObserver(GObject.Object):
def _notify_meta_cb(self, meta_container, item, value):
current_value = self.metas.get(item)
action = MetaChangedAction(meta_container, item, current_value, value)
- self.metas[item] = value
+ self.__update_meta(item, value)
self.action_log.push(action)
def release(self):
self.meta_container.disconnect_by_func(self._notify_meta_cb)
self.meta_container = None
+ def __update_meta(self, item, value):
+ self.metas[item] = value
+ if isinstance(self.metas[item], GES.MarkerList):
+ from pitivi.undo.markers import MarkerListObserver
+ observer = MarkerListObserver(self.metas[item], self.action_log)
+ self.marker_list_observers[self.metas[item]] = observer
+
class PropertyChangedAction(UndoableAutomaticObjectAction):
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index fea22797..d6d8a756 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -222,10 +222,29 @@ EDITOR_PERSPECTIVE_CSS = """
.Marquee {
background-color: rgba(224, 224, 224, 0.7);
+ color: rgba(224, 224, 224, 1);
+ }
+
+ .MarkersBox {
+ background-color: rgba(224, 224, 224, 0);
+ }
+
+ .Marker {
+ background-image: url('%(marker_unselected)s');
+ }
+
+ .Marker:hover {
+ background-image: url('%(marker_hovered)s');
+ }
+
+ .Marker:selected {
+ background-image: url('%(marker_hovered)s');
}
""" % ({'trimbar_normal': os.path.join(get_pixmap_dir(), "trimbar-normal.png"),
- 'trimbar_focused': os.path.join(get_pixmap_dir(), "trimbar-focused.png")})
+ 'trimbar_focused': os.path.join(get_pixmap_dir(), "trimbar-focused.png"),
+ 'marker_unselected': os.path.join(get_pixmap_dir(), "marker-unselect.png"),
+ 'marker_hovered': os.path.join(get_pixmap_dir(), "marker-hover.png")})
PREFERENCES_CSS = """
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c4129f0e..49d32d65 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -43,6 +43,7 @@ pitivi/viewer/viewer.py
pitivi/timeline/elements.py
pitivi/timeline/layer.py
+pitivi/timeline/markers.py
pitivi/timeline/ruler.py
pitivi/timeline/timeline.py
diff --git a/tests/common.py b/tests/common.py
index 9bbfb1fa..9da25bf2 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -305,6 +305,18 @@ class TestCase(unittest.TestCase, Loggable):
self.assertTrue(caps1.is_equal(caps2),
"%s != %s" % (caps1.to_string(), caps2.to_string()))
+ def assert_markers(self, ges_marker_list, expected_properties):
+ """Asserts the content of a GES.MarkerList."""
+ markers = ges_marker_list.get_markers()
+ expected_positions = [properties[0] for properties in expected_properties]
+ expected_comments = [properties[1] for properties in expected_properties]
+
+ positions = [ges_marker.props.position for ges_marker in markers]
+ self.assertListEqual(positions, expected_positions)
+
+ comments = [ges_marker.get_string("comment") for ges_marker in markers]
+ self.assertListEqual(comments, expected_comments)
+
@contextlib.contextmanager
def created_project_file(asset_uri):
diff --git a/tests/test_project.py b/tests/test_project.py
index b18cc845..5bc58475 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -192,6 +192,12 @@ class TestProjectManager(common.TestCase):
project = args[0]
self.assertTrue(project is self.manager.current_project)
+ def test_marker_container(self):
+ project = self.manager.new_blank_project()
+ self.assertIsNotNone(project)
+ self.assertIsNotNone(project.ges_timeline)
+ self.assertIsNotNone(project.ges_timeline.get_marker_list("markers"))
+
def testSaveProject(self):
self.manager.new_blank_project()
diff --git a/tests/test_timeline_markers.py b/tests/test_timeline_markers.py
new file mode 100644
index 00000000..f1245139
--- /dev/null
+++ b/tests/test_timeline_markers.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2009, Alessandro Decina <alessandro d gmail com>
+# Copyright (c) 2014, Alex Băluț <alexandru balut gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Tests for the timeline.markers module."""
+from unittest import mock
+
+from gi.repository import Gdk
+from gi.repository import Gtk
+
+from pitivi.utils.timeline import Zoomable
+from tests.test_undo_timeline import BaseTestUndoTimeline
+
+
+class TestMarkers(BaseTestUndoTimeline):
+ """Class for markers tests"""
+
+ def test_marker_added_ui(self):
+ "Checks the add marker ui"
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker_box = self.timeline_container.markers
+ marker_box.markers_container = markers
+
+ x = 100
+ event = mock.Mock(spec=Gdk.EventButton)
+ event.x = x
+ event.y = 1
+ event.button = Gdk.BUTTON_PRIMARY
+
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+ get_event_widget.return_value = marker_box
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS)
+ marker_box.do_button_press_event(event)
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE)
+ marker_box.do_button_release_event(event)
+
+ position = Zoomable.pixelToNs(event.x)
+ self.assert_markers(markers, [(position, None)])
+
+ def test_marker_removed_ui(self):
+ "Checks the remove marker ui"
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker_box = self.timeline_container.markers
+ marker_box.markers_container = markers
+
+ x = 200
+ position = Zoomable.pixelToNs(x)
+ marker = marker_box.markers_container.add(position)
+ self.assert_markers(markers, [(position, None)])
+
+ event = mock.Mock(spec=Gdk.EventButton)
+ event.x = x
+ event.y = 1
+ event.button = Gdk.BUTTON_SECONDARY
+
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+ get_event_widget.return_value = marker.ui
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS)
+ marker_box.do_button_press_event(event)
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE)
+ marker_box.do_button_release_event(event)
+
+ self.assert_markers(markers, [])
+
+ def test_marker_moved_ui(self):
+ """Checks the move marker UI."""
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker_box = self.timeline_container.markers
+ marker_box.markers_container = markers
+
+ x1 = 300
+ position1 = Zoomable.pixelToNs(x1)
+ marker = marker_box.markers_container.add(position1)
+ self.assert_markers(markers, [(position1, None)])
+
+ x2 = 400
+ position2 = Zoomable.pixelToNs(x2)
+
+ event = mock.Mock(spec=Gdk.EventButton)
+ event.x = x2
+ event.y = 1
+ event.type = Gdk.EventType.BUTTON_PRESS
+ event.button = Gdk.BUTTON_PRIMARY
+
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+ get_event_widget.return_value = marker.ui
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS)
+ marker_box.do_button_press_event(event)
+
+ with mock.patch.object(marker.ui, "translate_coordinates") as translate_coordinates:
+ translate_coordinates.return_value = (x2, 0)
+ marker_box.do_motion_notify_event(event)
+ marker_box.do_button_release_event(event)
+
+ self.assert_markers(markers, [(position2, None)])
+
+ # pylint: disable=unbalanced-tuple-unpacking
+ def test_marker_comment_ui(self):
+ """Checks the comments marker UI."""
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker_box = self.timeline_container.markers
+ marker_box.markers_container = markers
+
+ x = 500
+ position = Zoomable.pixelToNs(x)
+ marker = marker_box.markers_container.add(position)
+ self.assert_markers(markers, [(position, None)])
+
+ event = mock.Mock(spec=Gdk.EventButton)
+ event.x = x
+ event.y = 1
+ event.type = Gdk.EventType.BUTTON_PRESS
+ event.button = Gdk.BUTTON_PRIMARY
+
+ with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+ get_event_widget.return_value = marker.ui
+
+ def popup(markerpopover):
+ text_buffer = markerpopover.text_view.get_buffer()
+ text_buffer.set_text("com")
+ text_buffer.set_text("comment")
+ markerpopover.popdown()
+ original_popover_menu = Gtk.Popover.popup
+ Gtk.Popover.popup = popup
+ try:
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS)
+ marker_box.do_button_press_event(event)
+ event.guiEvent = Gdk.Event.new(Gdk.EventType.DOUBLE_BUTTON_PRESS)
+ event.type = Gdk.EventType.DOUBLE_BUTTON_PRESS
+ marker_box.do_button_press_event(event)
+ finally:
+ Gtk.Popover.popup = original_popover_menu
+
+ stack, = self.action_log.undo_stacks
+ self.assertEqual(len(stack.done_actions), 1, stack.done_actions)
+
+ self.assert_markers(markers, [(position, "comment")])
diff --git a/tests/test_undo_markers.py b/tests/test_undo_markers.py
new file mode 100644
index 00000000..c85984d3
--- /dev/null
+++ b/tests/test_undo_markers.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2019, Millan Castro <m castrovilarino gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Tests for the undo.markers module."""
+import tempfile
+
+from gi.repository import Gst
+
+from tests import common
+from tests.test_undo_timeline import BaseTestUndoTimeline
+
+
+class TestMarkers(BaseTestUndoTimeline):
+ """Tests for the various classes."""
+
+ def test_marker_added(self):
+ """Checks marker addition."""
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+
+ with self.action_log.started("Added marker"):
+ marker1 = markers.add(10)
+ self.assert_markers(markers, [(10, None)])
+
+ with self.action_log.started("new comment"):
+ marker1.set_string("comment", "comment 1")
+ self.assert_markers(markers, [(10, "comment 1")])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1")])
+
+ with self.action_log.started("Added marker"):
+ marker2 = markers.add(20)
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ with self.action_log.started("new comment"):
+ marker2.set_string("comment", "comment 2")
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.action_log.undo()
+ self.action_log.undo()
+ self.action_log.undo()
+ self.assert_markers(markers, [])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1")])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
+
+ def test_marker_removed(self):
+ """Checks marker removal."""
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker1 = markers.add(10)
+ marker2 = markers.add(20)
+
+ with self.action_log.started("Removed marker"):
+ markers.remove(marker1)
+ self.assert_markers(markers, [(20, None)])
+
+ with self.action_log.started("Removed marker"):
+ markers.remove(marker2)
+ self.assert_markers(markers, [])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.assert_markers(markers, [(20, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [])
+
+ def test_marker_moved(self):
+ """Checks marker moving."""
+ self.setup_timeline_container()
+ markers = self.timeline.get_marker_list("markers")
+ marker1 = markers.add(10)
+ markers.add(20)
+
+ with self.action_log.started("Move marker"):
+ markers.move(marker1, 40)
+ markers.move(marker1, 30)
+
+ stack, = self.action_log.undo_stacks
+ self.assertEqual(len(stack.done_actions), 1, stack.done_actions)
+
+ self.assert_markers(markers, [(20, None), (30, None)])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(20, None), (30, None)])
+
+ def test_marker_comment(self):
+ """Checks marker comment."""
+ self.setup_timeline_container()
+
+ markers = self.timeline.get_marker_list("markers")
+
+ with self.action_log.started("Added marker"):
+ marker1 = markers.add(10)
+ with self.action_log.started("Added marker"):
+ marker2 = markers.add(20)
+
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ with self.action_log.started("new comment"):
+ marker1.set_string("comment", "comment 1")
+ with self.action_log.started("new comment"):
+ marker2.set_string("comment", "comment 2")
+
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
+
+ def test_marker_load_project(self):
+ """Checks marker addition."""
+ # TODO: When there is nothing connected to closing-project,
+ # the default reply is "False", which means "abort saving". It should mean
+ # "OK" to get rid off the handler. The meaning of the default (False)
+ # must be changed
+ def closing(unused_manager, unused_project):
+ return True
+
+ def loaded_cb(project, timeline):
+ mainloop.quit()
+
+ markers = self.timeline.get_marker_list("markers")
+ markers.add(10)
+ markers.add(20)
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ project_uri = Gst.filename_to_uri(tempfile.NamedTemporaryFile().name)
+ self.app.project_manager.saveProject(project_uri)
+ self.app.project_manager.connect("closing-project", closing)
+
+ self.app.project_manager.closeRunningProject()
+ project = self.app.project_manager.new_blank_project()
+ markers = project.ges_timeline.get_marker_list("markers")
+ self.assert_markers(markers, [])
+
+ self.app.project_manager.closeRunningProject()
+ project = self.app.project_manager.load_project(project_uri)
+ project.connect("loaded", loaded_cb)
+ mainloop = common.create_main_loop()
+ mainloop.run()
+ self.action_log = self.app.action_log
+
+ markers = project.ges_timeline.get_marker_list("markers")
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ ges_markers = markers.get_markers()
+ marker1, marker2 = ges_markers
+
+ with self.action_log.started("new comment"):
+ marker1.set_string("comment", "comment 1")
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ with self.action_log.started("new comment"):
+ marker2.set_string("comment", "comment 2")
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
+
+ for _ in range(4):
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ self.action_log.undo()
+ self.assert_markers(markers, [(10, None), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, None)])
+
+ self.action_log.redo()
+ self.assert_markers(markers, [(10, "comment 1"), (20, "comment 2")])
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]