Home | History | Annotate | Download | only in idlelib
      1 import string
      2 
      3 from idlelib.delegator import Delegator
      4 
      5 # tkintter import not needed because module does not create widgets,
      6 # although many methods operate on text widget arguments.
      7 
      8 #$ event <<redo>>
      9 #$ win <Control-y>
     10 #$ unix <Alt-z>
     11 
     12 #$ event <<undo>>
     13 #$ win <Control-z>
     14 #$ unix <Control-z>
     15 
     16 #$ event <<dump-undo-state>>
     17 #$ win <Control-backslash>
     18 #$ unix <Control-backslash>
     19 
     20 
     21 class UndoDelegator(Delegator):
     22 
     23     max_undo = 1000
     24 
     25     def __init__(self):
     26         Delegator.__init__(self)
     27         self.reset_undo()
     28 
     29     def setdelegate(self, delegate):
     30         if self.delegate is not None:
     31             self.unbind("<<undo>>")
     32             self.unbind("<<redo>>")
     33             self.unbind("<<dump-undo-state>>")
     34         Delegator.setdelegate(self, delegate)
     35         if delegate is not None:
     36             self.bind("<<undo>>", self.undo_event)
     37             self.bind("<<redo>>", self.redo_event)
     38             self.bind("<<dump-undo-state>>", self.dump_event)
     39 
     40     def dump_event(self, event):
     41         from pprint import pprint
     42         pprint(self.undolist[:self.pointer])
     43         print("pointer:", self.pointer, end=' ')
     44         print("saved:", self.saved, end=' ')
     45         print("can_merge:", self.can_merge, end=' ')
     46         print("get_saved():", self.get_saved())
     47         pprint(self.undolist[self.pointer:])
     48         return "break"
     49 
     50     def reset_undo(self):
     51         self.was_saved = -1
     52         self.pointer = 0
     53         self.undolist = []
     54         self.undoblock = 0  # or a CommandSequence instance
     55         self.set_saved(1)
     56 
     57     def set_saved(self, flag):
     58         if flag:
     59             self.saved = self.pointer
     60         else:
     61             self.saved = -1
     62         self.can_merge = False
     63         self.check_saved()
     64 
     65     def get_saved(self):
     66         return self.saved == self.pointer
     67 
     68     saved_change_hook = None
     69 
     70     def set_saved_change_hook(self, hook):
     71         self.saved_change_hook = hook
     72 
     73     was_saved = -1
     74 
     75     def check_saved(self):
     76         is_saved = self.get_saved()
     77         if is_saved != self.was_saved:
     78             self.was_saved = is_saved
     79             if self.saved_change_hook:
     80                 self.saved_change_hook()
     81 
     82     def insert(self, index, chars, tags=None):
     83         self.addcmd(InsertCommand(index, chars, tags))
     84 
     85     def delete(self, index1, index2=None):
     86         self.addcmd(DeleteCommand(index1, index2))
     87 
     88     # Clients should call undo_block_start() and undo_block_stop()
     89     # around a sequence of editing cmds to be treated as a unit by
     90     # undo & redo.  Nested matching calls are OK, and the inner calls
     91     # then act like nops.  OK too if no editing cmds, or only one
     92     # editing cmd, is issued in between:  if no cmds, the whole
     93     # sequence has no effect; and if only one cmd, that cmd is entered
     94     # directly into the undo list, as if undo_block_xxx hadn't been
     95     # called.  The intent of all that is to make this scheme easy
     96     # to use:  all the client has to worry about is making sure each
     97     # _start() call is matched by a _stop() call.
     98 
     99     def undo_block_start(self):
    100         if self.undoblock == 0:
    101             self.undoblock = CommandSequence()
    102         self.undoblock.bump_depth()
    103 
    104     def undo_block_stop(self):
    105         if self.undoblock.bump_depth(-1) == 0:
    106             cmd = self.undoblock
    107             self.undoblock = 0
    108             if len(cmd) > 0:
    109                 if len(cmd) == 1:
    110                     # no need to wrap a single cmd
    111                     cmd = cmd.getcmd(0)
    112                 # this blk of cmds, or single cmd, has already
    113                 # been done, so don't execute it again
    114                 self.addcmd(cmd, 0)
    115 
    116     def addcmd(self, cmd, execute=True):
    117         if execute:
    118             cmd.do(self.delegate)
    119         if self.undoblock != 0:
    120             self.undoblock.append(cmd)
    121             return
    122         if self.can_merge and self.pointer > 0:
    123             lastcmd = self.undolist[self.pointer-1]
    124             if lastcmd.merge(cmd):
    125                 return
    126         self.undolist[self.pointer:] = [cmd]
    127         if self.saved > self.pointer:
    128             self.saved = -1
    129         self.pointer = self.pointer + 1
    130         if len(self.undolist) > self.max_undo:
    131             ##print "truncating undo list"
    132             del self.undolist[0]
    133             self.pointer = self.pointer - 1
    134             if self.saved >= 0:
    135                 self.saved = self.saved - 1
    136         self.can_merge = True
    137         self.check_saved()
    138 
    139     def undo_event(self, event):
    140         if self.pointer == 0:
    141             self.bell()
    142             return "break"
    143         cmd = self.undolist[self.pointer - 1]
    144         cmd.undo(self.delegate)
    145         self.pointer = self.pointer - 1
    146         self.can_merge = False
    147         self.check_saved()
    148         return "break"
    149 
    150     def redo_event(self, event):
    151         if self.pointer >= len(self.undolist):
    152             self.bell()
    153             return "break"
    154         cmd = self.undolist[self.pointer]
    155         cmd.redo(self.delegate)
    156         self.pointer = self.pointer + 1
    157         self.can_merge = False
    158         self.check_saved()
    159         return "break"
    160 
    161 
    162 class Command:
    163     # Base class for Undoable commands
    164 
    165     tags = None
    166 
    167     def __init__(self, index1, index2, chars, tags=None):
    168         self.marks_before = {}
    169         self.marks_after = {}
    170         self.index1 = index1
    171         self.index2 = index2
    172         self.chars = chars
    173         if tags:
    174             self.tags = tags
    175 
    176     def __repr__(self):
    177         s = self.__class__.__name__
    178         t = (self.index1, self.index2, self.chars, self.tags)
    179         if self.tags is None:
    180             t = t[:-1]
    181         return s + repr(t)
    182 
    183     def do(self, text):
    184         pass
    185 
    186     def redo(self, text):
    187         pass
    188 
    189     def undo(self, text):
    190         pass
    191 
    192     def merge(self, cmd):
    193         return 0
    194 
    195     def save_marks(self, text):
    196         marks = {}
    197         for name in text.mark_names():
    198             if name != "insert" and name != "current":
    199                 marks[name] = text.index(name)
    200         return marks
    201 
    202     def set_marks(self, text, marks):
    203         for name, index in marks.items():
    204             text.mark_set(name, index)
    205 
    206 
    207 class InsertCommand(Command):
    208     # Undoable insert command
    209 
    210     def __init__(self, index1, chars, tags=None):
    211         Command.__init__(self, index1, None, chars, tags)
    212 
    213     def do(self, text):
    214         self.marks_before = self.save_marks(text)
    215         self.index1 = text.index(self.index1)
    216         if text.compare(self.index1, ">", "end-1c"):
    217             # Insert before the final newline
    218             self.index1 = text.index("end-1c")
    219         text.insert(self.index1, self.chars, self.tags)
    220         self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
    221         self.marks_after = self.save_marks(text)
    222         ##sys.__stderr__.write("do: %s\n" % self)
    223 
    224     def redo(self, text):
    225         text.mark_set('insert', self.index1)
    226         text.insert(self.index1, self.chars, self.tags)
    227         self.set_marks(text, self.marks_after)
    228         text.see('insert')
    229         ##sys.__stderr__.write("redo: %s\n" % self)
    230 
    231     def undo(self, text):
    232         text.mark_set('insert', self.index1)
    233         text.delete(self.index1, self.index2)
    234         self.set_marks(text, self.marks_before)
    235         text.see('insert')
    236         ##sys.__stderr__.write("undo: %s\n" % self)
    237 
    238     def merge(self, cmd):
    239         if self.__class__ is not cmd.__class__:
    240             return False
    241         if self.index2 != cmd.index1:
    242             return False
    243         if self.tags != cmd.tags:
    244             return False
    245         if len(cmd.chars) != 1:
    246             return False
    247         if self.chars and \
    248            self.classify(self.chars[-1]) != self.classify(cmd.chars):
    249             return False
    250         self.index2 = cmd.index2
    251         self.chars = self.chars + cmd.chars
    252         return True
    253 
    254     alphanumeric = string.ascii_letters + string.digits + "_"
    255 
    256     def classify(self, c):
    257         if c in self.alphanumeric:
    258             return "alphanumeric"
    259         if c == "\n":
    260             return "newline"
    261         return "punctuation"
    262 
    263 
    264 class DeleteCommand(Command):
    265     # Undoable delete command
    266 
    267     def __init__(self, index1, index2=None):
    268         Command.__init__(self, index1, index2, None, None)
    269 
    270     def do(self, text):
    271         self.marks_before = self.save_marks(text)
    272         self.index1 = text.index(self.index1)
    273         if self.index2:
    274             self.index2 = text.index(self.index2)
    275         else:
    276             self.index2 = text.index(self.index1 + " +1c")
    277         if text.compare(self.index2, ">", "end-1c"):
    278             # Don't delete the final newline
    279             self.index2 = text.index("end-1c")
    280         self.chars = text.get(self.index1, self.index2)
    281         text.delete(self.index1, self.index2)
    282         self.marks_after = self.save_marks(text)
    283         ##sys.__stderr__.write("do: %s\n" % self)
    284 
    285     def redo(self, text):
    286         text.mark_set('insert', self.index1)
    287         text.delete(self.index1, self.index2)
    288         self.set_marks(text, self.marks_after)
    289         text.see('insert')
    290         ##sys.__stderr__.write("redo: %s\n" % self)
    291 
    292     def undo(self, text):
    293         text.mark_set('insert', self.index1)
    294         text.insert(self.index1, self.chars)
    295         self.set_marks(text, self.marks_before)
    296         text.see('insert')
    297         ##sys.__stderr__.write("undo: %s\n" % self)
    298 
    299 
    300 class CommandSequence(Command):
    301     # Wrapper for a sequence of undoable cmds to be undone/redone
    302     # as a unit
    303 
    304     def __init__(self):
    305         self.cmds = []
    306         self.depth = 0
    307 
    308     def __repr__(self):
    309         s = self.__class__.__name__
    310         strs = []
    311         for cmd in self.cmds:
    312             strs.append("    %r" % (cmd,))
    313         return s + "(\n" + ",\n".join(strs) + "\n)"
    314 
    315     def __len__(self):
    316         return len(self.cmds)
    317 
    318     def append(self, cmd):
    319         self.cmds.append(cmd)
    320 
    321     def getcmd(self, i):
    322         return self.cmds[i]
    323 
    324     def redo(self, text):
    325         for cmd in self.cmds:
    326             cmd.redo(text)
    327 
    328     def undo(self, text):
    329         cmds = self.cmds[:]
    330         cmds.reverse()
    331         for cmd in cmds:
    332             cmd.undo(text)
    333 
    334     def bump_depth(self, incr=1):
    335         self.depth = self.depth + incr
    336         return self.depth
    337 
    338 
    339 def _undo_delegator(parent):  # htest #
    340     from tkinter import Toplevel, Text, Button
    341     from idlelib.percolator import Percolator
    342     undowin = Toplevel(parent)
    343     undowin.title("Test UndoDelegator")
    344     x, y = map(int, parent.geometry().split('+')[1:])
    345     undowin.geometry("+%d+%d" % (x, y + 175))
    346 
    347     text = Text(undowin, height=10)
    348     text.pack()
    349     text.focus_set()
    350     p = Percolator(text)
    351     d = UndoDelegator()
    352     p.insertfilter(d)
    353 
    354     undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None))
    355     undo.pack(side='left')
    356     redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None))
    357     redo.pack(side='left')
    358     dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None))
    359     dump.pack(side='left')
    360 
    361 if __name__ == "__main__":
    362     import unittest
    363     unittest.main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
    364 
    365     from idlelib.idle_test.htest import run
    366     run(_undo_delegator)
    367