352 lines · 10.0 KB · Python
RawDownload
import sysimport stringfrom Tkinter import *from Delegator import Delegator #$ event <<redo>>#$ win <Control-y>#$ unix <Alt-z> #$ event <<undo>>#$ win <Control-z>#$ unix <Control-z> #$ event <<dump-undo-state>>#$ win <Control-backslash>#$ unix <Control-backslash>  class UndoDelegator(Delegator):     max_undo = 1000     def __init__(self):        Delegator.__init__(self)        self.reset_undo()     def setdelegate(self, delegate):        if self.delegate is not None:            self.unbind("<<undo>>")            self.unbind("<<redo>>")            self.unbind("<<dump-undo-state>>")        Delegator.setdelegate(self, delegate)        if delegate is not None:            self.bind("<<undo>>", self.undo_event)            self.bind("<<redo>>", self.redo_event)            self.bind("<<dump-undo-state>>", self.dump_event)     def dump_event(self, event):        from pprint import pprint        pprint(self.undolist[:self.pointer])        print "pointer:", self.pointer,        print "saved:", self.saved,        print "can_merge:", self.can_merge,        print "get_saved():", self.get_saved()        pprint(self.undolist[self.pointer:])        return "break"     def reset_undo(self):        self.was_saved = -1        self.pointer = 0        self.undolist = []        self.undoblock = 0  # or a CommandSequence instance        self.set_saved(1)     def set_saved(self, flag):        if flag:            self.saved = self.pointer        else:            self.saved = -1        self.can_merge = False        self.check_saved()     def get_saved(self):        return self.saved == self.pointer     saved_change_hook = None     def set_saved_change_hook(self, hook):        self.saved_change_hook = hook     was_saved = -1     def check_saved(self):        is_saved = self.get_saved()        if is_saved != self.was_saved:            self.was_saved = is_saved            if self.saved_change_hook:                self.saved_change_hook()     def insert(self, index, chars, tags=None):        self.addcmd(InsertCommand(index, chars, tags))     def delete(self, index1, index2=None):        self.addcmd(DeleteCommand(index1, index2))     # Clients should call undo_block_start() and undo_block_stop()    # around a sequence of editing cmds to be treated as a unit by    # undo & redo.  Nested matching calls are OK, and the inner calls    # then act like nops.  OK too if no editing cmds, or only one    # editing cmd, is issued in between:  if no cmds, the whole    # sequence has no effect; and if only one cmd, that cmd is entered    # directly into the undo list, as if undo_block_xxx hadn't been    # called.  The intent of all that is to make this scheme easy    # to use:  all the client has to worry about is making sure each    # _start() call is matched by a _stop() call.     def undo_block_start(self):        if self.undoblock == 0:            self.undoblock = CommandSequence()        self.undoblock.bump_depth()     def undo_block_stop(self):        if self.undoblock.bump_depth(-1) == 0:            cmd = self.undoblock            self.undoblock = 0            if len(cmd) > 0:                if len(cmd) == 1:                    # no need to wrap a single cmd                    cmd = cmd.getcmd(0)                # this blk of cmds, or single cmd, has already                # been done, so don't execute it again                self.addcmd(cmd, 0)     def addcmd(self, cmd, execute=True):        if execute:            cmd.do(self.delegate)        if self.undoblock != 0:            self.undoblock.append(cmd)            return        if self.can_merge and self.pointer > 0:            lastcmd = self.undolist[self.pointer-1]            if lastcmd.merge(cmd):                return        self.undolist[self.pointer:] = [cmd]        if self.saved > self.pointer:            self.saved = -1        self.pointer = self.pointer + 1        if len(self.undolist) > self.max_undo:            ##print "truncating undo list"            del self.undolist[0]            self.pointer = self.pointer - 1            if self.saved >= 0:                self.saved = self.saved - 1        self.can_merge = True        self.check_saved()     def undo_event(self, event):        if self.pointer == 0:            self.bell()            return "break"        cmd = self.undolist[self.pointer - 1]        cmd.undo(self.delegate)        self.pointer = self.pointer - 1        self.can_merge = False        self.check_saved()        return "break"     def redo_event(self, event):        if self.pointer >= len(self.undolist):            self.bell()            return "break"        cmd = self.undolist[self.pointer]        cmd.redo(self.delegate)        self.pointer = self.pointer + 1        self.can_merge = False        self.check_saved()        return "break"  class Command:     # Base class for Undoable commands     tags = None     def __init__(self, index1, index2, chars, tags=None):        self.marks_before = {}        self.marks_after = {}        self.index1 = index1        self.index2 = index2        self.chars = chars        if tags:            self.tags = tags     def __repr__(self):        s = self.__class__.__name__        t = (self.index1, self.index2, self.chars, self.tags)        if self.tags is None:            t = t[:-1]        return s + repr(t)     def do(self, text):        pass     def redo(self, text):        pass     def undo(self, text):        pass     def merge(self, cmd):        return 0     def save_marks(self, text):        marks = {}        for name in text.mark_names():            if name != "insert" and name != "current":                marks[name] = text.index(name)        return marks     def set_marks(self, text, marks):        for name, index in marks.items():            text.mark_set(name, index)  class InsertCommand(Command):     # Undoable insert command     def __init__(self, index1, chars, tags=None):        Command.__init__(self, index1, None, chars, tags)     def do(self, text):        self.marks_before = self.save_marks(text)        self.index1 = text.index(self.index1)        if text.compare(self.index1, ">", "end-1c"):            # Insert before the final newline            self.index1 = text.index("end-1c")        text.insert(self.index1, self.chars, self.tags)        self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))        self.marks_after = self.save_marks(text)        ##sys.__stderr__.write("do: %s\n" % self)     def redo(self, text):        text.mark_set('insert', self.index1)        text.insert(self.index1, self.chars, self.tags)        self.set_marks(text, self.marks_after)        text.see('insert')        ##sys.__stderr__.write("redo: %s\n" % self)     def undo(self, text):        text.mark_set('insert', self.index1)        text.delete(self.index1, self.index2)        self.set_marks(text, self.marks_before)        text.see('insert')        ##sys.__stderr__.write("undo: %s\n" % self)     def merge(self, cmd):        if self.__class__ is not cmd.__class__:            return False        if self.index2 != cmd.index1:            return False        if self.tags != cmd.tags:            return False        if len(cmd.chars) != 1:            return False        if self.chars and \           self.classify(self.chars[-1]) != self.classify(cmd.chars):            return False        self.index2 = cmd.index2        self.chars = self.chars + cmd.chars        return True     alphanumeric = string.ascii_letters + string.digits + "_"     def classify(self, c):        if c in self.alphanumeric:            return "alphanumeric"        if c == "\n":            return "newline"        return "punctuation"  class DeleteCommand(Command):     # Undoable delete command     def __init__(self, index1, index2=None):        Command.__init__(self, index1, index2, None, None)     def do(self, text):        self.marks_before = self.save_marks(text)        self.index1 = text.index(self.index1)        if self.index2:            self.index2 = text.index(self.index2)        else:            self.index2 = text.index(self.index1 + " +1c")        if text.compare(self.index2, ">", "end-1c"):            # Don't delete the final newline            self.index2 = text.index("end-1c")        self.chars = text.get(self.index1, self.index2)        text.delete(self.index1, self.index2)        self.marks_after = self.save_marks(text)        ##sys.__stderr__.write("do: %s\n" % self)     def redo(self, text):        text.mark_set('insert', self.index1)        text.delete(self.index1, self.index2)        self.set_marks(text, self.marks_after)        text.see('insert')        ##sys.__stderr__.write("redo: %s\n" % self)     def undo(self, text):        text.mark_set('insert', self.index1)        text.insert(self.index1, self.chars)        self.set_marks(text, self.marks_before)        text.see('insert')        ##sys.__stderr__.write("undo: %s\n" % self) class CommandSequence(Command):     # Wrapper for a sequence of undoable cmds to be undone/redone    # as a unit     def __init__(self):        self.cmds = []        self.depth = 0     def __repr__(self):        s = self.__class__.__name__        strs = []        for cmd in self.cmds:            strs.append("    %r" % (cmd,))        return s + "(\n" + ",\n".join(strs) + "\n)"     def __len__(self):        return len(self.cmds)     def append(self, cmd):        self.cmds.append(cmd)     def getcmd(self, i):        return self.cmds[i]     def redo(self, text):        for cmd in self.cmds:            cmd.redo(text)     def undo(self, text):        cmds = self.cmds[:]        cmds.reverse()        for cmd in cmds:            cmd.undo(text)     def bump_depth(self, incr=1):        self.depth = self.depth + incr        return self.depth def main():    from Percolator import Percolator    root = Tk()    root.wm_protocol("WM_DELETE_WINDOW", root.quit)    text = Text()    text.pack()    text.focus_set()    p = Percolator(text)    d = UndoDelegator()    p.insertfilter(d)    root.mainloop() if __name__ == "__main__":    main()