Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tkinter focus_get() with non-tkinter Tk widget #88758

Open
Akuli mannequin opened this issue Jul 9, 2021 · 13 comments
Open

tkinter focus_get() with non-tkinter Tk widget #88758

Akuli mannequin opened this issue Jul 9, 2021 · 13 comments
Labels
3.9 only security fixes 3.10 only security fixes 3.11 only security fixes topic-tkinter type-bug An unexpected behavior, bug, or error

Comments

@Akuli
Copy link
Mannequin

Akuli mannequin commented Jul 9, 2021

BPO 44592
Nosy @terryjreedy, @serhiy-storchaka, @Akuli, @E-Paine
Superseder
  • bpo-734176: Make Tkinter.py's nametowidget work with cloned menu widgets
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2021-07-09.15:14:18.546>
    labels = ['type-bug', 'expert-tkinter', '3.9', '3.10', '3.11']
    title = 'tkinter focus_get() with non-tkinter Tk widget'
    updated_at = <Date 2021-07-17.18:19:16.891>
    user = 'https://github.com/Akuli'

    bugs.python.org fields:

    activity = <Date 2021-07-17.18:19:16.891>
    actor = 'Akuli'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Tkinter']
    creation = <Date 2021-07-09.15:14:18.546>
    creator = 'Akuli'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 44592
    keywords = []
    message_count = 12.0
    messages = ['397199', '397200', '397211', '397213', '397216', '397217', '397220', '397222', '397224', '397681', '397726', '397727']
    nosy_count = 4.0
    nosy_names = ['terry.reedy', 'serhiy.storchaka', 'Akuli', 'epaine']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = 'resolved'
    status = 'open'
    superseder = '734176'
    type = 'behavior'
    url = 'https://bugs.python.org/issue44592'
    versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']

    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 9, 2021

    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
    

    @Akuli Akuli mannequin added 3.10 only security fixes topic-tkinter type-bug An unexpected behavior, bug, or error labels Jul 9, 2021
    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 9, 2021

    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.

    @E-Paine
    Copy link
    Mannequin

    E-Paine mannequin commented Jul 9, 2021

    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?

    @E-Paine E-Paine mannequin added 3.9 only security fixes 3.11 only security fixes labels Jul 9, 2021
    @E-Paine
    Copy link
    Mannequin

    E-Paine mannequin commented Jul 9, 2021

    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)

    @serhiy-storchaka
    Copy link
    Member

    It is a duplicate of bpo-734176.

    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 9, 2021

    I found bpo-734176 before I created this. It is NOT a duplicate. While bpo-734176 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.

    @terryjreedy
    Copy link
    Member

    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.

    @terryjreedy terryjreedy reopened this Jul 9, 2021
    @terryjreedy
    Copy link
    Member

    I am not quite convinced that this is a duplicate of bpo-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.

    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 9, 2021

    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!

    @Akuli Akuli mannequin reopened this Jul 9, 2021
    @terryjreedy
    Copy link
    Member

    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.

    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 17, 2021

    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.

    @Akuli
    Copy link
    Mannequin Author

    Akuli mannequin commented Jul 17, 2021

    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".

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @TheLizzard
    Copy link

    I ran into this problem with this code:

    from tkinter.filedialog import askopenfilename
    import tkinter as tk
    
    def f(e):
        print(root.focus_get())
    
    root = tk.Tk()
    root.bind("<FocusOut>", f)
    
    button = tk.Button(root, command=askopenfilename, text="click me")
    button.pack()
    
    root.mainloop()

    The code raises a KeyError on Ubuntu 22.04 but not on Windows 11.

    Using this to investigate:

    widgets = set()
    def g():
        widget = root.tk.call('focus')
        widgets.add(str(widget))
        root.after(10, g)
    g()

    I get that on Windows, tcl doesn't even try to name the widgets inside the dialogbox but on Ubuntu:

    >>> widgets
    {'', '.__tk_filedialog.contents.f2.cancel', '.', '.__tk_filedialog.contents.f2.ent', '.__tk_filedialog'}

    TheLizzard added a commit to TheLizzard/BetterTk that referenced this issue Sep 20, 2023
    * Renamed `NoteBook` to `Notebook`
    * Fixed a bug caused by cpython's implementation of tkinter: python/cpython#88758
    * Fixed a bug in popup centre
    * Fixed a bug in `Notebook` where renaming a selected tab will not resize the notch
    * Fixed a bug in `Notebook` where renaming to the same name will do a lot of work
    * `slave.py` now accepts `cd` commands
    * `Terminal`, `TerminalFrame`, and `TerminalTk` now can report if they are running or dead.
    * `TerminalTk` now has a `send_ping` method which waits for the pong before returning
    * Now commands that ended will not write "=== Process Ended [<exit_code>] ===" to the screen.
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.9 only security fixes 3.10 only security fixes 3.11 only security fixes topic-tkinter type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    3 participants