|
1 import sys |
|
2 import string |
|
3 from Tkinter import * |
|
4 from 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 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() |