textual icon indicating copy to clipboard operation
textual copied to clipboard

Input-related FooterKeys sorted differently since Textual 1.0.0

Open xavierog opened this issue 1 year ago • 4 comments

This issue affects Textual 1.0.0. It feels close to #4639.

MRE

from textual.app import App
from textual.binding import Binding
from textual.widget import Widget
from textual.widgets import Footer, Input

class Wid(Widget, can_focus=True):
    BINDINGS = [
        ('escape', 'escape', 'Widget Escape'),
        ('up', 'up', 'Widget Up'),
        ('down', 'down', 'Widget Down'),
    ]
    DEFAULT_CSS = "Wid { max-height: 5; }"

class Inp(Input):
    BINDINGS = [
        ('escape', 'escape', 'Input Escape'),
        ('up', 'up', 'Input Up'),
        ('down', 'down', 'Input Down'),
    ]

class MRE(App):
    def compose(self):
        yield Wid()
        yield Inp()
        yield Footer()

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

Expected behaviour

Textual 0.89.1 behaves as expected: focusing either the Wid Widget or the Inp Input leads to the following FooterKeys:

 esc Input Escape  ↑ Input Up  ↓ Input Down
 esc Widget Escape  ↑ Widget Up  ↓ Widget Down

Encountered behaviour

Textual 1.0.0 moves the escape FooterKey. This happens only to the Inp Input, not to the Wid Widget:

 ↑ Input Up  ↓ Input Down  esc Input Escape
 esc Widget Escape  ↑ Widget Up  ↓ Widget Down

xavierog avatar Dec 19 '24 17:12 xavierog

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

github-actions[bot] avatar Dec 19 '24 17:12 github-actions[bot]

I suspect this is because esc is also used to minimize a maximized widget. Presumably this existing 'binding' is further down the chain, so overriding this will cause it to appear last in the footer?

TomJGooding avatar Dec 19 '24 19:12 TomJGooding

It looks like this was introduced in e1bf6ff, where Input now inherits from ScrollView rather than Widget.

The multilevel inheritance seems to matter, for reasons I don't yet understand!

For example, where a widget simply inherits from Widget, the esc binding will be the first in the footer:

from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import Footer


class MyWidget(Widget, can_focus=True):
    BINDINGS = [
        ("escape", "escape", "Escape"),
        ("ctrl+a", "screen.maximize", "Maximize"),
    ]

    DEFAULT_CSS = """
    MyWidget {
        width: auto;
        height: auto;
    }
    """


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield MyWidget()
        yield Footer()


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

Whereas a widget with multilevel inheritance and a binding for esc binding will display this last in the footer:

from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import Footer


class MyWidget(Widget, can_focus=True):
    BINDINGS = [
        ("ctrl+a", "screen.maximize", "Maximize"),
    ]

    DEFAULT_CSS = """
    MyWidget {
        width: auto;
        height: auto;
    }
    """


class SubWidget(MyWidget):
    BINDINGS = [
        ("escape", "escape", "Escape"),
    ]


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield SubWidget()
        yield Footer()


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

TomJGooding avatar Dec 19 '24 21:12 TomJGooding

Possibly a consequence of DOMNode._merge_bindings relying on cls.__mro__?

xavierog avatar Dec 19 '24 21:12 xavierog