Mercurial > repos > rliterman > csp2
comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/undo.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 68:5028fdace37b |
---|---|
1 import string | |
2 | |
3 from idlelib.delegator import Delegator | |
4 | |
5 # tkinter import not needed because module does not create widgets, | |
6 # although many methods operate on text widget arguments. | |
7 | |
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 | |
21 class 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]) | |
43 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()) | |
47 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 | |
62 self.can_merge = False | |
63 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 | |
116 def addcmd(self, cmd, execute=True): | |
117 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 | |
136 self.can_merge = True | |
137 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 | |
146 self.can_merge = False | |
147 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 | |
157 self.can_merge = False | |
158 self.check_saved() | |
159 return "break" | |
160 | |
161 | |
162 class Command: | |
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] | |
181 return s + repr(t) | |
182 | |
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 | |
207 class InsertCommand(Command): | |
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 False | |
241 if self.index2 != cmd.index1: | |
242 return False | |
243 if self.tags != cmd.tags: | |
244 return False | |
245 if len(cmd.chars) != 1: | |
246 return False | |
247 if self.chars and \ | |
248 self.classify(self.chars[-1]) != self.classify(cmd.chars): | |
249 return False | |
250 self.index2 = cmd.index2 | |
251 self.chars = self.chars + cmd.chars | |
252 return True | |
253 | |
254 alphanumeric = string.ascii_letters + string.digits + "_" | |
255 | |
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 | |
264 class DeleteCommand(Command): | |
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 | |
299 | |
300 class CommandSequence(Command): | |
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: | |
312 strs.append(" %r" % (cmd,)) | |
313 return s + "(\n" + ",\n".join(strs) + "\n)" | |
314 | |
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 | |
338 | |
339 def _undo_delegator(parent): # htest # | |
340 from tkinter import Toplevel, Text, Button | |
341 from idlelib.percolator import Percolator | |
342 undowin = Toplevel(parent) | |
343 undowin.title("Test UndoDelegator") | |
344 x, y = map(int, parent.geometry().split('+')[1:]) | |
345 undowin.geometry("+%d+%d" % (x, y + 175)) | |
346 | |
347 text = Text(undowin, height=10) | |
348 text.pack() | |
349 text.focus_set() | |
350 p = Percolator(text) | |
351 d = UndoDelegator() | |
352 p.insertfilter(d) | |
353 | |
354 undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None)) | |
355 undo.pack(side='left') | |
356 redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) | |
357 redo.pack(side='left') | |
358 dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) | |
359 dump.pack(side='left') | |
360 | |
361 if __name__ == "__main__": | |
362 from unittest import main | |
363 main('idlelib.idle_test.test_undo', verbosity=2, exit=False) | |
364 | |
365 from idlelib.idle_test.htest import run | |
366 run(_undo_delegator) |