[pitivi] undo: Support grouping and ungrouping



commit 88dfc987fbd0a2d0cf84925408cb67b4e0d97699
Author: Alexandru Băluț <alexandru balut gmail com>
Date:   Sun Sep 4 02:10:46 2016 +0200

    undo: Support grouping and ungrouping
    
    Fixes https://phabricator.freedesktop.org/T7466
    
    Differential Revision: https://phabricator.freedesktop.org/D1297

 pitivi/timeline/timeline.py |   12 +++---
 pitivi/undo/timeline.py     |   90 +++++++++++++++++++++++++++++++++++++++++--
 pitivi/undo/undo.py         |    2 +-
 tests/test_undo_timeline.py |   55 ++++++++++++++++++++++++++
 4 files changed, 148 insertions(+), 11 deletions(-)
---
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 59c9689..d1fe861 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -331,7 +331,7 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
     def resetSelectionGroup(self):
         self.debug("Reset selection group")
         if self.current_group:
-            GES.Container.ungroup(self.current_group, False)
+            self.current_group.ungroup(recursive=False)
 
         self.current_group = GES.Group()
         self.current_group.props.serialize = False
@@ -1400,13 +1400,13 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
                                _("Delete selected clips"))
 
         self.group_action = Gio.SimpleAction.new("group-selected-clips", None)
-        self.group_action.connect("activate", self._groupSelected)
+        self.group_action.connect("activate", self._group_selected_cb)
         group.add_action(self.group_action)
         self.app.shortcuts.add("timeline.group-selected-clips", ["<Control>g"],
                                _("Group selected clips together"))
 
         self.ungroup_action = Gio.SimpleAction.new("ungroup-selected-clips", None)
-        self.ungroup_action.connect("activate", self._ungroupSelected)
+        self.ungroup_action.connect("activate", self._ungroup_selected_cb)
         group.add_action(self.ungroup_action)
         self.app.shortcuts.add("timeline.ungroup-selected-clips", ["<Shift><Control>g"],
                                _("Ungroup selected clips"))
@@ -1577,7 +1577,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
 
             self.timeline.selection.setSelection([], SELECT)
 
-    def _ungroupSelected(self, unused_action, unused_parameter):
+    def _ungroup_selected_cb(self, unused_action, unused_parameter):
         if not self.ges_timeline:
             self.info("No ges_timeline set yet!")
             return
@@ -1588,12 +1588,12 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
                 toplevel = obj.get_toplevel_parent()
                 if toplevel == self.timeline.current_group:
                     for child in toplevel.get_children(False):
-                        child.ungroup(False)
+                        child.ungroup(recursive=False)
 
         self.timeline.resetSelectionGroup()
         self.timeline.selection.setSelection([], SELECT)
 
-    def _groupSelected(self, unused_action, unused_parameter):
+    def _group_selected_cb(self, unused_action, unused_parameter):
         if not self.ges_timeline:
             self.info("No timeline set yet?")
             return
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index 8d18d16..6864eb6 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -318,7 +318,7 @@ class TransitionClipAction(UndoableAction):
 
     @staticmethod
     def get_video_element(ges_clip):
-        for track_element in ges_clip.get_children(True):
+        for track_element in ges_clip.get_children(recursive=True):
             if isinstance(track_element, GES.VideoTransition):
                 return track_element
         return None
@@ -385,7 +385,6 @@ class TransitionClipRemovedAction(TransitionClipAction):
 
     def undo(self):
         # Search the transition clip created automatically to update it.
-        transition_clip = None
         for ges_clip in self.ges_layer.get_clips():
             if isinstance(ges_clip, GES.TransitionClip) and \
                     ges_clip.props.start == self.start and \
@@ -591,7 +590,7 @@ class LayerObserver(MetaContainerObserver, Loggable):
         ges_clip.connect("child-added", self._clipTrackElementAddedCb)
         ges_clip.connect("child-removed", self._clipTrackElementRemovedCb)
 
-        for track_element in ges_clip.get_children(True):
+        for track_element in ges_clip.get_children(recursive=True):
             self._connectToTrackElement(track_element)
 
         if isinstance(ges_clip, GES.TransitionClip):
@@ -605,7 +604,7 @@ class LayerObserver(MetaContainerObserver, Loggable):
         ges_clip.disconnect_by_func(self._clipTrackElementAddedCb)
         ges_clip.disconnect_by_func(self._clipTrackElementRemovedCb)
 
-        for child in ges_clip.get_children(True):
+        for child in ges_clip.get_children(recursive=True):
             self._disconnectFromTrackElement(child)
 
         if isinstance(ges_clip, GES.TransitionClip):
@@ -706,6 +705,62 @@ class LayerObserver(MetaContainerObserver, Loggable):
         self.priority = current
 
 
+class TimelineElementAddedToGroup(UndoableAction):
+
+    def __init__(self, ges_group, ges_timeline_element):
+        UndoableAction.__init__(self)
+        self.ges_group = ges_group
+        self.ges_timeline_element = ges_timeline_element
+
+    def do(self):
+        self.ges_group.add(self.ges_timeline_element)
+
+    def undo(self):
+        self.ges_group.remove(self.ges_timeline_element)
+
+
+class TimelineElementRemovedFromGroup(UndoableAction):
+
+    def __init__(self, ges_group, ges_timeline_element):
+        UndoableAction.__init__(self)
+        self.ges_group = ges_group
+        self.ges_timeline_element = ges_timeline_element
+
+    def do(self):
+        self.ges_group.remove(self.ges_timeline_element)
+
+    def undo(self):
+        self.ges_group.add(self.ges_timeline_element)
+
+
+class GroupObserver(Loggable):
+    """Monitors a Group and reports UndoableActions.
+
+    Args:
+        ges_group (GES.Group): The group to observe.
+
+    Attributes:
+        action_log (UndoableActionLog): The action log where to report actions.
+    """
+
+    def __init__(self, ges_group, action_log):
+        Loggable.__init__(self)
+        self.log("INIT %s", ges_group)
+        self.ges_group = ges_group
+        self.action_log = action_log
+
+        ges_group.connect_after("child-added", self.__child_added_cb)
+        ges_group.connect("child-removed", self.__child_removed_cb)
+
+    def __child_added_cb(self, ges_group, ges_timeline_element):
+        action = TimelineElementAddedToGroup(ges_group, ges_timeline_element)
+        self.action_log.push(action)
+
+    def __child_removed_cb(self, ges_group, ges_timeline_element):
+        action = TimelineElementRemovedFromGroup(ges_group, ges_timeline_element)
+        self.action_log.push(action)
+
+
 class TimelineObserver(Loggable):
     """Monitors a project's timeline and reports UndoableActions.
 
@@ -720,12 +775,20 @@ class TimelineObserver(Loggable):
         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)
 
         ges_timeline.connect("layer-added", self.__layer_added_cb)
         ges_timeline.connect("layer-removed", self.__layer_removed_cb)
 
+        for ges_group in ges_timeline.get_groups():
+            self._connect_to_group(ges_group)
+
+        ges_timeline.connect("group-added", self.__group_added_cb)
+        # We don't care about the group-removed signal because this greatly
+        # simplifies the logic.
+
     def __layer_added_cb(self, ges_timeline, ges_layer):
         self._connect_to_layer(ges_layer)
 
@@ -738,3 +801,22 @@ class TimelineObserver(Loggable):
     def __layer_removed_cb(self, ges_timeline, ges_layer):
         action = LayerRemoved(ges_timeline, ges_layer)
         self.action_log.push(action)
+
+    def _connect_to_group(self, ges_group):
+        if not ges_group.props.serialize:
+            return
+
+        # A group is added when it gets its first element, thus
+        # when undoing/redoing a group can be added multiple times.
+        # This is the only complexity caused by the fact that we keep alive
+        # all the GroupObservers which have been created.
+        if ges_group not in self.group_observers:
+            group_observer = GroupObserver(ges_group, self.action_log)
+            self.group_observers[ges_group] = group_observer
+
+    def __group_added_cb(self, unused_ges_timeline, ges_group):
+        self._connect_to_group(ges_group)
+        # This should be a single clip.
+        for ges_clip in ges_group.get_children(recursive=False):
+            action = TimelineElementAddedToGroup(ges_group, ges_clip)
+            self.action_log.push(action)
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index 5c3d30e..625480e 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -220,7 +220,7 @@ class UndoableActionLog(GObject.Object, Loggable):
         self.emit("pre-push", action)
 
         if self.running:
-            self.debug("Ignore push because running")
+            self.debug("Ignore push because running: %s", action)
             return
 
         try:
diff --git a/tests/test_undo_timeline.py b/tests/test_undo_timeline.py
index 29e9105..70287e4 100644
--- a/tests/test_undo_timeline.py
+++ b/tests/test_undo_timeline.py
@@ -28,6 +28,7 @@ from gi.repository import Gtk
 
 from pitivi.timeline.layer import Layer
 from pitivi.timeline.timeline import Timeline
+from pitivi.timeline.timeline import TimelineContainer
 from pitivi.undo.project import AssetAddedAction
 from pitivi.undo.timeline import ClipAdded
 from pitivi.undo.timeline import ClipRemoved
@@ -48,6 +49,15 @@ class BaseTestUndoTimeline(TestCase):
         self.layer = self.timeline.append_layer()
         self.action_log = self.app.action_log
 
+    def setup_timeline_container(self):
+        project = self.app.project_manager.current_project
+        self.timeline_container = TimelineContainer(self.app)
+        self.timeline_container.setProject(project)
+
+        timeline = self.timeline_container.timeline
+        timeline.app.project_manager.current_project = project
+        timeline.get_parent = mock.MagicMock(return_value=self.timeline_container)
+
     def getTimelineClips(self):
         for layer in self.timeline.layers:
             for clip in layer.get_clips():
@@ -99,6 +109,51 @@ class TestTimelineObserver(BaseTestUndoTimeline):
         self.assertEqual([l.props.priority for l in [layer1, layer3]],
                          list(range(2)))
 
+    def test_group_ungroup_clips(self):
+        self.setup_timeline_container()
+
+        clip1 = common.create_test_clip(GES.TitleClip)
+        clip1.set_start(0 * Gst.SECOND)
+        clip1.set_duration(1 * Gst.SECOND)
+
+        uri = common.get_sample_uri("tears_of_steel.webm")
+        asset = GES.UriClipAsset.request_sync(uri)
+        clip2 = asset.extract()
+
+        self.layer.add_clip(clip1)
+        self.layer.add_clip(clip2)
+        # The selection does not care about GES.Groups, only about GES.Clips.
+        self.timeline_container.timeline.selection.select([clip1, clip2])
+
+        self.timeline_container.group_action.activate(None)
+        self.assertTrue(isinstance(clip1.get_parent(), GES.Group))
+        self.assertEqual(clip1.get_parent(), clip2.get_parent())
+
+        self.timeline_container.ungroup_action.activate(None)
+        self.assertIsNone(clip1.get_parent())
+        self.assertIsNone(clip2.get_parent())
+
+        for i in range(4):
+            # Undo ungrouping.
+            self.action_log.undo()
+            self.assertTrue(isinstance(clip1.get_parent(), GES.Group))
+            self.assertEqual(clip1.get_parent(), clip2.get_parent())
+
+            # Undo grouping.
+            self.action_log.undo()
+            self.assertIsNone(clip1.get_parent())
+            self.assertIsNone(clip2.get_parent())
+
+            # Redo grouping.
+            self.action_log.redo()
+            self.assertTrue(isinstance(clip1.get_parent(), GES.Group))
+            self.assertEqual(clip1.get_parent(), clip2.get_parent())
+
+            # Redo ungrouping.
+            self.action_log.redo()
+            self.assertIsNone(clip1.get_parent())
+            self.assertIsNone(clip2.get_parent())
+
 
 class TestLayerObserver(BaseTestUndoTimeline):
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]