5
# Copyright (c) 2009, Alessandro Decina <alessandro.d@gmail.com>
7
# This program is free software; you can redistribute it and/or
8
# modify it under the terms of the GNU Lesser General Public
9
# License as published by the Free Software Foundation; either
10
# version 2.1 of the License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# Lesser General Public License for more details.
17
# You should have received a copy of the GNU Lesser General Public
18
# License along with this program; if not, write to the
19
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20
# Boston, MA 02110-1301, USA.
23
Base classes for the undo/redo feature implementation
26
from pitivi.utils.signal import Signallable
27
from pitivi.utils.loggable import Loggable
30
class UndoError(Exception):
31
""" Any exception related to the undo/redo feature."""
35
class UndoWrongStateError(UndoError):
36
""" Exception related to the current state of the undo/redo stack. """
40
class UndoableAction(Signallable):
42
This class represents an action that can be undone.
43
In other words, when your object's state changes, create an UndoableAction
44
to allow reverting the change if needed later on.
53
raise NotImplementedError()
56
raise NotImplementedError()
59
# Meant to be overridden by UndoableActionStack?
69
class UndoableActionStack(UndoableAction):
71
Simply a stack of UndoableAction objects.
79
def __init__(self, action_group_name):
80
self.action_group_name = action_group_name
81
self.done_actions = []
82
self.undone_actions = []
85
def push(self, action):
86
self.done_actions.append(action)
88
def _runAction(self, action_list, method_name):
89
for action in action_list[::-1]:
90
method = getattr(action, method_name)
94
self._runAction(self.undone_actions, "do")
95
self.done_actions = self.undone_actions[::-1]
99
self._runAction(self.done_actions, "undo")
100
self.undone_actions = self.done_actions[::-1]
104
actions = self.done_actions + self.undone_actions
105
self.undone_actions = []
106
self.done_actions = []
107
self._runAction(actions, "clean")
111
class UndoableActionLog(Signallable, Loggable):
113
This is the "master" class that handles all the undo/redo system. There is
114
only one instance of it in Pitivi: application.py's "action_log" property.
117
"begin": ["stack", "nested"],
118
"push": ["stack", "action"],
119
"rollback": ["stack", "nested"],
120
"commit": ["stack", "nested"],
127
Loggable.__init__(self)
129
self.undo_stacks = []
130
self.redo_stacks = []
133
self._checkpoint = self._takeSnapshot()
135
def begin(self, action_group_name):
136
self.debug("Begining %s", action_group_name)
140
stack = UndoableActionStack(action_group_name)
141
nested = self._stackIsNested(stack)
142
self.stacks.append(stack)
143
self.emit("begin", stack, nested)
145
def push(self, action):
146
self.debug("Pushing %s", action)
151
stack = self._getTopmostStack()
152
except UndoWrongStateError:
156
self.emit("push", stack, action)
162
stack = self._getTopmostStack(pop=True)
165
nested = self._stackIsNested(stack)
166
self.emit("rollback", stack, nested)
173
stack = self._getTopmostStack(pop=True)
176
nested = self._stackIsNested(stack)
178
self.undo_stacks.append(stack)
180
self.stacks[-1].push(stack)
183
self.redo_stacks = []
185
self.debug("%s pushed", stack)
186
self.emit("commit", stack, nested)
189
if self.stacks or not self.undo_stacks:
190
raise UndoWrongStateError()
192
stack = self.undo_stacks.pop(-1)
193
self._runStack(stack, stack.undo)
194
self.redo_stacks.append(stack)
195
self.emit("undo", stack)
198
if self.stacks or not self.redo_stacks:
199
raise UndoWrongStateError()
201
stack = self.redo_stacks.pop(-1)
202
self._runStack(stack, stack.do)
203
self.undo_stacks.append(stack)
204
self.emit("redo", stack)
207
stacks = self.redo_stacks + self.undo_stacks
208
self.redo_stacks = []
209
self.undo_stacks = []
212
self._runStack(stack, stack.clean)
215
def _takeSnapshot(self):
216
return list(self.undo_stacks)
218
def checkpoint(self):
220
raise UndoWrongStateError()
222
self._checkpoint = self._takeSnapshot()
225
current_snapshot = self._takeSnapshot()
226
return current_snapshot != self._checkpoint
228
def _runStack(self, unused_stack, run):
235
def _getTopmostStack(self, pop=False):
239
stack = self.stacks.pop(-1)
241
stack = self.stacks[-1]
243
raise UndoWrongStateError()
247
def _stackIsNested(self, unused_stack):
248
return bool(len(self.stacks))
251
class DebugActionLogObserver(Loggable):
253
Allows getting more debug output from the UndoableActionLog than the default
255
# FIXME: this looks overengineered at first glance. This is used in only one
256
# place in the codebase (in application.py). Why not just put the debug
257
# directly in UndoableActionLog if we really need them anyway?
258
def startObserving(self, log):
259
self._connectToActionLog(log)
261
def stopObserving(self, log):
262
self._disconnectFromActionLog(log)
264
def _connectToActionLog(self, log):
265
log.connect("begin", self._actionLogBeginCb)
266
log.connect("commit", self._actionLogCommitCb)
267
log.connect("rollback", self._actionLogRollbackCb)
268
log.connect("push", self._actionLogPushCb)
270
def _disconnectFromActionLog(self, log):
271
for method in (self._actionLogBeginCb, self._actionLogCommitCb,
272
self._actionLogrollbackCb, self._actionLogPushCb):
273
log.disconnect_by_func(method)
275
def _actionLogBeginCb(self, unused_log, stack, nested):
276
self.debug("begin action %s nested %s", stack.action_group_name, nested)
278
def _actionLogCommitCb(self, unused_log, stack, nested):
279
self.debug("commit action %s nested %s", stack.action_group_name, nested)
281
def _actionLogRollbackCb(self, unused_log, stack, nested):
282
self.debug("rollback action %s nested %s", stack.action_group_name, nested)
284
def _actionLogPushCb(self, unused_log, stack, action):
285
self.debug("push %s in %s", action, stack.action_group_name)
288
class PropertyChangeTracker(Signallable):
290
BaseClass to track a class property, Used for undo/redo
298
def connectToObject(self, obj):
300
self.properties = self._takeCurrentSnapshot(obj)
301
for property_name in self.property_names:
302
signal_name = "notify::" + property_name
303
self.__signals__[signal_name] = []
304
obj.connect(signal_name,
305
self._propertyChangedCb, property_name)
307
def _takeCurrentSnapshot(self, obj):
309
for property_name in self.property_names:
310
properties[property_name] = obj.get_property(property_name.replace("-", "_"))
314
def disconnectFromObject(self, obj):
316
obj.disconnect_by_func(self._propertyChangedCb)
318
def _propertyChangedCb(self, object, value, property_name):
319
old_value = self.properties[property_name]
320
self.properties[property_name] = value
321
self.emit("notify::" + property_name, object, old_value, value)