blob: 2b836a8bac6bf3347a4967879e3d6cdf4b356c7b [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001import sys
2import string
3from Tkinter import *
Guido van Rossum36e0a922007-07-20 04:05:57 +00004
5from .Delegator import Delegator
David Scherer7aced172000-08-15 01:13:23 +00006
7#$ event <<redo>>
8#$ win <Control-y>
9#$ unix <Alt-z>
10
11#$ event <<undo>>
12#$ win <Control-z>
13#$ unix <Control-z>
14
15#$ event <<dump-undo-state>>
16#$ win <Control-backslash>
17#$ unix <Control-backslash>
18
19
20class UndoDelegator(Delegator):
21
22 max_undo = 1000
23
24 def __init__(self):
25 Delegator.__init__(self)
26 self.reset_undo()
27
28 def setdelegate(self, delegate):
29 if self.delegate is not None:
30 self.unbind("<<undo>>")
31 self.unbind("<<redo>>")
32 self.unbind("<<dump-undo-state>>")
33 Delegator.setdelegate(self, delegate)
34 if delegate is not None:
35 self.bind("<<undo>>", self.undo_event)
36 self.bind("<<redo>>", self.redo_event)
37 self.bind("<<dump-undo-state>>", self.dump_event)
38
39 def dump_event(self, event):
40 from pprint import pprint
41 pprint(self.undolist[:self.pointer])
Guido van Rossumbe19ed72007-02-09 05:37:30 +000042 print("pointer:", self.pointer, end=' ')
43 print("saved:", self.saved, end=' ')
44 print("can_merge:", self.can_merge, end=' ')
45 print("get_saved():", self.get_saved())
David Scherer7aced172000-08-15 01:13:23 +000046 pprint(self.undolist[self.pointer:])
47 return "break"
48
49 def reset_undo(self):
50 self.was_saved = -1
51 self.pointer = 0
52 self.undolist = []
53 self.undoblock = 0 # or a CommandSequence instance
54 self.set_saved(1)
55
56 def set_saved(self, flag):
57 if flag:
58 self.saved = self.pointer
59 else:
60 self.saved = -1
Neal Norwitz672ce572002-11-30 19:04:07 +000061 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +000062 self.check_saved()
63
64 def get_saved(self):
65 return self.saved == self.pointer
66
67 saved_change_hook = None
68
69 def set_saved_change_hook(self, hook):
70 self.saved_change_hook = hook
71
72 was_saved = -1
73
74 def check_saved(self):
75 is_saved = self.get_saved()
76 if is_saved != self.was_saved:
77 self.was_saved = is_saved
78 if self.saved_change_hook:
79 self.saved_change_hook()
80
81 def insert(self, index, chars, tags=None):
82 self.addcmd(InsertCommand(index, chars, tags))
83
84 def delete(self, index1, index2=None):
85 self.addcmd(DeleteCommand(index1, index2))
86
87 # Clients should call undo_block_start() and undo_block_stop()
88 # around a sequence of editing cmds to be treated as a unit by
89 # undo & redo. Nested matching calls are OK, and the inner calls
90 # then act like nops. OK too if no editing cmds, or only one
91 # editing cmd, is issued in between: if no cmds, the whole
92 # sequence has no effect; and if only one cmd, that cmd is entered
93 # directly into the undo list, as if undo_block_xxx hadn't been
94 # called. The intent of all that is to make this scheme easy
95 # to use: all the client has to worry about is making sure each
96 # _start() call is matched by a _stop() call.
97
98 def undo_block_start(self):
99 if self.undoblock == 0:
100 self.undoblock = CommandSequence()
101 self.undoblock.bump_depth()
102
103 def undo_block_stop(self):
104 if self.undoblock.bump_depth(-1) == 0:
105 cmd = self.undoblock
106 self.undoblock = 0
107 if len(cmd) > 0:
108 if len(cmd) == 1:
109 # no need to wrap a single cmd
110 cmd = cmd.getcmd(0)
111 # this blk of cmds, or single cmd, has already
112 # been done, so don't execute it again
113 self.addcmd(cmd, 0)
114
Neal Norwitz672ce572002-11-30 19:04:07 +0000115 def addcmd(self, cmd, execute=True):
David Scherer7aced172000-08-15 01:13:23 +0000116 if execute:
117 cmd.do(self.delegate)
118 if self.undoblock != 0:
119 self.undoblock.append(cmd)
120 return
121 if self.can_merge and self.pointer > 0:
122 lastcmd = self.undolist[self.pointer-1]
123 if lastcmd.merge(cmd):
124 return
125 self.undolist[self.pointer:] = [cmd]
126 if self.saved > self.pointer:
127 self.saved = -1
128 self.pointer = self.pointer + 1
129 if len(self.undolist) > self.max_undo:
130 ##print "truncating undo list"
131 del self.undolist[0]
132 self.pointer = self.pointer - 1
133 if self.saved >= 0:
134 self.saved = self.saved - 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000135 self.can_merge = True
David Scherer7aced172000-08-15 01:13:23 +0000136 self.check_saved()
137
138 def undo_event(self, event):
139 if self.pointer == 0:
140 self.bell()
141 return "break"
142 cmd = self.undolist[self.pointer - 1]
143 cmd.undo(self.delegate)
144 self.pointer = self.pointer - 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000145 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000146 self.check_saved()
147 return "break"
148
149 def redo_event(self, event):
150 if self.pointer >= len(self.undolist):
151 self.bell()
152 return "break"
153 cmd = self.undolist[self.pointer]
154 cmd.redo(self.delegate)
155 self.pointer = self.pointer + 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000156 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000157 self.check_saved()
158 return "break"
159
160
161class Command:
162
163 # Base class for Undoable commands
164
165 tags = None
166
167 def __init__(self, index1, index2, chars, tags=None):
168 self.marks_before = {}
169 self.marks_after = {}
170 self.index1 = index1
171 self.index2 = index2
172 self.chars = chars
173 if tags:
174 self.tags = tags
175
176 def __repr__(self):
177 s = self.__class__.__name__
178 t = (self.index1, self.index2, self.chars, self.tags)
179 if self.tags is None:
180 t = t[:-1]
Walter Dörwald70a6b492004-02-12 17:35:32 +0000181 return s + repr(t)
David Scherer7aced172000-08-15 01:13:23 +0000182
183 def do(self, text):
184 pass
185
186 def redo(self, text):
187 pass
188
189 def undo(self, text):
190 pass
191
192 def merge(self, cmd):
193 return 0
194
195 def save_marks(self, text):
196 marks = {}
197 for name in text.mark_names():
198 if name != "insert" and name != "current":
199 marks[name] = text.index(name)
200 return marks
201
202 def set_marks(self, text, marks):
203 for name, index in marks.items():
204 text.mark_set(name, index)
205
206
207class InsertCommand(Command):
208
209 # Undoable insert command
210
211 def __init__(self, index1, chars, tags=None):
212 Command.__init__(self, index1, None, chars, tags)
213
214 def do(self, text):
215 self.marks_before = self.save_marks(text)
216 self.index1 = text.index(self.index1)
217 if text.compare(self.index1, ">", "end-1c"):
218 # Insert before the final newline
219 self.index1 = text.index("end-1c")
220 text.insert(self.index1, self.chars, self.tags)
221 self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
222 self.marks_after = self.save_marks(text)
223 ##sys.__stderr__.write("do: %s\n" % self)
224
225 def redo(self, text):
226 text.mark_set('insert', self.index1)
227 text.insert(self.index1, self.chars, self.tags)
228 self.set_marks(text, self.marks_after)
229 text.see('insert')
230 ##sys.__stderr__.write("redo: %s\n" % self)
231
232 def undo(self, text):
233 text.mark_set('insert', self.index1)
234 text.delete(self.index1, self.index2)
235 self.set_marks(text, self.marks_before)
236 text.see('insert')
237 ##sys.__stderr__.write("undo: %s\n" % self)
238
239 def merge(self, cmd):
240 if self.__class__ is not cmd.__class__:
Neal Norwitz672ce572002-11-30 19:04:07 +0000241 return False
David Scherer7aced172000-08-15 01:13:23 +0000242 if self.index2 != cmd.index1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000243 return False
David Scherer7aced172000-08-15 01:13:23 +0000244 if self.tags != cmd.tags:
Neal Norwitz672ce572002-11-30 19:04:07 +0000245 return False
David Scherer7aced172000-08-15 01:13:23 +0000246 if len(cmd.chars) != 1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000247 return False
David Scherer7aced172000-08-15 01:13:23 +0000248 if self.chars and \
249 self.classify(self.chars[-1]) != self.classify(cmd.chars):
Neal Norwitz672ce572002-11-30 19:04:07 +0000250 return False
David Scherer7aced172000-08-15 01:13:23 +0000251 self.index2 = cmd.index2
252 self.chars = self.chars + cmd.chars
Neal Norwitz672ce572002-11-30 19:04:07 +0000253 return True
David Scherer7aced172000-08-15 01:13:23 +0000254
Kurt B. Kaiser5fab9c62002-09-18 03:30:12 +0000255 alphanumeric = string.ascii_letters + string.digits + "_"
David Scherer7aced172000-08-15 01:13:23 +0000256
257 def classify(self, c):
258 if c in self.alphanumeric:
259 return "alphanumeric"
260 if c == "\n":
261 return "newline"
262 return "punctuation"
263
264
265class DeleteCommand(Command):
266
267 # Undoable delete command
268
269 def __init__(self, index1, index2=None):
270 Command.__init__(self, index1, index2, None, None)
271
272 def do(self, text):
273 self.marks_before = self.save_marks(text)
274 self.index1 = text.index(self.index1)
275 if self.index2:
276 self.index2 = text.index(self.index2)
277 else:
278 self.index2 = text.index(self.index1 + " +1c")
279 if text.compare(self.index2, ">", "end-1c"):
280 # Don't delete the final newline
281 self.index2 = text.index("end-1c")
282 self.chars = text.get(self.index1, self.index2)
283 text.delete(self.index1, self.index2)
284 self.marks_after = self.save_marks(text)
285 ##sys.__stderr__.write("do: %s\n" % self)
286
287 def redo(self, text):
288 text.mark_set('insert', self.index1)
289 text.delete(self.index1, self.index2)
290 self.set_marks(text, self.marks_after)
291 text.see('insert')
292 ##sys.__stderr__.write("redo: %s\n" % self)
293
294 def undo(self, text):
295 text.mark_set('insert', self.index1)
296 text.insert(self.index1, self.chars)
297 self.set_marks(text, self.marks_before)
298 text.see('insert')
299 ##sys.__stderr__.write("undo: %s\n" % self)
300
301class CommandSequence(Command):
302
303 # Wrapper for a sequence of undoable cmds to be undone/redone
304 # as a unit
305
306 def __init__(self):
307 self.cmds = []
308 self.depth = 0
309
310 def __repr__(self):
311 s = self.__class__.__name__
312 strs = []
313 for cmd in self.cmds:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000314 strs.append(" %r" % (cmd,))
Kurt B. Kaiser5fab9c62002-09-18 03:30:12 +0000315 return s + "(\n" + ",\n".join(strs) + "\n)"
David Scherer7aced172000-08-15 01:13:23 +0000316
317 def __len__(self):
318 return len(self.cmds)
319
320 def append(self, cmd):
321 self.cmds.append(cmd)
322
323 def getcmd(self, i):
324 return self.cmds[i]
325
326 def redo(self, text):
327 for cmd in self.cmds:
328 cmd.redo(text)
329
330 def undo(self, text):
331 cmds = self.cmds[:]
332 cmds.reverse()
333 for cmd in cmds:
334 cmd.undo(text)
335
336 def bump_depth(self, incr=1):
337 self.depth = self.depth + incr
338 return self.depth
339
340def main():
Guido van Rossum36e0a922007-07-20 04:05:57 +0000341 from .Percolator import Percolator
David Scherer7aced172000-08-15 01:13:23 +0000342 root = Tk()
343 root.wm_protocol("WM_DELETE_WINDOW", root.quit)
344 text = Text()
345 text.pack()
346 text.focus_set()
347 p = Percolator(text)
348 d = UndoDelegator()
349 p.insertfilter(d)
350 root.mainloop()
351
352if __name__ == "__main__":
353 main()