457 lines
11 KiB
Python
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)
|