0

Is there any way to undo and redo the formatting's made to a tkinter text widget?

Here's the code:

from tkinter import *

root = Tk()

text = Text(root, width=65, height=20, undo=True, font="consolas 14")
text.pack()

undo_button = Button(root, text="Undo", command=text.edit_undo)
undo_button.pack()

redo_button = Button(root, text="Redo", command=text.edit_redo)
redo_button.pack()

text.insert('1.0', "Hello world")
text.tag_add('test', '1.0', '1.5')
text.tag_config('test', background='yellow')

mainloop()

Here, I have added a tag to the text widget, but the problem is that, when I click on the undo or redo button, only the text in the text widget is being modified, and not the formatting's made to it.

enter image description here

What I want is that the 'test' tag should be removed first when I press the undo_button, as adding that tag was the last thing I did to my text widget.

enter image description here

Is there any way to achieve this in tkinter?

It would be great if anyone could help me out.

Lenovo 360
  • 569
  • 1
  • 7
  • 27
  • I don't think there is a way. You might want to take a look at [this](https://www.tcl.tk/man/tcl8.4/TkCmd/text.htm#M73) – TheLizzard May 07 '21 at 10:48
  • I think the idea behind `undo` and `redo` is to revert back to events. Highlighting through code is not considered an event done by the user, I guess. – Delrius Euphoria May 07 '21 at 10:50
  • there is a way. each action that is made has to be registered to a list or sth, even marking text. So the function that marks the text should append something to the list so when undoing execute the added thing or sth – Matiiss May 07 '21 at 10:50
  • I think the first line, in the linked docs, answers the question. – Delrius Euphoria May 07 '21 at 10:54
  • From BryanOakley ([here](https://wiki.tcl-lang.org/page/Text+widget+undo%2Fredo+limitations+and+enhancements)): *"I think it would be a good thing to include in the emergent new undo/redo mechanism the ability to undo/redo tag configuration/insertion/deletion"*. From that I can conclude that the functionality you are after doesn't exist – TheLizzard May 07 '21 at 10:56
  • @TheLizzard I think you can post an answer here for future references. – Delrius Euphoria May 07 '21 at 10:58
  • 1
    @CoolCloud Ok, I will. First I will do a bit more Googling to try and find a work around. – TheLizzard May 07 '21 at 10:59
  • @CoolCloud Do you think [this](http://scidb.sourceforge.net/tk/editreset.html) is what OP is after? More specifically the `immediately` parameter there – TheLizzard May 07 '21 at 11:19
  • @TheLizzard Hmmmmm, not sure, but I don't think so – Delrius Euphoria May 07 '21 at 11:21

2 Answers2

3

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()
j_4321
  • 15,431
  • 3
  • 34
  • 61
  • Great answer but why do you have `.pop(-1)` instead of just `.pop()`. – TheLizzard May 07 '21 at 12:13
  • @TheLizzard Yes sure, I had forgotten -1 is the default index in `pop` – j_4321 May 07 '21 at 12:18
  • Thanks a lot for all the help. I'm really stunned! It was very hard for me to choose which answer should I accept, as both of them helped a lot. Thanks for all the help once again! – Lenovo 360 May 07 '21 at 12:54
  • @Lenovo360 This answer is the better one. For mine you have to install stuff and then there is no guarantee that it will work. This answer is simpler and cleaner. – TheLizzard May 07 '21 at 12:55
  • @TheLizzard: No problem! I have learned how the undo and redo mechanism actually works through your answer. Really appreciate your great help towards a beginner in programming! I have upvoted your answer for taking your time to help me! Thanks a lot! – Lenovo 360 May 07 '21 at 13:08
2

I don't think it is possible with the default tkinter installation because tcl only considers the text when undoing/redoing an operation. It doesn't care about the tags. That is why (in your example code), if you try to redo the undo, the text isn't highlighted. A rewrite of how tcl handles the undos/redos has been suggested in the past but I can't find any progress on those suggestions.

It might be possible to install something like this but it is out of my expertise. In there it talks about the undo mechanism and tags and I think that is what you are looking for. More specifically that revised text widget adds a new immediately parameter: "If option -immediately is specified then the separator will be pushed immediately; this is required if mark or tag operations should be separated." Therefore, it might add separators between tag additions.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • Is `immediately` a parameter of `Text()` or `edit_separator()`? I tried using the `immediately` parameter on both of them, but it gives the error `unknown option "-immediately"`. – Lenovo 360 May 07 '21 at 11:55
  • @Lenovo360 It only works if you have [that revised tkinter text widget](http://scidb.sourceforge.net/tk/download.html) installed. And you will have to use `.tk.call(._w, "edit", "separator", "immediately")` – TheLizzard May 07 '21 at 12:00