""" The base class for all messages (including events). """ from __future__ import annotations from typing import TYPE_CHECKING, ClassVar import rich.repr from typing_extensions import Self from textual import _time from textual._context import active_message_pump from textual.case import camel_to_snake if TYPE_CHECKING: from textual.dom import DOMNode from textual.message_pump import MessagePump @rich.repr.auto class Message: """Base class for a message.""" __slots__ = [ "_sender", "time", "_forwarded", "_no_default_action", "_stop_propagation", "_prevent", ] ALLOW_SELECTOR_MATCH: ClassVar[set[str]] = set() """Additional attributes that can be used with the [`on` decorator][textual.on]. These attributes must be widgets. """ bubble: ClassVar[bool] = True # Message will bubble to parent verbose: ClassVar[bool] = False # Message is verbose no_dispatch: ClassVar[bool] = False # Message may not be handled by client code namespace: ClassVar[str] = "" # Namespace to disambiguate messages handler_name: ClassVar[str] """Name of the default message handler.""" def __init__(self) -> None: self.__post_init__() def __post_init__(self) -> None: """Allow dataclasses to initialize the object.""" self._sender: MessagePump | None = active_message_pump.get(None) self.time: float = _time.get_time() self._forwarded = False self._no_default_action = False self._stop_propagation = False self._prevent: set[type[Message]] = set() def __rich_repr__(self) -> rich.repr.Result: yield from () def __init_subclass__( cls, bubble: bool | None = True, verbose: bool = False, no_dispatch: bool | None = False, namespace: str | None = None, ) -> None: super().__init_subclass__() if bubble is not None: cls.bubble = bubble cls.verbose = verbose if no_dispatch is not None: cls.no_dispatch = no_dispatch if namespace is not None: cls.namespace = namespace name = f"{namespace}_{camel_to_snake(cls.__name__)}" else: # a class defined inside of a function will have a qualified name like func..Class, # so make sure we only use the actual class name(s) qualname = cls.__qualname__.rsplit(".", 1)[-1] # only keep the last two parts of the qualified name of deeply nested classes # for backwards compatibility, e.g. A.B.C.D becomes C.D namespace = qualname.rsplit(".", 2)[-2:] name = "_".join(camel_to_snake(part) for part in namespace) cls.handler_name = f"on_{name}" @property def control(self) -> DOMNode | None: """The widget associated with this message, or None by default.""" return None @property def is_forwarded(self) -> bool: """Has the message been forwarded?""" return self._forwarded def _set_forwarded(self) -> None: """Mark this event as being forwarded.""" self._forwarded = True def set_sender(self, sender: MessagePump) -> Self: """Set the sender of the message. Args: sender: The sender. Note: When creating a message the sender is automatically set. Normally there will be no need for this method to be called. This method will be used when strict control is required over the sender of a message. Returns: Self. """ self._sender = sender return self def can_replace(self, message: "Message") -> bool: """Check if another message may supersede this one. Args: message: Another message. Returns: True if this message may replace the given message """ return False def prevent_default(self, prevent: bool = True) -> Message: """Suppress the default action(s). This will prevent handlers in any base classes from being called. Args: prevent: True if the default action should be suppressed, or False if the default actions should be performed. """ self._no_default_action = prevent return self def stop(self, stop: bool = True) -> Message: """Stop propagation of the message to parent. Args: stop: The stop flag. """ self._stop_propagation = stop return self def _bubble_to(self, widget: MessagePump) -> None: """Bubble to a widget (typically the parent). Args: widget: Target of bubble. """ self._no_default_action = False widget.post_message(self)