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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top