123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- import string
- from idlelib.delegator import Delegator
- # tkinter import not needed because module does not create widgets,
- # although many methods operate on text widget arguments.
- #$ 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, end=' ')
- print("saved:", self.saved, end=' ')
- print("can_merge:", self.can_merge, end=' ')
- 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(f" {cmd!r}")
- 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 _undo_delegator(parent): # htest #
- from tkinter import Toplevel, Text, Button
- from idlelib.percolator import Percolator
- undowin = Toplevel(parent)
- undowin.title("Test UndoDelegator")
- x, y = map(int, parent.geometry().split('+')[1:])
- undowin.geometry("+%d+%d" % (x, y + 175))
- text = Text(undowin, height=10)
- text.pack()
- text.focus_set()
- p = Percolator(text)
- d = UndoDelegator()
- p.insertfilter(d)
- undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None))
- undo.pack(side='left')
- redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None))
- redo.pack(side='left')
- dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None))
- dump.pack(side='left')
- if __name__ == "__main__":
- from unittest import main
- main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
- from idlelib.idle_test.htest import run
- run(_undo_delegator)
|