mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Widget focus keybindings (#546)
* Support binding widget focus to keypress, fix focus-within * Update src/textual/app.py Co-authored-by: Olivier Philippon <olivier@rougemine.com> Co-authored-by: Olivier Philippon <olivier@rougemine.com>
This commit is contained in:
54
sandbox/focus_keybindings.py
Normal file
54
sandbox/focus_keybindings.py
Normal file
@@ -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()
|
||||
57
sandbox/focus_keybindings.scss
Normal file
57
sandbox/focus_keybindings.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -47,6 +47,7 @@ from ._context import active_app
|
||||
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
|
||||
@@ -1090,6 +1091,15 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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)
|
||||
|
||||
|
||||
@@ -994,9 +994,21 @@ class Widget(DOMNode):
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user