blob: 3ef14c30ef671904435325afec492e2e2eb8aaae [file] [log] [blame]
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +00001import sys
2import string
3from Tkinter import *
4from Delegator import Delegator
5
Guido van Rossum504b0bf1999-01-02 21:28:54 +00006#$ 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
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000018
19class UndoDelegator(Delegator):
20
21 max_undo = 1000
22
23 def __init__(self):
24 Delegator.__init__(self)
25 self.reset_undo()
Guido van Rossum504b0bf1999-01-02 21:28:54 +000026
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000027 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 = []
Guido van Rossum318a70d1999-05-03 15:49:52 +000052 self.undoblock = 0 # or a CommandSequence instance
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +000053 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 = 0
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
Guido van Rossum318a70d1999-05-03 15:49:52 +000086 # 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=1):
115 if execute:
116 cmd.do(self.delegate)
117 if self.undoblock != 0:
118 self.undoblock.append(cmd)
119 return
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000120 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 = 1
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 = 0
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 = 0
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 + `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 0
241 if self.index2 != cmd.index1:
242 return 0
243 if self.tags != cmd.tags:
244 return 0
245 if len(cmd.chars) != 1:
246 return 0
247 if self.chars and \
248 self.classify(self.chars[-1]) != self.classify(cmd.chars):
249 return 0
250 self.index2 = cmd.index2
251 self.chars = self.chars + cmd.chars
252 return 1
253
Fred Drake79e75e12001-07-20 19:05:50 +0000254 alphanumeric = string.ascii_letters + string.digits + "_"
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000255
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
Guido van Rossum318a70d1999-05-03 15:49:52 +0000300class 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(" " + `cmd`)
314 return s + "(\n" + string.join(strs, ",\n") + "\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
Guido van Rossum3b4ca0d1998-10-10 18:48:31 +0000338
339def 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
351if __name__ == "__main__":
352 main()