tkinter multi-Toplevel windows releases all windows grab_set when only one of them releases the grabbed status

I am trying to build a GUI with tkinter with several level windows. When I open a new Toplevel window, I need that his parens gets grabbed. For doing this I use grab_set() at the begining of the Toplevel class.

The new Toplevel child window will also have some other functions that allow to open new Toplevel grandchild windows. The problem is that when any of the Toplevel grandchild window get close, the hierarchi of windows in grabbed status get free and then are all of them again manipulable.

How can I mantain the grab_set() function operative for all the windows when a grab_settled grandchild window is released?

Here is a simple code example to reproduce this behaviour

from tkinter import *


class GUI(Frame):
    def __init__(self, master, *args, **kwargs):
        Frame.__init__(self, master, *args, **kwargs)

        self.master = master
        self.my_frame = Frame(self.master)
        self.my_frame.pack()

        self.button1 = Button(self.master, text="Open New Window", command=OpenFirstToplevelWindow)
        self.button1.pack()

        self.text = Text(self.master, width=30, height=3)
        self.text.pack()
        self.text.insert(END, "Some text")


class OpenFirstToplevelWindow(Toplevel):
    def __init__(self, *args, **kwargs):
        Toplevel.__init__(self, *args, **kwargs)
        self.grab_set()
        top_button = Button(self, text="Open a second window", command=OpenSecondToplevelWindow)
        top_button.pack()
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition should be inhibited")
        # Toplevel.wait_window(self)


class OpenSecondToplevelWindow(Toplevel):
    def __init__(self, *args, **kwargs):
        Toplevel.__init__(self, *args, **kwargs)
        self.grab_set()
        top_button = Button(self, text="close second window", command=self.close_window)
        top_button.pack()
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition still inhibited")

    def close_window(self):
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition should keep inhibited but it IS NOT")
        self.destroy()


if __name__ == "__main__":
    root = Tk()
    app = GUI(root)
    root.mainloop()

— EDITED TO EXPAND @acw1668 answer —


I’ve been working with your answer and it looks to work very nice. The problem is that I have realized that if I try to put a self to the name of the close button in the last window for further uses, then the method does not work:

class OpenSecondToplevelWindow(MyToplevel):
    def __init__(self, *args, **kwargs):
        MyToplevel.__init__(self, *args, **kwargs)
        enable_button = Button(self, text="Enable close button", command=self.enable_click)
        enable_button.pack()
        self.low_button = Button(self, text="Close second window", command=self.close_window, state=DISABLED)
        self.low_button.pack()
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition still inhibited")

    def enable_click(self):
        self.low_button["state"] = NORMAL

    def close_window(self):
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition should keep inhibited but it IS NOT")
        self.destroy()

Answer

One of the way is to save the reference of the window that has grab set if any before calling grab_set(). Then restore the grab set to the saved window after the current window is closed.

The following custom class (inherited from Toplevel) will have the above feature:

class MyToplevel(Toplevel):
    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        # save the reference of current window having the grab set
        self._last_grab_set_win = self.grab_current()
        self.grab_set()

    def __del__(self):
        if self._last_grab_set_win:
            # restore the grab set to saved window
            self._last_grab_set_win.grab_set()

Then use the above custom MyToplevel instead of Toplevel in your code:

class OpenFirstToplevelWindow(MyToplevel):
    def __init__(self, *args, **kwargs):
        MyToplevel.__init__(self, *args, **kwargs)
        top_button = Button(self, text="Open a second window", command=OpenSecondToplevelWindow)
        top_button.pack()
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition should be inhibited")
        # Toplevel.wait_window(self)


class OpenSecondToplevelWindow(MyToplevel):
    def __init__(self, *args, **kwargs):
        MyToplevel.__init__(self, *args, **kwargs)
        top_button = Button(self, text="close second window", command=self.close_window)
        top_button.pack()
        app.text.delete(1.0, END)
        app.text.insert(END, "Text edition still inhibited")

    ...

Update: I don’t know why __del__() is not executed if instance widget is used instead of local widget. However, override destroy() seems to fix the issue:

class MyToplevel(Toplevel):
    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        # save the reference of current window having the grab set
        self._last_grab_set_win = self.grab_current()
        self.grab_set()

    # override destroy()
    def destroy(self):
        if self._last_grab_set_win:
            # restore the grab set to saved window
            self._last_grab_set_win.grab_set()
        # call Toplevel.destroy()
        super().destroy()