[pitivi] undo: Refactor base classes into a separate module
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] undo: Refactor base classes into a separate module
- Date: Fri, 24 Jun 2022 21:18:11 +0000 (UTC)
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]