[pitivi/wip-titles: 4/5] undo: Join successive similar operations on title clips
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi/wip-titles: 4/5] undo: Join successive similar operations on title clips
- Date: Wed, 27 Jan 2021 20:55:00 +0000 (UTC)
commit eec855a1bba51dbb974debac7b895410df919b17
Author: Alexandru Băluț <alexandru balut gmail com>
Date: Sun Jan 24 20:44:25 2021 +0100
undo: Join successive similar operations on title clips
Fixes #2022
pitivi/clip_properties/title.py | 9 ++---
pitivi/undo/timeline.py | 9 +++++
pitivi/undo/undo.py | 73 +++++++++++++++++++++++++++++++++--------
tests/test_undo.py | 37 +++++++++++++++++++--
4 files changed, 108 insertions(+), 20 deletions(-)
---
diff --git a/pitivi/clip_properties/title.py b/pitivi/clip_properties/title.py
index 7a612b7a0..a1a61b62b 100644
--- a/pitivi/clip_properties/title.py
+++ b/pitivi/clip_properties/title.py
@@ -114,8 +114,9 @@ class TitleProperties(Gtk.Expander, Loggable):
self.show_all()
- def _set_child_property(self, name, value):
- with self.app.action_log.started("Title change property",
+ def _set_child_property(self, name, value, mergeable=False):
+ with self.app.action_log.started("Title change property %s" % name,
+ mergeable=mergeable,
toplevel=True):
self._setting_props = True
try:
@@ -173,7 +174,7 @@ class TitleProperties(Gtk.Expander, Loggable):
escaped_text = html.escape(self.textbuffer.props.text)
self.log("Source text updated to %s", escaped_text)
- self._set_child_property("text", escaped_text)
+ self._set_child_property("text", escaped_text, mergeable=True)
def _alignment_changed_cb(self, combo):
"""Handles changes in the h/v alignment widgets."""
@@ -201,7 +202,7 @@ class TitleProperties(Gtk.Expander, Loggable):
else:
prop_name = "y-absolute"
value = spin.get_value()
- self._set_child_property(prop_name, value)
+ self._set_child_property(prop_name, value, mergeable=True)
def _update_absolute_alignment_widgets_visibility(self):
visible = self.valignment_combo.get_active_id() == "absolute"
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index 1b478754d..7d03e1670 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -77,6 +77,15 @@ class TrackElementPropertyChanged(UndoableAction):
st['value'] = GObject.Value(pspec.value_type, value)
return st
+ def expand(self, action):
+ if not isinstance(action, TrackElementPropertyChanged) or \
+ not action.track_element == self.track_element or \
+ not action.property_name == self.property_name:
+ return False
+
+ self.new_value = action.new_value
+ return True
+
class TimelineElementObserver(Loggable):
"""Monitors the props of an element and all its children.
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index 39de7b247..8e7ec0f19 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -121,26 +121,57 @@ class UndoableActionStack(UndoableAction, Loggable):
action_group_name (str): The name of the operation.
done_actions (List[UndoableAction]): The UndoableActions pushed in
the stack.
+ mergeable (bool): Whether this stack accepts merges with other
+ compatible stacks.
finalizing_action (FinalizingAction): The action to be performed
at the end of undoing or redoing the stacked actions.
"""
- def __init__(self, action_group_name, finalizing_action=None):
+ def __init__(self, action_group_name, mergeable, finalizing_action=None):
UndoableAction.__init__(self)
Loggable.__init__(self)
self.action_group_name = action_group_name
self.done_actions = []
+ self.mergeable = mergeable
self.finalizing_action = finalizing_action
+ def __len__(self):
+ return len(self.done_actions)
+
def __repr__(self):
return "%s: %s" % (self.action_group_name, self.done_actions)
+ def attempt_merge(self, stack, action):
+ """Merges the action into the previous one if possible.
+
+ Returns:
+ bool: Whether the merge has been done.
+ """
+ if not self.mergeable:
+ return False
+
+ if not self.done_actions:
+ return False
+
+ if not self.action_group_name == stack.action_group_name:
+ return False
+
+ return self.attempt_expand_action(action)
+
+ def attempt_expand_action(self, action):
+ """Expands the last action with the specified action if possible."""
+ if not self.done_actions:
+ return False
+
+ last_action = self.done_actions[-1]
+ return last_action.expand(action)
+
def push(self, action):
- if self.done_actions:
- last_action = self.done_actions[-1]
- if last_action.expand(action):
- # The action has been included in the previous one.
- return
+ """Adds an action unless it's possible to expand the previous."""
+ if self.attempt_expand_action(action):
+ # The action has been merged into the last one.
+ return
+
self.done_actions.append(action)
def _run_action(self, actions, method_name):
@@ -202,7 +233,7 @@ class UndoableActionLog(GObject.Object, Loggable):
else:
self.commit(action_group_name)
- def begin(self, action_group_name, finalizing_action=None, toplevel=False):
+ def begin(self, action_group_name, finalizing_action=None, mergeable=False, toplevel=False):
"""Starts recording a high-level operation which later can be undone.
The recording can be stopped by calling the `commit` method or
@@ -218,14 +249,14 @@ class UndoableActionLog(GObject.Object, Loggable):
if toplevel and self.is_in_transaction():
raise UndoWrongStateError("Toplevel operation started as suboperation", self.stacks)
- stack = UndoableActionStack(action_group_name, finalizing_action)
+ stack = UndoableActionStack(action_group_name, mergeable, finalizing_action)
self.stacks.append(stack)
self.debug("begin action group %s, nested %s",
stack.action_group_name, len(self.stacks))
self.emit("begin", stack)
def push(self, action):
- """Reports a change.
+ """Records a change noticed by the monitoring system.
Args:
action (Action): The action representing the change.
@@ -249,11 +280,24 @@ class UndoableActionLog(GObject.Object, Loggable):
try:
stack = self._get_last_stack()
except UndoWrongStateError as e:
- self.warning("Failed pushing '%s' because: %s", action, e)
+ self.warning("Failed pushing '%s' because no transaction started: %s", action, e)
return
- stack.push(action)
- self.debug("push action %s in action group %s",
- action, stack.action_group_name)
+
+ merged = False
+ if stack.mergeable and len(self.stacks[0]) == 0 and self.undo_stacks:
+ # The current undoable operation is empty, this is the first action.
+ # Check if it can be merged with the previous operation.
+ previous_operation = self.undo_stacks[-1]
+ if previous_operation.attempt_merge(stack, action):
+ self.debug("Merging undoable operations")
+ self.stacks = [self.undo_stacks.pop()]
+ merged = True
+
+ if not merged:
+ stack.push(action)
+ self.debug("push action %s in action group %s",
+ action, stack.action_group_name)
+
self.emit("push", stack, action)
def rollback(self, undo=True):
@@ -299,9 +343,11 @@ class UndoableActionLog(GObject.Object, Loggable):
stack = self._get_last_stack(pop=True)
if action_group_name != stack.action_group_name:
raise UndoWrongStateError("Unexpected commit", action_group_name, stack, self.stacks)
+
if not stack.done_actions:
self.debug("Ignore empty stack %s", stack.action_group_name)
return
+
if not self.stacks:
self.undo_stacks.append(stack)
stack.finish_operation()
@@ -461,6 +507,7 @@ class PropertyChangedAction(UndoableAutomaticObjectAction):
self.auto_object != action.auto_object or \
self.field_name != action.field_name:
return False
+
self.new_value = action.new_value
return True
diff --git a/tests/test_undo.py b/tests/test_undo.py
index 7c87a684e..be81dac3f 100644
--- a/tests/test_undo.py
+++ b/tests/test_undo.py
@@ -46,7 +46,7 @@ class TestUndoableActionStack(common.TestCase):
def undo(self):
state["actions"] -= 1
- stack = UndoableActionStack("meh")
+ stack = UndoableActionStack("meh", mergeable=False)
action1 = Action()
action2 = Action()
stack.push(action1)
@@ -60,7 +60,7 @@ class TestUndoableActionStack(common.TestCase):
def test_undo_error(self):
"""Checks undo a stack containing a failing action."""
- stack = UndoableActionStack("meh")
+ stack = UndoableActionStack("meh", mergeable=False)
action1 = mock.Mock(spec=UndoableAction)
action1.expand.return_value = False
action2 = mock.Mock(spec=UndoableAction)
@@ -414,6 +414,37 @@ class TestUndoableActionLog(common.TestCase):
self.assertEqual(len(self.log.undo_stacks), 0)
self.assertEqual(len(self.log.redo_stacks), 0)
+ def test_merging(self):
+ with self.log.started("one", mergeable=False):
+ action = mock.Mock(spec=UndoableAction)
+ action.expand.side_effect = Exception("should not have been called")
+ self.log.push(action)
+ self.assertEqual(len(self.log.undo_stacks), 1)
+
+ with self.log.started("one", mergeable=True):
+ action = mock.Mock(spec=UndoableAction)
+ action.expand.return_value = False
+ self.log.push(action)
+ self.assertEqual(len(self.log.undo_stacks), 2)
+
+ with self.log.started("one", mergeable=True):
+ action = mock.Mock(spec=UndoableAction)
+ action.expand.return_value = True
+ self.log.push(action)
+ self.assertEqual(len(self.log.undo_stacks), 3)
+
+ with self.log.started("one", mergeable=True):
+ action = mock.Mock(spec=UndoableAction)
+ action.expand.return_value = True
+ self.log.push(action)
+ self.assertEqual(len(self.log.undo_stacks), 3)
+
+ with self.log.started("one", mergeable=False):
+ action = mock.Mock(spec=UndoableAction)
+ action.expand.side_effect = Exception("should not have been called")
+ self.log.push(action)
+ self.assertEqual(len(self.log.undo_stacks), 4)
+
class TestRollback(common.TestCase):
@@ -473,7 +504,7 @@ class TestGObjectObserver(common.TestCase):
class TestPropertyChangedAction(common.TestCase):
def test_expand(self):
- stack = UndoableActionStack("good one!")
+ stack = UndoableActionStack("good one!", mergeable=False)
gobject = mock.Mock()
stack.push(PropertyChangedAction(gobject, "field", 5, 7))
stack.push(PropertyChangedAction(gobject, "field", 11, 13))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]