from __future__ import annotations import asyncio from collections.abc import Awaitable, Iterator from contextlib import nullcontext from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, Union, cast from . import background_tasks, core, helpers from .awaitable_response import AwaitableResponse from .dataclasses import KWONLY_SLOTS from .slot import Slot if TYPE_CHECKING: from .client import Client from .element import Element from .elements.slide_item import SlideSide from .elements.upload_files import FileUpload from .observables import ObservableCollection @dataclass(**KWONLY_SLOTS) class EventArguments: pass @dataclass(**KWONLY_SLOTS) class ObservableChangeEventArguments(EventArguments): sender: ObservableCollection @dataclass(**KWONLY_SLOTS) class UiEventArguments(EventArguments): sender: Element client: Client @dataclass(**KWONLY_SLOTS) class GenericEventArguments(UiEventArguments): args: Any @dataclass(**KWONLY_SLOTS) class ClickEventArguments(UiEventArguments): pass @dataclass(**KWONLY_SLOTS) class SlideEventArguments(UiEventArguments): side: SlideSide @dataclass(**KWONLY_SLOTS) class EChartPointClickEventArguments(UiEventArguments): component_type: str series_type: str series_index: int series_name: str name: str data_index: int data: float | int | str data_type: str value: float | int | list @dataclass(**KWONLY_SLOTS) class MermaidNodeClickEventArguments(UiEventArguments): node_id: str @dataclass(**KWONLY_SLOTS) class SceneClickHit: object_id: str object_name: str x: float y: float z: float @dataclass(**KWONLY_SLOTS) class SceneClickEventArguments(ClickEventArguments): click_type: str button: int alt: bool ctrl: bool meta: bool shift: bool hits: list[SceneClickHit] @dataclass(**KWONLY_SLOTS) class SceneDragEventArguments(ClickEventArguments): type: Literal['dragstart', 'dragend'] object_id: str object_name: str x: float y: float z: float @dataclass(**KWONLY_SLOTS) class ColorPickEventArguments(UiEventArguments): color: str @dataclass(**KWONLY_SLOTS) class MouseEventArguments(UiEventArguments): type: str image_x: float image_y: float button: int buttons: int alt: bool ctrl: bool meta: bool shift: bool @dataclass(**KWONLY_SLOTS) class JoystickEventArguments(UiEventArguments): action: str x: float | None = None y: float | None = None @dataclass(**KWONLY_SLOTS) class UploadEventArguments(UiEventArguments): file: FileUpload @dataclass(**KWONLY_SLOTS) class MultiUploadEventArguments(UiEventArguments): files: list[FileUpload] @dataclass(**KWONLY_SLOTS) class ValueChangeEventArguments(UiEventArguments): value: Any previous_value: Any = ... def __post_init__(self): # DEPRECATED: previous_value will be required in NiceGUI 4.0 if self.previous_value is ...: helpers.warn_once('The new event argument `ValueChangeEventArguments.previous_value` is not set. ' 'In NiceGUI 4.0 this will raise an error.') @dataclass(**KWONLY_SLOTS) class TableSelectionEventArguments(UiEventArguments): selection: list[Any] @dataclass(**KWONLY_SLOTS) class KeyboardAction: keydown: bool keyup: bool repeat: bool @dataclass(**KWONLY_SLOTS) class KeyboardModifiers: alt: bool ctrl: bool meta: bool shift: bool def __iter__(self) -> Iterator[bool]: return iter([self.alt, self.ctrl, self.meta, self.shift]) def __len__(self) -> int: return sum(self) @dataclass(**KWONLY_SLOTS) class KeyboardKey: name: str code: str location: int def __eq__(self, other: object) -> bool: if isinstance(other, str): return other in {self.name, self.code} if isinstance(other, KeyboardKey): return (self.name, self.code, self.location) == (other.name, other.code, other.location) return False def __hash__(self) -> int: return hash((self.name, self.code, self.location)) def __repr__(self): return str(self.name) @property def is_cursorkey(self) -> bool: """Whether the key is a cursor key (arrow keys).""" return self.code.startswith('Arrow') @property def number(self) -> int | None: """Integer value of a number key.""" return int(self.code[len('Digit'):]) if self.code.startswith('Digit') else None @property def backspace(self) -> bool: """Whether the key is the backspace key.""" return self.name == 'Backspace' @property def tab(self) -> bool: """Whether the key is the tab key.""" return self.name == 'Tab' @property def enter(self) -> bool: """Whether the key is the enter key.""" return self.name == 'Enter' @property def shift(self) -> bool: """Whether the key is the shift key.""" return self.name == 'Shift' @property def control(self) -> bool: """Whether the key is the control key.""" return self.name == 'Control' @property def alt(self) -> bool: """Whether the key is the alt key.""" return self.name == 'Alt' @property def pause(self) -> bool: """Whether the key is the pause key.""" return self.name == 'Pause' @property def caps_lock(self) -> bool: """Whether the key is the caps lock key.""" return self.name == 'CapsLock' @property def escape(self) -> bool: """Whether the key is the escape key.""" return self.name == 'Escape' @property def space(self) -> bool: """Whether the key is the space key.""" return self.name == ' ' @property def page_up(self) -> bool: """Whether the key is the page up key.""" return self.name == 'PageUp' @property def page_down(self) -> bool: """Whether the key is the page down key.""" return self.name == 'PageDown' @property def end(self) -> bool: """Whether the key is the end key.""" return self.name == 'End' @property def home(self) -> bool: """Whether the key is the home key.""" return self.name == 'Home' @property def arrow_left(self) -> bool: """Whether the key is the arrow left key.""" return self.name == 'ArrowLeft' @property def arrow_up(self) -> bool: """Whether the key is the arrow up key.""" return self.name == 'ArrowUp' @property def arrow_right(self) -> bool: """Whether the key is the arrow right key.""" return self.name == 'ArrowRight' @property def arrow_down(self) -> bool: """Whether the key is the arrow down key.""" return self.name == 'ArrowDown' @property def print_screen(self) -> bool: """Whether the key is the print screen key.""" return self.name == 'PrintScreen' @property def insert(self) -> bool: """Whether the key is the insert key.""" return self.name == 'Insert' @property def delete(self) -> bool: """Whether the key is the delete key.""" return self.name == 'Delete' @property def meta(self) -> bool: """Whether the key is the meta key.""" return self.name == 'Meta' @property def f1(self) -> bool: """Whether the key is the F1 key.""" return self.name == 'F1' @property def f2(self) -> bool: """Whether the key is the F2 key.""" return self.name == 'F2' @property def f3(self) -> bool: """Whether the key is the F3 key.""" return self.name == 'F3' @property def f4(self) -> bool: """Whether the key is the F4 key.""" return self.name == 'F4' @property def f5(self) -> bool: """Whether the key is the F5 key.""" return self.name == 'F5' @property def f6(self) -> bool: """Whether the key is the F6 key.""" return self.name == 'F6' @property def f7(self) -> bool: """Whether the key is the F7 key.""" return self.name == 'F7' @property def f8(self) -> bool: """Whether the key is the F8 key.""" return self.name == 'F8' @property def f9(self) -> bool: """Whether the key is the F9 key.""" return self.name == 'F9' @property def f10(self) -> bool: """Whether the key is the F10 key.""" return self.name == 'F10' @property def f11(self) -> bool: """Whether the key is the F11 key.""" return self.name == 'F11' @property def f12(self) -> bool: """Whether the key is the F12 key.""" return self.name == 'F12' @dataclass(**KWONLY_SLOTS) class KeyEventArguments(UiEventArguments): action: KeyboardAction key: KeyboardKey modifiers: KeyboardModifiers @dataclass(**KWONLY_SLOTS) class ScrollEventArguments(UiEventArguments): vertical_position: float vertical_percentage: float vertical_size: float vertical_container_size: float horizontal_position: float horizontal_percentage: float horizontal_size: float horizontal_container_size: float @dataclass(**KWONLY_SLOTS) class JsonEditorSelectEventArguments(UiEventArguments): selection: dict @dataclass(**KWONLY_SLOTS) class JsonEditorChangeEventArguments(UiEventArguments): content: dict errors: dict = field(default_factory=dict) @dataclass(**KWONLY_SLOTS) class XtermBellEventArguments(UiEventArguments): pass @dataclass(**KWONLY_SLOTS) class XtermDataEventArguments(UiEventArguments): data: str EventT = TypeVar('EventT', bound=EventArguments) Handler = Union[Callable[[EventT], Any], Callable[[], Any]] def handle_event(handler: Handler[EventT] | None, arguments: EventT) -> None: """Call the given event handler. The handler is called within the context of the parent slot of the sender. If the handler is a coroutine, it is scheduled as a background task. If the handler expects arguments, the arguments are passed to the handler. Exceptions are caught and handled globally. :param handler: the event handler :param arguments: the event arguments """ if handler is None: return try: parent_slot: Slot | nullcontext if isinstance(arguments, UiEventArguments): parent_slot = arguments.sender.parent_slot or arguments.sender.client.layout.default_slot else: parent_slot = nullcontext() with parent_slot: if helpers.expects_arguments(handler): result = cast(Callable[[EventT], Any], handler)(arguments) else: result = cast(Callable[[], Any], handler)() if isinstance(result, Awaitable) and not isinstance(result, AwaitableResponse) and not isinstance(result, asyncio.Task): # NOTE: await an awaitable result even if the handler is not a coroutine (like a lambda statement) async def wait_for_result(): with parent_slot: try: await result except Exception as e: core.app.handle_exception(e) if core.loop and core.loop.is_running(): background_tasks.create(wait_for_result(), name=str(handler)) else: core.app.on_startup(wait_for_result()) except Exception as e: core.app.handle_exception(e)