• Destroying a Combobox prevents Entry focus

    From Jonathan Lahav@21:1/5 to All on Sun Dec 26 01:46:55 2021
    I develop the GUI to a complex tool at work.
    In one use case, an option selection in a combobox should recreate the GUI layout, so the widgets are removed, destroyed, and a new set is created.

    The issue is that after the combobox is destroyed, widgets in the window can't get focus until an alt+tab to another window and back.
    Buttons can be clicked, but entries, for example, can't be used becuase they need focus to write into them.

    The issue happens on Windows, doesn't happen on Linux.

    I have to apologize in advance, since I don't know TCL, and I use Python to access Tk. I know it might be impolite to give a Python example, but that's what I know, so again, sorry.
    I created a minimal example to reproduce the issue. Even though it's in Python, the Tk calls should be understandable.
    If someone could please let me know if I used Tk incorrectly or if it's Python's fault I'll appreciate it.

    Thanks you!

    The code:
    import tkinter as tk
    from tkinter import ttk

    def combo_changed(_varname, _index, _operation):
    print('changed')
    reset()

    # main window
    window = tk.Tk()

    # main frame
    frame = ttk.Frame(window)
    frame.grid()

    # combobox
    cb = ttk.Combobox(frame, values=['1','2'])
    cb.grid()

    # entry
    entry = ttk.Entry(frame)
    entry.grid()

    # combobox var and event
    tk_var = tk.StringVar()
    cb.configure(textvariable=tk_var)
    trace_id = tk_var.trace_add('write', combo_changed)

    def reset():
    tk_var.trace_remove('write', trace_id)
    cb.grid_forget()
    cb.destroy()

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Jonathan Lahav@21:1/5 to All on Sun Dec 26 01:50:48 2021
    An easier way to read the code:
    https://pastebin.com/yP0JTFNi

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Scott Pitcher@21:1/5 to .........@gmail.com on Mon Dec 27 02:32:49 2021
    On Sunday, December 26, 2021 at 8:50:49 PM UTC+11, .........@gmail.com wrote:
    An easier way to read the code:
    https://pastebin.com/yP0JTFNi

    Please excuse me I parted ways with Python over 20 years ago. I had to fumble around and find a way of running the event loop which I've added at the bottom (window.mainloop()).
    I've changed the call to reset() from combo_changed() and moved it to an after event (after(0...)). The entry box had focus and was editable after that with both options '1' and '2'. I'm hoping (guessing) that's what you want?

    ```import tkinter as tk
    from tkinter import ttk

    def combo_changed(_varname, _index, _operation):
    print('changed')
    window.after(0,reset)
    #reset()

    # main window
    window = tk.Tk()

    # main frame
    frame = ttk.Frame(window)
    frame.grid()

    # combobox
    cb = ttk.Combobox(frame, values=['1','2'])
    cb.grid()

    # entry
    entry = ttk.Entry(frame)
    entry.grid()

    # combobox var and event
    tk_var = tk.StringVar()
    cb.configure(textvariable=tk_var)
    trace_id = tk_var.trace_add('write', combo_changed)

    def reset():
    tk_var.trace_remove('write', trace_id)
    cb.grid_forget()
    cb.destroy()

    window.mainloop()```

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Jonathan Lahav@21:1/5 to All on Thu Dec 30 04:54:00 2021
    Thanks a lot for making the effort to help despite the language barrier. I appreciate it!

    1. The fact that it's possible to get to this state, that widgets can't get focus, isn't it a bug in itself? Should it be reported as such? Where?

    2. I checked and the line that makes a difference is this:
    window.after(0,reset)
    Why does it help? What's happening here?

    Jonathan

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Scott Pitcher@21:1/5 to j.l...@gmail.com on Sun Jan 2 15:21:06 2022
    On Thursday, December 30, 2021 at 11:54:03 PM UTC+11, j.l...@gmail.com wrote:
    Why does it help? What's happening here?

    Jonathan

    I coded the practical equivalent in tcl script:

    ```#!/usr/bin/wish
    #
    # TclTk equivalent of the python test script.
    # From comp.lang.tcl 26DEC2021
    # "Destroying a Combobox prevents Entry focus"

    package require Tk
    # import tkinter as tk
    # from tkinter import ttk

    proc combo_changed {varname index operation} {
    puts "changed"
    # after 0 reset
    reset
    }

    # main window
    set window "."

    # main frame
    set fr [ttk::frame "$window.f"]
    grid $fr

    # combobox
    set cb [ttk::combobox "$fr.cb" -values {1 2}]
    grid $cb

    # entry
    set en [ttk::entry $fr.en]
    grid $en

    # combobox var and event
    set tk_var ""
    $cb configure -textvariable tk_var
    trace add variable tk_var write combo_changed

    proc reset {} {
    trace remove variable tk_var write combo_changed
    grid forget $::cb
    destroy $::cb
    }
    ```

    When i ran this code, again on Windows 7 with tcl 8.6.11 I found a background error. I added a bgerror handler (not sure how you do that in python:

    ```proc bgerror {message} {
    puts "Background error: $message"
    puts "errorInfo: $::errorInfo"
    }
    ```

    And this is the text output to the console:
    Background error:
    errorInfo:
    while executing
    "$cb current $index"
    (procedure "SelectEntry" line 2)
    invoked from within
    "SelectEntry $cb [lindex $selection 0]"
    (procedure "LBSelect" line 5)
    invoked from within
    "LBSelect $lb"
    (procedure "ttk::combobox::LBSelected" line 3)
    invoked from within
    "ttk::combobox::LBSelected .f.cb.popdown.f.l "
    (command bound to event)


    I'm not familiar with ttk, but it looks like the combobox is trying to select the current entry, but, I daresay it's already been destroyed. I changed reset to this:
    ```proc reset {} {
    trace remove variable tk_var write combo_changed
    grid forget $::cb
    puts "Calling destroy"
    destroy $::cb
    puts "destroy returned"
    }
    ```
    ... and I see this output now:
    changed
    Calling destroy
    destroy returned
    Background error:
    errorInfo:
    while executing
    "$cb current $index"
    (procedure "SelectEntry" line 2)
    invoked from within
    "SelectEntry $cb [lindex $selection 0]"
    (procedure "LBSelect" line 5)
    invoked from within
    "LBSelect $lb"
    (procedure "ttk::combobox::LBSelected" line 3)
    invoked from within
    "ttk::combobox::LBSelected .f.cb.popdown.f.l "
    (command bound to event)

    I probably wouldn't have called destroy. It's sufficient to call "grid forget" and let the combo box disappear.

    Is it a bug? I don't think so. When you can destroy your deleting the widget and if events trigger during this time and invoke the widget or try to, then background errors might arise. In tcl I would probably make more checks like wrapping the destroy
    calls in "if {[winfo exists $cb]} ....". Often a simple catch can trap the error like ```catch "destroy $::cb"```.

    Usually for production code some of these methods have to be used to ensure the app works cleanly, that you don't get error dialogs when the user closes windows and things like that.

    Just some general tcl advice I hope might be useful. I'm not sure how you do those things in Python.

    Scott

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Scott Pitcher@21:1/5 to j.l...@gmail.com on Sun Jan 2 19:07:13 2022
    On Thursday, December 30, 2021 at 11:54:03 PM UTC+11, j.l...@gmail.com wrote:
    2. I checked and the line that makes a difference is this: window.after(0,reset)
    Why does it help? What's happening here?

    To understand what is happening, you have to understand the flow of execution. Please bear with me here, I'm not familiar with the ttk::xxx widgets or the code, but the one good thing about ttk is that we can examine a lot of the event binding code as
    the modules are all pure tcl. So I had a look at the lib/tk8.6/ttk/combobox.tcl module. It's about 12K and 450 lines long but here are some important ones:

    Around line 63 we have -
    ```### Combobox listbox bindings.
    #
    bind ComboboxListbox <ButtonRelease-1> { ttk::combobox::LBSelected %W }
    bind ComboboxListbox <Return> { ttk::combobox::LBSelected %W }
    ```

    and at line 93 -
    ```## LBSelected $lb -- Activation binding for listbox
    # Set the combobox value to the currently-selected listbox value
    # and unpost the listbox.
    #
    proc ttk::combobox::LBSelected {lb} {
    set cb [LBMaster $lb]
    LBSelect $lb
    Unpost $cb
    focus $cb
    }```

    This is probably the function that runs when you release the mouse after clicking on the listbox, and it will then go ahead and make a new selection in the widget. I inserted some debug output at each line here so I could "checkpoint" and see where the
    error was popping up:
    ```proc ttk::combobox::LBSelected {lb} {
    puts "ttk::combobox::LBSelected lb=$lb"
    set cb [LBMaster $lb]
    puts " calling LBSelect"
    LBSelect $lb
    puts " calling Unpost"
    Unpost $cb
    puts " calling focus"
    focus $cb
    puts " ---LBSelected finished!"
    }
    ```

    I also took the LBSelect proc and inserted some there:
    ```## LBSelect $lb --
    # Transfer listbox selection to combobox value.
    #
    proc ttk::combobox::LBSelect {lb} {
    puts "ttk::combobox::LBSelect lb=$lb"
    set cb [LBMaster $lb]
    puts " calling curselection"
    set selection [$lb curselection]
    if {[llength $selection] == 1} {
    puts " calling SelectEntry"
    SelectEntry $cb [lindex $selection 0]
    }
    puts " ---LBSelect finished"
    }
    ```

    When I ran the test script again-
    ttk::combobox::LBSelected lb=.f.cb.popdown.f.l
    calling LBSelect
    ttk::combobox::LBSelect lb=.f.cb.popdown.f.l
    calling curselection
    calling SelectEntry
    changed
    Calling destroy
    destroy returned
    Background error:
    errorInfo:
    while executing
    "$cb current $index"
    (procedure "SelectEntry" line 2)
    invoked from within
    "SelectEntry $cb [lindex $selection 0]"
    (procedure "LBSelect" line 8)
    invoked from within
    "LBSelect $lb"
    (procedure "ttk::combobox::LBSelected" line 5)
    invoked from within
    "ttk::combobox::LBSelected .f.cb.popdown.f.l "
    (command bound to event)

    So what's happening is that the user is clicking on the combobox, the event runs and sets the selection and during the SelectEntry proc sets your variable is set, which activates the trace and calls combo_changed, which calls reset, which destroys the
    combobox, and when execution returns to SelectEntry, the comboxbox no longer exists and we get a background error and the focus is never set to the entry box.

    By putting the reset call into the after event ("after 0" basically translates to something like "after return") we decouple the combobox work from your reset function and the destroy will run later, no longer upsetting the combobox. I'll insert the
    after 0 and try it again and it all runs properly now:
    scottyw@officewinvm MINGW32 /n/Projects, Software/python/Test.comp.lang.tcl.26DEC2021
    $ wish script.tcl
    ttk::combobox::LBSelected lb=.f.cb.popdown.f.l
    calling LBSelect
    ttk::combobox::LBSelect lb=.f.cb.popdown.f.l
    calling curselection
    calling SelectEntry
    changed
    ---LBSelect finished
    calling Unpost
    calling focus
    ---LBSelected finished!
    Calling destroy
    destroy returned

    The thing to remember it that these actions are all happening as events, and if you want to do something drastic like destroy the widget that is calling you, during the call back itself, your best to place that outside the widget's execution in something
    like an "after 0" call back. If you must do it immediately then as a safety you should wrap the calls in catch "..." however in this case it's not going to help you, because the call to destroy during the trace call back is the problem itself.

    Hope these ramblings make sense. I'm 7 days into a very very bad flu I don't think what I've written it all that coherent. Normally when I'm trying to get to the bottom of problems with event code I'll use lots of puts "..." messages to see who is
    calling who and when. I had major headaches with a snit "messageentry" widget I'd made a couple of years ago in one commercial application, and that widget had become a real swiss army knife and now included popup suggestions list using a listbox that
    pops up dynamically. I found it most difficult to catch things like the user clicking away from the application and getting the button click in the widget to work correctly. It was all the added debug output from the event handlers that helped or at
    least, gave me enough information to pour over and sort out how to get it working.

    Scott

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Jonathan Lahav@21:1/5 to All on Thu Jan 13 01:32:03 2022
    These are not ramblings and thanks to you I solved the issue in our application. Thanks a lot for your help!
    Sorry to hear about the flu. Yes, it's terrible these days. Feels like a third of the population either got Covid or something else.

    I agree that it's probably a good idea to put the destroy() inside an after(0,) and indeed, this is what solved the issue in my application.

    However, it feels wrong that getting to a state where you can't click on any widget until alt+tab is even possible. The UI is in a weird state.
    I can leave it here, sure, but I'll take your word here as you know much more than I do.
    Should I report it or leave it?

    Thanks again
    Jonathan

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Scott Pitcher@21:1/5 to j.l...@gmail.com on Mon Jan 17 12:38:07 2022
    On Thursday, January 13, 2022 at 8:32:06 PM UTC+11, j.l...@gmail.com wrote:
    However, it feels wrong that getting to a state where you can't click on any widget until alt+tab is even possible. The UI is in a weird state.
    I can leave it here, sure, but I'll take your word here as you know much more than I do.
    Should I report it or leave it?

    Reporting it is probably a good idea. At the very least the widget should probably check it's state after it sets the variable's value.

    Kind regards,
    Scott

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)