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