HomeDashboard/.venv/lib/python3.12/site-packages/nicegui/events.py
2026-01-03 14:54:18 +01:00

457 lines
11 KiB
Python

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)