Undo and Redo in an Tkinter Entry widget?

Question:

Is there a way to add undo and redo capabilities in Tkinter Entry widgets or must I use single line Text widgets for this type of functionality?

If the latter, are there any tips I should follow when configuring a Text widget to act as an Entry widget?

Some features that might need tweaking include trapping the Return KeyPress, converting tab keypresses into a request to change focus, and removing newlines from text being pasted from the clipboard.

Asked By: Malcolm

||

Answers:

Disclaimer: these are just thoughts that come into my mind on how to implement it.

class History(object):

    def __init__(self):
        self.l = ['']
        self.i = 0

    def next(self):
        if self.i == len(self.l):
            return None
        self.i += 1
        return self.l[self.i]

    def prev(self):
        if self.i == 0:
            return None
        self.i -= 1
        return self.l[self.i]

    def add(self, s):
        del self.l[self.i+1:]
        self.l.append(s)
        self.i += 1

    def current(self):
        return self.l[self.i]

Run a thread that every X seconds (0.5?) save the state of the entry:

history = History()
...
history.add(stringval.get())

You can also set up events that save the Entry’s status too, such as the pressure of Return.

prev = history.prev()
if prev is not None:
    stringvar.set(prev)

or

next = history.next()
if next is not None:
    stringvar.set(next)

Beware to set locks as needed.

Answered By: mg.

Update on using this method for Undo/Redo:

I am creating a GUI with lot of frames and each contains at least ten or more ‘entry’ widgets.
I used the History class and created one history object for each entry field that I had. I was able to store all entry widgets values in a list as done here.
I am using ‘trace’ method attached to each entry widget which will call ‘add’ function of History class and store each changes. In this way, I was able to do it without running any thread separately.
But the biggest drawback of doing this is, we cannot do multiple undos/redos with this method.

Issue:
When I trace each and every change of the entry widget and add that to the list, it also ‘traces’ the change that happens when we ‘undo/redo’ which means we cannot go more one step back. once u do a undo, it is a change that will be traced and hence the ‘undo’ value will be added to the list at the end. Hence this is not the right method.

Solution:
Perfect way to do this is by creating two stacks for each entry widget. One for ‘Undo’ and one for ‘redo’. When ever there is a change in entry, push that value into the undo stack. When user presses undo, pop the last stored value from the undo stack and importantly push this one to the ‘redo stack’. hence, when the user presses redo, pop the last value from redo stack.

Answered By: Mugesh Sg

Check the Tkinter Custom Entry. I have added Cut, Copy, Paste context menu, and undo redo functions.

# -*- coding: utf-8 -*-
from tkinter import *


class CEntry(Entry):
    def __init__(self, parent, *args, **kwargs):
        Entry.__init__(self, parent, *args, **kwargs)

        self.changes = [""]
        self.steps = int()

        self.context_menu = Menu(self, tearoff=0)
        self.context_menu.add_command(label="Cut")
        self.context_menu.add_command(label="Copy")
        self.context_menu.add_command(label="Paste")

        self.bind("<Button-3>", self.popup)

        self.bind("<Control-z>", self.undo)
        self.bind("<Control-y>", self.redo)

        self.bind("<Key>", self.add_changes)

    def popup(self, event):
        self.context_menu.post(event.x_root, event.y_root)
        self.context_menu.entryconfigure("Cut", command=lambda: self.event_generate("<<Cut>>"))
        self.context_menu.entryconfigure("Copy", command=lambda: self.event_generate("<<Copy>>"))
        self.context_menu.entryconfigure("Paste", command=lambda: self.event_generate("<<Paste>>"))

    def undo(self, event=None):
        if self.steps != 0:
            self.steps -= 1
            self.delete(0, END)
            self.insert(END, self.changes[self.steps])

    def redo(self, event=None):
        if self.steps < len(self.changes):
            self.delete(0, END)
            self.insert(END, self.changes[self.steps])
            self.steps += 1

    def add_changes(self, event=None):
        if self.get() != self.changes[-1]:
            self.changes.append(self.get())
            self.steps += 1
Answered By: Evgeny Tretyakov

Based on Evgeny’s answer with a custom Entry, but added a tkinter StringVar with a trace to the widget to more accurately track when changes are made to its contents (not just when any Key is pressed, which seemed to add empty Undo/Redo items to the stack). Also added a max depth using a Python deque.

If we’re changing the contents of the Entry via code rather than keyboard input, we can temporarily disable the trace (e.g. see in the undo method below).

Code:


class CEntry(tk.Entry):
    def __init__(self, master, **kw):
        super().__init__(master=master, **kw)
        self._undo_stack = deque(maxlen=100)
        self._redo_stack = deque(maxlen=100)
        self.bind("<Control-z>", self.undo)
        self.bind("<Control-y>", self.redo)
        # traces whenever the Entry's contents are changed
        self.tkvar = tk.StringVar()
        self.config(textvariable=self.tkvar)
        self.trace_id = self.tkvar.trace("w", self.on_changes)
        self.reset_undo_stacks()
        # USE THESE TO TURN TRACE OFF THEN BACK ON AGAIN
        # self.tkvar.trace_vdelete("w", self.trace_id)
        # self.trace_id = self.tkvar.trace("w", self.on_changes)

    def undo(self, event=None):  # noqa
        if len(self._undo_stack) <= 1:
            return
        content = self._undo_stack.pop()
        self._redo_stack.append(content)
        content = self._undo_stack[-1]
        self.tkvar.trace_vdelete("w", self.trace_id)
        self.delete(0, tk.END)
        self.insert(0, content)
        self.trace_id = self.tkvar.trace("w", self.on_changes)

    def redo(self, event=None):  # noqa
        if not self._redo_stack:
            return
        content = self._redo_stack.pop()
        self._undo_stack.append(content)
        self.tkvar.trace_vdelete("w", self.trace_id)
        self.delete(0, tk.END)
        self.insert(0, content)
        self.trace_id = self.tkvar.trace("w", self.on_changes)

    def on_changes(self, a=None, b=None, c=None):  # noqa
        self._undo_stack.append(self.tkvar.get())
        self._redo_stack.clear()

    def reset_undo_stacks(self):
        self._undo_stack.clear()
        self._redo_stack.clear()
        self._undo_stack.append(self.tkvar.get())
Answered By: TomBCodes