from idlelib.Delegator import Delegator
#$ event <<dump-undo-state>>
#$ win <Control-backslash>
#$ unix <Control-backslash>
class UndoDelegator(Delegator):
def setdelegate(self, delegate):
if self.delegate is not None:
self.unbind("<<dump-undo-state>>")
Delegator.setdelegate(self, delegate)
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:])
self.undoblock = 0 # or a CommandSequence instance
def set_saved(self, flag):
self.saved = self.pointer
return self.saved == self.pointer
def set_saved_change_hook(self, hook):
self.saved_change_hook = hook
is_saved = self.get_saved()
if is_saved != self.was_saved:
self.was_saved = is_saved
if 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):
self.undoblock = CommandSequence()
self.undoblock.bump_depth()
def undo_block_stop(self):
if self.undoblock.bump_depth(-1) == 0:
# no need to wrap a single cmd
# this blk of cmds, or single cmd, has already
# been done, so don't execute it again
def addcmd(self, cmd, execute=True):
self.undoblock.append(cmd)
if self.can_merge and self.pointer > 0:
lastcmd = self.undolist[self.pointer-1]
self.undolist[self.pointer:] = [cmd]
if self.saved > self.pointer:
self.pointer = self.pointer + 1
if len(self.undolist) > self.max_undo:
##print "truncating undo list"
self.pointer = self.pointer - 1
self.saved = self.saved - 1
def undo_event(self, event):
cmd = self.undolist[self.pointer - 1]
self.pointer = self.pointer - 1
def redo_event(self, event):
if self.pointer >= len(self.undolist):
cmd = self.undolist[self.pointer]
self.pointer = self.pointer + 1
# Base class for Undoable commands
def __init__(self, index1, index2, chars, tags=None):
s = self.__class__.__name__
t = (self.index1, self.index2, self.chars, self.tags)
def save_marks(self, text):
for name in text.mark_names():
if name != "insert" and name != "current":
marks[name] = text.index(name)
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)
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)
text.mark_set('insert', self.index1)
text.insert(self.index1, self.chars, self.tags)
self.set_marks(text, self.marks_after)
##sys.__stderr__.write("redo: %s\n" % self)
text.mark_set('insert', self.index1)
text.delete(self.index1, self.index2)
self.set_marks(text, self.marks_before)
##sys.__stderr__.write("undo: %s\n" % self)
if self.__class__ is not cmd.__class__:
if self.index2 != cmd.index1:
if self.tags != cmd.tags:
self.classify(self.chars[-1]) != self.classify(cmd.chars):
self.chars = self.chars + cmd.chars
alphanumeric = string.ascii_letters + string.digits + "_"
if c in self.alphanumeric:
class DeleteCommand(Command):
# Undoable delete command
def __init__(self, index1, index2=None):
Command.__init__(self, index1, index2, None, None)
self.marks_before = self.save_marks(text)
self.index1 = text.index(self.index1)
self.index2 = text.index(self.index2)
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)
text.mark_set('insert', self.index1)
text.delete(self.index1, self.index2)
self.set_marks(text, self.marks_after)
##sys.__stderr__.write("redo: %s\n" % self)
text.mark_set('insert', self.index1)
text.insert(self.index1, self.chars)
self.set_marks(text, self.marks_before)
##sys.__stderr__.write("undo: %s\n" % self)
class CommandSequence(Command):
# Wrapper for a sequence of undoable cmds to be undone/redone
s = self.__class__.__name__
strs.append(" %r" % (cmd,))
return s + "(\n" + ",\n".join(strs) + "\n)"
def bump_depth(self, incr=1):
self.depth = self.depth + incr
def _undo_delegator(parent):
from idlelib.Percolator import Percolator
root.title("Test UndoDelegator")
width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
root.geometry("+%d+%d"%(x, y + 150))
undo = Button(root, text="Undo", command=lambda:d.undo_event(None))
redo = Button(root, text="Redo", command=lambda:d.redo_event(None))
dump = Button(root, text="Dump", command=lambda:d.dump_event(None))
if __name__ == "__main__":
from idlelib.idle_test.htest import run