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

Issues with new Footer #4930

Closed
davidbrochart opened this issue Aug 24, 2024 · 18 comments · Fixed by #4940
Closed

Issues with new Footer #4930

davidbrochart opened this issue Aug 24, 2024 · 18 comments · Fixed by #4940

Comments

@davidbrochart
Copy link
Contributor

There are two problems with the following code:

  • The footer doesn't show del Delete the thing (it does if there are no Input widget).
  • Clicking on q Quit the app enters the character q in the input widget, but doesn't quit the app.
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer
from textual.widgets import Input


class MyApp(App):
    BINDINGS = [
        Binding(key="q", action="quit", description="Quit the app"),
        Binding(
            key="question_mark",
            action="help",
            description="Show help screen",
            key_display="?",
        ),
        Binding(key="delete", action="delete", description="Delete the thing"),
        Binding(key="j", action="down", description="Scroll down", show=False),
    ]

    def compose(self) -> ComposeResult:
        yield Input(placeholder="First Name")
        yield Input(placeholder="Last Name")
        yield Footer()


if __name__ == "__main__":
    app = MyApp()
    app.run()
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.77.0 |
| Rich    | 13.7.1 |

## Python

| Name           | Value                                                                     |
|----------------|---------------------------------------------------------------------------|
| Version        | 3.12.5                                                                    |
| Implementation | CPython                                                                   |
| Compiler       | GCC 12.4.0                                                                |
| Executable     | /home/david/.local/share/hatch/env/virtual/jpterm/N1y3coPp/dev/bin/python |

## Operating System

| Name    | Value                                                       |
|---------|-------------------------------------------------------------|
| System  | Linux                                                       |
| Release | 6.8.0-40-generic                                            |
| Version | #40-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul  5 10:34:03 UTC 2024 |

## Terminal

| Name                 | Value                              |
|----------------------|------------------------------------|
| Terminal Application | WezTerm (20240203-110809-5046fc22) |
| TERM                 | xterm-256color                     |
| COLORTERM            | truecolor                          |
| FORCE_COLOR          | *Not set*                          |
| NO_COLOR             | *Not set*                          |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=135, height=31 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 135                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 31                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |
@Textualize Textualize deleted a comment from github-actions bot Aug 24, 2024
@willmcgugan
Copy link
Collaborator

When a widget has focus, it's bindings take precedence. The input binds delete, which has show=False on the Input itself. Hence it doesn't show.

If you set priority=True on the app bindings they will override any bindings on the Input.

The binding to quit the app is a bit of an edge case. Clicking the footer simulates the key. When the Input has focused, that of course produces a "q". If you set priority=True on the binding it should work as expected, but then you wouldn't be able to enter a "q" in the input...

@davidbrochart
Copy link
Contributor Author

Thanks for the details.
Are you sure simulating the key is the best solution? I would say that clicking on the footer should directly call the action (quitting the app in this case), not indirectly by simulating the key. But maybe I'm missing something.

@willmcgugan
Copy link
Collaborator

Maybe. But simulating the key when you click the footer does mean that it goes through the same logic. If clicking ran the action, then you have this strange situation where pressing the key could have a different effect to clicking the footer. Like in the q binding.

I think the best solution may be to trust the dev to pick keys that don't clash. If you have Inputs, then your app/screen bindings should have a modifier, like ctrl+q

@darrenburns
Copy link
Member

I don't think it's ever a reasonable expectation that clicking an action in the footer would insert text into an Input rather than running the action. Wouldn't the vast majority of users expect the app to quit if they click a button that says "Quit"?

@willmcgugan
Copy link
Collaborator

They would also expect when the footer says "q Quit" that pressing "q" would quit. But it won't if the input is consuming the printable keys.

We could have something on the widget that declares it will consume printable keys. That way we would know to hide the key from the footer.

@davidbrochart
Copy link
Contributor Author

We could have something on the widget that declares it will consume printable keys. That way we would know to hide the key from the footer.

I agree, a child widget consuming a key should lead to the corresponding action being hidden in parent widgets.

@darrenburns
Copy link
Member

I think having some flag just saying the widget consumes printable keys is reasonable!

Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@xavierog
Copy link
Contributor

I almost opened another issue to share this:

Clicking the Footer simulates a key press:

async def on_mouse_down(self) -> None:
if self._disabled:
self.app.bell()
else:
self.app.simulate_key(self.key)

This works fine most of the time, but it produces a counter-intuitive behaviour when an input field (or, presumably, a text area) is focused: clicking an action appends a character to the focused input field instead of triggering the adequate action.

MRE:

#!/usr/bin/env python3
import os
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer, Input, Label

class FooterAndInput(App):
	BINDINGS = [
		Binding("a", "notify('A')", "AAAAA"),
		Binding("b", "notify('B')", "BBBBB"),
		Binding("c", "notify('C')", "CCCCC"),
		Binding("d", "notify('D')", "DDDDD"),
		Binding("e", "notify('E')", "EEEEE"),
	]

	def action_notify(self, value: str) -> None:
		self.notify(value)

	def compose(self) -> ComposeResult:
		if 'NO_INPUT' not in os.environ:
			yield Label("This input field is focused:")
			yield Input()
			yield Label("... therefore it is not possible to trigger the actions shown in the footer using the keyboard.")
			yield Label("But that should be possible using the mouse.")
		yield Footer()

if __name__ == '__main__':
	app = FooterAndInput()
	app.run()

My expectations are:

  1. a, b, c, d and e should keep typing in the input field and thus override the keybindings (current behaviour with 0.78.0);
  2. the actions should still appear in the footer (current behaviour with 0.78.0);
  3. clicking an action in the footer should trigger that action.

Point 2 is important as it highlights the assumption that the footer should display available actions rather than available keybindings. If the keybinding cannot be used, maybe this could be reflected through a visual change, but that seems optional to me: most users typing into a text field/area quickly realise that limitation.

@willmcgugan
Copy link
Collaborator

Hi @xavierog There is a solution to this in main. Widgets can declare they will consume a key before the bindings are processed with check_consume_key.

@davidbrochart
Copy link
Contributor Author

So the user has to make sure check_consume_key is in sync with e.g. an on_key handler?

@darrenburns
Copy link
Member

@davidbrochart Pretty much.

@willmcgugan
Copy link
Collaborator

It is only really a concern with widgets that don't exclusively use bindings. In the core lib that is only Input and TextArea.

@xavierog
Copy link
Contributor

Hi @xavierog There is a solution to this in main. Widgets can declare they will consume a key before the bindings are processed with check_consume_key.

Hi @willmcgugan
I have tested the technical solution in main with my MRE to ensure I understand it well, and it seems I do.
I think the current solution introduces another counter-intuitive behaviour (namely, masking actions that should remain visible unless explicitly overridden by another key binding) to mask the previous counter-intuitive behaviour (namely, the fact that clicking an action in the footer simulates a key press event instead of directly triggering the action).

To address this issue, I think it is important to focus on intuitive behaviours. Here,the most important of those has been expressed by David:

clicking on the footer should directly call the action (quitting the app in this case), not indirectly by simulating the key.

As to:

this strange situation where pressing the key could have a different effect to clicking the footer.

You are correct, it is a strange situation. But it's okay, really. Specifically:

They would also expect when the footer says "q Quit" that pressing "q" would quit. But it won't if the input is consuming the printable keys.

They expect the key they have pressed to do the right thing ©. Here, the right thing is to append the right character to the current Input/TextArea widget because typing text is fundamental and thus overrides everything else. If they are not typing text (i.e. a non-input widget is focused), then of course, the keybinding and its action should be triggered.
As you mentioned, all of this is an edge-case. And in that edge case, I think it is much more acceptable to show mouse-only actions than it is to hide actions. This is especially true for an action like "Quit".

What do you think?

@willmcgugan
Copy link
Collaborator

willmcgugan commented Aug 28, 2024

The footer is intended to show what you can do with the keyboard (or at least a subset). The fact that you can click it with the mouse is a convenience, but should never be the primary interface. Having the possibility of "mouse only" actions violates that.

I also think that pressing the key should have exactly the same behavior as clicking the footer. Not usually, but always. If pressing q doesn't quit, then telling the user it does is a red line for me.

Ultimately though, this should rarely be an issue. Devs can pick keys that don't clash. ctrl+q is a far better choice.

@TomJGooding
Copy link
Contributor

I completely agree with Will on this, for what it's worth.

The Footer is designed to "[display] available keybindings for the currently focused widget". It is not a clickable menu for every action defined in the app, simply displayed beside keybindings that may not work.

@willmcgugan
Copy link
Collaborator

@xavierog didn't mean to sound combative. I appreciate your input on this!

@TomJGooding
Copy link
Contributor

Hopefully that's not a reflection on my post - I didn't mean to sound combative either. I've shared my input so will leave any further discussion to the maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants