jpayne@68
|
1 import string
|
jpayne@68
|
2
|
jpayne@68
|
3 from idlelib.delegator import Delegator
|
jpayne@68
|
4
|
jpayne@68
|
5 # tkinter import not needed because module does not create widgets,
|
jpayne@68
|
6 # although many methods operate on text widget arguments.
|
jpayne@68
|
7
|
jpayne@68
|
8 #$ event <<redo>>
|
jpayne@68
|
9 #$ win <Control-y>
|
jpayne@68
|
10 #$ unix <Alt-z>
|
jpayne@68
|
11
|
jpayne@68
|
12 #$ event <<undo>>
|
jpayne@68
|
13 #$ win <Control-z>
|
jpayne@68
|
14 #$ unix <Control-z>
|
jpayne@68
|
15
|
jpayne@68
|
16 #$ event <<dump-undo-state>>
|
jpayne@68
|
17 #$ win <Control-backslash>
|
jpayne@68
|
18 #$ unix <Control-backslash>
|
jpayne@68
|
19
|
jpayne@68
|
20
|
jpayne@68
|
21 class UndoDelegator(Delegator):
|
jpayne@68
|
22
|
jpayne@68
|
23 max_undo = 1000
|
jpayne@68
|
24
|
jpayne@68
|
25 def __init__(self):
|
jpayne@68
|
26 Delegator.__init__(self)
|
jpayne@68
|
27 self.reset_undo()
|
jpayne@68
|
28
|
jpayne@68
|
29 def setdelegate(self, delegate):
|
jpayne@68
|
30 if self.delegate is not None:
|
jpayne@68
|
31 self.unbind("<<undo>>")
|
jpayne@68
|
32 self.unbind("<<redo>>")
|
jpayne@68
|
33 self.unbind("<<dump-undo-state>>")
|
jpayne@68
|
34 Delegator.setdelegate(self, delegate)
|
jpayne@68
|
35 if delegate is not None:
|
jpayne@68
|
36 self.bind("<<undo>>", self.undo_event)
|
jpayne@68
|
37 self.bind("<<redo>>", self.redo_event)
|
jpayne@68
|
38 self.bind("<<dump-undo-state>>", self.dump_event)
|
jpayne@68
|
39
|
jpayne@68
|
40 def dump_event(self, event):
|
jpayne@68
|
41 from pprint import pprint
|
jpayne@68
|
42 pprint(self.undolist[:self.pointer])
|
jpayne@68
|
43 print("pointer:", self.pointer, end=' ')
|
jpayne@68
|
44 print("saved:", self.saved, end=' ')
|
jpayne@68
|
45 print("can_merge:", self.can_merge, end=' ')
|
jpayne@68
|
46 print("get_saved():", self.get_saved())
|
jpayne@68
|
47 pprint(self.undolist[self.pointer:])
|
jpayne@68
|
48 return "break"
|
jpayne@68
|
49
|
jpayne@68
|
50 def reset_undo(self):
|
jpayne@68
|
51 self.was_saved = -1
|
jpayne@68
|
52 self.pointer = 0
|
jpayne@68
|
53 self.undolist = []
|
jpayne@68
|
54 self.undoblock = 0 # or a CommandSequence instance
|
jpayne@68
|
55 self.set_saved(1)
|
jpayne@68
|
56
|
jpayne@68
|
57 def set_saved(self, flag):
|
jpayne@68
|
58 if flag:
|
jpayne@68
|
59 self.saved = self.pointer
|
jpayne@68
|
60 else:
|
jpayne@68
|
61 self.saved = -1
|
jpayne@68
|
62 self.can_merge = False
|
jpayne@68
|
63 self.check_saved()
|
jpayne@68
|
64
|
jpayne@68
|
65 def get_saved(self):
|
jpayne@68
|
66 return self.saved == self.pointer
|
jpayne@68
|
67
|
jpayne@68
|
68 saved_change_hook = None
|
jpayne@68
|
69
|
jpayne@68
|
70 def set_saved_change_hook(self, hook):
|
jpayne@68
|
71 self.saved_change_hook = hook
|
jpayne@68
|
72
|
jpayne@68
|
73 was_saved = -1
|
jpayne@68
|
74
|
jpayne@68
|
75 def check_saved(self):
|
jpayne@68
|
76 is_saved = self.get_saved()
|
jpayne@68
|
77 if is_saved != self.was_saved:
|
jpayne@68
|
78 self.was_saved = is_saved
|
jpayne@68
|
79 if self.saved_change_hook:
|
jpayne@68
|
80 self.saved_change_hook()
|
jpayne@68
|
81
|
jpayne@68
|
82 def insert(self, index, chars, tags=None):
|
jpayne@68
|
83 self.addcmd(InsertCommand(index, chars, tags))
|
jpayne@68
|
84
|
jpayne@68
|
85 def delete(self, index1, index2=None):
|
jpayne@68
|
86 self.addcmd(DeleteCommand(index1, index2))
|
jpayne@68
|
87
|
jpayne@68
|
88 # Clients should call undo_block_start() and undo_block_stop()
|
jpayne@68
|
89 # around a sequence of editing cmds to be treated as a unit by
|
jpayne@68
|
90 # undo & redo. Nested matching calls are OK, and the inner calls
|
jpayne@68
|
91 # then act like nops. OK too if no editing cmds, or only one
|
jpayne@68
|
92 # editing cmd, is issued in between: if no cmds, the whole
|
jpayne@68
|
93 # sequence has no effect; and if only one cmd, that cmd is entered
|
jpayne@68
|
94 # directly into the undo list, as if undo_block_xxx hadn't been
|
jpayne@68
|
95 # called. The intent of all that is to make this scheme easy
|
jpayne@68
|
96 # to use: all the client has to worry about is making sure each
|
jpayne@68
|
97 # _start() call is matched by a _stop() call.
|
jpayne@68
|
98
|
jpayne@68
|
99 def undo_block_start(self):
|
jpayne@68
|
100 if self.undoblock == 0:
|
jpayne@68
|
101 self.undoblock = CommandSequence()
|
jpayne@68
|
102 self.undoblock.bump_depth()
|
jpayne@68
|
103
|
jpayne@68
|
104 def undo_block_stop(self):
|
jpayne@68
|
105 if self.undoblock.bump_depth(-1) == 0:
|
jpayne@68
|
106 cmd = self.undoblock
|
jpayne@68
|
107 self.undoblock = 0
|
jpayne@68
|
108 if len(cmd) > 0:
|
jpayne@68
|
109 if len(cmd) == 1:
|
jpayne@68
|
110 # no need to wrap a single cmd
|
jpayne@68
|
111 cmd = cmd.getcmd(0)
|
jpayne@68
|
112 # this blk of cmds, or single cmd, has already
|
jpayne@68
|
113 # been done, so don't execute it again
|
jpayne@68
|
114 self.addcmd(cmd, 0)
|
jpayne@68
|
115
|
jpayne@68
|
116 def addcmd(self, cmd, execute=True):
|
jpayne@68
|
117 if execute:
|
jpayne@68
|
118 cmd.do(self.delegate)
|
jpayne@68
|
119 if self.undoblock != 0:
|
jpayne@68
|
120 self.undoblock.append(cmd)
|
jpayne@68
|
121 return
|
jpayne@68
|
122 if self.can_merge and self.pointer > 0:
|
jpayne@68
|
123 lastcmd = self.undolist[self.pointer-1]
|
jpayne@68
|
124 if lastcmd.merge(cmd):
|
jpayne@68
|
125 return
|
jpayne@68
|
126 self.undolist[self.pointer:] = [cmd]
|
jpayne@68
|
127 if self.saved > self.pointer:
|
jpayne@68
|
128 self.saved = -1
|
jpayne@68
|
129 self.pointer = self.pointer + 1
|
jpayne@68
|
130 if len(self.undolist) > self.max_undo:
|
jpayne@68
|
131 ##print "truncating undo list"
|
jpayne@68
|
132 del self.undolist[0]
|
jpayne@68
|
133 self.pointer = self.pointer - 1
|
jpayne@68
|
134 if self.saved >= 0:
|
jpayne@68
|
135 self.saved = self.saved - 1
|
jpayne@68
|
136 self.can_merge = True
|
jpayne@68
|
137 self.check_saved()
|
jpayne@68
|
138
|
jpayne@68
|
139 def undo_event(self, event):
|
jpayne@68
|
140 if self.pointer == 0:
|
jpayne@68
|
141 self.bell()
|
jpayne@68
|
142 return "break"
|
jpayne@68
|
143 cmd = self.undolist[self.pointer - 1]
|
jpayne@68
|
144 cmd.undo(self.delegate)
|
jpayne@68
|
145 self.pointer = self.pointer - 1
|
jpayne@68
|
146 self.can_merge = False
|
jpayne@68
|
147 self.check_saved()
|
jpayne@68
|
148 return "break"
|
jpayne@68
|
149
|
jpayne@68
|
150 def redo_event(self, event):
|
jpayne@68
|
151 if self.pointer >= len(self.undolist):
|
jpayne@68
|
152 self.bell()
|
jpayne@68
|
153 return "break"
|
jpayne@68
|
154 cmd = self.undolist[self.pointer]
|
jpayne@68
|
155 cmd.redo(self.delegate)
|
jpayne@68
|
156 self.pointer = self.pointer + 1
|
jpayne@68
|
157 self.can_merge = False
|
jpayne@68
|
158 self.check_saved()
|
jpayne@68
|
159 return "break"
|
jpayne@68
|
160
|
jpayne@68
|
161
|
jpayne@68
|
162 class Command:
|
jpayne@68
|
163 # Base class for Undoable commands
|
jpayne@68
|
164
|
jpayne@68
|
165 tags = None
|
jpayne@68
|
166
|
jpayne@68
|
167 def __init__(self, index1, index2, chars, tags=None):
|
jpayne@68
|
168 self.marks_before = {}
|
jpayne@68
|
169 self.marks_after = {}
|
jpayne@68
|
170 self.index1 = index1
|
jpayne@68
|
171 self.index2 = index2
|
jpayne@68
|
172 self.chars = chars
|
jpayne@68
|
173 if tags:
|
jpayne@68
|
174 self.tags = tags
|
jpayne@68
|
175
|
jpayne@68
|
176 def __repr__(self):
|
jpayne@68
|
177 s = self.__class__.__name__
|
jpayne@68
|
178 t = (self.index1, self.index2, self.chars, self.tags)
|
jpayne@68
|
179 if self.tags is None:
|
jpayne@68
|
180 t = t[:-1]
|
jpayne@68
|
181 return s + repr(t)
|
jpayne@68
|
182
|
jpayne@68
|
183 def do(self, text):
|
jpayne@68
|
184 pass
|
jpayne@68
|
185
|
jpayne@68
|
186 def redo(self, text):
|
jpayne@68
|
187 pass
|
jpayne@68
|
188
|
jpayne@68
|
189 def undo(self, text):
|
jpayne@68
|
190 pass
|
jpayne@68
|
191
|
jpayne@68
|
192 def merge(self, cmd):
|
jpayne@68
|
193 return 0
|
jpayne@68
|
194
|
jpayne@68
|
195 def save_marks(self, text):
|
jpayne@68
|
196 marks = {}
|
jpayne@68
|
197 for name in text.mark_names():
|
jpayne@68
|
198 if name != "insert" and name != "current":
|
jpayne@68
|
199 marks[name] = text.index(name)
|
jpayne@68
|
200 return marks
|
jpayne@68
|
201
|
jpayne@68
|
202 def set_marks(self, text, marks):
|
jpayne@68
|
203 for name, index in marks.items():
|
jpayne@68
|
204 text.mark_set(name, index)
|
jpayne@68
|
205
|
jpayne@68
|
206
|
jpayne@68
|
207 class InsertCommand(Command):
|
jpayne@68
|
208 # Undoable insert command
|
jpayne@68
|
209
|
jpayne@68
|
210 def __init__(self, index1, chars, tags=None):
|
jpayne@68
|
211 Command.__init__(self, index1, None, chars, tags)
|
jpayne@68
|
212
|
jpayne@68
|
213 def do(self, text):
|
jpayne@68
|
214 self.marks_before = self.save_marks(text)
|
jpayne@68
|
215 self.index1 = text.index(self.index1)
|
jpayne@68
|
216 if text.compare(self.index1, ">", "end-1c"):
|
jpayne@68
|
217 # Insert before the final newline
|
jpayne@68
|
218 self.index1 = text.index("end-1c")
|
jpayne@68
|
219 text.insert(self.index1, self.chars, self.tags)
|
jpayne@68
|
220 self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
|
jpayne@68
|
221 self.marks_after = self.save_marks(text)
|
jpayne@68
|
222 ##sys.__stderr__.write("do: %s\n" % self)
|
jpayne@68
|
223
|
jpayne@68
|
224 def redo(self, text):
|
jpayne@68
|
225 text.mark_set('insert', self.index1)
|
jpayne@68
|
226 text.insert(self.index1, self.chars, self.tags)
|
jpayne@68
|
227 self.set_marks(text, self.marks_after)
|
jpayne@68
|
228 text.see('insert')
|
jpayne@68
|
229 ##sys.__stderr__.write("redo: %s\n" % self)
|
jpayne@68
|
230
|
jpayne@68
|
231 def undo(self, text):
|
jpayne@68
|
232 text.mark_set('insert', self.index1)
|
jpayne@68
|
233 text.delete(self.index1, self.index2)
|
jpayne@68
|
234 self.set_marks(text, self.marks_before)
|
jpayne@68
|
235 text.see('insert')
|
jpayne@68
|
236 ##sys.__stderr__.write("undo: %s\n" % self)
|
jpayne@68
|
237
|
jpayne@68
|
238 def merge(self, cmd):
|
jpayne@68
|
239 if self.__class__ is not cmd.__class__:
|
jpayne@68
|
240 return False
|
jpayne@68
|
241 if self.index2 != cmd.index1:
|
jpayne@68
|
242 return False
|
jpayne@68
|
243 if self.tags != cmd.tags:
|
jpayne@68
|
244 return False
|
jpayne@68
|
245 if len(cmd.chars) != 1:
|
jpayne@68
|
246 return False
|
jpayne@68
|
247 if self.chars and \
|
jpayne@68
|
248 self.classify(self.chars[-1]) != self.classify(cmd.chars):
|
jpayne@68
|
249 return False
|
jpayne@68
|
250 self.index2 = cmd.index2
|
jpayne@68
|
251 self.chars = self.chars + cmd.chars
|
jpayne@68
|
252 return True
|
jpayne@68
|
253
|
jpayne@68
|
254 alphanumeric = string.ascii_letters + string.digits + "_"
|
jpayne@68
|
255
|
jpayne@68
|
256 def classify(self, c):
|
jpayne@68
|
257 if c in self.alphanumeric:
|
jpayne@68
|
258 return "alphanumeric"
|
jpayne@68
|
259 if c == "\n":
|
jpayne@68
|
260 return "newline"
|
jpayne@68
|
261 return "punctuation"
|
jpayne@68
|
262
|
jpayne@68
|
263
|
jpayne@68
|
264 class DeleteCommand(Command):
|
jpayne@68
|
265 # Undoable delete command
|
jpayne@68
|
266
|
jpayne@68
|
267 def __init__(self, index1, index2=None):
|
jpayne@68
|
268 Command.__init__(self, index1, index2, None, None)
|
jpayne@68
|
269
|
jpayne@68
|
270 def do(self, text):
|
jpayne@68
|
271 self.marks_before = self.save_marks(text)
|
jpayne@68
|
272 self.index1 = text.index(self.index1)
|
jpayne@68
|
273 if self.index2:
|
jpayne@68
|
274 self.index2 = text.index(self.index2)
|
jpayne@68
|
275 else:
|
jpayne@68
|
276 self.index2 = text.index(self.index1 + " +1c")
|
jpayne@68
|
277 if text.compare(self.index2, ">", "end-1c"):
|
jpayne@68
|
278 # Don't delete the final newline
|
jpayne@68
|
279 self.index2 = text.index("end-1c")
|
jpayne@68
|
280 self.chars = text.get(self.index1, self.index2)
|
jpayne@68
|
281 text.delete(self.index1, self.index2)
|
jpayne@68
|
282 self.marks_after = self.save_marks(text)
|
jpayne@68
|
283 ##sys.__stderr__.write("do: %s\n" % self)
|
jpayne@68
|
284
|
jpayne@68
|
285 def redo(self, text):
|
jpayne@68
|
286 text.mark_set('insert', self.index1)
|
jpayne@68
|
287 text.delete(self.index1, self.index2)
|
jpayne@68
|
288 self.set_marks(text, self.marks_after)
|
jpayne@68
|
289 text.see('insert')
|
jpayne@68
|
290 ##sys.__stderr__.write("redo: %s\n" % self)
|
jpayne@68
|
291
|
jpayne@68
|
292 def undo(self, text):
|
jpayne@68
|
293 text.mark_set('insert', self.index1)
|
jpayne@68
|
294 text.insert(self.index1, self.chars)
|
jpayne@68
|
295 self.set_marks(text, self.marks_before)
|
jpayne@68
|
296 text.see('insert')
|
jpayne@68
|
297 ##sys.__stderr__.write("undo: %s\n" % self)
|
jpayne@68
|
298
|
jpayne@68
|
299
|
jpayne@68
|
300 class CommandSequence(Command):
|
jpayne@68
|
301 # Wrapper for a sequence of undoable cmds to be undone/redone
|
jpayne@68
|
302 # as a unit
|
jpayne@68
|
303
|
jpayne@68
|
304 def __init__(self):
|
jpayne@68
|
305 self.cmds = []
|
jpayne@68
|
306 self.depth = 0
|
jpayne@68
|
307
|
jpayne@68
|
308 def __repr__(self):
|
jpayne@68
|
309 s = self.__class__.__name__
|
jpayne@68
|
310 strs = []
|
jpayne@68
|
311 for cmd in self.cmds:
|
jpayne@68
|
312 strs.append(" %r" % (cmd,))
|
jpayne@68
|
313 return s + "(\n" + ",\n".join(strs) + "\n)"
|
jpayne@68
|
314
|
jpayne@68
|
315 def __len__(self):
|
jpayne@68
|
316 return len(self.cmds)
|
jpayne@68
|
317
|
jpayne@68
|
318 def append(self, cmd):
|
jpayne@68
|
319 self.cmds.append(cmd)
|
jpayne@68
|
320
|
jpayne@68
|
321 def getcmd(self, i):
|
jpayne@68
|
322 return self.cmds[i]
|
jpayne@68
|
323
|
jpayne@68
|
324 def redo(self, text):
|
jpayne@68
|
325 for cmd in self.cmds:
|
jpayne@68
|
326 cmd.redo(text)
|
jpayne@68
|
327
|
jpayne@68
|
328 def undo(self, text):
|
jpayne@68
|
329 cmds = self.cmds[:]
|
jpayne@68
|
330 cmds.reverse()
|
jpayne@68
|
331 for cmd in cmds:
|
jpayne@68
|
332 cmd.undo(text)
|
jpayne@68
|
333
|
jpayne@68
|
334 def bump_depth(self, incr=1):
|
jpayne@68
|
335 self.depth = self.depth + incr
|
jpayne@68
|
336 return self.depth
|
jpayne@68
|
337
|
jpayne@68
|
338
|
jpayne@68
|
339 def _undo_delegator(parent): # htest #
|
jpayne@68
|
340 from tkinter import Toplevel, Text, Button
|
jpayne@68
|
341 from idlelib.percolator import Percolator
|
jpayne@68
|
342 undowin = Toplevel(parent)
|
jpayne@68
|
343 undowin.title("Test UndoDelegator")
|
jpayne@68
|
344 x, y = map(int, parent.geometry().split('+')[1:])
|
jpayne@68
|
345 undowin.geometry("+%d+%d" % (x, y + 175))
|
jpayne@68
|
346
|
jpayne@68
|
347 text = Text(undowin, height=10)
|
jpayne@68
|
348 text.pack()
|
jpayne@68
|
349 text.focus_set()
|
jpayne@68
|
350 p = Percolator(text)
|
jpayne@68
|
351 d = UndoDelegator()
|
jpayne@68
|
352 p.insertfilter(d)
|
jpayne@68
|
353
|
jpayne@68
|
354 undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None))
|
jpayne@68
|
355 undo.pack(side='left')
|
jpayne@68
|
356 redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None))
|
jpayne@68
|
357 redo.pack(side='left')
|
jpayne@68
|
358 dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None))
|
jpayne@68
|
359 dump.pack(side='left')
|
jpayne@68
|
360
|
jpayne@68
|
361 if __name__ == "__main__":
|
jpayne@68
|
362 from unittest import main
|
jpayne@68
|
363 main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
|
jpayne@68
|
364
|
jpayne@68
|
365 from idlelib.idle_test.htest import run
|
jpayne@68
|
366 run(_undo_delegator)
|