17db96d56Sopenharmony_ciimport string 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_cifrom idlelib.delegator import Delegator 47db96d56Sopenharmony_ci 57db96d56Sopenharmony_ci# tkinter import not needed because module does not create widgets, 67db96d56Sopenharmony_ci# although many methods operate on text widget arguments. 77db96d56Sopenharmony_ci 87db96d56Sopenharmony_ci#$ event <<redo>> 97db96d56Sopenharmony_ci#$ win <Control-y> 107db96d56Sopenharmony_ci#$ unix <Alt-z> 117db96d56Sopenharmony_ci 127db96d56Sopenharmony_ci#$ event <<undo>> 137db96d56Sopenharmony_ci#$ win <Control-z> 147db96d56Sopenharmony_ci#$ unix <Control-z> 157db96d56Sopenharmony_ci 167db96d56Sopenharmony_ci#$ event <<dump-undo-state>> 177db96d56Sopenharmony_ci#$ win <Control-backslash> 187db96d56Sopenharmony_ci#$ unix <Control-backslash> 197db96d56Sopenharmony_ci 207db96d56Sopenharmony_ci 217db96d56Sopenharmony_ciclass UndoDelegator(Delegator): 227db96d56Sopenharmony_ci 237db96d56Sopenharmony_ci max_undo = 1000 247db96d56Sopenharmony_ci 257db96d56Sopenharmony_ci def __init__(self): 267db96d56Sopenharmony_ci Delegator.__init__(self) 277db96d56Sopenharmony_ci self.reset_undo() 287db96d56Sopenharmony_ci 297db96d56Sopenharmony_ci def setdelegate(self, delegate): 307db96d56Sopenharmony_ci if self.delegate is not None: 317db96d56Sopenharmony_ci self.unbind("<<undo>>") 327db96d56Sopenharmony_ci self.unbind("<<redo>>") 337db96d56Sopenharmony_ci self.unbind("<<dump-undo-state>>") 347db96d56Sopenharmony_ci Delegator.setdelegate(self, delegate) 357db96d56Sopenharmony_ci if delegate is not None: 367db96d56Sopenharmony_ci self.bind("<<undo>>", self.undo_event) 377db96d56Sopenharmony_ci self.bind("<<redo>>", self.redo_event) 387db96d56Sopenharmony_ci self.bind("<<dump-undo-state>>", self.dump_event) 397db96d56Sopenharmony_ci 407db96d56Sopenharmony_ci def dump_event(self, event): 417db96d56Sopenharmony_ci from pprint import pprint 427db96d56Sopenharmony_ci pprint(self.undolist[:self.pointer]) 437db96d56Sopenharmony_ci print("pointer:", self.pointer, end=' ') 447db96d56Sopenharmony_ci print("saved:", self.saved, end=' ') 457db96d56Sopenharmony_ci print("can_merge:", self.can_merge, end=' ') 467db96d56Sopenharmony_ci print("get_saved():", self.get_saved()) 477db96d56Sopenharmony_ci pprint(self.undolist[self.pointer:]) 487db96d56Sopenharmony_ci return "break" 497db96d56Sopenharmony_ci 507db96d56Sopenharmony_ci def reset_undo(self): 517db96d56Sopenharmony_ci self.was_saved = -1 527db96d56Sopenharmony_ci self.pointer = 0 537db96d56Sopenharmony_ci self.undolist = [] 547db96d56Sopenharmony_ci self.undoblock = 0 # or a CommandSequence instance 557db96d56Sopenharmony_ci self.set_saved(1) 567db96d56Sopenharmony_ci 577db96d56Sopenharmony_ci def set_saved(self, flag): 587db96d56Sopenharmony_ci if flag: 597db96d56Sopenharmony_ci self.saved = self.pointer 607db96d56Sopenharmony_ci else: 617db96d56Sopenharmony_ci self.saved = -1 627db96d56Sopenharmony_ci self.can_merge = False 637db96d56Sopenharmony_ci self.check_saved() 647db96d56Sopenharmony_ci 657db96d56Sopenharmony_ci def get_saved(self): 667db96d56Sopenharmony_ci return self.saved == self.pointer 677db96d56Sopenharmony_ci 687db96d56Sopenharmony_ci saved_change_hook = None 697db96d56Sopenharmony_ci 707db96d56Sopenharmony_ci def set_saved_change_hook(self, hook): 717db96d56Sopenharmony_ci self.saved_change_hook = hook 727db96d56Sopenharmony_ci 737db96d56Sopenharmony_ci was_saved = -1 747db96d56Sopenharmony_ci 757db96d56Sopenharmony_ci def check_saved(self): 767db96d56Sopenharmony_ci is_saved = self.get_saved() 777db96d56Sopenharmony_ci if is_saved != self.was_saved: 787db96d56Sopenharmony_ci self.was_saved = is_saved 797db96d56Sopenharmony_ci if self.saved_change_hook: 807db96d56Sopenharmony_ci self.saved_change_hook() 817db96d56Sopenharmony_ci 827db96d56Sopenharmony_ci def insert(self, index, chars, tags=None): 837db96d56Sopenharmony_ci self.addcmd(InsertCommand(index, chars, tags)) 847db96d56Sopenharmony_ci 857db96d56Sopenharmony_ci def delete(self, index1, index2=None): 867db96d56Sopenharmony_ci self.addcmd(DeleteCommand(index1, index2)) 877db96d56Sopenharmony_ci 887db96d56Sopenharmony_ci # Clients should call undo_block_start() and undo_block_stop() 897db96d56Sopenharmony_ci # around a sequence of editing cmds to be treated as a unit by 907db96d56Sopenharmony_ci # undo & redo. Nested matching calls are OK, and the inner calls 917db96d56Sopenharmony_ci # then act like nops. OK too if no editing cmds, or only one 927db96d56Sopenharmony_ci # editing cmd, is issued in between: if no cmds, the whole 937db96d56Sopenharmony_ci # sequence has no effect; and if only one cmd, that cmd is entered 947db96d56Sopenharmony_ci # directly into the undo list, as if undo_block_xxx hadn't been 957db96d56Sopenharmony_ci # called. The intent of all that is to make this scheme easy 967db96d56Sopenharmony_ci # to use: all the client has to worry about is making sure each 977db96d56Sopenharmony_ci # _start() call is matched by a _stop() call. 987db96d56Sopenharmony_ci 997db96d56Sopenharmony_ci def undo_block_start(self): 1007db96d56Sopenharmony_ci if self.undoblock == 0: 1017db96d56Sopenharmony_ci self.undoblock = CommandSequence() 1027db96d56Sopenharmony_ci self.undoblock.bump_depth() 1037db96d56Sopenharmony_ci 1047db96d56Sopenharmony_ci def undo_block_stop(self): 1057db96d56Sopenharmony_ci if self.undoblock.bump_depth(-1) == 0: 1067db96d56Sopenharmony_ci cmd = self.undoblock 1077db96d56Sopenharmony_ci self.undoblock = 0 1087db96d56Sopenharmony_ci if len(cmd) > 0: 1097db96d56Sopenharmony_ci if len(cmd) == 1: 1107db96d56Sopenharmony_ci # no need to wrap a single cmd 1117db96d56Sopenharmony_ci cmd = cmd.getcmd(0) 1127db96d56Sopenharmony_ci # this blk of cmds, or single cmd, has already 1137db96d56Sopenharmony_ci # been done, so don't execute it again 1147db96d56Sopenharmony_ci self.addcmd(cmd, 0) 1157db96d56Sopenharmony_ci 1167db96d56Sopenharmony_ci def addcmd(self, cmd, execute=True): 1177db96d56Sopenharmony_ci if execute: 1187db96d56Sopenharmony_ci cmd.do(self.delegate) 1197db96d56Sopenharmony_ci if self.undoblock != 0: 1207db96d56Sopenharmony_ci self.undoblock.append(cmd) 1217db96d56Sopenharmony_ci return 1227db96d56Sopenharmony_ci if self.can_merge and self.pointer > 0: 1237db96d56Sopenharmony_ci lastcmd = self.undolist[self.pointer-1] 1247db96d56Sopenharmony_ci if lastcmd.merge(cmd): 1257db96d56Sopenharmony_ci return 1267db96d56Sopenharmony_ci self.undolist[self.pointer:] = [cmd] 1277db96d56Sopenharmony_ci if self.saved > self.pointer: 1287db96d56Sopenharmony_ci self.saved = -1 1297db96d56Sopenharmony_ci self.pointer = self.pointer + 1 1307db96d56Sopenharmony_ci if len(self.undolist) > self.max_undo: 1317db96d56Sopenharmony_ci ##print "truncating undo list" 1327db96d56Sopenharmony_ci del self.undolist[0] 1337db96d56Sopenharmony_ci self.pointer = self.pointer - 1 1347db96d56Sopenharmony_ci if self.saved >= 0: 1357db96d56Sopenharmony_ci self.saved = self.saved - 1 1367db96d56Sopenharmony_ci self.can_merge = True 1377db96d56Sopenharmony_ci self.check_saved() 1387db96d56Sopenharmony_ci 1397db96d56Sopenharmony_ci def undo_event(self, event): 1407db96d56Sopenharmony_ci if self.pointer == 0: 1417db96d56Sopenharmony_ci self.bell() 1427db96d56Sopenharmony_ci return "break" 1437db96d56Sopenharmony_ci cmd = self.undolist[self.pointer - 1] 1447db96d56Sopenharmony_ci cmd.undo(self.delegate) 1457db96d56Sopenharmony_ci self.pointer = self.pointer - 1 1467db96d56Sopenharmony_ci self.can_merge = False 1477db96d56Sopenharmony_ci self.check_saved() 1487db96d56Sopenharmony_ci return "break" 1497db96d56Sopenharmony_ci 1507db96d56Sopenharmony_ci def redo_event(self, event): 1517db96d56Sopenharmony_ci if self.pointer >= len(self.undolist): 1527db96d56Sopenharmony_ci self.bell() 1537db96d56Sopenharmony_ci return "break" 1547db96d56Sopenharmony_ci cmd = self.undolist[self.pointer] 1557db96d56Sopenharmony_ci cmd.redo(self.delegate) 1567db96d56Sopenharmony_ci self.pointer = self.pointer + 1 1577db96d56Sopenharmony_ci self.can_merge = False 1587db96d56Sopenharmony_ci self.check_saved() 1597db96d56Sopenharmony_ci return "break" 1607db96d56Sopenharmony_ci 1617db96d56Sopenharmony_ci 1627db96d56Sopenharmony_ciclass Command: 1637db96d56Sopenharmony_ci # Base class for Undoable commands 1647db96d56Sopenharmony_ci 1657db96d56Sopenharmony_ci tags = None 1667db96d56Sopenharmony_ci 1677db96d56Sopenharmony_ci def __init__(self, index1, index2, chars, tags=None): 1687db96d56Sopenharmony_ci self.marks_before = {} 1697db96d56Sopenharmony_ci self.marks_after = {} 1707db96d56Sopenharmony_ci self.index1 = index1 1717db96d56Sopenharmony_ci self.index2 = index2 1727db96d56Sopenharmony_ci self.chars = chars 1737db96d56Sopenharmony_ci if tags: 1747db96d56Sopenharmony_ci self.tags = tags 1757db96d56Sopenharmony_ci 1767db96d56Sopenharmony_ci def __repr__(self): 1777db96d56Sopenharmony_ci s = self.__class__.__name__ 1787db96d56Sopenharmony_ci t = (self.index1, self.index2, self.chars, self.tags) 1797db96d56Sopenharmony_ci if self.tags is None: 1807db96d56Sopenharmony_ci t = t[:-1] 1817db96d56Sopenharmony_ci return s + repr(t) 1827db96d56Sopenharmony_ci 1837db96d56Sopenharmony_ci def do(self, text): 1847db96d56Sopenharmony_ci pass 1857db96d56Sopenharmony_ci 1867db96d56Sopenharmony_ci def redo(self, text): 1877db96d56Sopenharmony_ci pass 1887db96d56Sopenharmony_ci 1897db96d56Sopenharmony_ci def undo(self, text): 1907db96d56Sopenharmony_ci pass 1917db96d56Sopenharmony_ci 1927db96d56Sopenharmony_ci def merge(self, cmd): 1937db96d56Sopenharmony_ci return 0 1947db96d56Sopenharmony_ci 1957db96d56Sopenharmony_ci def save_marks(self, text): 1967db96d56Sopenharmony_ci marks = {} 1977db96d56Sopenharmony_ci for name in text.mark_names(): 1987db96d56Sopenharmony_ci if name != "insert" and name != "current": 1997db96d56Sopenharmony_ci marks[name] = text.index(name) 2007db96d56Sopenharmony_ci return marks 2017db96d56Sopenharmony_ci 2027db96d56Sopenharmony_ci def set_marks(self, text, marks): 2037db96d56Sopenharmony_ci for name, index in marks.items(): 2047db96d56Sopenharmony_ci text.mark_set(name, index) 2057db96d56Sopenharmony_ci 2067db96d56Sopenharmony_ci 2077db96d56Sopenharmony_ciclass InsertCommand(Command): 2087db96d56Sopenharmony_ci # Undoable insert command 2097db96d56Sopenharmony_ci 2107db96d56Sopenharmony_ci def __init__(self, index1, chars, tags=None): 2117db96d56Sopenharmony_ci Command.__init__(self, index1, None, chars, tags) 2127db96d56Sopenharmony_ci 2137db96d56Sopenharmony_ci def do(self, text): 2147db96d56Sopenharmony_ci self.marks_before = self.save_marks(text) 2157db96d56Sopenharmony_ci self.index1 = text.index(self.index1) 2167db96d56Sopenharmony_ci if text.compare(self.index1, ">", "end-1c"): 2177db96d56Sopenharmony_ci # Insert before the final newline 2187db96d56Sopenharmony_ci self.index1 = text.index("end-1c") 2197db96d56Sopenharmony_ci text.insert(self.index1, self.chars, self.tags) 2207db96d56Sopenharmony_ci self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) 2217db96d56Sopenharmony_ci self.marks_after = self.save_marks(text) 2227db96d56Sopenharmony_ci ##sys.__stderr__.write("do: %s\n" % self) 2237db96d56Sopenharmony_ci 2247db96d56Sopenharmony_ci def redo(self, text): 2257db96d56Sopenharmony_ci text.mark_set('insert', self.index1) 2267db96d56Sopenharmony_ci text.insert(self.index1, self.chars, self.tags) 2277db96d56Sopenharmony_ci self.set_marks(text, self.marks_after) 2287db96d56Sopenharmony_ci text.see('insert') 2297db96d56Sopenharmony_ci ##sys.__stderr__.write("redo: %s\n" % self) 2307db96d56Sopenharmony_ci 2317db96d56Sopenharmony_ci def undo(self, text): 2327db96d56Sopenharmony_ci text.mark_set('insert', self.index1) 2337db96d56Sopenharmony_ci text.delete(self.index1, self.index2) 2347db96d56Sopenharmony_ci self.set_marks(text, self.marks_before) 2357db96d56Sopenharmony_ci text.see('insert') 2367db96d56Sopenharmony_ci ##sys.__stderr__.write("undo: %s\n" % self) 2377db96d56Sopenharmony_ci 2387db96d56Sopenharmony_ci def merge(self, cmd): 2397db96d56Sopenharmony_ci if self.__class__ is not cmd.__class__: 2407db96d56Sopenharmony_ci return False 2417db96d56Sopenharmony_ci if self.index2 != cmd.index1: 2427db96d56Sopenharmony_ci return False 2437db96d56Sopenharmony_ci if self.tags != cmd.tags: 2447db96d56Sopenharmony_ci return False 2457db96d56Sopenharmony_ci if len(cmd.chars) != 1: 2467db96d56Sopenharmony_ci return False 2477db96d56Sopenharmony_ci if self.chars and \ 2487db96d56Sopenharmony_ci self.classify(self.chars[-1]) != self.classify(cmd.chars): 2497db96d56Sopenharmony_ci return False 2507db96d56Sopenharmony_ci self.index2 = cmd.index2 2517db96d56Sopenharmony_ci self.chars = self.chars + cmd.chars 2527db96d56Sopenharmony_ci return True 2537db96d56Sopenharmony_ci 2547db96d56Sopenharmony_ci alphanumeric = string.ascii_letters + string.digits + "_" 2557db96d56Sopenharmony_ci 2567db96d56Sopenharmony_ci def classify(self, c): 2577db96d56Sopenharmony_ci if c in self.alphanumeric: 2587db96d56Sopenharmony_ci return "alphanumeric" 2597db96d56Sopenharmony_ci if c == "\n": 2607db96d56Sopenharmony_ci return "newline" 2617db96d56Sopenharmony_ci return "punctuation" 2627db96d56Sopenharmony_ci 2637db96d56Sopenharmony_ci 2647db96d56Sopenharmony_ciclass DeleteCommand(Command): 2657db96d56Sopenharmony_ci # Undoable delete command 2667db96d56Sopenharmony_ci 2677db96d56Sopenharmony_ci def __init__(self, index1, index2=None): 2687db96d56Sopenharmony_ci Command.__init__(self, index1, index2, None, None) 2697db96d56Sopenharmony_ci 2707db96d56Sopenharmony_ci def do(self, text): 2717db96d56Sopenharmony_ci self.marks_before = self.save_marks(text) 2727db96d56Sopenharmony_ci self.index1 = text.index(self.index1) 2737db96d56Sopenharmony_ci if self.index2: 2747db96d56Sopenharmony_ci self.index2 = text.index(self.index2) 2757db96d56Sopenharmony_ci else: 2767db96d56Sopenharmony_ci self.index2 = text.index(self.index1 + " +1c") 2777db96d56Sopenharmony_ci if text.compare(self.index2, ">", "end-1c"): 2787db96d56Sopenharmony_ci # Don't delete the final newline 2797db96d56Sopenharmony_ci self.index2 = text.index("end-1c") 2807db96d56Sopenharmony_ci self.chars = text.get(self.index1, self.index2) 2817db96d56Sopenharmony_ci text.delete(self.index1, self.index2) 2827db96d56Sopenharmony_ci self.marks_after = self.save_marks(text) 2837db96d56Sopenharmony_ci ##sys.__stderr__.write("do: %s\n" % self) 2847db96d56Sopenharmony_ci 2857db96d56Sopenharmony_ci def redo(self, text): 2867db96d56Sopenharmony_ci text.mark_set('insert', self.index1) 2877db96d56Sopenharmony_ci text.delete(self.index1, self.index2) 2887db96d56Sopenharmony_ci self.set_marks(text, self.marks_after) 2897db96d56Sopenharmony_ci text.see('insert') 2907db96d56Sopenharmony_ci ##sys.__stderr__.write("redo: %s\n" % self) 2917db96d56Sopenharmony_ci 2927db96d56Sopenharmony_ci def undo(self, text): 2937db96d56Sopenharmony_ci text.mark_set('insert', self.index1) 2947db96d56Sopenharmony_ci text.insert(self.index1, self.chars) 2957db96d56Sopenharmony_ci self.set_marks(text, self.marks_before) 2967db96d56Sopenharmony_ci text.see('insert') 2977db96d56Sopenharmony_ci ##sys.__stderr__.write("undo: %s\n" % self) 2987db96d56Sopenharmony_ci 2997db96d56Sopenharmony_ci 3007db96d56Sopenharmony_ciclass CommandSequence(Command): 3017db96d56Sopenharmony_ci # Wrapper for a sequence of undoable cmds to be undone/redone 3027db96d56Sopenharmony_ci # as a unit 3037db96d56Sopenharmony_ci 3047db96d56Sopenharmony_ci def __init__(self): 3057db96d56Sopenharmony_ci self.cmds = [] 3067db96d56Sopenharmony_ci self.depth = 0 3077db96d56Sopenharmony_ci 3087db96d56Sopenharmony_ci def __repr__(self): 3097db96d56Sopenharmony_ci s = self.__class__.__name__ 3107db96d56Sopenharmony_ci strs = [] 3117db96d56Sopenharmony_ci for cmd in self.cmds: 3127db96d56Sopenharmony_ci strs.append(f" {cmd!r}") 3137db96d56Sopenharmony_ci return s + "(\n" + ",\n".join(strs) + "\n)" 3147db96d56Sopenharmony_ci 3157db96d56Sopenharmony_ci def __len__(self): 3167db96d56Sopenharmony_ci return len(self.cmds) 3177db96d56Sopenharmony_ci 3187db96d56Sopenharmony_ci def append(self, cmd): 3197db96d56Sopenharmony_ci self.cmds.append(cmd) 3207db96d56Sopenharmony_ci 3217db96d56Sopenharmony_ci def getcmd(self, i): 3227db96d56Sopenharmony_ci return self.cmds[i] 3237db96d56Sopenharmony_ci 3247db96d56Sopenharmony_ci def redo(self, text): 3257db96d56Sopenharmony_ci for cmd in self.cmds: 3267db96d56Sopenharmony_ci cmd.redo(text) 3277db96d56Sopenharmony_ci 3287db96d56Sopenharmony_ci def undo(self, text): 3297db96d56Sopenharmony_ci cmds = self.cmds[:] 3307db96d56Sopenharmony_ci cmds.reverse() 3317db96d56Sopenharmony_ci for cmd in cmds: 3327db96d56Sopenharmony_ci cmd.undo(text) 3337db96d56Sopenharmony_ci 3347db96d56Sopenharmony_ci def bump_depth(self, incr=1): 3357db96d56Sopenharmony_ci self.depth = self.depth + incr 3367db96d56Sopenharmony_ci return self.depth 3377db96d56Sopenharmony_ci 3387db96d56Sopenharmony_ci 3397db96d56Sopenharmony_cidef _undo_delegator(parent): # htest # 3407db96d56Sopenharmony_ci from tkinter import Toplevel, Text, Button 3417db96d56Sopenharmony_ci from idlelib.percolator import Percolator 3427db96d56Sopenharmony_ci undowin = Toplevel(parent) 3437db96d56Sopenharmony_ci undowin.title("Test UndoDelegator") 3447db96d56Sopenharmony_ci x, y = map(int, parent.geometry().split('+')[1:]) 3457db96d56Sopenharmony_ci undowin.geometry("+%d+%d" % (x, y + 175)) 3467db96d56Sopenharmony_ci 3477db96d56Sopenharmony_ci text = Text(undowin, height=10) 3487db96d56Sopenharmony_ci text.pack() 3497db96d56Sopenharmony_ci text.focus_set() 3507db96d56Sopenharmony_ci p = Percolator(text) 3517db96d56Sopenharmony_ci d = UndoDelegator() 3527db96d56Sopenharmony_ci p.insertfilter(d) 3537db96d56Sopenharmony_ci 3547db96d56Sopenharmony_ci undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None)) 3557db96d56Sopenharmony_ci undo.pack(side='left') 3567db96d56Sopenharmony_ci redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) 3577db96d56Sopenharmony_ci redo.pack(side='left') 3587db96d56Sopenharmony_ci dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) 3597db96d56Sopenharmony_ci dump.pack(side='left') 3607db96d56Sopenharmony_ci 3617db96d56Sopenharmony_ciif __name__ == "__main__": 3627db96d56Sopenharmony_ci from unittest import main 3637db96d56Sopenharmony_ci main('idlelib.idle_test.test_undo', verbosity=2, exit=False) 3647db96d56Sopenharmony_ci 3657db96d56Sopenharmony_ci from idlelib.idle_test.htest import run 3667db96d56Sopenharmony_ci run(_undo_delegator) 367