mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
key bindings refactor
This commit is contained in:
24
sandbox/will/screen_actions.py
Normal file
24
sandbox/will/screen_actions.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Footer
|
||||
|
||||
|
||||
class DefaultScreen(Screen):
|
||||
|
||||
BINDINGS = [("f", "foo", "FOO")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Footer()
|
||||
|
||||
def action_foo(self) -> None:
|
||||
self.app.bell()
|
||||
|
||||
|
||||
class ScreenApp(App):
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen(DefaultScreen())
|
||||
|
||||
|
||||
app = ScreenApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import platform
|
||||
import sys
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from datetime import datetime
|
||||
from pathlib import Path, PurePath
|
||||
@@ -31,7 +32,7 @@ from ._callback import invoke
|
||||
from ._context import active_app
|
||||
from ._event_broker import NoHandler, extract_handler_actions
|
||||
from ._filter import LineFilter, Monochrome
|
||||
from .binding import Bindings, NoBinding
|
||||
from .binding import Binding, Bindings, NoBinding
|
||||
from .css.query import NoMatches
|
||||
from .css.stylesheet import Stylesheet
|
||||
from .design import ColorSystem
|
||||
@@ -320,16 +321,29 @@ class App(Generic[ReturnType], DOMNode):
|
||||
return self.screen.focused
|
||||
|
||||
@property
|
||||
def bindings(self) -> Bindings:
|
||||
def namespace_bindings(self) -> dict[str, tuple[object, Binding]]:
|
||||
"""Get current bindings. If no widget is focused, then the app-level bindings
|
||||
are returned. If a widget is focused, then any bindings present in the active
|
||||
screen and app are merged and returned."""
|
||||
if self.focused is None:
|
||||
return Bindings.merge([self.screen._bindings, self._bindings])
|
||||
|
||||
focused = self.focused
|
||||
namespace_bindings: list[tuple[object, Bindings]]
|
||||
if focused is None:
|
||||
namespace_bindings = [
|
||||
(self, self._bindings),
|
||||
(self.screen, self.screen._bindings),
|
||||
]
|
||||
else:
|
||||
return Bindings.merge(
|
||||
node._bindings for node in reversed(self.focused.ancestors)
|
||||
)
|
||||
namespace_bindings = [
|
||||
(node, node._bindings) for node in reversed(focused.ancestors)
|
||||
]
|
||||
|
||||
namespace_binding_map: dict[str, tuple[object, Binding]] = {}
|
||||
for namespace, bindings in namespace_bindings:
|
||||
for key, binding in bindings.keys.items():
|
||||
namespace_binding_map[key] = (namespace, binding)
|
||||
|
||||
return namespace_binding_map
|
||||
|
||||
def _set_active(self) -> None:
|
||||
"""Set this app to be the currently active app."""
|
||||
@@ -1261,7 +1275,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if not self.is_headless:
|
||||
self.console.bell()
|
||||
|
||||
async def press(self, key: str) -> bool:
|
||||
async def check_bindings(self, key: str) -> bool:
|
||||
"""Handle a key press.
|
||||
|
||||
Args:
|
||||
@@ -1271,11 +1285,11 @@ class App(Generic[ReturnType], DOMNode):
|
||||
bool: True if the key was handled by a binding, otherwise False
|
||||
"""
|
||||
try:
|
||||
binding = self.bindings.get_key(key)
|
||||
except NoBinding:
|
||||
namespace, binding = self.namespace_bindings[key]
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
await self.action(binding.action)
|
||||
await self.action(binding.action, default_namespace=namespace)
|
||||
return True
|
||||
|
||||
async def on_event(self, event: events.Event) -> None:
|
||||
@@ -1291,16 +1305,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if isinstance(event, events.MouseEvent):
|
||||
# Record current mouse position on App
|
||||
self.mouse_position = Offset(event.x, event.y)
|
||||
if isinstance(event, events.Key) and self.focused is not None:
|
||||
# Key events are sent direct to focused widget of the currently active screen
|
||||
if self.bindings.allow_forward(event.key):
|
||||
await self.focused._forward_event(event)
|
||||
else:
|
||||
# Key has allow_forward=False which disallows forward to focused widget
|
||||
await super().on_event(event)
|
||||
if isinstance(event, events.Key):
|
||||
forward_target = self.focused or self.screen
|
||||
await forward_target._forward_event(event)
|
||||
else:
|
||||
# Forward the event to the currently active Screen
|
||||
await self.screen._forward_event(event)
|
||||
|
||||
elif isinstance(event, events.Paste):
|
||||
if self.focused is not None:
|
||||
await self.focused._forward_event(event)
|
||||
@@ -1404,7 +1414,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
elif event.key == "shift+tab":
|
||||
self.screen.focus_previous()
|
||||
else:
|
||||
if not (await self.press(event.key)):
|
||||
if not (await self.check_bindings(event.key)):
|
||||
await self.dispatch_key(event)
|
||||
|
||||
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
@@ -1430,8 +1440,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if parent is not None:
|
||||
parent.refresh(layout=True)
|
||||
|
||||
async def action_press(self, key: str) -> None:
|
||||
await self.press(key)
|
||||
async def action_check_binding(self, key: str) -> None:
|
||||
await self.check_bindings(key)
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
"""Quit the app as soon as possible."""
|
||||
|
||||
@@ -571,6 +571,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
return public_handler or private_handler
|
||||
|
||||
handled = False
|
||||
invoked_method = None
|
||||
key_name = event.key_name
|
||||
if not key_name:
|
||||
@@ -583,10 +584,10 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
_raise_duplicate_key_handlers_error(
|
||||
key_name, invoked_method.__name__, key_method.__name__
|
||||
)
|
||||
await invoke(key_method, event)
|
||||
handled = await invoke(key_method, event)
|
||||
invoked_method = key_method
|
||||
|
||||
return invoked_method is not None
|
||||
return handled
|
||||
|
||||
async def on_timer(self, event: events.Timer) -> None:
|
||||
event.prevent_default()
|
||||
|
||||
@@ -1900,12 +1900,7 @@ class Widget(DOMNode):
|
||||
await self.handle_key(event)
|
||||
|
||||
async def handle_key(self, event: events.Key) -> bool:
|
||||
try:
|
||||
binding = self._bindings.get_key(event.key)
|
||||
except NoBinding:
|
||||
return await self.dispatch_key(event)
|
||||
await self.action(binding.action)
|
||||
return True
|
||||
return await self.dispatch_key(event)
|
||||
|
||||
async def _on_compose(self, event: events.Compose) -> None:
|
||||
widgets = list(self.compose())
|
||||
|
||||
@@ -87,7 +87,11 @@ class Footer(Widget):
|
||||
highlight_key_style = self.get_component_rich_style("footer--highlight-key")
|
||||
key_style = self.get_component_rich_style("footer--key")
|
||||
|
||||
bindings = self.app.bindings.shown_keys
|
||||
bindings = [
|
||||
binding
|
||||
for (_namespace, binding) in self.app.namespace_bindings.values()
|
||||
if binding.show
|
||||
]
|
||||
|
||||
action_to_bindings = defaultdict(list)
|
||||
for binding in bindings:
|
||||
@@ -107,7 +111,10 @@ class Footer(Widget):
|
||||
f" {binding.description} ",
|
||||
highlight_style if hovered else base_style,
|
||||
),
|
||||
meta={"@click": f"app.press('{binding.key}')", "key": binding.key},
|
||||
meta={
|
||||
"@click": f"app.check_binding('{binding.key}')",
|
||||
"key": binding.key,
|
||||
},
|
||||
)
|
||||
text.append_text(key_text)
|
||||
return text
|
||||
|
||||
@@ -239,10 +239,12 @@ class Input(Widget, can_focus=True):
|
||||
|
||||
# Do key bindings first
|
||||
if await self.handle_key(event):
|
||||
print("HANDLE KEY STOPPED")
|
||||
event.prevent_default()
|
||||
event.stop()
|
||||
return
|
||||
elif event.is_printable:
|
||||
print("PRINTABLE STOPPED")
|
||||
event.stop()
|
||||
assert event.char is not None
|
||||
self.insert_text_at_cursor(event.char)
|
||||
|
||||
Reference in New Issue
Block a user