[pitivi] undo: Refactor base classes into a separate module



commit 4888690860295603a87bba18f2222669ae8912f8
Author: Alexandru Băluț <alexandru balut gmail com>
Date:   Sun Jun 12 22:54:03 2022 +0200

    undo: Refactor base classes into a separate module
    
    This will allow the observers to be aware of undoable action types.

 pitivi/undo/base.py                       | 176 ++++++++++++++++++++++++++++++
 pitivi/undo/markers.py                    |   2 +-
 pitivi/undo/project.py                    |   4 +-
 pitivi/undo/timeline.py                   |  41 +++----
 pitivi/undo/undo.py                       | 149 +------------------------
 tests/test_undo_base.py                   |  71 ++++++++++++
 tests/test_undo_timeline.py               |   2 +-
 tests/{test_undo.py => test_undo_undo.py} |  48 +-------
 8 files changed, 271 insertions(+), 222 deletions(-)
---
diff --git a/pitivi/undo/base.py b/pitivi/undo/base.py
new file mode 100644
index 000000000..fd8a7ac98
--- /dev/null
+++ b/pitivi/undo/base.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2009, Alessandro Decina <alessandro d 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/>.
+"""Undo/redo."""
+from gi.repository import GObject
+
+from pitivi.utils.loggable import Loggable
+
+
+class UndoError(Exception):
+    """Base class for undo/redo exceptions."""
+
+
+class UndoWrongStateError(UndoError):
+    """Exception related to the current state of the undo/redo stack."""
+
+
+class Action(GObject.Object, Loggable):
+    """Something which might worth logging in a scenario."""
+
+    def __init__(self):
+        GObject.Object.__init__(self)
+        Loggable.__init__(self)
+
+    def as_scenario_action(self):
+        """Converts the action to a Gst.Structure for a `.scenario` file."""
+        return None
+
+
+class UndoableAction(Action):
+    """An action that can be undone.
+
+    When your object's state changes, create an UndoableAction to allow
+    reverting the change later on.
+    """
+
+    def do(self):
+        raise NotImplementedError()
+
+    def undo(self):
+        raise NotImplementedError()
+
+    # pylint: disable=unused-argument
+    def expand(self, action):
+        """Allows the action to expand by including the specified action.
+
+        Args:
+            action (UndoableAction): The action to include.
+
+        Returns:
+            bool: Whether the action has been included, in which case
+                it should not be used for anything else.
+        """
+        return False
+
+
+class UndoableAutomaticObjectAction(UndoableAction):
+    """An action on an automatically created object.
+
+    Attributes:
+        auto_object (object): The object which has been automatically created
+            and might become obsolete later.
+    """
+
+    # pylint: disable=abstract-method
+
+    __updates = {}
+
+    def __init__(self, auto_object):
+        UndoableAction.__init__(self)
+        self.__auto_object = auto_object
+
+    @property
+    def auto_object(self):
+        """The latest object which identifies the same thing as the original."""
+        return self.__updates.get(self.__auto_object, self.__auto_object)
+
+    @classmethod
+    def update_object(cls, auto_object, new_auto_object):
+        """Provides a replacement for an object.
+
+        Args:
+            auto_object (object): The object being replaced.
+            new_auto_object (object): The replacement.
+        """
+        cls.__updates[auto_object] = new_auto_object
+        others = [key
+                  for key, value in cls.__updates.items()
+                  if value == auto_object]
+        for other in others:
+            cls.__updates[other] = new_auto_object
+
+
+class FinalizingAction:
+    """Base class for actions applied when an undo or redo is performed."""
+
+    def do(self):
+        raise NotImplementedError()
+
+
+class PropertyChangedAction(UndoableAutomaticObjectAction):
+
+    def __init__(self, gobject, field_name, old_value, new_value):
+        UndoableAutomaticObjectAction.__init__(self, gobject)
+        self.field_name = field_name
+        self.old_value = old_value
+        self.new_value = new_value
+
+    def __repr__(self):
+        return "<PropertyChanged %s.%s: %s -> %s>" % (self.auto_object, self.field_name, self.old_value, 
self.new_value)
+
+    def do(self):
+        self.auto_object.set_property(self.field_name, self.new_value)
+
+    def undo(self):
+        self.auto_object.set_property(self.field_name, self.old_value)
+
+    def expand(self, action):
+        if not isinstance(action, PropertyChangedAction) or \
+                self.auto_object != action.auto_object or \
+                self.field_name != action.field_name:
+            return False
+
+        self.new_value = action.new_value
+        return True
+
+
+class GObjectObserver(GObject.Object):
+    """Monitor for GObject.Object's props, reporting UndoableActions.
+
+    Attributes:
+        gobject (GObject.Object): The object to be monitored.
+        property_names (List[str]): The props to be monitored.
+    """
+
+    def __init__(self, gobject, property_names, action_log):
+        GObject.Object.__init__(self)
+        self.gobject = gobject
+        self.property_names = property_names
+        self.action_log = action_log
+
+        self.properties = {}
+        for property_name in self.property_names:
+            field_name = property_name.replace("-", "_")
+            self.properties[property_name] = gobject.get_property(field_name)
+            # Connect to obj to keep track when the monitored props change.
+            signal_name = "notify::%s" % property_name
+            gobject.connect(signal_name, self._property_changed_cb,
+                            property_name, field_name)
+
+    def release(self):
+        self.gobject.disconnect_by_func(self._property_changed_cb)
+        self.gobject = None
+
+    def _property_changed_cb(self, gobject, pspec, property_name, field_name):
+        old_value = self.properties[property_name]
+        property_value = gobject.get_property(field_name)
+        if old_value == property_value:
+            return
+        self.properties[property_name] = property_value
+        action = PropertyChangedAction(gobject, field_name,
+                                       old_value, property_value)
+        self.action_log.push(action)
diff --git a/pitivi/undo/markers.py b/pitivi/undo/markers.py
index 52128d36e..b2fd0d1d7 100644
--- a/pitivi/undo/markers.py
+++ b/pitivi/undo/markers.py
@@ -21,7 +21,7 @@ from gi.repository import GES
 from gi.repository import GObject
 from gi.repository import Gst
 
-from pitivi.undo.undo import UndoableAutomaticObjectAction
+from pitivi.undo.base import UndoableAutomaticObjectAction
 from pitivi.utils.loggable import Loggable
 
 
diff --git a/pitivi/undo/project.py b/pitivi/undo/project.py
index ce2f052a2..95fb3b127 100644
--- a/pitivi/undo/project.py
+++ b/pitivi/undo/project.py
@@ -19,10 +19,10 @@ from gi.repository import GES
 from gi.repository import GObject
 from gi.repository import Gst
 
+from pitivi.undo.base import Action
+from pitivi.undo.base import UndoableAction
 from pitivi.undo.markers import MetaContainerObserver
 from pitivi.undo.timeline import TimelineObserver
-from pitivi.undo.undo import Action
-from pitivi.undo.undo import UndoableAction
 
 
 class AssetAddedIntention(UndoableAction):
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index be9f9bd53..4d7a3040f 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -14,16 +14,18 @@
 #
 # 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 typing import Optional
+
 from gi.repository import GES
 from gi.repository import GObject
 from gi.repository import Gst
 
 from pitivi.effects import PROPS_TO_IGNORE
+from pitivi.undo.base import FinalizingAction
+from pitivi.undo.base import GObjectObserver
+from pitivi.undo.base import UndoableAction
+from pitivi.undo.base import UndoableAutomaticObjectAction
 from pitivi.undo.markers import MetaContainerObserver
-from pitivi.undo.undo import FinalizingAction
-from pitivi.undo.undo import GObjectObserver
-from pitivi.undo.undo import UndoableAction
-from pitivi.undo.undo import UndoableAutomaticObjectAction
 from pitivi.utils.loggable import Loggable
 
 
@@ -391,13 +393,13 @@ class TransitionClipAction(UndoableAction):
         self.track_element = track_element
 
     @staticmethod
-    def get_video_element(ges_clip):
+    def get_video_element(ges_clip: GES.TransitionClip) -> Optional[GES.VideoTransition]:
         for track_element in ges_clip.get_children(recursive=True):
             if isinstance(track_element, GES.VideoTransition):
                 return track_element
         return None
 
-    def find_video_transition(self):
+    def find_video_transition(self) -> Optional[GES.VideoTransition]:
         for ges_clip in self.ges_layer.get_clips():
             if isinstance(ges_clip, GES.TransitionClip) and \
                     ges_clip.props.start == self.start and \
@@ -405,7 +407,7 @@ class TransitionClipAction(UndoableAction):
                 # Got the transition clip, now find its video element, if any.
                 track_element = TransitionClipAction.get_video_element(ges_clip)
                 if not track_element:
-                    # Probably the audio transition clip.
+                    # This must be the audio transition clip.
                     continue
                 # Double lucky!
                 return track_element
@@ -428,7 +430,7 @@ class TransitionClipAddedAction(TransitionClipAction):
         UndoableAutomaticObjectAction.update_object(self.track_element, track_element)
 
     def undo(self):
-        # The transition is being removed, nothing to do.
+        # The transition will be removed automatically, no need to do it here.
         pass
 
 
@@ -451,25 +453,18 @@ class TransitionClipRemovedAction(TransitionClipAction):
         return cls(ges_layer, ges_clip, track_element)
 
     def do(self):
-        # The transition is being removed, nothing to do.
+        # The transition will be removed automatically, no need to do it here.
         pass
 
     def undo(self):
         # Search the transition clip created automatically to update it.
-        for ges_clip in self.ges_layer.get_clips():
-            if isinstance(ges_clip, GES.TransitionClip) and \
-                    ges_clip.props.start == self.start and \
-                    ges_clip.props.duration == self.duration:
-                # Got the transition clip, now find its video element, if any.
-                track_element = self.get_video_element(ges_clip)
-                if not track_element:
-                    # Probably the audio transition clip.
-                    continue
-                # Double lucky!
-                UndoableAutomaticObjectAction.update_object(self.track_element, track_element)
-                for prop_name, value in self.properties:
-                    track_element.set_property(prop_name, value)
-                break
+        track_element = self.find_video_transition()
+        if not track_element:
+            return
+
+        UndoableAutomaticObjectAction.update_object(self.track_element, track_element)
+        for prop_name, value in self.properties:
+            track_element.set_property(prop_name, value)
 
 
 class LayerAdded(UndoableAction):
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index edc61f222..e1683dcf2 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -19,6 +19,7 @@ import contextlib
 
 from gi.repository import GObject
 
+from pitivi.undo.base import UndoableAction
 from pitivi.utils.loggable import Loggable
 
 
@@ -30,89 +31,6 @@ class UndoWrongStateError(UndoError):
     """Exception related to the current state of the undo/redo stack."""
 
 
-class Action(GObject.Object, Loggable):
-    """Something which might worth logging in a scenario."""
-
-    def __init__(self):
-        GObject.Object.__init__(self)
-        Loggable.__init__(self)
-
-    def as_scenario_action(self):
-        """Converts the action to a Gst.Structure for a `.scenario` file."""
-        return None
-
-
-class UndoableAction(Action):
-    """An action that can be undone.
-
-    When your object's state changes, create an UndoableAction to allow
-    reverting the change later on.
-    """
-
-    def do(self):
-        raise NotImplementedError()
-
-    def undo(self):
-        raise NotImplementedError()
-
-    # pylint: disable=unused-argument
-    def expand(self, action):
-        """Allows the action to expand by including the specified action.
-
-        Args:
-            action (UndoableAction): The action to include.
-
-        Returns:
-            bool: Whether the action has been included, in which case
-                it should not be used for anything else.
-        """
-        return False
-
-
-class UndoableAutomaticObjectAction(UndoableAction):
-    """An action on an automatically created object.
-
-    Attributes:
-        auto_object (object): The object which has been automatically created
-            and might become obsolete later.
-    """
-
-    # pylint: disable=abstract-method
-
-    __updates = {}
-
-    def __init__(self, auto_object):
-        UndoableAction.__init__(self)
-        self.__auto_object = auto_object
-
-    @property
-    def auto_object(self):
-        """The latest object which identifies the same thing as the original."""
-        return self.__updates.get(self.__auto_object, self.__auto_object)
-
-    @classmethod
-    def update_object(cls, auto_object, new_auto_object):
-        """Provides a replacement for an object.
-
-        Args:
-            auto_object (object): The object being replaced.
-            new_auto_object (object): The replacement.
-        """
-        cls.__updates[auto_object] = new_auto_object
-        others = [key
-                  for key, value in cls.__updates.items()
-                  if value == auto_object]
-        for other in others:
-            cls.__updates[other] = new_auto_object
-
-
-class FinalizingAction:
-    """Base class for actions applied when an undo or redo is performed."""
-
-    def do(self):
-        raise NotImplementedError()
-
-
 class UndoableActionStack(UndoableAction, Loggable):
     """A stack of UndoableAction objects.
 
@@ -427,68 +345,3 @@ class UndoableActionLog(GObject.Object, Loggable):
             if stack.action_group_name in ["assets-addition", "assets-removal"]:
                 return True
         return False
-
-
-class PropertyChangedAction(UndoableAutomaticObjectAction):
-
-    def __init__(self, gobject, field_name, old_value, new_value):
-        UndoableAutomaticObjectAction.__init__(self, gobject)
-        self.field_name = field_name
-        self.old_value = old_value
-        self.new_value = new_value
-
-    def __repr__(self):
-        return "<PropertyChanged %s.%s: %s -> %s>" % (self.auto_object, self.field_name, self.old_value, 
self.new_value)
-
-    def do(self):
-        self.auto_object.set_property(self.field_name, self.new_value)
-
-    def undo(self):
-        self.auto_object.set_property(self.field_name, self.old_value)
-
-    def expand(self, action):
-        if not isinstance(action, PropertyChangedAction) or \
-                self.auto_object != action.auto_object or \
-                self.field_name != action.field_name:
-            return False
-
-        self.new_value = action.new_value
-        return True
-
-
-class GObjectObserver(GObject.Object):
-    """Monitor for GObject.Object's props, reporting UndoableActions.
-
-    Attributes:
-        gobject (GObject.Object): The object to be monitored.
-        property_names (List[str]): The props to be monitored.
-    """
-
-    def __init__(self, gobject, property_names, action_log):
-        GObject.Object.__init__(self)
-        self.gobject = gobject
-        self.property_names = property_names
-        self.action_log = action_log
-
-        self.properties = {}
-        for property_name in self.property_names:
-            field_name = property_name.replace("-", "_")
-            self.properties[property_name] = gobject.get_property(field_name)
-            # Connect to obj to keep track when the monitored props change.
-            signal_name = "notify::%s" % property_name
-            gobject.connect(signal_name, self._property_changed_cb,
-                            property_name, field_name)
-
-    def release(self):
-        self.gobject.disconnect_by_func(self._property_changed_cb)
-        self.gobject = None
-
-    def _property_changed_cb(self, gobject, pspec, property_name, field_name):
-        old_value = self.properties[property_name]
-        property_value = gobject.get_property(field_name)
-        if old_value == property_value:
-            return
-        self.properties[property_name] = property_value
-        action = PropertyChangedAction(gobject, field_name,
-                                       old_value, property_value)
-        self.action_log.push(action)
diff --git a/tests/test_undo_base.py b/tests/test_undo_base.py
new file mode 100644
index 000000000..09eb20fff
--- /dev/null
+++ b/tests/test_undo_base.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2016, Alex B <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, see <http://www.gnu.org/licenses/>.
+"""Tests for the pitivi.undo.base module."""
+# pylint: disable=protected-access
+from unittest import mock
+
+from gi.repository import GES
+
+from pitivi.undo.base import GObjectObserver
+from pitivi.undo.base import PropertyChangedAction
+from pitivi.undo.undo import UndoableActionLog
+from pitivi.undo.undo import UndoableActionStack
+from tests import common
+
+
+class TestGObjectObserver(common.TestCase):
+    """Tests for the GObjectObserver class."""
+
+    def test_property_change(self):
+        action_log = UndoableActionLog()
+        action_log.begin("complex stuff")
+        stack = action_log.stacks[0]
+
+        clip = GES.TitleClip()
+        clip.props.start = 1
+        unused_observer = GObjectObserver(clip, ["start"], action_log)
+
+        self.assertEqual(len(stack.done_actions), 0)
+        clip.props.start = 2
+        self.assertEqual(len(stack.done_actions), 1)
+
+        clip.props.start = 2
+        self.assertEqual(len(stack.done_actions), 1)
+
+        clip.props.start = 4
+        self.assertEqual(len(stack.done_actions), 1)
+        action = stack.done_actions[-1]
+        self.assertEqual(action.old_value, 1)
+        self.assertEqual(action.new_value, 4)
+
+
+class TestPropertyChangedAction(common.TestCase):
+
+    def test_expand(self):
+        stack = UndoableActionStack("good one!", mergeable=False)
+        gobject = mock.Mock()
+        stack.push(PropertyChangedAction(gobject, "field", 5, 7))
+        stack.push(PropertyChangedAction(gobject, "field", 11, 13))
+        self.assertEqual(len(stack.done_actions), 1, stack.done_actions)
+        self.assertEqual(stack.done_actions[0].old_value, 5)
+        self.assertEqual(stack.done_actions[0].new_value, 13)
+
+        stack.push(PropertyChangedAction(gobject, "field2", 0, 1))
+        self.assertEqual(len(stack.done_actions), 2, stack.done_actions)
+
+        stack.push(PropertyChangedAction(mock.Mock(), "field", 0, 1))
+        self.assertEqual(len(stack.done_actions), 3, stack.done_actions)
diff --git a/tests/test_undo_timeline.py b/tests/test_undo_timeline.py
index 409aa8706..3f20003cb 100644
--- a/tests/test_undo_timeline.py
+++ b/tests/test_undo_timeline.py
@@ -26,11 +26,11 @@ from gi.repository import GstController
 from gi.repository import Gtk
 
 from pitivi.timeline.layer import Layer
+from pitivi.undo.base import PropertyChangedAction
 from pitivi.undo.project import AssetAddedAction
 from pitivi.undo.timeline import ClipAdded
 from pitivi.undo.timeline import ClipRemoved
 from pitivi.undo.timeline import TrackElementAdded
-from pitivi.undo.undo import PropertyChangedAction
 from pitivi.utils.ui import LAYER_HEIGHT
 from pitivi.utils.ui import URI_TARGET_ENTRY
 from tests import common
diff --git a/tests/test_undo.py b/tests/test_undo_undo.py
similarity index 91%
rename from tests/test_undo.py
rename to tests/test_undo_undo.py
index be81dac3f..fba0f1c77 100644
--- a/tests/test_undo.py
+++ b/tests/test_undo_undo.py
@@ -21,9 +21,7 @@ from unittest import mock
 from gi.repository import GES
 from gi.repository import Gst
 
-from pitivi.undo.undo import GObjectObserver
-from pitivi.undo.undo import PropertyChangedAction
-from pitivi.undo.undo import UndoableAction
+from pitivi.undo.base import UndoableAction
 from pitivi.undo.undo import UndoableActionLog
 from pitivi.undo.undo import UndoableActionStack
 from pitivi.undo.undo import UndoError
@@ -473,47 +471,3 @@ class TestRollback(common.TestCase):
 
         self.action_log.rollback()
         self.assertListEqual(self.action_log._get_last_stack().done_actions, stack_snapshot)
-
-
-class TestGObjectObserver(common.TestCase):
-    """Tests for the GObjectObserver class."""
-
-    def test_property_change(self):
-        action_log = UndoableActionLog()
-        action_log.begin("complex stuff")
-        stack = action_log.stacks[0]
-
-        clip = GES.TitleClip()
-        clip.props.start = 1
-        unused_observer = GObjectObserver(clip, ["start"], action_log)
-
-        self.assertEqual(len(stack.done_actions), 0)
-        clip.props.start = 2
-        self.assertEqual(len(stack.done_actions), 1)
-
-        clip.props.start = 2
-        self.assertEqual(len(stack.done_actions), 1)
-
-        clip.props.start = 4
-        self.assertEqual(len(stack.done_actions), 1)
-        action = stack.done_actions[-1]
-        self.assertEqual(action.old_value, 1)
-        self.assertEqual(action.new_value, 4)
-
-
-class TestPropertyChangedAction(common.TestCase):
-
-    def test_expand(self):
-        stack = UndoableActionStack("good one!", mergeable=False)
-        gobject = mock.Mock()
-        stack.push(PropertyChangedAction(gobject, "field", 5, 7))
-        stack.push(PropertyChangedAction(gobject, "field", 11, 13))
-        self.assertEqual(len(stack.done_actions), 1, stack.done_actions)
-        self.assertEqual(stack.done_actions[0].old_value, 5)
-        self.assertEqual(stack.done_actions[0].new_value, 13)
-
-        stack.push(PropertyChangedAction(gobject, "field2", 0, 1))
-        self.assertEqual(len(stack.done_actions), 2, stack.done_actions)
-
-        stack.push(PropertyChangedAction(mock.Mock(), "field", 0, 1))
-        self.assertEqual(len(stack.done_actions), 3, stack.done_actions)


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