1import string 2from Tkinter import * 3 4from idlelib.Delegator import Delegator 5 6#$ event <<redo>> 7#$ win <Control-y> 8#$ unix <Alt-z> 9 10#$ event <<undo>> 11#$ win <Control-z> 12#$ unix <Control-z> 13 14#$ event <<dump-undo-state>> 15#$ win <Control-backslash> 16#$ unix <Control-backslash> 17 18 19class UndoDelegator(Delegator): 20 21 max_undo = 1000 22 23 def __init__(self): 24 Delegator.__init__(self) 25 self.reset_undo() 26 27 def setdelegate(self, delegate): 28 if self.delegate is not None: 29 self.unbind("<<undo>>") 30 self.unbind("<<redo>>") 31 self.unbind("<<dump-undo-state>>") 32 Delegator.setdelegate(self, delegate) 33 if delegate is not None: 34 self.bind("<<undo>>", self.undo_event) 35 self.bind("<<redo>>", self.redo_event) 36 self.bind("<<dump-undo-state>>", self.dump_event) 37 38 def dump_event(self, event): 39 from pprint import pprint 40 pprint(self.undolist[:self.pointer]) 41 print "pointer:", self.pointer, 42 print "saved:", self.saved, 43 print "can_merge:", self.can_merge, 44 print "get_saved():", self.get_saved() 45 pprint(self.undolist[self.pointer:]) 46 return "break" 47 48 def reset_undo(self): 49 self.was_saved = -1 50 self.pointer = 0 51 self.undolist = [] 52 self.undoblock = 0 # or a CommandSequence instance 53 self.set_saved(1) 54 55 def set_saved(self, flag): 56 if flag: 57 self.saved = self.pointer 58 else: 59 self.saved = -1 60 self.can_merge = False 61 self.check_saved() 62 63 def get_saved(self): 64 return self.saved == self.pointer 65 66 saved_change_hook = None 67 68 def set_saved_change_hook(self, hook): 69 self.saved_change_hook = hook 70 71 was_saved = -1 72 73 def check_saved(self): 74 is_saved = self.get_saved() 75 if is_saved != self.was_saved: 76 self.was_saved = is_saved 77 if self.saved_change_hook: 78 self.saved_change_hook() 79 80 def insert(self, index, chars, tags=None): 81 self.addcmd(InsertCommand(index, chars, tags)) 82 83 def delete(self, index1, index2=None): 84 self.addcmd(DeleteCommand(index1, index2)) 85 86 # Clients should call undo_block_start() and undo_block_stop() 87 # around a sequence of editing cmds to be treated as a unit by 88 # undo & redo. Nested matching calls are OK, and the inner calls 89 # then act like nops. OK too if no editing cmds, or only one 90 # editing cmd, is issued in between: if no cmds, the whole 91 # sequence has no effect; and if only one cmd, that cmd is entered 92 # directly into the undo list, as if undo_block_xxx hadn't been 93 # called. The intent of all that is to make this scheme easy 94 # to use: all the client has to worry about is making sure each 95 # _start() call is matched by a _stop() call. 96 97 def undo_block_start(self): 98 if self.undoblock == 0: 99 self.undoblock = CommandSequence() 100 self.undoblock.bump_depth() 101 102 def undo_block_stop(self): 103 if self.undoblock.bump_depth(-1) == 0: 104 cmd = self.undoblock 105 self.undoblock = 0 106 if len(cmd) > 0: 107 if len(cmd) == 1: 108 # no need to wrap a single cmd 109 cmd = cmd.getcmd(0) 110 # this blk of cmds, or single cmd, has already 111 # been done, so don't execute it again 112 self.addcmd(cmd, 0) 113 114 def addcmd(self, cmd, execute=True): 115 if execute: 116 cmd.do(self.delegate) 117 if self.undoblock != 0: 118 self.undoblock.append(cmd) 119 return 120 if self.can_merge and self.pointer > 0: 121 lastcmd = self.undolist[self.pointer-1] 122 if lastcmd.merge(cmd): 123 return 124 self.undolist[self.pointer:] = [cmd] 125 if self.saved > self.pointer: 126 self.saved = -1 127 self.pointer = self.pointer + 1 128 if len(self.undolist) > self.max_undo: 129 ##print "truncating undo list" 130 del self.undolist[0] 131 self.pointer = self.pointer - 1 132 if self.saved >= 0: 133 self.saved = self.saved - 1 134 self.can_merge = True 135 self.check_saved() 136 137 def undo_event(self, event): 138 if self.pointer == 0: 139 self.bell() 140 return "break" 141 cmd = self.undolist[self.pointer - 1] 142 cmd.undo(self.delegate) 143 self.pointer = self.pointer - 1 144 self.can_merge = False 145 self.check_saved() 146 return "break" 147 148 def redo_event(self, event): 149 if self.pointer >= len(self.undolist): 150 self.bell() 151 return "break" 152 cmd = self.undolist[self.pointer] 153 cmd.redo(self.delegate) 154 self.pointer = self.pointer + 1 155 self.can_merge = False 156 self.check_saved() 157 return "break" 158 159 160class Command: 161 162 # Base class for Undoable commands 163 164 tags = None 165 166 def __init__(self, index1, index2, chars, tags=None): 167 self.marks_before = {} 168 self.marks_after = {} 169 self.index1 = index1 170 self.index2 = index2 171 self.chars = chars 172 if tags: 173 self.tags = tags 174 175 def __repr__(self): 176 s = self.__class__.__name__ 177 t = (self.index1, self.index2, self.chars, self.tags) 178 if self.tags is None: 179 t = t[:-1] 180 return s + repr(t) 181 182 def do(self, text): 183 pass 184 185 def redo(self, text): 186 pass 187 188 def undo(self, text): 189 pass 190 191 def merge(self, cmd): 192 return 0 193 194 def save_marks(self, text): 195 marks = {} 196 for name in text.mark_names(): 197 if name != "insert" and name != "current": 198 marks[name] = text.index(name) 199 return marks 200 201 def set_marks(self, text, marks): 202 for name, index in marks.items(): 203 text.mark_set(name, index) 204 205 206class InsertCommand(Command): 207 208 # Undoable insert command 209 210 def __init__(self, index1, chars, tags=None): 211 Command.__init__(self, index1, None, chars, tags) 212 213 def do(self, text): 214 self.marks_before = self.save_marks(text) 215 self.index1 = text.index(self.index1) 216 if text.compare(self.index1, ">", "end-1c"): 217 # Insert before the final newline 218 self.index1 = text.index("end-1c") 219 text.insert(self.index1, self.chars, self.tags) 220 self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) 221 self.marks_after = self.save_marks(text) 222 ##sys.__stderr__.write("do: %s\n" % self) 223 224 def redo(self, text): 225 text.mark_set('insert', self.index1) 226 text.insert(self.index1, self.chars, self.tags) 227 self.set_marks(text, self.marks_after) 228 text.see('insert') 229 ##sys.__stderr__.write("redo: %s\n" % self) 230 231 def undo(self, text): 232 text.mark_set('insert', self.index1) 233 text.delete(self.index1, self.index2) 234 self.set_marks(text, self.marks_before) 235 text.see('insert') 236 ##sys.__stderr__.write("undo: %s\n" % self) 237 238 def merge(self, cmd): 239 if self.__class__ is not cmd.__class__: 240 return False 241 if self.index2 != cmd.index1: 242 return False 243 if self.tags != cmd.tags: 244 return False 245 if len(cmd.chars) != 1: 246 return False 247 if self.chars and \ 248 self.classify(self.chars[-1]) != self.classify(cmd.chars): 249 return False 250 self.index2 = cmd.index2 251 self.chars = self.chars + cmd.chars 252 return True 253 254 alphanumeric = string.ascii_letters + string.digits + "_" 255 256 def classify(self, c): 257 if c in self.alphanumeric: 258 return "alphanumeric" 259 if c == "\n": 260 return "newline" 261 return "punctuation" 262 263 264class DeleteCommand(Command): 265 266 # Undoable delete command 267 268 def __init__(self, index1, index2=None): 269 Command.__init__(self, index1, index2, None, None) 270 271 def do(self, text): 272 self.marks_before = self.save_marks(text) 273 self.index1 = text.index(self.index1) 274 if self.index2: 275 self.index2 = text.index(self.index2) 276 else: 277 self.index2 = text.index(self.index1 + " +1c") 278 if text.compare(self.index2, ">", "end-1c"): 279 # Don't delete the final newline 280 self.index2 = text.index("end-1c") 281 self.chars = text.get(self.index1, self.index2) 282 text.delete(self.index1, self.index2) 283 self.marks_after = self.save_marks(text) 284 ##sys.__stderr__.write("do: %s\n" % self) 285 286 def redo(self, text): 287 text.mark_set('insert', self.index1) 288 text.delete(self.index1, self.index2) 289 self.set_marks(text, self.marks_after) 290 text.see('insert') 291 ##sys.__stderr__.write("redo: %s\n" % self) 292 293 def undo(self, text): 294 text.mark_set('insert', self.index1) 295 text.insert(self.index1, self.chars) 296 self.set_marks(text, self.marks_before) 297 text.see('insert') 298 ##sys.__stderr__.write("undo: %s\n" % self) 299 300class CommandSequence(Command): 301 302 # Wrapper for a sequence of undoable cmds to be undone/redone 303 # as a unit 304 305 def __init__(self): 306 self.cmds = [] 307 self.depth = 0 308 309 def __repr__(self): 310 s = self.__class__.__name__ 311 strs = [] 312 for cmd in self.cmds: 313 strs.append(" %r" % (cmd,)) 314 return s + "(\n" + ",\n".join(strs) + "\n)" 315 316 def __len__(self): 317 return len(self.cmds) 318 319 def append(self, cmd): 320 self.cmds.append(cmd) 321 322 def getcmd(self, i): 323 return self.cmds[i] 324 325 def redo(self, text): 326 for cmd in self.cmds: 327 cmd.redo(text) 328 329 def undo(self, text): 330 cmds = self.cmds[:] 331 cmds.reverse() 332 for cmd in cmds: 333 cmd.undo(text) 334 335 def bump_depth(self, incr=1): 336 self.depth = self.depth + incr 337 return self.depth 338 339def _undo_delegator(parent): 340 from idlelib.Percolator import Percolator 341 root = Tk() 342 root.title("Test UndoDelegator") 343 width, height, x, y = list(map(int, re.split('[x+]', parent.geometry()))) 344 root.geometry("+%d+%d"%(x, y + 150)) 345 346 text = Text(root) 347 text.config(height=10) 348 text.pack() 349 text.focus_set() 350 p = Percolator(text) 351 d = UndoDelegator() 352 p.insertfilter(d) 353 354 undo = Button(root, text="Undo", command=lambda:d.undo_event(None)) 355 undo.pack(side='left') 356 redo = Button(root, text="Redo", command=lambda:d.redo_event(None)) 357 redo.pack(side='left') 358 dump = Button(root, text="Dump", command=lambda:d.dump_event(None)) 359 dump.pack(side='left') 360 361 root.mainloop() 362 363if __name__ == "__main__": 364 from idlelib.idle_test.htest import run 365 run(_undo_delegator) 366