diff --git a/sandbox/focus_keybindings.py b/sandbox/focus_keybindings.py new file mode 100644 index 0000000000..fa5028747f --- /dev/null +++ b/sandbox/focus_keybindings.py @@ -0,0 +1,54 @@ +from textual.app import App +from textual.widget import Widget +from textual.widgets import Static + + +class FocusKeybindsApp(App): + dark = True + + def on_load(self) -> None: + self.bind("1", "focus('widget1')") + self.bind("2", "focus('widget2')") + self.bind("3", "focus('widget3')") + self.bind("4", "focus('widget4')") + self.bind("q", "focus('widgetq')") + self.bind("w", "focus('widgetw')") + self.bind("e", "focus('widgete')") + self.bind("r", "focus('widgetr')") + + def on_mount(self) -> None: + info = Static( + "Use keybinds to shift focus between the widgets in the lists below", + ) + self.mount(info=info) + + self.mount( + body=Widget( + Widget( + Static("Press 1 to focus", id="widget1", classes="list-item"), + Static("Press 2 to focus", id="widget2", classes="list-item"), + Static("Press 3 to focus", id="widget3", classes="list-item"), + Static("Press 4 to focus", id="widget4", classes="list-item"), + classes="list", + id="left_list", + ), + Widget( + Static("Press Q to focus", id="widgetq", classes="list-item"), + Static("Press W to focus", id="widgetw", classes="list-item"), + Static("Press E to focus", id="widgete", classes="list-item"), + Static("Press R to focus", id="widgetr", classes="list-item"), + classes="list", + id="right_list", + ), + ), + ) + self.mount(footer=Static("No widget focused")) + + def on_descendant_focus(self): + self.get_child("footer").update( + f"Focused: {self.focused.id}" or "No widget focused" + ) + + +app = FocusKeybindsApp(css_path="focus_keybindings.scss", watch_css=True) +app.run() diff --git a/sandbox/focus_keybindings.scss b/sandbox/focus_keybindings.scss new file mode 100644 index 0000000000..a6803fe5ae --- /dev/null +++ b/sandbox/focus_keybindings.scss @@ -0,0 +1,57 @@ +App > Screen { + layout: dock; + docks: left=left top=top; +} + +#info { + background: $primary; + dock: top; + height: 3; + padding: 1; +} + +#body { + dock: top; + layout: dock; + docks: bodylhs=left; +} + +#left_list { + dock: bodylhs; + padding: 2; +} + +#right_list { + dock: bodylhs; + padding: 2; +} + +#footer { + height: 1; + background: $secondary; + padding: 0 1; + dock: top; +} + +.list { + background: $surface; + border-top: hkey $surface-darken-1; +} + +.list:focus-within { + background: $primary-darken-1; + outline-top: $accent-lighten-1; + outline-bottom: $accent-lighten-1; +} + +.list-item { + background: $surface; + height: auto; + border: $surface-darken-1 tall; + padding: 0 1; +} + +.list-item:focus { + background: $surface-darken-1; + outline: $accent tall; +} diff --git a/src/textual/app.py b/src/textual/app.py index e1446a9abc..99da5b18f3 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -48,6 +48,7 @@ from ._event_broker import extract_handler_actions, NoHandler from .binding import Bindings, NoBinding from .css.stylesheet import Stylesheet +from .css.query import NoMatchingNodesError from .design import ColorSystem from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog from .devtools.redirect_output import StdoutRedirector @@ -1081,6 +1082,15 @@ async def action_bang(self) -> None: async def action_bell(self) -> None: self.bell() + async def action_focus(self, widget_id: str) -> None: + try: + node = self.query(f"#{widget_id}").first() + except NoMatchingNodesError: + pass + else: + if isinstance(node, Widget): + self.set_focus(node) + async def action_add_class_(self, selector: str, class_name: str) -> None: self.screen.query(selector).add_class(class_name) diff --git a/src/textual/widget.py b/src/textual/widget.py index bdd5c36f31..7682526b74 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -993,9 +993,21 @@ def on_blur(self, event: events.Blur) -> None: def on_descendant_focus(self, event: events.DescendantFocus) -> None: self.descendant_has_focus = True + if "focus-within" in self.pseudo_classes: + sender = event.sender + for child in self.walk_children(False): + child.refresh() + if child is sender: + break def on_descendant_blur(self, event: events.DescendantBlur) -> None: self.descendant_has_focus = False + if "focus-within" in self.pseudo_classes: + sender = event.sender + for child in self.walk_children(False): + child.refresh() + if child is sender: + break def on_mouse_scroll_down(self, event) -> None: if self.is_container: