annotate 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
rev   line source
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)