root/gaphor/tags/gaphor-0.9.2/gaphor/undomanager.py

Revision 1135, 7.7 kB (checked in by arjanmol, 2 years ago)

cleanup

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 # vim:sw=4:et:
2 """
3 Undo management for Gaphor.
4
5 Undoing and redoing actions is managed through the UndoManager.
6
7 An undo action should be a callable object (called with no arguments).
8
9 An undo action should return a callable object that acts as redo function.
10 If None is returned the undo action is considered to be the redo action as well.
11
12 NOTE: it would be nice to use actions in conjunction with functools.partial,
13       but that's Python2.5 stuff..
14       A replacement is available in gaphor.misc.partial.
15 """
16
17
18 def get_undo_manager():
19     """Return the default undo manager.
20     """
21     return _default_undo_manager
22
23
24 def transactional(func):
25     """
26     Descriptor. Begins a transaction around the method/function.
27
28     TODO: In casse of an exception, roll back the transaction if the transaction
29     level == 1.
30     """
31     def wrapper(*args, **kwargs):
32         undo_manager = get_undo_manager()
33         undo_manager.begin_transaction()
34         try:
35             func(*args, **kwargs)
36         except:
37             undo_manager.rollback_transaction()
38         else:
39             undo_manager.commit_transaction()
40     return wrapper
41
42
43 class undoableproperty(object):
44     """
45     Property with state preservation.
46     Set and del statements preserve the original value before the operation
47     is performed
48     """
49
50     def __init__(self, fget=None, fset=None, fdel=None, doc=None,
51                  property=None, undo_manager=None):
52         if property:
53             self.fget = property.fget
54             self.fset = property.fset
55             self.fdel = property.fdel
56             self.__doc__ = property.__doc__
57         else:
58             self.fget = fget
59             self.fset = fset
60             self.fdel = fdel
61             self.__doc__ = doc
62         self._undo_manager = undo_manager or get_undo_manager()
63
64     def _preserve_state(self, obj):
65         try:
66             val = self.__get__(obj)
67         except AttributeError:
68             def _del_action():
69                 action = self._preserve_state(obj)
70                 self.__delete__(obj)
71                 return action
72             return _del_action
73         else:
74             def _set_action():
75                 action = self._preserve_state(obj)
76                 self.__set__(obj, val)
77                 return action
78             return _set_action
79
80     def __get__(self, obj, class_=None):
81         if obj:
82             return self.fget(obj)
83         return self.fget
84
85     def __set__(self, obj, value):
86         self._undo_manager.add_undo_action(self._preserve_state(obj))
87         return self.fset(obj, value)
88
89     def __delete__(self, obj):
90         self._undo_manager.add_undo_action(self._preserve_state(obj))
91         return self.fdel(obj)
92
93
94 class TransactionError(Exception):
95
96     def __init__(self, msg):
97         self.args = msg
98
99
100 class Transaction(object):
101     """
102     A transaction. Every action that is added between a begin_transaction()
103     and a commit_transaction() call is recorded in a transaction, so it can
104     be played back when a transaction is executed. This executing a
105     transaction has the effect of performing the actions recorded, which will
106     typically undo actions performed by the user.
107     """
108
109     def __init__(self):
110         self._actions = []
111
112     def add(self, action):
113         self._actions.append(action)
114
115     def can_execute(self):
116         return self._actions and True or False
117
118     def execute(self):
119         self._actions.reverse()
120         contra_transaction = Transaction()
121         for action in self._actions:
122             try:
123                 contra_action = action()
124                 contra_transaction.add(contra_action or action)
125             except Exception, e:
126                 log.error('Error while undoing action %s' % action, e)
127         return contra_transaction
128
129 class UndoManager(object):
130     """
131     Simple transaction manager for Gaphor.
132     This transaction manager supports nested transactions.
133     
134     The Undo manager sports an undo and a redo stack. Each stack contains
135     a set of actions that can be executed, just by calling them (e.i action())
136     If something is returned by an action, that is considered the callable
137     to be used to undo or redo the last performed action.
138     """
139
140     def __init__(self):
141         self._in_undo = False
142         self._undo_stack = []
143         self._redo_stack = []
144         self._stack_depth = 20
145         self._current_transaction = None
146         self._transaction_depth = 0
147
148     def clear_undo_stack(self):
149         self._undo_stack = []
150         self._current_transaction = None
151
152     def clear_redo_stack(self):
153         self._redo_stack = []
154
155     def begin_transaction(self):
156         """
157         Add an action to the current transaction
158         """
159         if self._in_undo:
160             return
161
162         #log.debug('begin_transaction')
163         if self._current_transaction:
164             self._transaction_depth += 1
165             #raise TransactionError, 'Already in a transaction'
166             return
167
168         self._current_transaction = Transaction()
169         self.clear_redo_stack()
170         self._transaction_depth += 1
171
172     def add_undo_action(self, action):
173         """
174         Add an action to undo. An action
175         """
176         #log.debug('add_undo_action: %s %s' % (self._current_transaction, action))
177         if not self._current_transaction:
178             return
179
180         if self._redo_stack:
181             self.clear_redo_stack()
182
183         self._current_transaction.add(action)
184
185     def commit_transaction(self):
186         if self._in_undo:
187             return
188
189         #log.debug('commit_transaction')
190         if not self._current_transaction:
191             return #raise TransactionError, 'No transaction to commit'
192
193         self._transaction_depth -= 1
194         if self._transaction_depth == 0:
195             if self._current_transaction.can_execute():
196                 self._undo_stack.append(self._current_transaction)
197             else:
198                 pass #log.debug('nothing to commit')
199
200             self._current_transaction = None
201
202     def rollback_transaction(self):
203         """
204         Roll back the transaction we're in.
205         """
206         if self._in_undo:
207             return
208
209         if not self._current_transaction:
210             raise TransactionError, 'No transaction to rollback'
211
212         self._transaction_depth -= 1
213         if self._transaction_depth == 0:
214             self._current_transaction.execute()
215             self._current_transaction = None
216         # else: mark for rollback?
217
218     def discard_transaction(self):
219         if self._in_undo:
220             return
221
222         if not self._current_transaction:
223             raise TransactionError, 'No transaction to discard'
224
225         self._transaction_depth -= 1
226         if self._transaction_depth == 0:
227             self._current_transaction = None
228
229     def undo_transaction(self):
230         if not self._undo_stack:
231             return
232
233         if self._current_transaction:
234             log.warning('Trying to undo a transaction, while in a transaction')
235             self.commit_transaction()
236         transaction = self._undo_stack.pop()
237         try:
238             self._in_undo = True
239             redo_transaction = transaction.execute()
240         finally:
241             self._in_undo = False
242         self._redo_stack.append(redo_transaction)
243
244     def redo_transaction(self):
245         if not self._redo_stack:
246             return
247
248         transaction = self._redo_stack.pop()
249         try:
250             self._in_undo = True
251             undo_transaction = transaction.execute()
252         finally:
253             self._in_undo = False
254         self._undo_stack.append(undo_transaction)
255
256     def in_transaction(self):
257         return self._current_transaction is not None
258
259     def can_undo(self):
260         return bool(self._current_transaction or self._undo_stack)
261
262     def can_redo(self):
263         return bool(self._redo_stack)
264
265
266 # Register as resource:
267 import gaphor
268 _default_undo_manager = gaphor.resource(UndoManager)
269 del gaphor
270
Note: See TracBrowser for help on using the browser.