94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Callable, TypeVar
|
|
|
|
from textual.css.model import SelectorSet
|
|
from textual.css.parse import parse_selectors
|
|
from textual.css.tokenizer import TokenError
|
|
from textual.message import Message
|
|
|
|
DecoratedType = TypeVar("DecoratedType")
|
|
|
|
|
|
class OnDecoratorError(Exception):
|
|
"""Errors related to the `on` decorator.
|
|
|
|
Typically raised at import time as an early warning system.
|
|
"""
|
|
|
|
|
|
class OnNoWidget(Exception):
|
|
"""A selector was applied to an attribute that isn't a widget."""
|
|
|
|
|
|
def on(
|
|
message_type: type[Message], selector: str | None = None, **kwargs: str
|
|
) -> Callable[[DecoratedType], DecoratedType]:
|
|
"""Decorator to declare that the method is a message handler.
|
|
|
|
The decorator accepts an optional CSS selector that will be matched against a widget exposed by
|
|
a `control` property on the message.
|
|
|
|
Example:
|
|
```python
|
|
# Handle the press of buttons with ID "#quit".
|
|
@on(Button.Pressed, "#quit")
|
|
def quit_button(self) -> None:
|
|
self.app.quit()
|
|
```
|
|
|
|
Keyword arguments can be used to match additional selectors for attributes
|
|
listed in [`ALLOW_SELECTOR_MATCH`][textual.message.Message.ALLOW_SELECTOR_MATCH].
|
|
|
|
Example:
|
|
```python
|
|
# Handle the activation of the tab "#home" within the `TabbedContent` "#tabs".
|
|
@on(TabbedContent.TabActivated, "#tabs", pane="#home")
|
|
def switch_to_home(self) -> None:
|
|
self.log("Switching back to the home tab.")
|
|
...
|
|
```
|
|
|
|
Args:
|
|
message_type: The message type (i.e. the class).
|
|
selector: An optional [selector](/guide/CSS#selectors). If supplied, the handler will only be called if `selector`
|
|
matches the widget from the `control` attribute of the message.
|
|
**kwargs: Additional selectors for other attributes of the message.
|
|
"""
|
|
|
|
selectors: dict[str, str] = {}
|
|
if selector is not None:
|
|
selectors["control"] = selector
|
|
if kwargs:
|
|
selectors.update(kwargs)
|
|
|
|
parsed_selectors: dict[str, tuple[SelectorSet, ...]] = {}
|
|
for attribute, css_selector in selectors.items():
|
|
if attribute == "control":
|
|
if message_type.control == Message.control:
|
|
raise OnDecoratorError(
|
|
"The message class must have a 'control' to match with the on decorator"
|
|
)
|
|
elif attribute not in message_type.ALLOW_SELECTOR_MATCH:
|
|
raise OnDecoratorError(
|
|
f"The attribute {attribute!r} can't be matched; have you added it to "
|
|
+ f"{message_type.__name__}.ALLOW_SELECTOR_MATCH?"
|
|
)
|
|
try:
|
|
parsed_selectors[attribute] = parse_selectors(css_selector)
|
|
except TokenError:
|
|
raise OnDecoratorError(
|
|
f"Unable to parse selector {css_selector!r} for {attribute}; check for syntax errors"
|
|
) from None
|
|
|
|
def decorator(method: DecoratedType) -> DecoratedType:
|
|
"""Store message and selector in function attribute, return callable unaltered."""
|
|
|
|
if not hasattr(method, "_textual_on"):
|
|
setattr(method, "_textual_on", [])
|
|
getattr(method, "_textual_on").append((message_type, parsed_selectors))
|
|
|
|
return method
|
|
|
|
return decorator
|