0001 import sys 0002 import string 0003 from Tkinter import * 0004 from Delegator import Delegator 0005 0006 #$ event <<redo>> 0007 #$ win <Control-y> 0008 #$ unix <Alt-z> 0009 0010 #$ event <<undo>> 0011 #$ win <Control-z> 0012 #$ unix <Control-z> 0013 0014 #$ event <<dump-undo-state>> 0015 #$ win <Control-backslash> 0016 #$ unix <Control-backslash> 0017 0018 0019 class UndoDelegator(Delegator): 0020 0021 max_undo = 1000 0022 0023 def __init__(self): 0024 Delegator.__init__(self) 0025 self.reset_undo() 0026 0027 def setdelegate(self, delegate): 0028 if self.delegate is not None: 0029 self.unbind("<<undo>>") 0030 self.unbind("<<redo>>") 0031 self.unbind("<<dump-undo-state>>") 0032 Delegator.setdelegate(self, delegate) 0033 if delegate is not None: 0034 self.bind("<<undo>>", self.undo_event) 0035 self.bind("<<redo>>", self.redo_event) 0036 self.bind("<<dump-undo-state>>", self.dump_event) 0037 0038 def dump_event(self, event): 0039 from pprint import pprint 0040 pprint(self.undolist[:self.pointer]) 0041 print "pointer:", self.pointer, 0042 print "saved:", self.saved, 0043 print "can_merge:", self.can_merge, 0044 print "get_saved():", self.get_saved() 0045 pprint(self.undolist[self.pointer:]) 0046 return "break" 0047 0048 def reset_undo(self): 0049 self.was_saved = -1 0050 self.pointer = 0 0051 self.undolist = [] 0052 self.undoblock = 0 # or a CommandSequence instance 0053 self.set_saved(1) 0054 0055 def set_saved(self, flag): 0056 if flag: 0057 self.saved = self.pointer 0058 else: 0059 self.saved = -1 0060 self.can_merge = False 0061 self.check_saved() 0062 0063 def get_saved(self): 0064 return self.saved == self.pointer 0065 0066 saved_change_hook = None 0067 0068 def set_saved_change_hook(self, hook): 0069 self.saved_change_hook = hook 0070 0071 was_saved = -1 0072 0073 def check_saved(self): 0074 is_saved = self.get_saved() 0075 if is_saved != self.was_saved: 0076 self.was_saved = is_saved 0077 if self.saved_change_hook: 0078 self.saved_change_hook() 0079 0080 def insert(self, index, chars, tags=None): 0081 self.addcmd(InsertCommand(index, chars, tags)) 0082 0083 def delete(self, index1, index2=None): 0084 self.addcmd(DeleteCommand(index1, index2)) 0085 0086 # Clients should call undo_block_start() and undo_block_stop() 0087 # around a sequence of editing cmds to be treated as a unit by 0088 # undo & redo. Nested matching calls are OK, and the inner calls 0089 # then act like nops. OK too if no editing cmds, or only one 0090 # editing cmd, is issued in between: if no cmds, the whole 0091 # sequence has no effect; and if only one cmd, that cmd is entered 0092 # directly into the undo list, as if undo_block_xxx hadn't been 0093 # called. The intent of all that is to make this scheme easy 0094 # to use: all the client has to worry about is making sure each 0095 # _start() call is matched by a _stop() call. 0096 0097 def undo_block_start(self): 0098 if self.undoblock == 0: 0099 self.undoblock = CommandSequence() 0100 self.undoblock.bump_depth() 0101 0102 def undo_block_stop(self): 0103 if self.undoblock.bump_depth(-1) == 0: 0104 cmd = self.undoblock 0105 self.undoblock = 0 0106 if len(cmd) > 0: 0107 if len(cmd) == 1: 0108 # no need to wrap a single cmd 0109 cmd = cmd.getcmd(0) 0110 # this blk of cmds, or single cmd, has already 0111 # been done, so don't execute it again 0112 self.addcmd(cmd, 0) 0113 0114 def addcmd(self, cmd, execute=True): 0115 if execute: 0116 cmd.do(self.delegate) 0117 if self.undoblock != 0: 0118 self.undoblock.append(cmd) 0119 return 0120 if self.can_merge and self.pointer > 0: 0121 lastcmd = self.undolist[self.pointer-1] 0122 if lastcmd.merge(cmd): 0123 return 0124 self.undolist[self.pointer:] = [cmd] 0125 if self.saved > self.pointer: 0126 self.saved = -1 0127 self.pointer = self.pointer + 1 0128 if len(self.undolist) > self.max_undo: 0129 ##print "truncating undo list" 0130 del self.undolist[0] 0131 self.pointer = self.pointer - 1 0132 if self.saved >= 0: 0133 self.saved = self.saved - 1 0134 self.can_merge = True 0135 self.check_saved() 0136 0137 def undo_event(self, event): 0138 if self.pointer == 0: 0139 self.bell() 0140 return "break" 0141 cmd = self.undolist[self.pointer - 1] 0142 cmd.undo(self.delegate) 0143 self.pointer = self.pointer - 1 0144 self.can_merge = False 0145 self.check_saved() 0146 return "break" 0147 0148 def redo_event(self, event): 0149 if self.pointer >= len(self.undolist): 0150 self.bell() 0151 return "break" 0152 cmd = self.undolist[self.pointer] 0153 cmd.redo(self.delegate) 0154 self.pointer = self.pointer + 1 0155 self.can_merge = False 0156 self.check_saved() 0157 return "break" 0158 0159 0160 class Command: 0161 0162 # Base class for Undoable commands 0163 0164 tags = None 0165 0166 def __init__(self, index1, index2, chars, tags=None): 0167 self.marks_before = {} 0168 self.marks_after = {} 0169 self.index1 = index1 0170 self.index2 = index2 0171 self.chars = chars 0172 if tags: 0173 self.tags = tags 0174 0175 def __repr__(self): 0176 s = self.__class__.__name__ 0177 t = (self.index1, self.index2, self.chars, self.tags) 0178 if self.tags is None: 0179 t = t[:-1] 0180 return s + repr(t) 0181 0182 def do(self, text): 0183 pass 0184 0185 def redo(self, text): 0186 pass 0187 0188 def undo(self, text): 0189 pass 0190 0191 def merge(self, cmd): 0192 return 0 0193 0194 def save_marks(self, text): 0195 marks = {} 0196 for name in text.mark_names(): 0197 if name != "insert" and name != "current": 0198 marks[name] = text.index(name) 0199 return marks 0200 0201 def set_marks(self, text, marks): 0202 for name, index in marks.items(): 0203 text.mark_set(name, index) 0204 0205 0206 class InsertCommand(Command): 0207 0208 # Undoable insert command 0209 0210 def __init__(self, index1, chars, tags=None): 0211 Command.__init__(self, index1, None, chars, tags) 0212 0213 def do(self, text): 0214 self.marks_before = self.save_marks(text) 0215 self.index1 = text.index(self.index1) 0216 if text.compare(self.index1, ">", "end-1c"): 0217 # Insert before the final newline 0218 self.index1 = text.index("end-1c") 0219 text.insert(self.index1, self.chars, self.tags) 0220 self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) 0221 self.marks_after = self.save_marks(text) 0222 ##sys.__stderr__.write("do: %s\n" % self) 0223 0224 def redo(self, text): 0225 text.mark_set('insert', self.index1) 0226 text.insert(self.index1, self.chars, self.tags) 0227 self.set_marks(text, self.marks_after) 0228 text.see('insert') 0229 ##sys.__stderr__.write("redo: %s\n" % self) 0230 0231 def undo(self, text): 0232 text.mark_set('insert', self.index1) 0233 text.delete(self.index1, self.index2) 0234 self.set_marks(text, self.marks_before) 0235 text.see('insert') 0236 ##sys.__stderr__.write("undo: %s\n" % self) 0237 0238 def merge(self, cmd): 0239 if self.__class__ is not cmd.__class__: 0240 return False 0241 if self.index2 != cmd.index1: 0242 return False 0243 if self.tags != cmd.tags: 0244 return False 0245 if len(cmd.chars) != 1: 0246 return False 0247 if self.chars and \ 0248 self.classify(self.chars[-1]) != self.classify(cmd.chars): 0249 return False 0250 self.index2 = cmd.index2 0251 self.chars = self.chars + cmd.chars 0252 return True 0253 0254 alphanumeric = string.ascii_letters + string.digits + "_" 0255 0256 def classify(self, c): 0257 if c in self.alphanumeric: 0258 return "alphanumeric" 0259 if c == "\n": 0260 return "newline" 0261 return "punctuation" 0262 0263 0264 class DeleteCommand(Command): 0265 0266 # Undoable delete command 0267 0268 def __init__(self, index1, index2=None): 0269 Command.__init__(self, index1, index2, None, None) 0270 0271 def do(self, text): 0272 self.marks_before = self.save_marks(text) 0273 self.index1 = text.index(self.index1) 0274 if self.index2: 0275 self.index2 = text.index(self.index2) 0276 else: 0277 self.index2 = text.index(self.index1 + " +1c") 0278 if text.compare(self.index2, ">", "end-1c"): 0279 # Don't delete the final newline 0280 self.index2 = text.index("end-1c") 0281 self.chars = text.get(self.index1, self.index2) 0282 text.delete(self.index1, self.index2) 0283 self.marks_after = self.save_marks(text) 0284 ##sys.__stderr__.write("do: %s\n" % self) 0285 0286 def redo(self, text): 0287 text.mark_set('insert', self.index1) 0288 text.delete(self.index1, self.index2) 0289 self.set_marks(text, self.marks_after) 0290 text.see('insert') 0291 ##sys.__stderr__.write("redo: %s\n" % self) 0292 0293 def undo(self, text): 0294 text.mark_set('insert', self.index1) 0295 text.insert(self.index1, self.chars) 0296 self.set_marks(text, self.marks_before) 0297 text.see('insert') 0298 ##sys.__stderr__.write("undo: %s\n" % self) 0299 0300 class CommandSequence(Command): 0301 0302 # Wrapper for a sequence of undoable cmds to be undone/redone 0303 # as a unit 0304 0305 def __init__(self): 0306 self.cmds = [] 0307 self.depth = 0 0308 0309 def __repr__(self): 0310 s = self.__class__.__name__ 0311 strs = [] 0312 for cmd in self.cmds: 0313 strs.append(" %r" % (cmd,)) 0314 return s + "(\n" + ",\n".join(strs) + "\n)" 0315 0316 def __len__(self): 0317 return len(self.cmds) 0318 0319 def append(self, cmd): 0320 self.cmds.append(cmd) 0321 0322 def getcmd(self, i): 0323 return self.cmds[i] 0324 0325 def redo(self, text): 0326 for cmd in self.cmds: 0327 cmd.redo(text) 0328 0329 def undo(self, text): 0330 cmds = self.cmds[:] 0331 cmds.reverse() 0332 for cmd in cmds: 0333 cmd.undo(text) 0334 0335 def bump_depth(self, incr=1): 0336 self.depth = self.depth + incr 0337 return self.depth 0338 0339 def main(): 0340 from Percolator import Percolator 0341 root = Tk() 0342 root.wm_protocol("WM_DELETE_WINDOW", root.quit) 0343 text = Text() 0344 text.pack() 0345 text.focus_set() 0346 p = Percolator(text) 0347 d = UndoDelegator() 0348 p.insertfilter(d) 0349 root.mainloop() 0350 0351 if __name__ == "__main__": 0352 main() 0353
Generated by PyXR 0.9.4