2

I'm trying to subclass Tk where it pauses audio if and only if the entire application loses focus (i.e. the Tk instance loses focus and focus wasn't passed to a Toplevel or messagebox widget).

I managed to get it sort-of working with a 'hack' - when a messagebox is open, it is the last child of the Tk instance and also has no children. Here is what I've tried:

class TkWin(Tk): 
   def __init__(self, title):
        super().__init__(className=title, baseName=title)
        self.bind('<FocusOut>', lambda event: self.pause_audio())

    def pause_audio(self):
        if self.has_focus():
            return
        else:
            pass
            # pause the audio

    def has_focus(self):
        children = self.winfo_children()
        if any(isinstance(x, Toplevel) for x in children):
            return True
        if len(children[-1].winfo_children()) == 0:
            return True
        return False


win = TkWin('test')
win.mainloop()

The above solution solves the problem of not pausing the audio if a Toplevel or messagebox is opened. However it fails if a Toplevel or messagebox widget is opened and then another window is given focus.

(I am aware this would fail if you opened a messagebox and then created a container with some widgets inside it, but it works for how I build my applications)

Is there a better way to go about this?

Tried @Atlas345 's solution and am met with this error when trying to open a messagebox:

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1705, in __call__
    return self.func(*args)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 749, in callit
    func(*args)
  File "/home/inkblot/Desktop/win test.py", line 12, in check
    if self.focus_get() is None:
  File "/usr/lib/python3.6/tkinter/__init__.py", line 699, in focus_get
    return self._nametowidget(name)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1353, in nametowidget
    w = w.children[n]
KeyError: '__tk__messagebox'

EDIT:

Trying to override the focus_get method gets me somewhere but I have one last issue:

from tkinter import messagebox, Tk, Button, Label, Toplevel

class TkWin(Tk): 
    def __init__(self, title):
        super().__init__(className=title, baseName=title)
        self.focus_id = self.after(10, self.has_focus)
        
    def focus_get(self):
        try:
            return super().focus_get() is not None
        except KeyError:
            print('messagebox is open')
            return True

    def has_focus(self):
        print('resume audio') if self.focus_get() else print('pause audio')
        self.focus_id = self.after(50, self.has_focus)

def popupmsg(msg):
    popup = Toplevel(win)
    popup.wm_title("Warning!")
    Label(popup, text=msg).pack(side="top", fill='both', expand=True)
    Button(popup, text="okay", command = popup.destroy).pack()
    
win = TkWin('test')
Button(win, text='top', command=lambda: Toplevel(win)).pack()
Button(win, text='msg', command=lambda: messagebox.showinfo('title', 'msg')).pack()
Button(win, text='popup', command= lambda: popupmsg('I dare you!')).pack()
win.mainloop()
        

This lets the above error fail silently but I have now noticed that the focus_get() method returns None if the topmost widget does not have focus. This means if any type of popup is open and I then click on the root window (or any popup that was not the most recently created), it assumes the entire application does not have focus and hence pauses the audio which is undesirable to say the least.

Inkblot
  • 708
  • 2
  • 8
  • 19

1 Answers1

2

I'm trying to subclass Tk where it pauses audio if and only if the entire application loses focus (i.e. the Tk instance loses focus and focus wasn't passed to a Toplevel or messagebox widget).

I think you get away with this:

import tkinter as tk

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()
def check ():
    condition = root.focus_get()
    root.after(500, check)
    if condition == None:
        print('pause music')
        pass
check()
root.mainloop()

Because root.focus_get() just returns None if all windows are minimized or loses focus. Most other methods I've tried was returning None as soon as the root was minimized.

This method also fails by a builtin messagebox, because these aren't related to the root window. Thats why you would need to build your own like:

def popupmsg(msg):
    popup = tk.Toplevel()
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill=BOTH, expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

So the full exampel would be like this:

import tkinter as tk
from tkinter import messagebox

def popupmsg(msg):
    popup = tk.Toplevel(root)
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill='both', expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()

b1 = tk.Button(root, text='msg', command= lambda: messagebox.showinfo('title', 'msg'))
b1.pack()

b2 = tk.Button(root, text='popup', command= lambda: popupmsg('I dare you!'))
b2.pack()
def check ():
    condition = root.focus_get()
    root.after(500, check)
    if condition == None:
        print('pause music')
        pass
check()
root.mainloop()

Another way

I found this code here: Determining what tkinter window is currently on top

and implemented in the code. It has the benefit that it works with messagebox, but only when another tk window that is related to your root is aktive. This code isn't about focus, it just tells you which window is currently on top.

So the conditions is all of the windows needs to be iconify.

import tkinter as tk
from tkinter import messagebox

def popupmsg(msg):
    popup = tk.Toplevel(root)
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill='both', expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()

b1 = tk.Button(root, text='msg', command= lambda: [root.iconify(), messagebox.showinfo('title', 'msg')])
b1.pack()

b2 = tk.Button(root, text='popup', command= lambda: popupmsg('I dare you!'))
b2.pack()
def check ():
    condition1 = root.tk.eval('wm stackorder '+str(root))
    root.after(500, check)
    if condition1 == "":
        print('pause music')
        pass
    else:
        print('return music')
    
check()
root.mainloop()
Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
  • Thanks for your solution, I get a `KeyError` when opening a `messagebox` (added trace to question), does that mean it's impossible for me to use them in my application? – Inkblot Jun 29 '20 at 15:12
  • I was using your code that you updated. It works for me or maybe I dont understand the problem. – Thingamabobs Jun 29 '20 at 18:50
  • BTW, you should take a look at this accepted answer. Because you should aviod a singleliner for config and layout management https://stackoverflow.com/questions/1101750/tkinter-attributeerror-nonetype-object-has-no-attribute-attribute-name – Thingamabobs Jun 29 '20 at 19:03
  • You noticed that I explicite said that this solutions doesnt work for messagebox? – Thingamabobs Jun 29 '20 at 19:15
  • Yes I noticed, it wasn't impossible but it highlighted an issue with your solution. See edit in question. – Inkblot Jun 29 '20 at 19:41
  • cant reproduce it, cant help here. – Thingamabobs Jun 29 '20 at 19:45
  • 1
    Thanks a ton for your help, however, I had to change the second boolean expression to `condition1 == "0"` for it to work. – Inkblot Jun 30 '20 at 10:05
  • @Inkblot I just noticed that you dont need the condition focus_get() because none of them could have focus if all of them are iconify. You may think about to shrink this to a single condition. – Thingamabobs Jun 30 '20 at 11:00