# UndoManager.py # This file is part of Labyrinth # # Copyright (C) 2006 - Don Scorgie # # Labyrinth is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Labyrinth 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Labyrinth; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA # import gtk # Different modes of operation - redo, undo UNDO = 0 REDO = 1 # Some general actions we should always expect. # Some require special handling within ourselves # (taking care of letter insertion / word insertion) INSERT_LETTER = 100 INSERT_WORD = 101 DELETE_LETTER = 102 DELETE_WORD = 103 TRANSFORM_CANVAS = 104 class UndoAction: def __init__(self, owner, undo_type, callback, *args): self.owner = owner self.undo_type = undo_type self.callback = callback self.text = "" if undo_type == INSERT_LETTER or undo_type == INSERT_WORD or undo_type == DELETE_LETTER or \ undo_type == DELETE_WORD: for z in args: if isinstance(z, basestring): self.text = z break self.args = args def add_arg (self, *args): for t in args: self.args += (t,) class UndoManager: ''' A basic manager for undoing and redoing actions.\ Doesn't do anything itself, instead it marshals \ all the minion classes to do its bidding. Any class \ can add items to its lists and they're corresponding \ methods will be called if and when needed. The \ manager doesn't care what you give it, so long as it has a method to call and an owner''' def __init__(self, top_owner, undo_widget = None, redo_widget = None): self.undo = undo_widget self.redo = redo_widget self.blocked = False self.undo_list = [] self.redo_list = [] if self.undo: self.undo.connect('activate', self.undo_action) if self.redo: self.redo.connect('activate', self.redo_action) self.owner = top_owner self.update_sensitive () def block (self): ''' Used as generally, when an undo is performed a \ signal will be emitted that causes an undo action \ to be added. Use this to block tht from happening \ To add actions again, call unblock''' self.blocked = True def unblock (self): self.blocked = False def set_widgets (self, undo, redo): self.undo = undo self.redo = redo self.undo.connect('activate', self.undo_action) self.redo.connect('activate', self.redo_action) self.update_sensitive () def update_sensitive (self): if not self.undo or not self.redo: return self.undo.set_sensitive(len(self.undo_list) > 0) self.redo.set_sensitive(len(self.redo_list) > 0) def undo_action (self, arg): result = self.undo_list.pop() self.redo_list.append (result) self.update_sensitive () result.callback (result, mode=UNDO) def forget_action (self): result = self.undo_list.pop() self.update_sensitive () result.callback (result, mode=UNDO) def redo_action (self, arg): result = self.redo_list.pop() self.undo_list.append (result) self.update_sensitive () result.callback (result, mode=REDO) def combine_insertions (self, action): final_text = action.text start_iter = action.args[0] length = action.args[2] owner = action.owner cb = action.callback old_attrs = action.args[3] new_attrs = action.args[4] if len (self.undo_list) > 0: back = self.undo_list.pop () else: self.undo_list.append (action) return add_back = True while back and back.owner == owner \ and (back.undo_type == INSERT_LETTER or back.undo_type == INSERT_WORD): if back.text.rfind(' ') != -1: break old_attrs = back.args[3] if back.args[0] <= start_iter: start_iter = back.args[0] final_text = back.text+final_text else: final_text += back.text length += back.args[2] if len (self.undo_list) == 0: add_back = False break back = self.undo_list.pop () if add_back: self.undo_list.append (back) combi = UndoAction (owner, INSERT_WORD, cb, start_iter, final_text, length, old_attrs, new_attrs) self.undo_list.append (combi) def combine_deletions (self, action): bytes = True byte_collection = action.args[3] final_text = action.text start_iter = action.args[0] length = action.args[2] owner = action.owner cb = action.callback old_attrs = action.args[4] new_attrs = action.args[5] if len (self.undo_list) > 0: back = self.undo_list.pop () else: self.undo_list.append (action) return add_back = True while back and back.owner == owner \ and (back.undo_type == DELETE_LETTER or back.undo_type == DELETE_WORD): if back.text.rfind(' ') != -1: break old_attrs = back.args[4] if back.args[0] <= start_iter: start_iter = back.args[0] final_text = back.text+final_text if bytes: byte_collection = back.args[3] + byte_collection else: final_text += back.text if bytes: byte_collection += back.args[3] length += back.args[2] if len (self.undo_list) == 0: add_back = False break back = self.undo_list.pop () if add_back: self.undo_list.append (back) if bytes: combi = UndoAction (owner, DELETE_WORD, cb, start_iter, final_text, length, byte_collection, old_attrs, new_attrs) else: combi = UndoAction (owner, DELETE_WORD, cb, start_iter, final_text, length, -1, old_attrs, new_attrs) self.undo_list.append (combi) def combine_transforms (self, action): if len (self.undo_list) > 0: back = self.undo_list.pop () else: self.undo_list.append (action) return add_back = True final_zoom = action.args[1] final_trans = action.args[3] orig_zoom = action.args[0] orig_trans = action.args[2] owner = action.owner cb = action.callback while back and back.owner == owner and \ back.undo_type == TRANSFORM_CANVAS: orig_zoom = back.args[0] orig_trans = back.args[2] if len (self.undo_list) == 0: add_back = False break back = self.undo_list.pop () if add_back: self.undo_list.append (back) self.undo_list.append (UndoAction (owner, TRANSFORM_CANVAS, cb, orig_zoom, final_zoom, orig_trans, final_trans)) def peak (self): if len (self.undo_list) > 0: return self.undo_list[-1] else: return UndoAction (None, None, None) def pop (self): if len (self.undo_list) > 0: return self.undo_list.pop () else: return None def exists_undo_action (self): return len(self.undo_list) > 0 def exists_redo_action (self): return len(self.redo_list) > 0 def add_undo (self, action): if self.blocked: return if not isinstance(action, UndoAction): print "Error: Not a valid undo action. Ignoring." return del self.redo_list[:] if action.undo_type == INSERT_LETTER: self.combine_insertions (action) elif action.undo_type == DELETE_LETTER: self.combine_deletions (action) elif action.undo_type == TRANSFORM_CANVAS: self.combine_transforms (action) else: self.undo_list.append (action) self.update_sensitive ()