root/gaphor/tags/gaphor-0.12.0/gaphor/services/undomanager.py

Revision 2099, 13.7 kB (checked in by arj..@yirdis.nl, 1 year ago)

Better handling when transactions are rolled back. Fix #68.

  • 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 from zope import interface
18 from zope import component
19
20 from gaphas import state
21 from gaphor.interfaces import IService, IServiceEvent, IActionProvider
22 from gaphor.event import TransactionBegin, TransactionCommit, TransactionRollback
23 from gaphor.transaction import TransactionError, transactional
24
25 from gaphor.UML.event import ElementCreateEvent, ElementDeleteEvent, \
26                              FlushFactoryEvent, ModelFactoryEvent, \
27                              AttributeChangeEvent, AssociationSetEvent, \
28                              AssociationAddEvent, AssociationDeleteEvent
29 from gaphor.UML.interfaces import IElementCreateEvent, IElementDeleteEvent, \
30                                   IAttributeChangeEvent, IModelFactoryEvent, \
31                                   IAssociationChangeEvent
32
33 from gaphor.action import action, build_action_group
34 from gaphor.event import ActionExecuted
35
36
37 class ActionStack(object):
38     """
39     A transaction. Every action that is added between a begin_transaction()
40     and a commit_transaction() call is recorded in a transaction, so it can
41     be played back when a transaction is executed. This executing a
42     transaction has the effect of performing the actions recorded, which will
43     typically undo actions performed by the user.
44     """
45
46     def __init__(self):
47         self._actions = []
48
49     def add(self, action):
50         self._actions.append(action)
51
52     def can_execute(self):
53         return self._actions and True or False
54
55     @transactional
56     def execute(self):
57         self._actions.reverse()
58         for action in self._actions:
59             try:
60                 action()
61             except Exception, e:
62                 log.error('Error while undoing action %s' % action, e)
63
64
65 class UndoManagerStateChanged(object):
66     """
67     Event class used to send state changes on the ndo Manager.
68     """
69     interface.implements(IServiceEvent)
70
71     def __init__(self, service):
72         self.service = service
73
74
75 class UndoManager(object):
76     """
77     Simple transaction manager for Gaphor.
78     This transaction manager supports nested transactions.
79     
80     The Undo manager sports an undo and a redo stack. Each stack contains
81     a set of actions that can be executed, just by calling them (e.i action())
82     If something is returned by an action, that is considered the callable
83     to be used to undo or redo the last performed action.
84     """
85
86     interface.implements(IService, IActionProvider)
87
88     menu_xml = """
89       <ui>
90         <menubar name="mainwindow">
91           <menu action="edit">
92             <placeholder name="primary">
93               <menuitem action="edit-undo" />
94               <menuitem action="edit-redo" />
95               <separator />
96             </placeholder>
97           </menu>
98         </menubar>
99         <toolbar action="mainwindow-toolbar">
100           <toolitem action="edit-undo" />
101           <toolitem action="edit-redo" />
102           <separator />
103         </toolbar>
104       </ui>
105     """
106
107     def __init__(self):
108         self._undo_stack = []
109         self._redo_stack = []
110         self._stack_depth = 20
111         self._current_transaction = None
112         self._transaction_depth = 0
113         self.action_group = build_action_group(self)
114
115     def init(self, app):
116         self._app = app
117         app.register_handler(self.reset)
118         app.register_handler(self.begin_transaction)
119         app.register_handler(self.commit_transaction)
120         app.register_handler(self.rollback_transaction)
121         app.register_handler(self._action_executed)
122         self._register_undo_handlers()
123         self._action_executed()
124
125     def shutdown(self):
126         self._app.unregister_handler(self.reset)
127         self._app.unregister_handler(self.begin_transaction)
128         self._app.unregister_handler(self.commit_transaction)
129         self._app.unregister_handler(self.rollback_transaction)
130         self._app.unregister_handler(self._action_executed)
131         self._unregister_undo_handlers()
132
133     def clear_undo_stack(self):
134         self._undo_stack = []
135         self._current_transaction = None
136
137     def clear_redo_stack(self):
138         del self._redo_stack[:]
139    
140     @component.adapter(IModelFactoryEvent)
141     def reset(self, event=None):
142         self.clear_redo_stack()
143         self.clear_undo_stack()
144         self._action_executed()
145
146     @component.adapter(TransactionBegin)
147     def begin_transaction(self, event=None):
148         """
149         Add an action to the current transaction
150         """
151         if self._current_transaction:
152             self._transaction_depth += 1
153             #raise TransactionError, 'Already in a transaction'
154             return
155
156         self._current_transaction = ActionStack()
157         self._transaction_depth += 1
158
159     def add_undo_action(self, action):
160         """
161         Add an action to undo. An action
162         """
163         #log.debug('add_undo_action: %s %s' % (self._current_transaction, action))
164         if not self._current_transaction:
165             return
166
167         self._current_transaction.add(action)
168         component.handle(UndoManagerStateChanged(self))
169
170         # TODO: should this be placed here?
171         self._action_executed()
172
173     @component.adapter(TransactionCommit)
174     def commit_transaction(self, event=None):
175         #log.debug('commit_transaction')
176         if not self._current_transaction:
177             return #raise TransactionError, 'No transaction to commit'
178
179         self._transaction_depth -= 1
180         if self._transaction_depth == 0:
181             if self._current_transaction.can_execute():
182                 self.clear_redo_stack()
183                 self._undo_stack.append(self._current_transaction)
184                 while len(self._undo_stack) > self._stack_depth:
185                     del self._undo_stack[0]
186             else:
187                 pass #log.debug('nothing to commit')
188
189             self._current_transaction = None
190         component.handle(UndoManagerStateChanged(self))
191         self._action_executed()
192
193     @component.adapter(TransactionRollback)
194     def rollback_transaction(self, event=None):
195         """
196         Roll back the transaction we're in.
197         """
198         if not self._current_transaction:
199             raise TransactionError, 'No transaction to rollback'
200
201         self._transaction_depth -= 1
202         if self._transaction_depth == 0:
203             errorous_tx = self._current_transaction
204             self._current_transaction = None
205             self.begin_transaction()
206             try:
207                 try:
208                     errorous_tx.execute()
209                 except Exception, e:
210                     log.error('Error while rolling back', e)
211             finally:
212                 # Discard all data collected in the rollback "transaction"
213                 self.discard_transaction()
214
215             component.handle(UndoManagerStateChanged(self))
216
217         self._action_executed()
218
219     def discard_transaction(self):
220         if not self._current_transaction:
221             raise TransactionError, 'No transaction to discard'
222
223         self._transaction_depth -= 1
224         if self._transaction_depth == 0:
225             self._current_transaction = None
226         component.handle(UndoManagerStateChanged(self))
227         self._action_executed()
228
229     @action(name='edit-undo', stock_id='gtk-undo', accel='<Control>z')
230     def undo_transaction(self):
231         if not self._undo_stack:
232             return
233
234         if self._current_transaction:
235             log.warning('Trying to undo a transaction, while in a transaction')
236             self.commit_transaction()
237         transaction = self._undo_stack.pop()
238
239         self._current_transaction = ActionStack()
240         self._transaction_depth += 1
241
242         transaction.execute()
243
244         assert self._transaction_depth == 1
245         self._redo_stack.append(self._current_transaction)
246         self._current_transaction = None
247         self._transaction_depth = 0
248         component.handle(UndoManagerStateChanged(self))
249         self._action_executed()
250
251     @action(name='edit-redo', stock_id='gtk-redo', accel='<Control>y')
252     def redo_transaction(self):
253         if not self._redo_stack:
254             return
255
256         transaction = self._redo_stack.pop()
257
258         self._current_transaction = ActionStack()
259         self._transaction_depth += 1
260
261         transaction.execute()
262
263         assert self._transaction_depth == 1
264         self._undo_stack.append(self._current_transaction)
265         self._current_transaction = None
266         self._transaction_depth = 0
267
268         component.handle(UndoManagerStateChanged(self))
269         self._action_executed()
270
271     def in_transaction(self):
272         return self._current_transaction is not None
273
274     def can_undo(self):
275         return bool(self._current_transaction or self._undo_stack)
276
277     def can_redo(self):
278         return bool(self._redo_stack)
279
280
281     @component.adapter(ActionExecuted)
282     def _action_executed(self, event=None):
283         self.action_group.get_action('edit-undo').set_sensitive(self.can_undo())
284         self.action_group.get_action('edit-redo').set_sensitive(self.can_redo())
285
286     ##
287     ## Undo Handlers
288     ##
289
290     def _undo_handler(self, event):
291         self.add_undo_action(lambda: state.saveapply(*event));
292
293     def _register_undo_handlers(self):
294         self._app.register_handler(self.undo_create_event)
295         self._app.register_handler(self.undo_delete_event)
296         self._app.register_handler(self.undo_attribute_change_event)
297         self._app.register_handler(self.undo_association_set_event)
298         self._app.register_handler(self.undo_association_add_event)
299         self._app.register_handler(self.undo_association_delete_event)
300
301         #
302         # Direct revert-statements from gaphas to the undomanager
303         state.observers.add(state.revert_handler)
304
305         state.subscribers.add(self._undo_handler)
306
307     def _unregister_undo_handlers(self):
308         self._app.unregister_handler(self.undo_create_event)
309         self._app.unregister_handler(self.undo_delete_event)
310         self._app.unregister_handler(self.undo_attribute_change_event)
311         self._app.unregister_handler(self.undo_association_set_event)
312         self._app.unregister_handler(self.undo_association_add_event)
313         self._app.unregister_handler(self.undo_association_delete_event)
314
315         from gaphas import state
316         state.observers.discard(state.revert_handler)
317
318         state.subscribers.discard(self._undo_handler)
319
320
321     @component.adapter(IElementCreateEvent)
322     def undo_create_event(self, event):
323         factory = event.service
324         # A factory is not always present, e.g. for DiagramItems
325         if not factory:
326             return
327         element = event.element
328         def _undo_create_event():
329             try:
330                 del factory._elements[element.id]
331             except KeyError:
332                 pass # Key was probably already removed in an unlink call
333             component.handle(ElementDeleteEvent(factory, element))
334         self.add_undo_action(_undo_create_event)
335
336
337     @component.adapter(IElementDeleteEvent)
338     def undo_delete_event(self, event):
339         factory = event.service
340         # A factory is not always present, e.g. for DiagramItems
341         if not factory:
342             return
343         element = event.element
344         assert factory, 'No factory defined for %s (%s)' % (element, factory)
345         def _undo_delete_event():
346             factory._elements[element.id] = element
347             component.handle(ElementCreateEvent(factory, element))
348         self.add_undo_action(_undo_delete_event)
349
350
351     @component.adapter(IAttributeChangeEvent)
352     def undo_attribute_change_event(self, event):
353         attribute = event.property
354         obj = event.element
355         value = event.old_value
356         def _undo_attribute_change_event():
357             attribute._set(obj, value)
358         self.add_undo_action(_undo_attribute_change_event)
359
360
361     @component.adapter(AssociationSetEvent)
362     def undo_association_set_event(self, event):
363         association = event.property
364         obj = event.element
365         value = event.old_value
366         #print 'got new set event', association, obj, value
367         def _undo_association_set_event():
368             #print 'undoing action', obj, value
369             # Tell the assoctaion it should not need to let the opposite
370             # side connect (it has it's own signal)
371             association._set(obj, value, from_opposite=True)
372         self.add_undo_action(_undo_association_set_event)
373
374
375     @component.adapter(AssociationAddEvent)
376     def undo_association_add_event(self, event):
377         association = event.property
378         obj = event.element
379         value = event.new_value
380         def _undo_association_add_event():
381             #print 'undoing action', obj, value
382             # Tell the assoctaion it should not need to let the opposite
383             # side connect (it has it's own signal)
384             association._del(obj, value, from_opposite=True)
385         self.add_undo_action(_undo_association_add_event)
386
387
388     @component.adapter(AssociationDeleteEvent)
389     def undo_association_delete_event(self, event):
390         association = event.property
391         obj = event.element
392         value = event.old_value
393         def _undo_association_delete_event():
394             #print 'undoing action', obj, value
395             # Tell the assoctaion it should not need to let the opposite
396             # side connect (it has it's own signal)
397             association._set(obj, value, from_opposite=True)
398         self.add_undo_action(_undo_association_delete_event)
399
400
401 # vim:sw=4:et:ai
Note: See TracBrowser for help on using the browser.