PageRenderTime 42ms CodeModel.GetById 22ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/idlelib/UndoDelegator.py

http://unladen-swallow.googlecode.com/
Python | 351 lines | 250 code | 65 blank | 36 comment | 49 complexity | fa36c5ee13d0a35b22231fecc2b2dce5 MD5 | raw file
  1import string
  2from Tkinter import *
  3from Delegator import Delegator
  4
  5#$ event <<redo>>
  6#$ win <Control-y>
  7#$ unix <Alt-z>
  8
  9#$ event <<undo>>
 10#$ win <Control-z>
 11#$ unix <Control-z>
 12
 13#$ event <<dump-undo-state>>
 14#$ win <Control-backslash>
 15#$ unix <Control-backslash>
 16
 17
 18class UndoDelegator(Delegator):
 19
 20    max_undo = 1000
 21
 22    def __init__(self):
 23        Delegator.__init__(self)
 24        self.reset_undo()
 25
 26    def setdelegate(self, delegate):
 27        if self.delegate is not None:
 28            self.unbind("<<undo>>")
 29            self.unbind("<<redo>>")
 30            self.unbind("<<dump-undo-state>>")
 31        Delegator.setdelegate(self, delegate)
 32        if delegate is not None:
 33            self.bind("<<undo>>", self.undo_event)
 34            self.bind("<<redo>>", self.redo_event)
 35            self.bind("<<dump-undo-state>>", self.dump_event)
 36
 37    def dump_event(self, event):
 38        from pprint import pprint
 39        pprint(self.undolist[:self.pointer])
 40        print "pointer:", self.pointer,
 41        print "saved:", self.saved,
 42        print "can_merge:", self.can_merge,
 43        print "get_saved():", self.get_saved()
 44        pprint(self.undolist[self.pointer:])
 45        return "break"
 46
 47    def reset_undo(self):
 48        self.was_saved = -1
 49        self.pointer = 0
 50        self.undolist = []
 51        self.undoblock = 0  # or a CommandSequence instance
 52        self.set_saved(1)
 53
 54    def set_saved(self, flag):
 55        if flag:
 56            self.saved = self.pointer
 57        else:
 58            self.saved = -1
 59        self.can_merge = False
 60        self.check_saved()
 61
 62    def get_saved(self):
 63        return self.saved == self.pointer
 64
 65    saved_change_hook = None
 66
 67    def set_saved_change_hook(self, hook):
 68        self.saved_change_hook = hook
 69
 70    was_saved = -1
 71
 72    def check_saved(self):
 73        is_saved = self.get_saved()
 74        if is_saved != self.was_saved:
 75            self.was_saved = is_saved
 76            if self.saved_change_hook:
 77                self.saved_change_hook()
 78
 79    def insert(self, index, chars, tags=None):
 80        self.addcmd(InsertCommand(index, chars, tags))
 81
 82    def delete(self, index1, index2=None):
 83        self.addcmd(DeleteCommand(index1, index2))
 84
 85    # Clients should call undo_block_start() and undo_block_stop()
 86    # around a sequence of editing cmds to be treated as a unit by
 87    # undo & redo.  Nested matching calls are OK, and the inner calls
 88    # then act like nops.  OK too if no editing cmds, or only one
 89    # editing cmd, is issued in between:  if no cmds, the whole
 90    # sequence has no effect; and if only one cmd, that cmd is entered
 91    # directly into the undo list, as if undo_block_xxx hadn't been
 92    # called.  The intent of all that is to make this scheme easy
 93    # to use:  all the client has to worry about is making sure each
 94    # _start() call is matched by a _stop() call.
 95
 96    def undo_block_start(self):
 97        if self.undoblock == 0:
 98            self.undoblock = CommandSequence()
 99        self.undoblock.bump_depth()
100
101    def undo_block_stop(self):
102        if self.undoblock.bump_depth(-1) == 0:
103            cmd = self.undoblock
104            self.undoblock = 0
105            if len(cmd) > 0:
106                if len(cmd) == 1:
107                    # no need to wrap a single cmd
108                    cmd = cmd.getcmd(0)
109                # this blk of cmds, or single cmd, has already
110                # been done, so don't execute it again
111                self.addcmd(cmd, 0)
112
113    def addcmd(self, cmd, execute=True):
114        if execute:
115            cmd.do(self.delegate)
116        if self.undoblock != 0:
117            self.undoblock.append(cmd)
118            return
119        if self.can_merge and self.pointer > 0:
120            lastcmd = self.undolist[self.pointer-1]
121            if lastcmd.merge(cmd):
122                return
123        self.undolist[self.pointer:] = [cmd]
124        if self.saved > self.pointer:
125            self.saved = -1
126        self.pointer = self.pointer + 1
127        if len(self.undolist) > self.max_undo:
128            ##print "truncating undo list"
129            del self.undolist[0]
130            self.pointer = self.pointer - 1
131            if self.saved >= 0:
132                self.saved = self.saved - 1
133        self.can_merge = True
134        self.check_saved()
135
136    def undo_event(self, event):
137        if self.pointer == 0:
138            self.bell()
139            return "break"
140        cmd = self.undolist[self.pointer - 1]
141        cmd.undo(self.delegate)
142        self.pointer = self.pointer - 1
143        self.can_merge = False
144        self.check_saved()
145        return "break"
146
147    def redo_event(self, event):
148        if self.pointer >= len(self.undolist):
149            self.bell()
150            return "break"
151        cmd = self.undolist[self.pointer]
152        cmd.redo(self.delegate)
153        self.pointer = self.pointer + 1
154        self.can_merge = False
155        self.check_saved()
156        return "break"
157
158
159class Command:
160
161    # Base class for Undoable commands
162
163    tags = None
164
165    def __init__(self, index1, index2, chars, tags=None):
166        self.marks_before = {}
167        self.marks_after = {}
168        self.index1 = index1
169        self.index2 = index2
170        self.chars = chars
171        if tags:
172            self.tags = tags
173
174    def __repr__(self):
175        s = self.__class__.__name__
176        t = (self.index1, self.index2, self.chars, self.tags)
177        if self.tags is None:
178            t = t[:-1]
179        return s + repr(t)
180
181    def do(self, text):
182        pass
183
184    def redo(self, text):
185        pass
186
187    def undo(self, text):
188        pass
189
190    def merge(self, cmd):
191        return 0
192
193    def save_marks(self, text):
194        marks = {}
195        for name in text.mark_names():
196            if name != "insert" and name != "current":
197                marks[name] = text.index(name)
198        return marks
199
200    def set_marks(self, text, marks):
201        for name, index in marks.items():
202            text.mark_set(name, index)
203
204
205class InsertCommand(Command):
206
207    # Undoable insert command
208
209    def __init__(self, index1, chars, tags=None):
210        Command.__init__(self, index1, None, chars, tags)
211
212    def do(self, text):
213        self.marks_before = self.save_marks(text)
214        self.index1 = text.index(self.index1)
215        if text.compare(self.index1, ">", "end-1c"):
216            # Insert before the final newline
217            self.index1 = text.index("end-1c")
218        text.insert(self.index1, self.chars, self.tags)
219        self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
220        self.marks_after = self.save_marks(text)
221        ##sys.__stderr__.write("do: %s\n" % self)
222
223    def redo(self, text):
224        text.mark_set('insert', self.index1)
225        text.insert(self.index1, self.chars, self.tags)
226        self.set_marks(text, self.marks_after)
227        text.see('insert')
228        ##sys.__stderr__.write("redo: %s\n" % self)
229
230    def undo(self, text):
231        text.mark_set('insert', self.index1)
232        text.delete(self.index1, self.index2)
233        self.set_marks(text, self.marks_before)
234        text.see('insert')
235        ##sys.__stderr__.write("undo: %s\n" % self)
236
237    def merge(self, cmd):
238        if self.__class__ is not cmd.__class__:
239            return False
240        if self.index2 != cmd.index1:
241            return False
242        if self.tags != cmd.tags:
243            return False
244        if len(cmd.chars) != 1:
245            return False
246        if self.chars and \
247           self.classify(self.chars[-1]) != self.classify(cmd.chars):
248            return False
249        self.index2 = cmd.index2
250        self.chars = self.chars + cmd.chars
251        return True
252
253    alphanumeric = string.ascii_letters + string.digits + "_"
254
255    def classify(self, c):
256        if c in self.alphanumeric:
257            return "alphanumeric"
258        if c == "\n":
259            return "newline"
260        return "punctuation"
261
262
263class DeleteCommand(Command):
264
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
299class CommandSequence(Command):
300
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
338def main():
339    from Percolator import Percolator
340    root = Tk()
341    root.wm_protocol("WM_DELETE_WINDOW", root.quit)
342    text = Text()
343    text.pack()
344    text.focus_set()
345    p = Percolator(text)
346    d = UndoDelegator()
347    p.insertfilter(d)
348    root.mainloop()
349
350if __name__ == "__main__":
351    main()