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