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

Unable to switch tabs in TabbedContent using a key binding #4955

Open
merriam opened this issue Aug 30, 2024 · 14 comments
Open

Unable to switch tabs in TabbedContent using a key binding #4955

merriam opened this issue Aug 30, 2024 · 14 comments
Labels
bug Something isn't working

Comments

@merriam
Copy link
Contributor

merriam commented Aug 30, 2024

So I have this annoying issue. I haven't pulled it down to a minimum example, as the area is poorly documented and it may be a user error. The only relevant points are that fills-search and txs-search are Input widgets and that ctrl+f and ctrl+t are bindings.

So I have this test:

    # part A
    await pilot.press("ctrl+f")
    await pilot.click('#fills-search')
    await pilot.press("s", "h")
    assert app.get_widget_by_id('fills-data', DataTable).get_cell_at((0,2)).strip().startswith("Sharks")

    # part B
    await pilot.press("ctrl+t")
    await pilot.click('#txs-search')
    await pilot.press("6", ".", "8", "5")
    assert app.get_widget_by_id('txs-data', DataTable).get_cell_at((0,2)).strip() == '-6.85'

On the second part, even if I switch part A and B, the pilot.click function will cause app.screen_stack to acquire a CommandPalette, which means the get_widget_by_id is looking at the wrong screen. That is, the app works differently in testing than in execution. I don't know why Textual does this, but I look side-eyed at pilot.py/345. If I try to work around the problem by adding ENABLE_COMMAND_PALETTE = False to my app, then it does not bring up the COMMAND_PALETTE, but it stops causing the @on(Input.Changed,...) to fire. Again, Pilot not working like my app.

Is this a bug or am I using it wrong?

Copy link

We found the following entry in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

@TomJGooding
Copy link
Contributor

What do the ctrl+f and ctrl+t bindings do?

@TomJGooding
Copy link
Contributor

TomJGooding commented Aug 30, 2024

I've just had a thought. Remember that the default size of a simulated app is (80, 24). so maybe your input widgets aren't positioned where you expect?

@merriam
Copy link
Contributor Author

merriam commented Aug 30, 2024

The ctrl+f and ctrl+t switch a TabbedContent, basically switching screen. Size of a simulated app shouldn't matter, the only coordinate is the one (0,0) generated by Pilot. It's likely that Pilot's click has that spot (line 345ish) where a click starts with a fake click at 0,0, which brings up a command palette. Haven't seen why it kills the Input.Changed message.

This only breaks on the second part being executed. My work-around was to separate the tests.

I do notice Input changes my bindings, though its likely unrelated. Any time I click into an Input, my bindings change.

I set these bindings:

  BINDINGS = [
      ("ctrl+a", "app.go_account", "Accounts"),
      ("ctrl+f", "app.go_fills", "Fills"),
      ("ctrl+t", "app.go_transactions", "Transactions"),
      ("ctrl+n", "app.new_fill", "New Fill"),
      ("ctrl+s", "app.go_stories", "Stories"),
      Binding("ctrl+q", "app.quit", "Quit", show=True),
  ]

The bottom line of my display has shortcuts for control+{AFTNSQ}, when I click in an input field, the bindings for A and F disappear, presumably because Input binds them to beginning of line and delete word right.

@willmcgugan
Copy link
Collaborator

Try calling app.save_screenshot to validate the screen is in the state you expect it to be.

@merriam
Copy link
Contributor Author

merriam commented Sep 1, 2024

Well, I looked at it again. I have a work around, so its not really a priority. Here's the weird stuff:

I use this for screenshots: import os; ss = pilot.app.save_screenshot(); os.system(f"open {ss}")

I use this for widget hierarchy: from rich.console import Console; Console().print(app.tree)

First, bindings break as soon as an Input is clicked. While the documentation suggests a few bindings are taken, no bindings from the original application are respected. For example, I created a binding off of 'ctrl+*' as there are no current bindings. It is not respected if in an Input and is ignored. That explains why I could not switch to another tab within the script. That is the sequence "click Input field; press a binding key to do something" does not work but "click Input field. click a datatable or label, then press a binding key to do something." At very least, this needs documentation in the Input Bindings section. As a hint, the app.active_bindings still lists the binding for 'ctrl+*' but doesn't call it.

Second, pilot happily tries to click on fields not currently displayed. When I click on #txs-search and that widget is not displayed because it is in a TabbedContent, then it just continues with a widget address of 0,0. Clicking at 0,0 is the same as clicking the header, which brings up the command palette.

Third, breakpoints in PyCharm are flaky for asynchronous function when run as the 'current file'. Made debugging a bit harder.

Fourth, at least on my MacOS system, textual keys gives bizarre values. For example, 'ctrl+7" is 'ctrl+underscore', '\x1f'.. Control 1 to 9 is "ctrl+1, ctrl+@, escape, ctrl+backslash, ctrl+right_square_bracket, ctrl+cirumflex_accent, ..."

My instincts are to back away slowly and try to get on with my work.

@TomJGooding
Copy link
Contributor

TomJGooding commented Sep 1, 2024

Thanks for reporting back, I was a bit stumped why the command palette was being launched but forgot about that feature in the header!

The Input and TextArea widgets do have a lot of bindings which might catch people out like this (and of course consuming printable keys!), so perhaps it might be worth flagging this somewhere in the docs?

Fourth, at least on my MacOS system, textual keys gives bizarre values...

It depends on your terminal emulator and the escape sequences it generates for certain key combinations. For example, Ctrl+7, Ctrl+/ and Ctrl+_ may produce identical escape sequences in many terminals, making it impossible for Textual to differentiate between them.

@merriam
Copy link
Contributor Author

merriam commented Sep 1, 2024

OK.

The bindings issue is not that Input has a lot of bindings. It is that Input overrides all bindings. My example was ctrl-*, which is not a binding anything uses but still no longer fired the binding action. Similarly, ctrl+s and ctrl+t should not have a binding. The work-around is to click outside the Input to restore bindings behavior.

Thie pilot issue is that I do not get a KeyError or ValueError when trying to click on a control that is not displayed.

The odd keys are indeed emulator specific. For example, MacOS Terminal refuses to do anything with control plus the number row.

@merriam
Copy link
Contributor Author

merriam commented Sep 2, 2024

As a personal failing, I cannot put a problem down....

First, I attached a random file of tabbed content. I have not added tests for it. bug4955.py.zip

Second, I noticed this additional oddity which may be the cause of some of the other effects. I thought the correct way to switch the active tab is to set the reactive element, e.g., self.get_widget_by_id('tabs').active = 't-tab'. What I see is that when doing from the processing of a binding, is that the active tab changes, changes back again, and then acts as if I hit a tab. In the sample, click on the 'T' tab. Type ctrl+s, but do not end in the S tab. This appears to happen with our without bindings. So that is the actual cause of what looked like a Bindings issue.

@TomJGooding
Copy link
Contributor

TomJGooding commented Sep 3, 2024

Thanks - I see the problem now. To clarify the steps to reproduce:

  1. Click to focus the input (or the button) within the tab pane.
  2. Press Ctrl+t to try to switch to the T tab.

The new tab is briefly activated, but the app seems confused about what widget should now be focused. This causes the original tab to be re-activated again instead.

Minimized example
from textual.app import App, ComposeResult
from textual.widgets import Button, Footer, Input, TabbedContent, TabPane


class Bug4955App(App):
    BINDINGS = [
        ("ctrl+s", "ctrl_s", "S"),
        ("ctrl+t", "ctrl_t", "T"),
    ]

    def action_ctrl_s(self) -> None:
        self.query_one(TabbedContent).active = "s-tab"

    def action_ctrl_t(self) -> None:
        self.query_one(TabbedContent).active = "t-tab"

    def compose(self) -> ComposeResult:
        with TabbedContent(initial="s-tab"):
            with TabPane("S Group", id="s-tab"):
                yield Input("sss")
                yield Button("S")
            with TabPane("T Group", id="t-tab"):
                yield Input("ttt")
                yield Button("T")
        yield Footer()


if __name__ == "__main__":
    app = Bug4955App()
    app.run()

@TomJGooding
Copy link
Contributor

# The new tab is activated...
TabActivated(TabbedContent(), ContentTab(id='--content-tab-t-tab'), TabPane(id='t-tab'))
# ... which hides the old tab
Hide() >>> TabPane(id='s-tab') method=<Widget.on_hide>
Hide() >>> Input(id='s-input') method=<Widget.on_hide>
# The problem is that after the input is hidden, the focus moves to the button...
Button(id='s-btn') was focused
# ...which causes a TabPane.Focused event...
TabPane.Focused(tab_pane=TabPane(id='s-tab'))
# ... which re-activates the original tab again
TabActivated(TabbedContent(), ContentTab(id='--content-tab-s-tab'), TabPane(id='s-tab'))

@darrenburns
Copy link
Member

This is due to the automatic tab switching added in 9a28549 - when you focus a widget that's inside a tab, the tab is automatically made active.

This is clashing with Textual's logic to automatically switch focus to the next widget when a widget is hidden.

@darrenburns darrenburns added the bug Something isn't working label Sep 12, 2024
@darrenburns darrenburns changed the title Pilot.click() brings up command palette if last cursor position was 0,0. Unable to switch tabs in TabbedContent using a key binding Sep 12, 2024
@merriam
Copy link
Contributor Author

merriam commented Sep 14, 2024

Should there be a second bug for Pilot allowing clicks on non-displayed buttons? Or do you want to treat that as an acceptable tester error?

@darrenburns
Copy link
Member

As far as I remember we agreed when that feature was first added that we weren't going to check that the button was actually visible on screen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants