1import string
2from Tkinter import *
3
4from 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
19class 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
160class 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
206class 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
264class 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
300class 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
339def _undo_delegator(parent):
340    from idlelib.Percolator import Percolator
341    root = Tk()
342    root.title("Test UndoDelegator")
343    width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
344    root.geometry("+%d+%d"%(x, y + 150))
345
346    text = Text(root)
347    text.config(height=10)
348    text.pack()
349    text.focus_set()
350    p = Percolator(text)
351    d = UndoDelegator()
352    p.insertfilter(d)
353
354    undo = Button(root, text="Undo", command=lambda:d.undo_event(None))
355    undo.pack(side='left')
356    redo = Button(root, text="Redo", command=lambda:d.redo_event(None))
357    redo.pack(side='left')
358    dump = Button(root, text="Dump", command=lambda:d.dump_event(None))
359    dump.pack(side='left')
360
361    root.mainloop()
362
363if __name__ == "__main__":
364    from idlelib.idle_test.htest import run
365    run(_undo_delegator)
366