Commit | Line | Data |
---|---|---|
86530b38 AT |
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() |