events.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. from __future__ import annotations
  2. from contextlib import nullcontext
  3. from dataclasses import dataclass
  4. from inspect import Parameter, signature
  5. from typing import TYPE_CHECKING, Any, Awaitable, BinaryIO, Callable, Dict, List, Literal, Optional, Union
  6. from . import background_tasks, core
  7. from .awaitable_response import AwaitableResponse
  8. from .dataclasses import KWONLY_SLOTS
  9. from .slot import Slot
  10. if TYPE_CHECKING:
  11. from .client import Client
  12. from .element import Element
  13. from .observables import ObservableCollection
  14. @dataclass(**KWONLY_SLOTS)
  15. class EventArguments:
  16. pass
  17. @dataclass(**KWONLY_SLOTS)
  18. class ObservableChangeEventArguments(EventArguments):
  19. sender: ObservableCollection
  20. @dataclass(**KWONLY_SLOTS)
  21. class UiEventArguments(EventArguments):
  22. sender: Element
  23. client: Client
  24. @dataclass(**KWONLY_SLOTS)
  25. class GenericEventArguments(UiEventArguments):
  26. args: Any
  27. @dataclass(**KWONLY_SLOTS)
  28. class ClickEventArguments(UiEventArguments):
  29. pass
  30. @dataclass(**KWONLY_SLOTS)
  31. class EChartPointClickEventArguments(UiEventArguments):
  32. component_type: str
  33. series_type: str
  34. series_index: int
  35. series_name: str
  36. name: str
  37. data_index: int
  38. data: Union[float, int, str]
  39. data_type: str
  40. value: Union[float, int, list]
  41. @dataclass(**KWONLY_SLOTS)
  42. class SceneClickHit:
  43. object_id: str
  44. object_name: str
  45. x: float
  46. y: float
  47. z: float
  48. @dataclass(**KWONLY_SLOTS)
  49. class SceneClickEventArguments(ClickEventArguments):
  50. click_type: str
  51. button: int
  52. alt: bool
  53. ctrl: bool
  54. meta: bool
  55. shift: bool
  56. hits: List[SceneClickHit]
  57. @dataclass(**KWONLY_SLOTS)
  58. class SceneDragEventArguments(ClickEventArguments):
  59. type: Literal['dragstart', 'dragend']
  60. object_id: str
  61. object_name: str
  62. x: float
  63. y: float
  64. z: float
  65. @dataclass(**KWONLY_SLOTS)
  66. class ColorPickEventArguments(UiEventArguments):
  67. color: str
  68. @dataclass(**KWONLY_SLOTS)
  69. class MouseEventArguments(UiEventArguments):
  70. type: str
  71. image_x: float
  72. image_y: float
  73. button: int
  74. buttons: int
  75. alt: bool
  76. ctrl: bool
  77. meta: bool
  78. shift: bool
  79. @dataclass(**KWONLY_SLOTS)
  80. class JoystickEventArguments(UiEventArguments):
  81. action: str
  82. x: Optional[float] = None
  83. y: Optional[float] = None
  84. @dataclass(**KWONLY_SLOTS)
  85. class UploadEventArguments(UiEventArguments):
  86. content: BinaryIO
  87. name: str
  88. type: str
  89. @dataclass(**KWONLY_SLOTS)
  90. class ValueChangeEventArguments(UiEventArguments):
  91. value: Any
  92. @dataclass(**KWONLY_SLOTS)
  93. class TableSelectionEventArguments(UiEventArguments):
  94. selection: List[Any]
  95. @dataclass(**KWONLY_SLOTS)
  96. class KeyboardAction:
  97. keydown: bool
  98. keyup: bool
  99. repeat: bool
  100. @dataclass(**KWONLY_SLOTS)
  101. class KeyboardModifiers:
  102. alt: bool
  103. ctrl: bool
  104. meta: bool
  105. shift: bool
  106. @dataclass(**KWONLY_SLOTS)
  107. class KeyboardKey:
  108. name: str
  109. code: str
  110. location: int
  111. def __eq__(self, other: object) -> bool:
  112. if isinstance(other, str):
  113. return other in {self.name, self.code}
  114. if isinstance(other, KeyboardKey):
  115. return self == other
  116. return False
  117. def __repr__(self):
  118. return str(self.name)
  119. @property
  120. def is_cursorkey(self) -> bool:
  121. """Whether the key is a cursor key (arrow keys)."""
  122. return self.code.startswith('Arrow')
  123. @property
  124. def number(self) -> Optional[int]:
  125. """Integer value of a number key."""
  126. return int(self.code.removeprefix('Digit')) if self.code.startswith('Digit') else None
  127. @property
  128. def backspace(self) -> bool:
  129. """Whether the key is the backspace key."""
  130. return self.name == 'Backspace'
  131. @property
  132. def tab(self) -> bool:
  133. """Whether the key is the tab key."""
  134. return self.name == 'Tab'
  135. @property
  136. def enter(self) -> bool:
  137. """Whether the key is the enter key."""
  138. return self.name == 'Enter'
  139. @property
  140. def shift(self) -> bool:
  141. """Whether the key is the shift key."""
  142. return self.name == 'Shift'
  143. @property
  144. def control(self) -> bool:
  145. """Whether the key is the control key."""
  146. return self.name == 'Control'
  147. @property
  148. def alt(self) -> bool:
  149. """Whether the key is the alt key."""
  150. return self.name == 'Alt'
  151. @property
  152. def pause(self) -> bool:
  153. """Whether the key is the pause key."""
  154. return self.name == 'Pause'
  155. @property
  156. def caps_lock(self) -> bool:
  157. """Whether the key is the caps lock key."""
  158. return self.name == 'CapsLock'
  159. @property
  160. def escape(self) -> bool:
  161. """Whether the key is the escape key."""
  162. return self.name == 'Escape'
  163. @property
  164. def space(self) -> bool:
  165. """Whether the key is the space key."""
  166. return self.name == 'Space'
  167. @property
  168. def page_up(self) -> bool:
  169. """Whether the key is the page up key."""
  170. return self.name == 'PageUp'
  171. @property
  172. def page_down(self) -> bool:
  173. """Whether the key is the page down key."""
  174. return self.name == 'PageDown'
  175. @property
  176. def end(self) -> bool:
  177. """Whether the key is the end key."""
  178. return self.name == 'End'
  179. @property
  180. def home(self) -> bool:
  181. """Whether the key is the home key."""
  182. return self.name == 'Home'
  183. @property
  184. def arrow_left(self) -> bool:
  185. """Whether the key is the arrow left key."""
  186. return self.name == 'ArrowLeft'
  187. @property
  188. def arrow_up(self) -> bool:
  189. """Whether the key is the arrow up key."""
  190. return self.name == 'ArrowUp'
  191. @property
  192. def arrow_right(self) -> bool:
  193. """Whether the key is the arrow right key."""
  194. return self.name == 'ArrowRight'
  195. @property
  196. def arrow_down(self) -> bool:
  197. """Whether the key is the arrow down key."""
  198. return self.name == 'ArrowDown'
  199. @property
  200. def print_screen(self) -> bool:
  201. """Whether the key is the print screen key."""
  202. return self.name == 'PrintScreen'
  203. @property
  204. def insert(self) -> bool:
  205. """Whether the key is the insert key."""
  206. return self.name == 'Insert'
  207. @property
  208. def delete(self) -> bool:
  209. """Whether the key is the delete key."""
  210. return self.name == 'Delete'
  211. @property
  212. def meta(self) -> bool:
  213. """Whether the key is the meta key."""
  214. return self.name == 'Meta'
  215. @property
  216. def f1(self) -> bool:
  217. """Whether the key is the F1 key."""
  218. return self.name == 'F1'
  219. @property
  220. def f2(self) -> bool:
  221. """Whether the key is the F2 key."""
  222. return self.name == 'F2'
  223. @property
  224. def f3(self) -> bool:
  225. """Whether the key is the F3 key."""
  226. return self.name == 'F3'
  227. @property
  228. def f4(self) -> bool:
  229. """Whether the key is the F4 key."""
  230. return self.name == 'F4'
  231. @property
  232. def f5(self) -> bool:
  233. """Whether the key is the F5 key."""
  234. return self.name == 'F5'
  235. @property
  236. def f6(self) -> bool:
  237. """Whether the key is the F6 key."""
  238. return self.name == 'F6'
  239. @property
  240. def f7(self) -> bool:
  241. """Whether the key is the F7 key."""
  242. return self.name == 'F7'
  243. @property
  244. def f8(self) -> bool:
  245. """Whether the key is the F8 key."""
  246. return self.name == 'F8'
  247. @property
  248. def f9(self) -> bool:
  249. """Whether the key is the F9 key."""
  250. return self.name == 'F9'
  251. @property
  252. def f10(self) -> bool:
  253. """Whether the key is the F10 key."""
  254. return self.name == 'F10'
  255. @property
  256. def f11(self) -> bool:
  257. """Whether the key is the F11 key."""
  258. return self.name == 'F11'
  259. @property
  260. def f12(self) -> bool:
  261. """Whether the key is the F12 key."""
  262. return self.name == 'F12'
  263. @dataclass(**KWONLY_SLOTS)
  264. class KeyEventArguments(UiEventArguments):
  265. action: KeyboardAction
  266. key: KeyboardKey
  267. modifiers: KeyboardModifiers
  268. @dataclass(**KWONLY_SLOTS)
  269. class ScrollEventArguments(UiEventArguments):
  270. vertical_position: float
  271. vertical_percentage: float
  272. vertical_size: float
  273. vertical_container_size: float
  274. horizontal_position: float
  275. horizontal_percentage: float
  276. horizontal_size: float
  277. horizontal_container_size: float
  278. @dataclass(**KWONLY_SLOTS)
  279. class JsonEditorSelectEventArguments(UiEventArguments):
  280. selection: Dict
  281. @dataclass(**KWONLY_SLOTS)
  282. class JsonEditorChangeEventArguments(UiEventArguments):
  283. content: Dict
  284. errors: Dict
  285. def handle_event(handler: Optional[Callable[..., Any]], arguments: EventArguments) -> None:
  286. """Call the given event handler.
  287. The handler is called within the context of the parent slot of the sender.
  288. If the handler is a coroutine, it is scheduled as a background task.
  289. If the handler expects arguments, the arguments are passed to the handler.
  290. Exceptions are caught and handled globally.
  291. :param handler: the event handler
  292. :param arguments: the event arguments
  293. """
  294. if handler is None:
  295. return
  296. try:
  297. expects_arguments = any(p.default is Parameter.empty and
  298. p.kind is not Parameter.VAR_POSITIONAL and
  299. p.kind is not Parameter.VAR_KEYWORD
  300. for p in signature(handler).parameters.values())
  301. parent_slot: Union[Slot, nullcontext]
  302. if isinstance(arguments, UiEventArguments):
  303. if arguments.sender.is_ignoring_events:
  304. return
  305. parent_slot = arguments.sender.parent_slot or arguments.sender.client.layout.default_slot
  306. else:
  307. parent_slot = nullcontext()
  308. with parent_slot:
  309. result = handler(arguments) if expects_arguments else handler()
  310. if isinstance(result, Awaitable) and not isinstance(result, AwaitableResponse):
  311. # NOTE: await an awaitable result even if the handler is not a coroutine (like a lambda statement)
  312. async def wait_for_result():
  313. with parent_slot:
  314. try:
  315. await result
  316. except Exception as e:
  317. core.app.handle_exception(e)
  318. if core.loop and core.loop.is_running():
  319. background_tasks.create(wait_for_result(), name=str(handler))
  320. else:
  321. core.app.on_startup(wait_for_result())
  322. except Exception as e:
  323. core.app.handle_exception(e)