diff --git a/src/textual/binding.py b/src/textual/binding.py index 05e0161ab2..329b62148c 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -83,6 +83,8 @@ class Binding: """ system: bool = False """Make this binding a system binding, which removes it from the key panel.""" + order_group: str | None = None + """Group used in sort, or `None` for default.""" @dataclass(frozen=True) class Group: @@ -94,6 +96,9 @@ class Group: compact: bool = False """Show keys in compact form (no spaces).""" + order_group: str | None = None + """Group used in sort, or `None` for default.""" + group: Group | None = None """Optional binding group (used to group related bindings in the footer).""" @@ -165,6 +170,7 @@ def make_bindings(cls, bindings: Iterable[BindingType]) -> Iterable[Binding]: id=binding.id, system=binding.system, group=binding.group, + order_group=binding.order_group, ) @@ -188,6 +194,7 @@ class BindingsMap: def __init__( self, bindings: Iterable[BindingType] | None = None, + order_group: str = "default", ) -> None: """Initialise a collection of bindings. diff --git a/src/textual/dom.py b/src/textual/dom.py index b359059790..b312473daa 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -144,6 +144,9 @@ class DOMNode(MessagePump): BINDING_GROUP_TITLE: str | None = None """Title of widget used where bindings are displayed (such as in the key panel).""" + BINDING_SORT_GROUP: str = "default" + """Binding sort group, if not specified in BINDINGS. Used to define Footer sort order.""" + BINDINGS: ClassVar[list[BindingType]] = [] """A list of key bindings.""" @@ -676,11 +679,7 @@ def _merge_bindings(cls) -> BindingsMap: if issubclass(base, DOMNode): if not base._inherit_bindings: bindings.clear() - bindings.append( - BindingsMap( - base.__dict__.get("BINDINGS", []), - ) - ) + bindings.append(BindingsMap(base.__dict__.get("BINDINGS", []))) keys: dict[str, list[Binding]] = {} for bindings_ in bindings: diff --git a/src/textual/screen.py b/src/textual/screen.py index 61d38376f0..87575617cc 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -558,6 +558,9 @@ def allow_select(self) -> bool: """Check if this widget permits text selection.""" return self.ALLOW_SELECT + def get_binding_sort_order(self) -> list[str]: + return ["default"] + def get_loading_widget(self) -> Widget: """Get a widget to display a loading indicator. diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index 14aa56cb81..31d7baec1b 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -27,6 +27,15 @@ class KeyGroup(HorizontalGroup): } """ + def __init__( + self, + id: str | None = None, + classes: str | None = None, + order_group: str = "default", + ): + self.order_group = order_group + super().__init__(id=id, classes=classes) + @rich.repr.auto class FooterKey(Widget): @@ -87,11 +96,13 @@ def __init__( disabled: bool = False, tooltip: str = "", classes="", + order_group: str = "default", ) -> None: self.key = key self.key_display = key_display self.description = description self.action = action + self.order_group = order_group self._disabled = disabled if disabled: classes += " -disabled" @@ -284,7 +295,12 @@ def compose(self) -> ComposeResult: ): multi_bindings = list(multi_bindings_iterable) if group is not None and len(multi_bindings) > 1: - with KeyGroup(classes="-compact" if group.compact else ""): + with KeyGroup( + classes="-compact" if group.compact else "", + order_group=( + "default" if group.order_group is None else group.order_group + ), + ): for multi_bindings in multi_bindings: binding, enabled, tooltip = multi_bindings[0] yield FooterKey( @@ -296,7 +312,7 @@ def compose(self) -> ComposeResult: tooltip=tooltip or binding.description, classes="-grouped", ).data_bind(compact=Footer.compact) - yield FooterLabel(group.description) + yield FooterLabel(group.description) else: for multi_bindings in multi_bindings: binding, enabled, tooltip = multi_bindings[0] @@ -307,6 +323,11 @@ def compose(self) -> ComposeResult: binding.action, disabled=not enabled, tooltip=tooltip, + order_group=( + "default" + if binding.order_group is None + else binding.order_group + ), ).data_bind(compact=Footer.compact) if self.show_command_palette and self.app.ENABLE_COMMAND_PALETTE: try: @@ -324,8 +345,21 @@ def compose(self) -> ComposeResult: classes="-command-palette", disabled=not enabled, tooltip=binding.tooltip or binding.description, + order_group=( + "default" + if binding.order_group is None + else binding.order_group + ), ) + sort_order = { + group: index + for index, group in enumerate(self.screen.get_binding_sort_order()) + } + self.sort_children( + key=lambda widget: sort_order.get(sort_order.get(widget.order_group, 0)) + ) + def bindings_changed(self, screen: Screen) -> None: self._bindings_ready = True if not screen.app.app_focus: