classification
Title: tkinter focus_get() with non-tkinter Tk widget
Type: behavior Stage: resolved
Components: Tkinter Versions: Python 3.11, Python 3.10, Python 3.9
process
Status: open Resolution:
Dependencies: Superseder: Make Tkinter.py's nametowidget work with cloned menu widgets
View: 734176
Assigned To: Nosy List: Akuli, epaine, serhiy.storchaka, terry.reedy
Priority: normal Keywords:

Created on 2021-07-09 15:14 by Akuli, last changed 2021-07-17 18:19 by Akuli.

Messages (12)
msg397199 - (view) Author: Akuli (Akuli) Date: 2021-07-09 15:14
The purpose of focus_get() is to return the widget that currently has the focus. It tries to convert its result to a tkinter widget, which can fail, because not all Tk widgets are known to tkinter. Consider this, for example:

    import tkinter

    def print_focused_widget():
        print(repr(root.focus_get()))
        root.after(1000, print_focused_widget)

    root = tkinter.Tk()
    menu = root["menu"] = tkinter.Menu()
    menu.add_cascade(label="Click here", menu=tkinter.Menu())
    print_focused_widget()
    tkinter.mainloop()

Output, with menu clicked after a couple seconds (on Linux):

    None
    <tkinter.Tk object .>
    <tkinter.Tk object .>
    Exception in Tkinter callback
    Traceback (most recent call last):
      File "/home/akuli/.local/lib/python3.10/tkinter/__init__.py", line 1916, in __call__
        return self.func(*args)
      File "/home/akuli/.local/lib/python3.10/tkinter/__init__.py", line 838, in callit
        func(*args)
      File "/home/akuli/porcu/foo.py", line 4, in print_focused_widget
        print(repr(root.focus_get()))
      File "/home/akuli/.local/lib/python3.10/tkinter/__init__.py", line 782, in focus_get
        return self._nametowidget(name)
      File "/home/akuli/.local/lib/python3.10/tkinter/__init__.py", line 1531, in nametowidget
        w = w.children[n]
    KeyError: '#!menu'

Some nametowidget() calls in tkinter/__init__.py already handle this correctly. Consider winfo_children(), for example:

            try:
                # Tcl sometimes returns extra windows, e.g. for
                # menus; those need to be skipped
                result.append(self._nametowidget(child))
            except KeyError:
                pass
msg397200 - (view) Author: Akuli (Akuli) Date: 2021-07-09 15:16
Forgot to mention: The correct fix IMO would be to return None when a KeyError occurs. This way code like `focus_get() == some_tkinter_widget` would always do the right thing, for example.
msg397211 - (view) Author: E. Paine (epaine) * Date: 2021-07-09 19:44
I agree with Akuli that raising a KeyError is not expected behaviour (combined with the fact this is caught elsewhere), and therefore is probably a regression.

While we could use `winfo class` to determine the type of Tk widget, this would probably require a reasonably sized refactor of tkinter (and we would still need to support cases when it's a type we don't know). Therefore, I think returning `None` is the best solution.

Akuli, would you like to create a pull request for this?
msg397213 - (view) Author: E. Paine (epaine) * Date: 2021-07-09 19:50
Sorry, I should specify that we would use `winfo class` in order to then return a new tkinter object for the existing Tk widget (which currently cannot be done)
msg397216 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2021-07-09 20:27
It is a duplicate of issue734176.
msg397217 - (view) Author: Akuli (Akuli) Date: 2021-07-09 20:33
I found issue734176 before I created this. It is NOT a duplicate. While issue734176 is about menus, this one is about focus_get(), and not necessarily related to menus. In fact, I initially noticed this with an "open file" dialog, not with a menu.

I'm not putting my address into your CLA, thank you very much.
msg397220 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-07-09 20:46
Akuli, what tk widgets do you think are not known to tkinter?  In any case, tk menu is known to tkinter.

I cannot reproduce when running on Windows with 3.10.0b3:  Add "print(root.children)" (after add_cascade) results in {'!menu': <tkinter.Menu object .!menu>, '!menu2': <tkinter.Menu object .!menu2>}.  The names are created in tkinter.py lines 2564-2573.

I then see 'None' once and then '<tkinter.Tk object .>' indefinitely even while hovering over and clicking 'click me' and the dropdown.  If I click outside the tk box, the print returns to 'None'.

Maybe there is an OS difference in what is considered to have 'focus'.

Key '#!menu' looks like '!menu' with '#' prepended.  Someone could try changing the tkinter code referenced above and see if the change appears in the bad key.  Also check the contents of root.children.
msg397222 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-07-09 21:02
I am not quite convinced that this is a duplicate of #734176. The latter is about tearoff clones and nothing is cloned here. But I do notice that number 'names were also prefixed with '#'.  What happens if 'tearoff=0' is added to the cascade so that it is not even clonable.  The tkinter naming of instances after the class was added less than 10 years ago.
msg397224 - (view) Author: Akuli (Akuli) Date: 2021-07-09 21:03
Unfortunately I don't know any real-world examples of this on Windows. The open file dialog works very differently on Windows: it uses the native Windows dialog, whereas on Linux, it's implemented in Tcl.

Meanwhile, here's a platform-independent toy example:

    import tkinter

    root = tkinter.Tk()
    root.tk.eval("""
    entry .e
    pack .e
    focus .e
    """)
    root.after(500, root.focus_get)
    root.mainloop()

Also, thanks for reopening!
msg397681 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2021-07-17 02:25
Traceback (most recent call last):
  File "C:\Programs\Python310\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
  File "C:\Programs\Python310\lib\tkinter\__init__.py", line 839, in callit
    func(*args)
  File "C:\Programs\Python310\lib\tkinter\__init__.py", line 783, in focus_get
    return self._nametowidget(name)
  File "C:\Programs\Python310\lib\tkinter\__init__.py", line 1536, in nametowidget
    w = w.children[n]
KeyError: 'e'

Is catching KeyError in the following
            try:
                # Tcl sometimes returns extra windows, e.g. for
                # menus; those need to be skipped
                result.append(self._nametowidget(child))
            except KeyError:
                pass
really correct?  It appears to skip things that *can* get focus by key or mouse action.  But what choice is there?

Silently failing when asked to focus on something is even less obviously correct.  For 'widget = root.focus_get' to assign None to widget is not obviously useful as it likely just delays the error.
msg397726 - (view) Author: Akuli (Akuli) Date: 2021-07-17 18:17
Here are the options:

- Do nothing. My program will error in some corner cases.

- Change it to return `None`, so `widget.focus_get() is not None` no longer means "this application doesn't have focus", but rather "this application doesn't have focus or the focused widget was not created in tkinter". Note that `focus_get()` can already return None, and we would just add one more situation where it does so.

- Change tkinter so that it doesn't matter whether a widget was created in tkinter or not. This doesn't seem to be easy.
msg397727 - (view) Author: Akuli (Akuli) Date: 2021-07-17 18:19
Typo in previous message: I meant `widget.focus_get() is None`. It currently means "this application doesn't have focus", while `is not None` currently means "this application has focus".
History
Date User Action Args
2021-07-17 18:19:16Akulisetmessages: + msg397727
2021-07-17 18:17:08Akulisetmessages: + msg397726
2021-07-17 02:25:48terry.reedysetmessages: + msg397681
2021-07-09 21:03:13Akulisetstatus: closed -> open
resolution: duplicate ->
messages: + msg397224
2021-07-09 21:02:12terry.reedysetstatus: open -> closed
superseder: Make Tkinter.py's nametowidget work with cloned menu widgets
messages: + msg397222

resolution: duplicate
stage: resolved
2021-07-09 20:46:54terry.reedysetstatus: closed -> open

superseder: Make Tkinter.py's nametowidget work with cloned menu widgets -> (no value)

nosy: + terry.reedy
messages: + msg397220
resolution: duplicate -> (no value)
stage: resolved -> (no value)
2021-07-09 20:33:16Akulisetmessages: + msg397217
2021-07-09 20:27:25serhiy.storchakasetstatus: open -> closed
superseder: Make Tkinter.py's nametowidget work with cloned menu widgets
messages: + msg397216

resolution: duplicate
stage: resolved
2021-07-09 19:50:59epainesetmessages: + msg397213
2021-07-09 19:44:29epainesetnosy: + serhiy.storchaka, epaine

messages: + msg397211
versions: + Python 3.9, Python 3.11
2021-07-09 15:16:04Akulisetmessages: + msg397200
2021-07-09 15:14:18Akulicreate