undo.py 11 KB

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