[pitivi] utils: Add MarkerListManager
- From: Mathieu Duponchelle <mathieudu src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] utils: Add MarkerListManager
- Date: Wed, 25 Aug 2021 12:28:29 +0000 (UTC)
commit 62664c036e89f1728f2bd686195ed9c7698f1383
Author: Piotrek Brzeziński <thewildtree outlook com>
Date: Sat Aug 7 20:32:02 2021 +0200
utils: Add MarkerListManager
Acts as an abstraction layer between the clip and UI:
handles changing the currently active list, its snappability state,
as well as exposes convienience methods for adding,
removing and retrieving all marker lists of a given clip.
pitivi/timeline/elements.py | 6 +-
pitivi/utils/markers.py | 264 ++++++++++++++++++++++++++++++++++++++++++++
tests/common.py | 1 +
tests/test_utils_markers.py | 146 ++++++++++++++++++++++++
4 files changed, 416 insertions(+), 1 deletion(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index b6f8c1940..1d027143b 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -43,6 +43,7 @@ from pitivi.timeline.previewers import TitlePreviewer
from pitivi.timeline.previewers import VideoPreviewer
from pitivi.undo.timeline import CommitTimelineFinalizingAction
from pitivi.utils.loggable import Loggable
+from pitivi.utils.markers import MarkerListManager
from pitivi.utils.misc import disconnect_all_by_func
from pitivi.utils.misc import filename_from_uri
from pitivi.utils.pipeline import PipelineError
@@ -667,6 +668,8 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
self.add(self.__background)
self.markers = ClipMarkersBox(self.app, self._ges_elem)
+ self._ges_elem.markers_manager.set_markers_box(self.markers)
+
self.add(self.markers)
self.keyframe_curve = None
@@ -688,6 +691,7 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
self.__previewer.release()
if self.markers:
+ self._ges_elem.markers_manager.set_markers_box(None)
self.markers.release()
# Public API
@@ -1480,7 +1484,7 @@ class SourceClip(Clip):
return
if not hasattr(ges_timeline_element, "markers_manager"):
- ges_timeline_element.markers_manager = MarkerListManager(self.app, ges_timeline_element)
+ ges_timeline_element.markers_manager = MarkerListManager(self.app.settings, ges_timeline_element)
def _remove_child(self, ges_timeline_element):
if ges_timeline_element.ui:
diff --git a/pitivi/utils/markers.py b/pitivi/utils/markers.py
new file mode 100644
index 000000000..7314719c4
--- /dev/null
+++ b/pitivi/utils/markers.py
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2021, Piotr Brzeziński <thewildtreee 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, see <http://www.gnu.org/licenses/>.
+from gettext import gettext as _
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+from gi.repository import GES
+from gi.repository import GObject
+
+from pitivi.settings import GlobalSettings
+from pitivi.timeline.markers import MarkersBox
+
+# FIXME: Remove this once we depend on GES 1.20
+GES_MARKERS_SNAPPABLE = hasattr(GES.MarkerList.new().props, "flags")
+
+DEFAULT_LIST_KEY = "user_markers"
+NAMES_DICT = {
+ DEFAULT_LIST_KEY: _("User markers"),
+}
+
+
+class MarkerListManager(GObject.Object):
+ """An abstraction layer between UI components and individual GESSources's marker lists.
+
+ Attaches to a single GESSource, initialising a default marker list and tracking the
+ addition / removal of any other ones. Keeps track of the currently active list, which
+ is shown to the user if a MarkersBox has been attached via set_markers_box().
+ """
+
+ __gsignals__ = {
+ # Emitted when a list is added or removed.
+ "lists-modified": (GObject.SignalFlags.RUN_LAST, None, ()),
+ # Emitted when the current list changes, along with its metadata key.
+ "current-list-changed": (GObject.SignalFlags.RUN_LAST, None, (str,))
+ }
+
+ def __init__(self, settings: GlobalSettings, ges_source: GES.Source):
+ GObject.Object.__init__(self)
+ self._ges_source: Optional[GES.Source] = ges_source
+ self._settings: GlobalSettings = settings
+ self._box: Optional[MarkersBox] = None
+ self._current_key: Optional[str] = None
+ self.__ensure_default_list_exists()
+ self._load_previous_or_default()
+ self._set_default_snappability()
+
+ def set_markers_box(self, markers_box: Optional[MarkersBox]):
+ """Sets the MarkersBox used to display contents of the current marker list."""
+ if self._box == markers_box:
+ return
+
+ if markers_box and self._ges_source != markers_box.ges_elem:
+ raise ValueError("Marker box has to be attached to the same GESSource.")
+
+ self._box = markers_box
+ if not self._box:
+ return
+
+ self._box.markers_container = self.current_list
+
+ def get_all_keys(self) -> List[str]:
+ """Returns a list of metadata keys under which a marker list can be found."""
+ list_keys = []
+ self._ges_source.foreach(MarkerListManager.__clip_meta_foreach_func, list_keys)
+ return list_keys
+
+ def get_all_keys_with_names(self) -> List[Tuple[str, str]]:
+ """Returns a list of list keys along with their human-readable names.
+
+ This exists for ease of use with the MarkerProperties component.
+ """
+ list_keys = self.get_all_keys()
+ # If no name is found just display the key directly.
+ return [(key, NAMES_DICT.get(key, key)) for key in list_keys]
+
+ def get_all_lists(self) -> List[GES.MarkerList]:
+ """Returns a list of all existing marker lists."""
+ list_keys = self.get_all_keys()
+ return [self._ges_source.get_marker_list(key) for key in list_keys]
+
+ def list_exists(self, list_key: str) -> bool:
+ """Returns whether a list under the given metadata key exists."""
+ return list_key in self.get_all_keys()
+
+ def add_list(self, key: str, marker_timestamps: Optional[List[int]] = None) -> GES.MarkerList:
+ """Creates an empty marker list and saves it under the given key.
+
+ The key cannot be empty, can't contain spaces, and another list cannot
+ already exist under the given key.
+ A list of timestamps can be provided to automatically add corresponding
+ markers to the newly created list.
+
+ Emits the "lists-modified" signal after the list is successfully added.
+ """
+ if not key:
+ raise ValueError("You must provide a key for the list.")
+ if self._ges_source.get_marker_list(key):
+ raise ValueError("A list already exists under the given key.")
+ if " " in key:
+ raise ValueError("List key cannot contain a space character.")
+
+ marker_list = GES.MarkerList.new()
+
+ if GES_MARKERS_SNAPPABLE and self._settings.markersSnappableByDefault:
+ marker_list.props.flags |= GES.MarkerFlags.SNAPPABLE
+
+ if marker_timestamps:
+ for timestamp in marker_timestamps:
+ marker_list.add(timestamp)
+
+ self._ges_source.set_marker_list(key, marker_list)
+ self.emit("lists-modified")
+ return marker_list
+
+ def remove_list(self, key: str):
+ """Removes the marker list found under the given key.
+
+ If the list being removed is currently active, the default list
+ will be set as active instead.
+
+ Note: the default "user_markers" cannot be removed.
+ """
+ if not key:
+ raise ValueError("You must provide a key for the list.")
+
+ if key == DEFAULT_LIST_KEY:
+ raise ValueError("Cannot remove the default marker list.")
+
+ if key == self.current_list_key:
+ self._load_default()
+
+ self._ges_source.set_meta(key, None)
+ self.emit("lists-modified")
+
+ @property
+ def current_list_key(self) -> Optional[str]:
+ """Returns the metadata key under which the current list can be found."""
+ return self._current_key
+
+ @current_list_key.setter
+ def current_list_key(self, list_key: str):
+ """Sets the marker list found under the given key as the currently active one.
+
+ If no list should be active, an empty string needs to be given as the key.
+ """
+ if list_key is None:
+ raise ValueError("No metadata key has been provided.")
+
+ if self.current_list_key == list_key:
+ return
+
+ # Don't retrieve the list if the key is an empty string.
+ new_list = None
+ if list_key:
+ new_list = self._ges_source.get_marker_list(list_key)
+ if not new_list:
+ raise ValueError("Invalid metadata key has been provided.")
+
+ # Turn snappability off for lists going inactive.
+ # The was_snappable value is not being serialized, thus
+ # after reloading it will be set to the default user-preferred value.
+ if self.current_list:
+ self.current_list.was_snappable = self.snappable
+ self.snappable = False
+
+ # Set the new list as active and restore its snappable state if it's not None.
+ self._current_key = list_key
+
+ if new_list and hasattr(new_list, "was_snappable"):
+ self.snappable = new_list.was_snappable
+ if self._box:
+ self._box.markers_container = new_list
+
+ # This lets us preserve the current list between sessions.
+ self._ges_source.set_string("last_chosen_list", list_key)
+ self.emit("current-list-changed", self.current_list_key)
+
+ @property
+ def current_list(self) -> Optional[GES.MarkerList]:
+ """Returns the currently active marker list."""
+ if self.current_list_key is None:
+ return None
+
+ return self._ges_source.get_marker_list(self.current_list_key)
+
+ @property
+ def snappable(self) -> bool:
+ """Returns whether the current list is considered a snapping target."""
+ if not GES_MARKERS_SNAPPABLE:
+ return False
+
+ if not self.current_list:
+ return False
+
+ return self.current_list.props.flags & GES.MarkerFlags.SNAPPABLE
+
+ @snappable.setter
+ def snappable(self, snappable: bool):
+ """Sets the snappable flag of the current list to a given value."""
+ if not GES_MARKERS_SNAPPABLE:
+ return
+
+ if not self.current_list:
+ return
+
+ if self.snappable == snappable:
+ return
+
+ if snappable:
+ self.current_list.props.flags |= GES.MarkerFlags.SNAPPABLE
+ else:
+ self.current_list.props.flags &= ~GES.MarkerFlags.SNAPPABLE
+
+ def _load_previous_or_default(self):
+ last_list_key = self._ges_source.get_string("last_chosen_list")
+
+ # The default list is guaranteed to exist at this point.
+ list_key = DEFAULT_LIST_KEY if last_list_key is None else last_list_key
+ self.current_list_key = list_key
+
+ def _load_default(self):
+ self.current_list_key = DEFAULT_LIST_KEY
+
+ def _set_default_snappability(self):
+ """Sets user-chosen snappability state for all lists except for the active one.
+
+ This is assumed to be only called once, after the default / previously chosen
+ list has been loaded up.
+ """
+ snappable_by_default = self._settings.markersSnappableByDefault
+ all_lists = self.get_all_lists()
+
+ for marker_list in all_lists:
+ # Ignore the active list - its flags were loaded from the project file.
+ if marker_list == self.current_list:
+ continue
+
+ marker_list.was_snappable = snappable_by_default
+
+ def __ensure_default_list_exists(self):
+ if self._ges_source.get_marker_list(DEFAULT_LIST_KEY):
+ return
+
+ self.add_list(DEFAULT_LIST_KEY)
+
+ @staticmethod
+ def __clip_meta_foreach_func(container, key, value, keys):
+ if isinstance(value, GES.MarkerList):
+ keys.append(key)
diff --git a/tests/common.py b/tests/common.py
index cd99ea432..bba4738cd 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -314,6 +314,7 @@ def setup_clipproperties(func):
self.transformation_box._new_project_loaded_cb(None, self.project)
self.speed_box = self.clipproperties.speed_expander
+ self.markers_box = self.clipproperties.marker_expander
func(self)
diff --git a/tests/test_utils_markers.py b/tests/test_utils_markers.py
new file mode 100644
index 000000000..dc9c9077c
--- /dev/null
+++ b/tests/test_utils_markers.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2021, Piotr Brzeziński <thewildtreee 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, see <http://www.gnu.org/licenses/>.
+from gi.repository import GES
+from gi.repository import Gst
+
+from pitivi.timeline.markers import ClipMarkersBox
+from pitivi.utils.markers import DEFAULT_LIST_KEY
+from tests import common
+
+
+class TestMarkerListManager(common.TestCase):
+ def add_single_clip(self):
+ clip = GES.TitleClip()
+ clip.set_start(5 * Gst.SECOND)
+ clip.set_duration(20 * Gst.SECOND)
+ self.layer.add_clip(clip)
+ return clip
+
+ @common.setup_timeline
+ def test_manager_created_with_default(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+
+ manager = source.markers_manager
+ self.assertTrue(manager)
+
+ self.assertTrue(manager.list_exists(DEFAULT_LIST_KEY))
+ self.assertEqual(manager.current_list_key, DEFAULT_LIST_KEY)
+
+ @common.setup_timeline
+ def test_manager_list_add(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+ manager = source.markers_manager
+
+ self.assertRaises(ValueError, manager.add_list, DEFAULT_LIST_KEY)
+ self.assertRaises(ValueError, manager.add_list, "key with spaces")
+ self.assertRaises(ValueError, manager.add_list, None)
+
+ test_key = "test_list"
+ markers = [1, 5, 10]
+ test_list = manager.add_list(test_key, markers)
+
+ self.assertTrue(manager.list_exists(test_key))
+ self.assertEqual(test_list, source.get_marker_list(test_key))
+ self.assert_markers(test_list, [(pos, None) for pos in markers])
+
+ @common.setup_timeline
+ def test_manager_list_remove(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+ manager = source.markers_manager
+
+ self.assertRaises(ValueError, manager.remove_list, DEFAULT_LIST_KEY)
+ self.assertRaises(ValueError, manager.remove_list, None)
+
+ test_key = "test_list"
+ manager.add_list(test_key)
+ manager.current_list_key = test_key
+
+ self.assertEqual(manager.current_list_key, test_key)
+ manager.remove_list(test_key)
+ self.assertEqual(manager.current_list_key, DEFAULT_LIST_KEY)
+ self.assertIsNone(source.get_marker_list(test_key))
+
+ @common.setup_timeline
+ def test_manager_default_snappability(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+ manager = source.markers_manager
+
+ test_key = "test_list"
+ default_snappable = self.app.settings.markersSnappableByDefault
+
+ manager.add_list(test_key)
+ manager.current_list_key = test_key
+ self.assertEqual(manager.snappable, default_snappable)
+
+ @common.setup_timeline
+ def test_manager_current_list(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+ manager = source.markers_manager
+
+ test_key = "test_list"
+ manager.add_list(test_key)
+
+ # Toggle snappability on the default list, switch to a diff. one,
+ # turn off snappability there, and test if they're both
+ # correctly kept between active list changes.
+ manager.snappable = True
+ manager.current_list_key = test_key
+ manager.snappable = False
+
+ manager.current_list_key = DEFAULT_LIST_KEY
+ self.assertEqual(manager.current_list, source.get_marker_list(DEFAULT_LIST_KEY))
+ self.assertTrue(manager.snappable)
+ manager.current_list_key = test_key
+ self.assertEqual(manager.current_list, source.get_marker_list(test_key))
+ self.assertFalse(manager.snappable)
+ manager.current_list_key = ""
+ self.assertIsNone(manager.current_list)
+ self.assertFalse(manager.snappable)
+ manager.current_list_key = DEFAULT_LIST_KEY
+ self.assertEqual(manager.current_list, source.get_marker_list(DEFAULT_LIST_KEY))
+ self.assertTrue(manager.snappable)
+
+ @common.setup_timeline
+ def test_manager_marker_box(self):
+ clip = self.add_single_clip()
+ source, = clip.get_children(False)
+ manager = source.markers_manager
+ box = ClipMarkersBox(self.app, source)
+
+ test_key = "test_list"
+ manager.add_list(test_key)
+
+ self.assertIsNone(box.markers_container)
+ manager.set_markers_box(box)
+ self.assertEqual(box.markers_container, manager.current_list)
+ manager.current_list_key = test_key
+ self.assertEqual(box.markers_container, manager.current_list)
+ manager.current_list_key = ""
+ self.assertIsNone(box.markers_container)
+
+ # Disconnect the box, make a few changes and attach again.
+ manager.set_markers_box(None)
+ manager.current_list_key = DEFAULT_LIST_KEY
+ self.assertIsNone(box.markers_container)
+ manager.current_list_key = test_key
+ manager.set_markers_box(box)
+ self.assertEqual(box.markers_container, manager.current_list)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]