""" Builtin events sent by Textual. Events may be marked as "Bubbles" and "Verbose". See the [events guide](/guide/events/#bubbling) for an explanation of bubbling. Verbose events are excluded from the textual console, unless you explicitly request them with the `-v` switch as follows: ``` textual console -v ``` """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Type, TypeVar import rich.repr from rich.style import Style from typing_extensions import Self from textual._types import CallbackType from textual.geometry import Offset, Size from textual.keys import _get_key_aliases from textual.message import Message MouseEventT = TypeVar("MouseEventT", bound="MouseEvent") if TYPE_CHECKING: from textual.dom import DOMNode from textual.timer import Timer as TimerClass from textual.timer import TimerCallback from textual.widget import Widget @rich.repr.auto class Event(Message): """The base class for all events.""" @rich.repr.auto class Callback(Event, bubble=False, verbose=True): """Sent by Textual to invoke a callback (see [call_next][textual.message_pump.MessagePump.call_next] and [call_later][textual.message_pump.MessagePump.call_later]). """ def __init__(self, callback: CallbackType) -> None: self.callback = callback super().__init__() def __rich_repr__(self) -> rich.repr.Result: yield "callback", self.callback @dataclass class CursorPosition(Event, bubble=False): """Internal event used to retrieve the terminal's cursor position.""" x: int y: int class Load(Event, bubble=False): """ Sent when the App is running but *before* the terminal is in application mode. Use this event to run any setup that doesn't require any visuals such as loading configuration and binding keys. - [ ] Bubbles - [ ] Verbose """ class Idle(Event, bubble=False): """Sent when there are no more items in the message queue. This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue. - [ ] Bubbles - [ ] Verbose """ class Action(Event): __slots__ = ["action"] def __init__(self, action: str) -> None: super().__init__() self.action = action def __rich_repr__(self) -> rich.repr.Result: yield "action", self.action class Resize(Event, bubble=False): """Sent when the app or widget has been resized. - [ ] Bubbles - [ ] Verbose Args: size: The new size of the Widget. virtual_size: The virtual size (scrollable size) of the Widget. container_size: The size of the Widget's container widget. """ __slots__ = ["size", "virtual_size", "container_size"] def __init__( self, size: Size, virtual_size: Size, container_size: Size | None = None, pixel_size: Size | None = None, ) -> None: self.size = size """The new size of the Widget.""" self.virtual_size = virtual_size """The virtual size (scrollable size) of the Widget.""" self.container_size = size if container_size is None else container_size """The size of the Widget's container widget.""" self.pixel_size = pixel_size """Size of terminal window in pixels if known, or `None` if not known.""" super().__init__() @classmethod def from_dimensions( cls, cells: tuple[int, int], pixels: tuple[int, int] | None ) -> Resize: """Construct from basic dimensions. Args: cells: tuple of (, ) in cells. pixels: tuple of (, ) in pixels if known, or `None` if not known. """ size = Size(*cells) pixel_size = Size(*pixels) if pixels is not None else None return Resize(size, size, size, pixel_size) def can_replace(self, message: "Message") -> bool: return isinstance(message, Resize) def __rich_repr__(self) -> rich.repr.Result: yield "size", self.size yield "virtual_size", self.virtual_size, self.size yield "container_size", self.container_size, self.size yield "pixel_size", self.pixel_size, None class Compose(Event, bubble=False, verbose=True): """Sent to a widget to request it to compose and mount children. This event is used internally by Textual. You won't typically need to explicitly handle it, - [ ] Bubbles - [X] Verbose """ class Mount(Event, bubble=False, verbose=False): """Sent when a widget is *mounted* and may receive messages. - [ ] Bubbles - [ ] Verbose """ class Unmount(Event, bubble=False, verbose=False): """Sent when a widget is unmounted and may no longer receive messages. - [ ] Bubbles - [ ] Verbose """ class Show(Event, bubble=False): """Sent when a widget is first displayed. - [ ] Bubbles - [ ] Verbose """ class Hide(Event, bubble=False): """Sent when a widget has been hidden. - [ ] Bubbles - [ ] Verbose Sent when any of the following conditions apply: - The widget is removed from the DOM. - The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container. - The widget has its `display` attribute set to `False`. - The widget's `display` style is set to `"none"`. """ class Ready(Event, bubble=False): """Sent to the `App` when the DOM is ready and the first frame has been displayed. - [ ] Bubbles - [ ] Verbose """ @rich.repr.auto class MouseCapture(Event, bubble=False): """Sent when the mouse has been captured. - [ ] Bubbles - [ ] Verbose When a mouse has been captured, all further mouse events will be sent to the capturing widget. Args: mouse_position: The position of the mouse when captured. """ def __init__(self, mouse_position: Offset) -> None: super().__init__() self.mouse_position = mouse_position """The position of the mouse when captured.""" def __rich_repr__(self) -> rich.repr.Result: yield None, self.mouse_position @rich.repr.auto class MouseRelease(Event, bubble=False): """Mouse has been released. - [ ] Bubbles - [ ] Verbose Args: mouse_position: The position of the mouse when released. """ def __init__(self, mouse_position: Offset) -> None: super().__init__() self.mouse_position = mouse_position """The position of the mouse when released.""" def __rich_repr__(self) -> rich.repr.Result: yield None, self.mouse_position class InputEvent(Event): """Base class for input events.""" @rich.repr.auto class Key(InputEvent): """Sent when the user hits a key on the keyboard. - [X] Bubbles - [ ] Verbose Args: key: The key that was pressed. character: A printable character or `None` if it is not printable. """ __slots__ = ["key", "character", "aliases"] def __init__(self, key: str, character: str | None) -> None: super().__init__() self.key = key """The key that was pressed.""" self.character = ( (key if len(key) == 1 else None) if character is None else character ) """A printable character or ``None`` if it is not printable.""" self.aliases: list[str] = _get_key_aliases(key) """The aliases for the key, including the key itself.""" def __rich_repr__(self) -> rich.repr.Result: yield "key", self.key yield "character", self.character yield "name", self.name yield "is_printable", self.is_printable yield "aliases", self.aliases, [self.key] @property def name(self) -> str: """Name of a key suitable for use as a Python identifier.""" return _key_to_identifier(self.key).lower() @property def name_aliases(self) -> list[str]: """The corresponding name for every alias in `aliases` list.""" return [_key_to_identifier(key) for key in self.aliases] @property def is_printable(self) -> bool: """Check if the key is printable (produces a unicode character). Returns: `True` if the key is printable. """ return False if self.character is None else self.character.isprintable() def _key_to_identifier(key: str) -> str: """Convert the key string to a name suitable for use as a Python identifier.""" key_no_modifiers = key.split("+")[-1] if len(key_no_modifiers) == 1 and key_no_modifiers.isupper(): if "+" in key: key = f"{key.rpartition('+')[0]}+upper_{key_no_modifiers}" else: key = f"upper_{key_no_modifiers}" return key.replace("+", "_").lower() @rich.repr.auto class MouseEvent(InputEvent, bubble=True): """Sent in response to a mouse event. - [X] Bubbles - [ ] Verbose Args: widget: The widget under the mouse. x: The relative x coordinate. y: The relative y coordinate. delta_x: Change in x since the last message. delta_y: Change in y since the last message. button: Indexed of the pressed button. shift: True if the shift key is pressed. meta: True if the meta key is pressed. ctrl: True if the ctrl key is pressed. screen_x: The absolute x coordinate. screen_y: The absolute y coordinate. style: The Rich Style under the mouse cursor. """ __slots__ = [ "widget", "_x", "_y", "_delta_x", "_delta_y", "button", "shift", "meta", "ctrl", "_screen_x", "_screen_y", "_style", ] def __init__( self, widget: Widget | None, x: float, y: float, delta_x: int, delta_y: int, button: int, shift: bool, meta: bool, ctrl: bool, screen_x: float | None = None, screen_y: float | None = None, style: Style | None = None, ) -> None: super().__init__() self.widget: Widget | None = widget """The widget under the mouse at the time of a click.""" self._x = x """The relative x coordinate.""" self._y = y """The relative y coordinate.""" self._delta_x = delta_x """Change in x since the last message.""" self._delta_y = delta_y """Change in y since the last message.""" self.button = button """Indexed of the pressed button.""" self.shift = shift """`True` if the shift key is pressed.""" self.meta = meta """`True` if the meta key is pressed.""" self.ctrl = ctrl """`True` if the ctrl key is pressed.""" self._screen_x = x if screen_x is None else screen_x """The absolute x coordinate.""" self._screen_y = y if screen_y is None else screen_y """The absolute y coordinate.""" self._style = style or Style() @property def x(self) -> int: """The relative X coordinate of the cell under the mouse.""" return int(self._x) @property def y(self) -> int: """The relative Y coordinate of the cell under the mouse.""" return int(self._y) @property def delta_x(self) -> int: """Change in `x` since last message.""" return self._delta_x @property def delta_y(self) -> int: """Change in `y` since the last message.""" return self._delta_y @property def screen_x(self) -> int: """X coordinate of the cell relative to top left of screen.""" return int(self._screen_x) @property def screen_y(self) -> int: """Y coordinate of the cell relative to top left of screen.""" return int(self._screen_y) @property def pointer_x(self) -> float: """The relative X coordinate of the pointer.""" return self._x @property def pointer_y(self) -> float: """The relative Y coordinate of the pointer.""" return self._y @property def pointer_screen_x(self) -> float: """The X coordinate of the pointer relative to the screen.""" return self._screen_x @property def pointer_screen_y(self) -> float: """The Y coordinate of the pointer relative to the screen.""" return self._screen_y @classmethod def from_event( cls: Type[MouseEventT], widget: Widget, event: MouseEvent ) -> MouseEventT: new_event = cls( widget, event._x, event._y, event._delta_x, event._delta_y, event.button, event.shift, event.meta, event.ctrl, event._screen_x, event._screen_y, event._style, ) return new_event def __rich_repr__(self) -> rich.repr.Result: yield self.widget yield "x", self.x yield "y", self.y yield "pointer_x", self.pointer_x yield "pointer_y", self.pointer_y yield "delta_x", self.delta_x, 0 yield "delta_y", self.delta_y, 0 if self.screen_x != self.x: yield "screen_x", self._screen_x if self.screen_y != self.y: yield "screen_y", self._screen_y yield "button", self.button, 0 yield "shift", self.shift, False yield "meta", self.meta, False yield "ctrl", self.ctrl, False if self.style: yield "style", self.style @property def control(self) -> Widget | None: return self.widget @property def offset(self) -> Offset: """The mouse coordinate as an offset. Returns: Mouse coordinate. """ return Offset(self.x, self.y) @property def screen_offset(self) -> Offset: """Mouse coordinate relative to the screen.""" return Offset(self.screen_x, self.screen_y) @property def delta(self) -> Offset: """Mouse coordinate delta (change since last event).""" return Offset(self.delta_x, self.delta_y) @property def style(self) -> Style: """The (Rich) Style under the cursor.""" return self._style or Style() @style.setter def style(self, style: Style) -> None: self._style = style def get_content_offset(self, widget: Widget) -> Offset | None: """Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border). Args: widget: Widget receiving the event. Returns: An offset where the origin is at the top left of the content area. """ if self.screen_offset not in widget.content_region: return None return self.get_content_offset_capture(widget) def get_content_offset_capture(self, widget: Widget) -> Offset: """Get offset from a widget's content area. This method works even if the offset is outside the widget content region. Args: widget: Widget receiving the event. Returns: An offset where the origin is at the top left of the content area. """ return self.offset - widget.gutter.top_left def _apply_offset(self, x: int, y: int) -> MouseEvent: return self.__class__( self.widget, x=self._x + x, y=self._y + y, delta_x=self._delta_x, delta_y=self._delta_y, button=self.button, shift=self.shift, meta=self.meta, ctrl=self.ctrl, screen_x=self._screen_x, screen_y=self._screen_y, style=self.style, ) @rich.repr.auto class MouseMove(MouseEvent, bubble=True, verbose=True): """Sent when the mouse cursor moves. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseDown(MouseEvent, bubble=True, verbose=True): """Sent when a mouse button is pressed. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseUp(MouseEvent, bubble=True, verbose=True): """Sent when a mouse button is released. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseScrollDown(MouseEvent, bubble=True, verbose=True): """Sent when the mouse wheel is scrolled *down*. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseScrollUp(MouseEvent, bubble=True, verbose=True): """Sent when the mouse wheel is scrolled *up*. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseScrollRight(MouseEvent, bubble=True, verbose=True): """Sent when the mouse wheel is scrolled *right*. - [X] Bubbles - [X] Verbose """ @rich.repr.auto class MouseScrollLeft(MouseEvent, bubble=True, verbose=True): """Sent when the mouse wheel is scrolled *left*. - [X] Bubbles - [X] Verbose """ class Click(MouseEvent, bubble=True): """Sent when a widget is clicked. - [X] Bubbles - [ ] Verbose Args: chain: The number of clicks in the chain. 2 is a double click, 3 is a triple click, etc. """ def __init__( self, widget: Widget | None, x: int, y: int, delta_x: int, delta_y: int, button: int, shift: bool, meta: bool, ctrl: bool, screen_x: int | None = None, screen_y: int | None = None, style: Style | None = None, chain: int = 1, ) -> None: super().__init__( widget, x, y, delta_x, delta_y, button, shift, meta, ctrl, screen_x, screen_y, style, ) self.chain = chain @classmethod def from_event( cls: Type[Self], widget: Widget, event: MouseEvent, chain: int = 1, ) -> Self: new_event = cls( widget, event.x, event.y, event.delta_x, event.delta_y, event.button, event.shift, event.meta, event.ctrl, event.screen_x, event.screen_y, event._style, chain=chain, ) return new_event def _apply_offset(self, x: int, y: int) -> Self: return self.__class__( self.widget, x=self.x + x, y=self.y + y, delta_x=self.delta_x, delta_y=self.delta_y, button=self.button, shift=self.shift, meta=self.meta, ctrl=self.ctrl, screen_x=self.screen_x, screen_y=self.screen_y, style=self.style, chain=self.chain, ) def __rich_repr__(self) -> rich.repr.Result: yield from super().__rich_repr__() yield "chain", self.chain @rich.repr.auto class Timer(Event, bubble=False, verbose=True): """Sent in response to a timer. - [ ] Bubbles - [X] Verbose """ __slots__ = ["timer", "time", "count", "callback"] def __init__( self, timer: "TimerClass", time: float, count: int = 0, callback: TimerCallback | None = None, ) -> None: super().__init__() self.timer = timer self.time = time self.count = count self.callback = callback def __rich_repr__(self) -> rich.repr.Result: yield self.timer.name yield "count", self.count class Enter(Event, bubble=True, verbose=True): """Sent when the mouse is moved over a widget. Note that this event bubbles, so a widget may receive this event when the mouse moves over a child widget. Check the `node` attribute for the widget directly under the mouse. - [X] Bubbles - [X] Verbose """ __slots__ = ["node"] def __init__(self, node: DOMNode) -> None: self.node = node """The node directly under the mouse.""" super().__init__() @property def control(self) -> DOMNode: """Alias for the `node` under the mouse.""" return self.node class Leave(Event, bubble=True, verbose=True): """Sent when the mouse is moved away from a widget, or if a widget is programmatically disabled while hovered. Note that this widget bubbles, so a widget may receive Leave events for any child widgets. Check the `node` parameter for the original widget that was previously under the mouse. - [X] Bubbles - [X] Verbose """ __slots__ = ["node"] def __init__(self, node: DOMNode) -> None: self.node = node """The node that was previously directly under the mouse.""" super().__init__() @property def control(self) -> DOMNode: """Alias for the `node` that was previously under the mouse.""" return self.node class Focus(Event, bubble=False): """Sent when a widget is focussed. - [ ] Bubbles - [ ] Verbose Args: from_app_focus: True if this focus event has been sent because the app itself has regained focus (via an AppFocus event). False if the focus came from within the Textual app (e.g. via the user pressing tab or a programmatic setting of the focused widget). """ def __init__(self, from_app_focus: bool = False) -> None: self.from_app_focus = from_app_focus super().__init__() def __rich_repr__(self) -> rich.repr.Result: yield from super().__rich_repr__() yield "from_app_focus", self.from_app_focus class Blur(Event, bubble=False): """Sent when a widget is blurred (un-focussed). - [ ] Bubbles - [ ] Verbose """ class AppFocus(Event, bubble=False): """Sent when the app has focus. - [ ] Bubbles - [ ] Verbose Note: Only available when running within a terminal that supports `FocusIn`, or when running via textual-web. """ class AppBlur(Event, bubble=False): """Sent when the app loses focus. - [ ] Bubbles - [ ] Verbose Note: Only available when running within a terminal that supports `FocusOut`, or when running via textual-web. """ @dataclass class DescendantFocus(Event, bubble=True, verbose=True): """Sent when a child widget is focussed. - [X] Bubbles - [X] Verbose """ widget: Widget """The widget that was focused.""" @property def control(self) -> Widget: """The widget that was focused (alias of `widget`).""" return self.widget @dataclass class DescendantBlur(Event, bubble=True, verbose=True): """Sent when a child widget is blurred. - [X] Bubbles - [X] Verbose """ widget: Widget """The widget that was blurred.""" @property def control(self) -> Widget: """The widget that was blurred (alias of `widget`).""" return self.widget @rich.repr.auto class Paste(Event, bubble=True): """Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down. - [X] Bubbles - [ ] Verbose Args: text: The text that has been pasted. """ def __init__(self, text: str) -> None: super().__init__() self.text = text """The text that was pasted.""" def __rich_repr__(self) -> rich.repr.Result: yield "text", self.text class ScreenResume(Event, bubble=False): """Sent to screen that has been made active. - [ ] Bubbles - [ ] Verbose """ class ScreenSuspend(Event, bubble=False): """Sent to screen when it is no longer active. - [ ] Bubbles - [ ] Verbose """ @rich.repr.auto class Print(Event, bubble=False): """Sent to a widget that is capturing [`print`][print]. - [ ] Bubbles - [ ] Verbose Args: text: Text that was printed. stderr: `True` if the print was to stderr, or `False` for stdout. Note: Python's [`print`][print] output can be captured with [`App.begin_capture_print`][textual.app.App.begin_capture_print]. """ def __init__(self, text: str, stderr: bool = False) -> None: super().__init__() self.text = text """The text that was printed.""" self.stderr = stderr """`True` if the print was to stderr, or `False` for stdout.""" def __rich_repr__(self) -> rich.repr.Result: yield self.text yield self.stderr @dataclass class DeliveryComplete(Event, bubble=False): """Sent to App when a file has been delivered.""" key: str """The delivery key associated with the delivery. This is the same key that was returned by `App.deliver_text`/`App.deliver_binary`. """ path: Path | None = None """The path where the file was saved, or `None` if the path is not available, for example if the file was delivered via web browser. """ name: str | None = None """Optional name returned to the app to identify the download.""" @dataclass class DeliveryFailed(Event, bubble=False): """Sent to App when a file delivery fails.""" key: str """The delivery key associated with the delivery.""" exception: BaseException """The exception that was raised during the delivery.""" name: str | None = None """Optional name returned to the app to identify the download.""" class TextSelected(Event, bubble=True): """Sent from the screen when text is selected (Not Input and TextArea)"""