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 _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 
363 if __name__ == "__main__":
364     from idlelib.idle_test.htest import run
365     run(_undo_delegator)
366