blob: 5896bd5d1b6e36e3d018804b2d7a3bce4eb8cdf9 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001import string
Georg Brandl6634bf22008-05-20 07:13:37 +00002from Tkinter import *
David Scherer7aced172000-08-15 01:13:23 +00003from 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
Neal Norwitz672ce572002-11-30 19:04:07 +000059 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +000060 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
Neal Norwitz672ce572002-11-30 19:04:07 +0000113 def addcmd(self, cmd, execute=True):
David Scherer7aced172000-08-15 01:13:23 +0000114 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
Neal Norwitz672ce572002-11-30 19:04:07 +0000133 self.can_merge = True
David Scherer7aced172000-08-15 01:13:23 +0000134 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
Neal Norwitz672ce572002-11-30 19:04:07 +0000143 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000144 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
Neal Norwitz672ce572002-11-30 19:04:07 +0000154 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000155 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]
Walter Dörwald70a6b492004-02-12 17:35:32 +0000179 return s + repr(t)
David Scherer7aced172000-08-15 01:13:23 +0000180
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__:
Neal Norwitz672ce572002-11-30 19:04:07 +0000239 return False
David Scherer7aced172000-08-15 01:13:23 +0000240 if self.index2 != cmd.index1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000241 return False
David Scherer7aced172000-08-15 01:13:23 +0000242 if self.tags != cmd.tags:
Neal Norwitz672ce572002-11-30 19:04:07 +0000243 return False
David Scherer7aced172000-08-15 01:13:23 +0000244 if len(cmd.chars) != 1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000245 return False
David Scherer7aced172000-08-15 01:13:23 +0000246 if self.chars and \
247 self.classify(self.chars[-1]) != self.classify(cmd.chars):
Neal Norwitz672ce572002-11-30 19:04:07 +0000248 return False
David Scherer7aced172000-08-15 01:13:23 +0000249 self.index2 = cmd.index2
250 self.chars = self.chars + cmd.chars
Neal Norwitz672ce572002-11-30 19:04:07 +0000251 return True
David Scherer7aced172000-08-15 01:13:23 +0000252
Kurt B. Kaiser5fab9c62002-09-18 03:30:12 +0000253 alphanumeric = string.ascii_letters + string.digits + "_"
David Scherer7aced172000-08-15 01:13:23 +0000254
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:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000312 strs.append(" %r" % (cmd,))
Kurt B. Kaiser5fab9c62002-09-18 03:30:12 +0000313 return s + "(\n" + ",\n".join(strs) + "\n)"
David Scherer7aced172000-08-15 01:13:23 +0000314
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()