Home | History | Annotate | Download | only in idlelib
      1 import string
      2 from Tkinter import *
      3 
      4 from idlelib.Delegator import Delegator
      5 
      6 #$ event <<redo>>
      7 #$ win <Control-y>
      8 #$ unix <Alt-z>
      9 
     10 #$ event <<undo>>
     11 #$ win <Control-z>
     12 #$ unix <Control-z>
     13 
     14 #$ event <<dump-undo-state>>
     15 #$ win <Control-backslash>
     16 #$ unix <Control-backslash>
     17 
     18 
     19 class UndoDelegator(Delegator):
     20 
     21     max_undo = 1000
     22 
     23     def __init__(self):
     24         Delegator.__init__(self)
     25         self.reset_undo()
     26 
     27     def setdelegate(self, delegate):
     28         if self.delegate is not None:
     29             self.unbind("<<undo>>")
     30             self.unbind("<<redo>>")
     31             self.unbind("<<dump-undo-state>>")
     32         Delegator.setdelegate(self, delegate)
     33         if delegate is not None:
     34             self.bind("<<undo>>", self.undo_event)
     35             self.bind("<<redo>>", self.redo_event)
     36             self.bind("<<dump-undo-state>>", self.dump_event)
     37 
     38     def dump_event(self, event):
     39         from pprint import pprint
     40         pprint(self.undolist[:self.pointer])
     41         print "pointer:", self.pointer,
     42         print "saved:", self.saved,
     43         print "can_merge:", self.can_merge,
     44         print "get_saved():", self.get_saved()
     45         pprint(self.undolist[self.pointer:])
     46         return "break"
     47 
     48     def reset_undo(self):
     49         self.was_saved = -1
     50         self.pointer = 0
     51         self.undolist = []
     52         self.undoblock = 0  # or a CommandSequence instance
     53         self.set_saved(1)
     54 
     55     def set_saved(self, flag):
     56         if flag:
     57             self.saved = self.pointer
     58         else:
     59             self.saved = -1
     60         self.can_merge = False
     61         self.check_saved()
     62 
     63     def get_saved(self):
     64         return self.saved == self.pointer
     65 
     66     saved_change_hook = None
     67 
     68     def set_saved_change_hook(self, hook):
     69         self.saved_change_hook = hook
     70 
     71     was_saved = -1
     72 
     73     def check_saved(self):
     74         is_saved = self.get_saved()
     75         if is_saved != self.was_saved:
     76             self.was_saved = is_saved
     77             if self.saved_change_hook:
     78                 self.saved_change_hook()
     79 
     80     def insert(self, index, chars, tags=None):
     81         self.addcmd(InsertCommand(index, chars, tags))
     82 
     83     def delete(self, index1, index2=None):
     84         self.addcmd(DeleteCommand(index1, index2))
     85 
     86     # Clients should call undo_block_start() and undo_block_stop()
     87     # around a sequence of editing cmds to be treated as a unit by
     88     # undo & redo.  Nested matching calls are OK, and the inner calls
     89     # then act like nops.  OK too if no editing cmds, or only one
     90     # editing cmd, is issued in between:  if no cmds, the whole
     91     # sequence has no effect; and if only one cmd, that cmd is entered
     92     # directly into the undo list, as if undo_block_xxx hadn't been
     93     # called.  The intent of all that is to make this scheme easy
     94     # to use:  all the client has to worry about is making sure each
     95     # _start() call is matched by a _stop() call.
     96 
     97     def undo_block_start(self):
     98         if self.undoblock == 0:
     99             self.undoblock = CommandSequence()
    100         self.undoblock.bump_depth()
    101 
    102     def undo_block_stop(self):
    103         if self.undoblock.bump_depth(-1) == 0:
    104             cmd = self.undoblock
    105             self.undoblock = 0
    106             if len(cmd) > 0:
    107                 if len(cmd) == 1:
    108                     # no need to wrap a single cmd
    109                     cmd = cmd.getcmd(0)
    110                 # this blk of cmds, or single cmd, has already
    111                 # been done, so don't execute it again
    112                 self.addcmd(cmd, 0)
    113 
    114     def addcmd(self, cmd, execute=True):
    115         if execute:
    116             cmd.do(self.delegate)
    117         if self.undoblock != 0:
    118             self.undoblock.append(cmd)
    119             return
    120         if self.can_merge and self.pointer > 0:
    121             lastcmd = self.undolist[self.pointer-1]
    122             if lastcmd.merge(cmd):
    123                 return
    124         self.undolist[self.pointer:] = [cmd]
    125         if self.saved > self.pointer:
    126             self.saved = -1
    127         self.pointer = self.pointer + 1
    128         if len(self.undolist) > self.max_undo:
    129             ##print "truncating undo list"
    130             del self.undolist[0]
    131             self.pointer = self.pointer - 1
    132             if self.saved >= 0:
    133                 self.saved = self.saved - 1
    134         self.can_merge = True
    135         self.check_saved()
    136 
    137     def undo_event(self, event):
    138         if self.pointer == 0:
    139             self.bell()
    140             return "break"
    141         cmd = self.undolist[self.pointer - 1]
    142         cmd.undo(self.delegate)
    143         self.pointer = self.pointer - 1
    144         self.can_merge = False
    145         self.check_saved()
    146         return "break"
    147 
    148     def redo_event(self, event):
    149         if self.pointer >= len(self.undolist):
    150             self.bell()
    151             return "break"
    152         cmd = self.undolist[self.pointer]
    153         cmd.redo(self.delegate)
    154         self.pointer = self.pointer + 1
    155         self.can_merge = False
    156         self.check_saved()
    157         return "break"
    158 
    159 
    160 class Command:
    161 
    162     # Base class for Undoable commands
    163 
    164     tags = None
    165 
    166     def __init__(self, index1, index2, chars, tags=None):
    167         self.marks_before = {}
    168         self.marks_after = {}
    169         self.index1 = index1
    170         self.index2 = index2
    171         self.chars = chars
    172         if tags:
    173             self.tags = tags
    174 
    175     def __repr__(self):
    176         s = self.__class__.__name__
    177         t = (self.index1, self.index2, self.chars, self.tags)
    178         if self.tags is None:
    179             t = t[:-1]
    180         return s + repr(t)
    181 
    182     def do(self, text):
    183         pass
    184 
    185     def redo(self, text):
    186         pass
    187 
    188     def undo(self, text):
    189         pass
    190 
    191     def merge(self, cmd):
    192         return 0
    193 
    194     def save_marks(self, text):
    195         marks = {}
    196         for name in text.mark_names():
    197             if name != "insert" and name != "current":
    198                 marks[name] = text.index(name)
    199         return marks
    200 
    201     def set_marks(self, text, marks):
    202         for name, index in marks.items():
    203             text.mark_set(name, index)
    204 
    205 
    206 class InsertCommand(Command):
    207 
    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 
    266     # Undoable delete command
    267 
    268     def __init__(self, index1, index2=None):
    269         Command.__init__(self, index1, index2, None, None)
    270 
    271     def do(self, text):
    272         self.marks_before = self.save_marks(text)
    273         self.index1 = text.index(self.index1)
    274         if self.index2:
    275             self.index2 = text.index(self.index2)
    276         else:
    277             self.index2 = text.index(self.index1 + " +1c")
    278         if text.compare(self.index2, ">", "end-1c"):
    279             # Don't delete the final newline
    280             self.index2 = text.index("end-1c")
    281         self.chars = text.get(self.index1, self.index2)
    282         text.delete(self.index1, self.index2)
    283         self.marks_after = self.save_marks(text)
    284         ##sys.__stderr__.write("do: %s\n" % self)
    285 
    286     def redo(self, text):
    287         text.mark_set('insert', self.index1)
    288         text.delete(self.index1, self.index2)
    289         self.set_marks(text, self.marks_after)
    290         text.see('insert')
    291         ##sys.__stderr__.write("redo: %s\n" % self)
    292 
    293     def undo(self, text):
    294         text.mark_set('insert', self.index1)
    295         text.insert(self.index1, self.chars)
    296         self.set_marks(text, self.marks_before)
    297         text.see('insert')
    298         ##sys.__stderr__.write("undo: %s\n" % self)
    299 
    300 class CommandSequence(Command):
    301 
    302     # Wrapper for a sequence of undoable cmds to be undone/redone
    303     # as a unit
    304 
    305     def __init__(self):
    306         self.cmds = []
    307         self.depth = 0
    308 
    309     def __repr__(self):
    310         s = self.__class__.__name__
    311         strs = []
    312         for cmd in self.cmds:
    313             strs.append("    %r" % (cmd,))
    314         return s + "(\n" + ",\n".join(strs) + "\n)"
    315 
    316     def __len__(self):
    317         return len(self.cmds)
    318 
    319     def append(self, cmd):
    320         self.cmds.append(cmd)
    321 
    322     def getcmd(self, i):
    323         return self.cmds[i]
    324 
    325     def redo(self, text):
    326         for cmd in self.cmds:
    327             cmd.redo(text)
    328 
    329     def undo(self, text):
    330         cmds = self.cmds[:]
    331         cmds.reverse()
    332         for cmd in cmds:
    333             cmd.undo(text)
    334 
    335     def bump_depth(self, incr=1):
    336         self.depth = self.depth + incr
    337         return self.depth
    338 
    339 def main():
    340     from idlelib.Percolator import Percolator
    341     root = Tk()
    342     root.wm_protocol("WM_DELETE_WINDOW", root.quit)
    343     text = Text()
    344     text.pack()
    345     text.focus_set()
    346     p = Percolator(text)
    347     d = UndoDelegator()
    348     p.insertfilter(d)
    349     root.mainloop()
    350 
    351 if __name__ == "__main__":
    352     main()
    353