prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.28k stars 715 forks source link

Buttons in DynamicContainer don't seem to receive focus #1324

Open robintw opened 3 years ago

robintw commented 3 years ago

I've been trying to use some buttons inside a DynamicContainer, but I can't seem to get the buttons to have focus properly so that I can select them.

My code is below:

from prompt_toolkit import Application
from prompt_toolkit.application.current import get_app
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.key_binding.key_bindings import KeyBindings
from prompt_toolkit.layout import HSplit, Layout
from prompt_toolkit.layout.containers import DynamicContainer, FloatContainer
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Button

def get_container_contents():
    return HSplit([Button(text="Click here", handler=lambda: print("Hello")), Button("Click here instead")])

def create_prompt_toolkit_app():
    kb = KeyBindings()

    kb.add("tab")(focus_next)
    kb.add("s-tab")(focus_previous)

    @kb.add("escape")
    def exit_handler(event=None) -> None:
        "Button handler that exits the app"
        get_app().exit()

    style = Style(
        [
            ("button", "#00ff00"),
            ("button.focused", "fg:#ff0000"),
        ]
    )

    root_container = DynamicContainer(get_container_contents)
    #root_container = FloatContainer(get_container_contents(), floats=[])

    layout = Layout(root_container)

    app = Application(layout=layout, full_screen=True, key_bindings=kb, style=style)

    return app

app = create_prompt_toolkit_app()
app.run()

You can see there are two lines of code defining the root_container, and one is commented out. If I use the DynamicContainer one of those lines, then I get an app where I can't move the focus between the two buttons. If I press Enter in this app, then hello gets printed, so it seems like the button sort-of has focus (in that it responds to an Enter keypress), but it isn't shown as having focus (according to the style), and I can't seem to move the focus.

If, I replace that line of code with the FloatContainer one of those lines, then everything works completely normally: I can move the focus around, and pressing Enter on the different buttons behaves as expected.

I've tried setting the focus manually using app.layout.focus() but that doesn't seem to change anything.

Does anyone have any idea what is going on here? I'm sure I've used DynamicContainers ok before without this problem.

robintw commented 3 years ago

I've looked into this some more, and if I change the Button instances to be stored in global variables, rather than defined inline in the function, then it works. The top of the new code looks like this:

button1 = Button(text="Click here", handler=lambda: print("Hello"))
button2 = Button("Click here instead")

def get_container_contents():
    return Box(body=HSplit([button1, button2]))

I'm not sure what's going on with this, and why it only works if the variables are in a different scope. I've found similar things in my actual code (this is a simplified example for posting here), where if I store the Button object as a member variable of the class, and create it in the __init__, then it works fine, but if I have it as a member variable but create it in the get_container_contents function then it doesn't.