beeware / toga

A Python native, OS native GUI toolkit.
https://toga.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
4.29k stars 666 forks source link

Tab not working when trying to switch between textInputs generated during runtime #2766

Open drew0ps opened 1 month ago

drew0ps commented 1 month ago

Describe the bug

I came across a strange bug while developing my application with Toga under Beeware default layout. When I place textInputs during startup I can use tab to switch between them. But when I use a button to generate more I can no longer switch between the new ones using tab - although I can still tab between the ones originally placed. They are in the same box.

code snippet:

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW, CENTER, LEFT
from string import ascii_uppercase as auc

colors = ['#9DB2BF', '#526D82', '#27374D', '#27374D']

class reprod(toga.App):
    def startup(self):
        self.training_days = 1
        self.gui(self)

    def gui(self, widget):
        self.main_box = toga.Box(style=Pack(direction=ROW, padding=0, alignment=LEFT, background_color=colors[0]))
        self.nav_box = toga.Box(style=Pack(direction=COLUMN, padding=20, alignment=CENTER, background_color=colors[2]))

        self.program_name_field = toga.TextInput(placeholder="Program Name", style=Pack(padding=10, width=150, alignment=LEFT))
        self.sheet_id_field = toga.TextInput(placeholder="Sheet ID", style=Pack(padding=10, width=150, alignment=LEFT))
        add_day_button = toga.Button("Add Day", on_press=self.add_training_day, style=Pack(padding=10, width=150, alignment=LEFT))

        self.nav_box.add(self.program_name_field, self.sheet_id_field, add_day_button)
        self.main_box.add(self.nav_box)
        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = self.main_box
        self.main_window.show()

    def add_training_day(self, widget):
        self.training_days = 1
        day_frame = toga.Box(style=Pack(direction=ROW, alignment=LEFT, padding=10, background_color=colors[2]))
        day_box = toga.Box(style=Pack(direction=COLUMN, padding=20, alignment=CENTER, background_color=colors[1]))

        training_day = toga.TextInput(placeholder=f"Training day {self.training_days}", style=Pack(padding=10, width=300, alignment=LEFT))
        muscle_groups = toga.TextInput(placeholder="Muscle groups", style=Pack(padding=10, width=300, alignment=LEFT))

        day_box.add(training_day, muscle_groups)
        day_frame.add(day_box)
        day_container = toga.ScrollContainer(content=day_frame, horizontal=False, style=Pack(direction=COLUMN, padding=20, alignment=CENTER, background_color=colors[2], height=600, width=400))
        self.main_box.add(day_container)
        self.training_days += 1

def main():
    return reprod()

As I enter stuff I would love to use tab to make it easier. It does not seem possible however with generated textinputs.

Steps to reproduce

  1. Paste the above code to the default briefcase new structure's app.py
  2. Run the app using briefcase dev (or run) and observe that you can switch between the textinputs using tab
  3. Click the add day button
  4. Observe that you can not switch between the newly generated textinputs using tab

Expected behavior

I can switch between textinputs at least in the same box with tab.

Screenshots

No response

Environment

Logs

Additional context

No response

freakboy3742 commented 1 month ago

Thanks for the report - I can't think of any reason the behaviour you're reporting would be happening.

I'd like to be able to help more, but I can't reproduce the problem, because your reproduction code doesn't reproduce the problem. It might be part of a reproduction case, but it's not complete (it references variables like colors that aren't defined), it doesn't actually display anything as written (it doesn't ever add anything to the nav_box) - and AFAICT, theres's nothing present to actually reproduce the problem as described (i.e., there's no logic that adds new text inputs that can't get input focus).

A working reproduction case is an essential part of any bug report; can I ask you to provide an updated - and complete - reproduction case?

drew0ps commented 1 month ago

Hi Russell,

Thanks for your answer and your help. Indeed, the quality of the reprod code was not adequate. I have created a new one with relevant code and made sure it works and it reproduces the problem. I've edited the issue description with the updated code.

freakboy3742 commented 1 month ago

Thanks - I can now reproduce the problem you're seeing. I think I understand why it is occurring. Unfortunately, it's a bug, not an error of usage; and I can't point you at an easy cross-platform workaround.

The underlying problem is that Cocoa tracks the focus chain as a linked list - each widget has a field nextKeyView that points to the next widget in the focus chain. When the original pair of inputs is created, they link to each other in a loop; when the new widgets are added, for some reason they aren't inserted into the existing loop.

You can work around this on a platform-specific basis by reaching into the internals. For example, if you add the following to the end of add_training_day():

            self.sheet_id_field._impl.native.nextKeyView = training_day._impl.native
            training_day._impl.native.nextKeyView = muscle_groups._impl.native
            muscle_groups._impl.native.nextKeyView = (
                self.program_name_field._impl.native
            )

This reaches into the native Cocoa widgets backing the Toga widgets, and tells the SheetID that it should point at the input of the first training day; the training day should point at the muscle group; and the muscle group should point back at the program name. You'll need to modify this so that adding the second training day inserts the new widgets into the appropriate spot in the chain, but hopefully the general approach should be clear.

I should also note that this code is macOS only, so if you're planning to target any other platform, you'll need to gate this logic with if sys.platform == 'darwin' or similar.

I would imaging that a full implementation of #1650 will likely fix the problem inadvertently, because part of adding a new widget will invoke re-computing the tab index sequence for all other widgets.

drew0ps commented 1 month ago

Thanks a lot for the explanation and the suggested workaround, it makes a lot of sense. For context, my code is a tad bit more complicated for this app, in addition to all the add operations I also have remove operations which I can imagine would require me to modify linkedview field of the previous field according to what comes next in my dict[somelist][somewidget] where I store all the objects.

For the record, @rmartin16 managed to reproduce this issue on Windows and Linux too as per his message on Discord