events.py 9.1 KB

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