blob: f048994b7d15c3a80239155360e415e64775d800 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001import string
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04002
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -04003from idlelib.delegator import Delegator
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04004
Terry Jan Reedyb60adc52016-06-21 18:41:38 -04005# tkintter import not needed because module does not create widgets,
6# although many methods operate on text widget arguments.
David Scherer7aced172000-08-15 01:13:23 +00007
8#$ event <<redo>>
9#$ win <Control-y>
10#$ unix <Alt-z>
11
12#$ event <<undo>>
13#$ win <Control-z>
14#$ unix <Control-z>
15
16#$ event <<dump-undo-state>>
17#$ win <Control-backslash>
18#$ unix <Control-backslash>
19
20
21class UndoDelegator(Delegator):
22
23 max_undo = 1000
24
25 def __init__(self):
26 Delegator.__init__(self)
27 self.reset_undo()
28
29 def setdelegate(self, delegate):
30 if self.delegate is not None:
31 self.unbind("<<undo>>")
32 self.unbind("<<redo>>")
33 self.unbind("<<dump-undo-state>>")
34 Delegator.setdelegate(self, delegate)
35 if delegate is not None:
36 self.bind("<<undo>>", self.undo_event)
37 self.bind("<<redo>>", self.redo_event)
38 self.bind("<<dump-undo-state>>", self.dump_event)
39
40 def dump_event(self, event):
41 from pprint import pprint
42 pprint(self.undolist[:self.pointer])
Guido van Rossumbe19ed72007-02-09 05:37:30 +000043 print("pointer:", self.pointer, end=' ')
44 print("saved:", self.saved, end=' ')
45 print("can_merge:", self.can_merge, end=' ')
46 print("get_saved():", self.get_saved())
David Scherer7aced172000-08-15 01:13:23 +000047 pprint(self.undolist[self.pointer:])
48 return "break"
49
50 def reset_undo(self):
51 self.was_saved = -1
52 self.pointer = 0
53 self.undolist = []
54 self.undoblock = 0 # or a CommandSequence instance
55 self.set_saved(1)
56
57 def set_saved(self, flag):
58 if flag:
59 self.saved = self.pointer
60 else:
61 self.saved = -1
Neal Norwitz672ce572002-11-30 19:04:07 +000062 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +000063 self.check_saved()
64
65 def get_saved(self):
66 return self.saved == self.pointer
67
68 saved_change_hook = None
69
70 def set_saved_change_hook(self, hook):
71 self.saved_change_hook = hook
72
73 was_saved = -1
74
75 def check_saved(self):
76 is_saved = self.get_saved()
77 if is_saved != self.was_saved:
78 self.was_saved = is_saved
79 if self.saved_change_hook:
80 self.saved_change_hook()
81
82 def insert(self, index, chars, tags=None):
83 self.addcmd(InsertCommand(index, chars, tags))
84
85 def delete(self, index1, index2=None):
86 self.addcmd(DeleteCommand(index1, index2))
87
88 # Clients should call undo_block_start() and undo_block_stop()
89 # around a sequence of editing cmds to be treated as a unit by
90 # undo & redo. Nested matching calls are OK, and the inner calls
91 # then act like nops. OK too if no editing cmds, or only one
92 # editing cmd, is issued in between: if no cmds, the whole
93 # sequence has no effect; and if only one cmd, that cmd is entered
94 # directly into the undo list, as if undo_block_xxx hadn't been
95 # called. The intent of all that is to make this scheme easy
96 # to use: all the client has to worry about is making sure each
97 # _start() call is matched by a _stop() call.
98
99 def undo_block_start(self):
100 if self.undoblock == 0:
101 self.undoblock = CommandSequence()
102 self.undoblock.bump_depth()
103
104 def undo_block_stop(self):
105 if self.undoblock.bump_depth(-1) == 0:
106 cmd = self.undoblock
107 self.undoblock = 0
108 if len(cmd) > 0:
109 if len(cmd) == 1:
110 # no need to wrap a single cmd
111 cmd = cmd.getcmd(0)
112 # this blk of cmds, or single cmd, has already
113 # been done, so don't execute it again
114 self.addcmd(cmd, 0)
115
Neal Norwitz672ce572002-11-30 19:04:07 +0000116 def addcmd(self, cmd, execute=True):
David Scherer7aced172000-08-15 01:13:23 +0000117 if execute:
118 cmd.do(self.delegate)
119 if self.undoblock != 0:
120 self.undoblock.append(cmd)
121 return
122 if self.can_merge and self.pointer > 0:
123 lastcmd = self.undolist[self.pointer-1]
124 if lastcmd.merge(cmd):
125 return
126 self.undolist[self.pointer:] = [cmd]
127 if self.saved > self.pointer:
128 self.saved = -1
129 self.pointer = self.pointer + 1
130 if len(self.undolist) > self.max_undo:
131 ##print "truncating undo list"
132 del self.undolist[0]
133 self.pointer = self.pointer - 1
134 if self.saved >= 0:
135 self.saved = self.saved - 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000136 self.can_merge = True
David Scherer7aced172000-08-15 01:13:23 +0000137 self.check_saved()
138
139 def undo_event(self, event):
140 if self.pointer == 0:
141 self.bell()
142 return "break"
143 cmd = self.undolist[self.pointer - 1]
144 cmd.undo(self.delegate)
145 self.pointer = self.pointer - 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000146 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000147 self.check_saved()
148 return "break"
149
150 def redo_event(self, event):
151 if self.pointer >= len(self.undolist):
152 self.bell()
153 return "break"
154 cmd = self.undolist[self.pointer]
155 cmd.redo(self.delegate)
156 self.pointer = self.pointer + 1
Neal Norwitz672ce572002-11-30 19:04:07 +0000157 self.can_merge = False
David Scherer7aced172000-08-15 01:13:23 +0000158 self.check_saved()
159 return "break"
160
161
162class Command:
David Scherer7aced172000-08-15 01:13:23 +0000163 # 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):
David Scherer7aced172000-08-15 01:13:23 +0000208 # 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__:
Neal Norwitz672ce572002-11-30 19:04:07 +0000240 return False
David Scherer7aced172000-08-15 01:13:23 +0000241 if self.index2 != cmd.index1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000242 return False
David Scherer7aced172000-08-15 01:13:23 +0000243 if self.tags != cmd.tags:
Neal Norwitz672ce572002-11-30 19:04:07 +0000244 return False
David Scherer7aced172000-08-15 01:13:23 +0000245 if len(cmd.chars) != 1:
Neal Norwitz672ce572002-11-30 19:04:07 +0000246 return False
David Scherer7aced172000-08-15 01:13:23 +0000247 if self.chars and \
248 self.classify(self.chars[-1]) != self.classify(cmd.chars):
Neal Norwitz672ce572002-11-30 19:04:07 +0000249 return False
David Scherer7aced172000-08-15 01:13:23 +0000250 self.index2 = cmd.index2
251 self.chars = self.chars + cmd.chars
Neal Norwitz672ce572002-11-30 19:04:07 +0000252 return True
David Scherer7aced172000-08-15 01:13:23 +0000253
Kurt B. Kaiser5fab9c62002-09-18 03:30:12 +0000254 alphanumeric = string.ascii_letters + string.digits + "_"
David Scherer7aced172000-08-15 01:13:23 +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):
David Scherer7aced172000-08-15 01:13:23 +0000265 # 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
David Scherer7aced172000-08-15 01:13:23 +0000299
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400300class CommandSequence(Command):
David Scherer7aced172000-08-15 01:13:23 +0000301 # 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
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400338
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400339def _undo_delegator(parent): # htest #
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400340 from tkinter import Toplevel, Text, Button
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400341 from idlelib.percolator import Percolator
Terry Jan Reedyb60adc52016-06-21 18:41:38 -0400342 undowin = Toplevel(parent)
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400343 undowin.title("Test UndoDelegator")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400344 x, y = map(int, parent.geometry().split('+')[1:])
345 undowin.geometry("+%d+%d" % (x, y + 175))
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400346
347 text = Text(undowin, height=10)
David Scherer7aced172000-08-15 01:13:23 +0000348 text.pack()
349 text.focus_set()
350 p = Percolator(text)
351 d = UndoDelegator()
352 p.insertfilter(d)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400353
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400354 undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None))
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400355 undo.pack(side='left')
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400356 redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None))
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400357 redo.pack(side='left')
Terry Jan Reedy0495fa82016-05-16 23:32:28 -0400358 dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None))
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400359 dump.pack(side='left')
360
David Scherer7aced172000-08-15 01:13:23 +0000361if __name__ == "__main__":
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400362 from unittest import main
363 main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
Terry Jan Reedya7480322016-07-10 17:28:10 -0400364
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -0400365 from idlelib.idle_test.htest import run
366 run(_undo_delegator)