UndoDelegator.py 11 KB

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