Input-related FooterKeys sorted differently since Textual 1.0.0
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
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
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?
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()
Possibly a consequence of DOMNode._merge_bindings relying on cls.__mro__?