Like TheLizzard said, this is not possible with the builtin undo/redo mechanism of the Text
widget. However, you can implement your own undo/redo mechanism that works with formatting.
The idea is to have an undo stack and a redo stack. I used lists for both. When you call the undo function, you take the last item in the undo stack, append it to the redo stack and use the information in the item to undo the modification. It will be typically an (undo_args, redo_args)
tuple. Then for the redo function, you do the same thing but taking the item from the redo stack and appending it to the undo stack.
However, you need to append the needed (undo_args, redo_args)
to the undo stack each time a modification occur and also clear the redo stack.
For that, I have adapted the proxy mechanism from Brayn Oakley's answer https://stackoverflow.com/a/16375233/6415268 (about automatically updating line numbers).
Each time a modification of the Text
widget occurs, the _proxy
method is called. If this modification is a text insertion, text deletion, tag addition or tag removal, the (undo_args, redo_args)
tuple is appended to the undo stack. So it is possible to undo the modification by calling self.tk.call((self._orig,) + undo_args)
and redo it with self.tk.call((self._orig,) + redo_args)
.
import tkinter as tk
class MyText(tk.Text):
def __init__(self, master=None, **kw):
tk.Text.__init__(self, master, undo=False, **kw)
self._undo_stack = []
self._redo_stack = []
# create proxy
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, *args):
if args[0] in ["insert", "delete"]:
if args[1] == "end":
index = self.index("end-1c")
else:
index = self.index(args[1])
if args[0] == "insert":
undo_args = ("delete", index, "{}+{}c".format(index, len(args[2])))
else: # args[0] == "delete":
undo_args = ("insert", index, self.get(*args[:1]))
self._redo_stack.clear()
self._undo_stack.append((undo_args, args))
elif args[0] == "tag":
if args[1] in ["add", "remove"] and args[2] != "sel":
indexes = tuple(self.index(ind) for ind in args[3:])
undo_args = ("tag", "remove" if args[1] == "add" else "add", args[2]) + indexes
self._redo_stack.clear()
self._undo_stack.append((undo_args, args))
result = self.tk.call((self._orig,) + args)
return result
def undo(self):
if not self._undo_stack:
return
undo_args, redo_args = self._undo_stack.pop()
self._redo_stack.append((undo_args, redo_args))
self.tk.call((self._orig,) + undo_args)
def redo(self):
if not self._redo_stack:
return
undo_args, redo_args = self._redo_stack.pop()
self._undo_stack.append((undo_args, redo_args))
self.tk.call((self._orig,) + redo_args)
root = tk.Tk()
text = MyText(root, width=65, height=20, font="consolas 14")
text.pack()
undo_button = tk.Button(root, text="Undo", command=text.undo)
undo_button.pack()
redo_button = tk.Button(root, text="Redo", command=text.redo)
redo_button.pack()
text.insert('end', "Hello world")
text.tag_add('test', '1.0', '1.5')
text.tag_config('test', background='yellow')
root.mainloop()