소스 검색

refactoring event handlers and arguments to support callables and awaitables and to catch exceptions

Falko Schindler 3 년 전
부모
커밋
4941ddc7d4

+ 3 - 2
main.py

@@ -9,6 +9,7 @@ import re
 import asyncio
 from nicegui.elements.markdown import Markdown
 from nicegui.elements.element import Element
+from nicegui.events import ClickEventArguments, KeyEventArguments
 from nicegui.globals import page_stack
 
 # add docutils css to webpage
@@ -392,7 +393,7 @@ with example(get_decorator):
     ui.link('Try yet another route!', '/another/route/1')
 
 with example(ui.keyboard):
-    def handle_keys(e):
+    def handle_key(e: KeyEventArguments):
         if e.key == 'f' and not e.action.repeat:
             if e.action.keyup:
                 ui.notify('f was just released')
@@ -408,7 +409,7 @@ with example(ui.keyboard):
             elif e.key.arrow_down:
                 ui.notify('going down')
 
-    keyboard = ui.keyboard(handle_keys)
+    keyboard = ui.keyboard(on_key=handle_key)
     ui.label('Key events can be caught globally by using the keyboard element.')
     ui.checkbox('Track key events').bind_value_to(keyboard, 'active')
 

+ 2 - 2
nicegui/elements/bool_element.py

@@ -1,5 +1,5 @@
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 from .value_element import ValueElement
 
 class BoolElement(ValueElement):
@@ -8,6 +8,6 @@ class BoolElement(ValueElement):
                  view: jp.HTMLBaseComponent,
                  *,
                  value: bool,
-                 on_change: Callable,
+                 on_change: Optional[Union[Callable, Awaitable]],
                  ):
         super().__init__(view, value=value, on_change=on_change)

+ 4 - 9
nicegui/elements/button.py

@@ -1,9 +1,8 @@
-import asyncio
-from typing import Awaitable, Callable, Union
+from typing import Awaitable, Callable, Optional, Union
 import justpy as jp
 
 from ..binding import bind_from, bind_to, BindableProperty
-from ..utils import handle_exceptions, provide_arguments, async_provide_arguments
+from ..events import ClickEventArguments, handle_event
 from .element import Element
 
 class Button(Element):
@@ -12,7 +11,7 @@ class Button(Element):
     def __init__(self,
                  text: str = '',
                  *,
-                 on_click: Union[Callable, Awaitable] = None,
+                 on_click: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Button Element
 
@@ -26,11 +25,7 @@ class Button(Element):
         self.text = text
         self.bind_text_to(self.view, 'label')
 
-        if on_click is not None:
-            if asyncio.iscoroutinefunction(on_click):
-                view.on('click', handle_exceptions(async_provide_arguments(func=on_click, update_function=view.update)))
-            else:
-                view.on('click', handle_exceptions(provide_arguments(on_click)))
+        view.on('click', lambda *_: handle_event(on_click, ClickEventArguments(sender=self), update_view=True))
 
     def set_text(self, text: str):
         self.text = text

+ 2 - 2
nicegui/elements/checkbox.py

@@ -1,4 +1,4 @@
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 import justpy as jp
 from .bool_element import BoolElement
 
@@ -8,7 +8,7 @@ class Checkbox(BoolElement):
                  text: str = '',
                  *,
                  value: bool = False,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Checkbox Element
 

+ 4 - 3
nicegui/elements/choice_element.py

@@ -1,15 +1,16 @@
 import justpy as jp
-from typing import Any, Union, List, Dict, Callable
+from typing import Any, Awaitable, Callable, Optional, Union
 from .value_element import ValueElement
 
 class ChoiceElement(ValueElement):
 
     def __init__(self,
                  view: jp.HTMLBaseComponent,
-                 options: Union[List, Dict],
+                 options: Union[list, dict],
                  *,
                  value: Any,
-                 on_change: Callable):
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
+                 ):
         if isinstance(options, list):
             view.options = [{'label': option, 'value': option} for option in options]
         else:

+ 2 - 2
nicegui/elements/float_element.py

@@ -1,5 +1,5 @@
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 from .value_element import ValueElement
 
 class FloatElement(ValueElement):
@@ -9,7 +9,7 @@ class FloatElement(ValueElement):
                  *,
                  value: float,
                  format: str = None,
-                 on_change: Callable,
+                 on_change: Optional[Union[Callable, Awaitable]],
                  ):
         self.format = format
 

+ 2 - 2
nicegui/elements/input.py

@@ -1,5 +1,5 @@
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 from .string_element import StringElement
 
 class Input(StringElement):
@@ -9,7 +9,7 @@ class Input(StringElement):
                  *,
                  placeholder: str = None,
                  value: str = '',
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Text Input Element
 

+ 11 - 6
nicegui/elements/joystick.py

@@ -1,10 +1,15 @@
-from typing import Callable, Dict
+from typing import Callable, Optional
 from .custom_view import CustomView
 from .element import Element
 
 class JoystickView(CustomView):
 
-    def __init__(self, on_start, on_move, on_end, **options):
+    def __init__(self,
+                 on_start: Optional[Callable],
+                 on_move: Optional[Callable],
+                 on_end: Optional[Callable],
+                 **options,
+                 ):
         super().__init__('joystick', __file__, ['nipplejs.min.js'], **options)
 
         self.on_start = on_start
@@ -35,10 +40,10 @@ class Joystick(Element):
 
     def __init__(self,
                  *,
-                 on_start: Callable = None,
-                 on_move: Callable = None,
-                 on_end: Callable = None,
-                 **options: Dict,
+                 on_start: Optional[Callable] = None,
+                 on_move: Optional[Callable] = None,
+                 on_end: Optional[Callable] = None,
+                 **options: dict,
                  ):
         """Joystick
 

+ 38 - 204
nicegui/elements/keyboard.py

@@ -1,50 +1,26 @@
-from pydantic import BaseModel
-from typing import Callable, Optional
+import traceback
+from typing import Awaitable, Callable, Optional, Union
 
-from ..binding import bind_to
-from ..utils import handle_exceptions, provide_arguments
+from ..events import KeyEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, handle_event
 from .custom_view import CustomView
 from .element import Element
 
 class KeyboardView(CustomView):
 
-    def __init__(self, handle_keys: Callable, active: bool = True):
+    def __init__(self, on_key: Callable):
         super().__init__('keyboard', __file__, activeJSEvents=['keydown', 'keyup', 'keypress'])
         self.allowed_events = ['keyboardEvent']
         self.style = 'display: none'
-        self.active = active
-
-        def execute_when_active(*args):
-            if not self.active:
-                return
-
-            event_args = args[1]
-            event_args.action = KeyboardAction(
-                keypress=event_args.key_data['action'] == 'keypress',
-                keydown=event_args.key_data['action'] == 'keydown',
-                keyup=event_args.key_data['action'] == 'keyup',
-                repeat=event_args.key_data['repeat'],
-            )
-            event_args.modifiers = KeyboardModifiers(
-                alt=event_args.key_data['altKey'],
-                ctrl=event_args.key_data['ctrlKey'],
-                meta=event_args.key_data['metaKey'],
-                shift=event_args.key_data['shiftKey'],
-            )
-            event_args.key = KeyboardKey(
-                name=event_args.key_data['key'],
-                code=event_args.key_data['code'],
-                location=event_args.key_data['location'],
-            )
-
-            handle_exceptions(provide_arguments(handle_keys, 'key_data', 'action', 'modifiers', 'key'))(*args)
-
-        self.initialize(temp=False, on_keyboardEvent=execute_when_active)
+        self.initialize(temp=False, on_keyboardEvent=on_key)
 
 
 class Keyboard(Element):
 
-    def __init__(self, handle_keys: Callable = None, active: bool = True):
+    def __init__(self,
+                 *,
+                 on_key: Optional[Union[Callable, Awaitable]] = None,
+                 active: bool = True,
+                 ):
         """
         Keyboard
 
@@ -53,174 +29,32 @@ class Keyboard(Element):
         :param handle_keys: callback to be executed when keyboard events occur.
         :param active: boolean flag indicating whether the callback should be executed or not
         """
-        super().__init__(KeyboardView(handle_keys=handle_keys, active=active))
+        super().__init__(KeyboardView(on_key=self.handle_key))
         self.active = active
-        bind_to(self, 'active', self.view, 'active', lambda x: x)
-
-class KeyboardAction(BaseModel):
-    keypress: bool
-    keydown: bool
-    keyup: bool
-    repeat: bool
-
-class KeyboardModifiers(BaseModel):
-    alt: bool
-    ctrl: bool
-    meta: bool
-    shift: bool
-
-class KeyboardKey(BaseModel):
-    name: str
-    code: str
-    location: int
-
-    def __eq__(self, other: str) -> bool:
-        return self.name == other or self.code == other
-
-    def __repr__(self):
-        return str(self.name)
-
-    @property
-    def is_cursorkey(self):
-        return self.code.startswith('Arrow')
-
-    @property
-    def number(self) -> Optional[int]:
-        """Integer value of a number key."""
-        return int(self.name) if self.code.startswith('Digit') else None
-
-    @property
-    def backspace(self) -> bool:
-        return self.name == 'Backspace'
-
-    @property
-    def tab(self) -> bool:
-        return self.name == 'Tab'
-
-    @property
-    def enter(self) -> bool:
-        return self.name == 'enter'
-
-    @property
-    def shift(self) -> bool:
-        return self.name == 'Shift'
-
-    @property
-    def control(self) -> bool:
-        return self.name == 'Control'
-
-    @property
-    def alt(self) -> bool:
-        return self.name == 'Alt'
-
-    @property
-    def pause(self) -> bool:
-        return self.name == 'Pause'
-
-    @property
-    def caps_lock(self) -> bool:
-        return self.name == 'CapsLock'
-
-    @property
-    def escape(self) -> bool:
-        return self.name == 'Escape'
-
-    @property
-    def space(self) -> bool:
-        return self.name == 'Space'
-
-    @property
-    def page_up(self) -> bool:
-        return self.name == 'PageUp'
-
-    @property
-    def page_down(self) -> bool:
-        return self.name == 'PageDown'
-
-    @property
-    def end(self) -> bool:
-        return self.name == 'End'
-
-    @property
-    def home(self) -> bool:
-        return self.name == 'Home'
-
-    @property
-    def arrow_left(self) -> bool:
-        return self.name == 'ArrowLeft'
-
-    @property
-    def arrow_up(self) -> bool:
-        return self.name == 'ArrowUp'
-
-    @property
-    def arrow_right(self) -> bool:
-        return self.name == 'ArrowRight'
-
-    @property
-    def arrow_down(self) -> bool:
-        return self.name == 'ArrowDown'
-
-    @property
-    def print_screen(self) -> bool:
-        return self.name == 'PrintScreen'
-
-    @property
-    def insert(self) -> bool:
-        return self.name == 'Insert'
-
-    @property
-    def delete(self) -> bool:
-        return self.name == 'Delete'
-
-    @property
-    def meta(self) -> bool:
-        return self.name == 'Meta'
-
-    @property
-    def f1(self) -> bool:
-        return self.name == 'F1'
-
-    @property
-    def f2(self) -> bool:
-        return self.name == 'F2'
-
-    @property
-    def f3(self) -> bool:
-        return self.name == 'F3'
-
-    @property
-    def f4(self) -> bool:
-        return self.name == 'F4'
-
-    @property
-    def f5(self) -> bool:
-        return self.name == 'F5'
-
-    @property
-    def f6(self) -> bool:
-        return self.name == 'F6'
-
-    @property
-    def f7(self) -> bool:
-        return self.name == 'F7'
-
-    @property
-    def f8(self) -> bool:
-        return self.name == 'F8'
-
-    @property
-    def f9(self) -> bool:
-        return self.name == 'F9'
-
-    @property
-    def f10(self) -> bool:
-        return self.name == 'F10'
-
-    @property
-    def f11(self) -> bool:
-        return self.name == 'F11'
-
-    @property
-    def f12(self) -> bool:
-        return self.name == 'F12'
+        self.key_handler = on_key
+
+    def handle_key(self, msg: dict):
+        if not self.active:
+            return
+
+        try:
+            action = KeyboardAction(
+                keypress=msg.key_data.action == 'keypress',
+                keydown=msg.key_data.action == 'keydown',
+                keyup=msg.key_data.action == 'keyup',
+                repeat=msg.key_data.repeat,
+            )
+            modifiers = KeyboardModifiers(
+                alt=msg.key_data.altKey,
+                ctrl=msg.key_data.ctrlKey,
+                meta=msg.key_data.metaKey,
+                shift=msg.key_data.shiftKey,
+            )
+            key = KeyboardKey(
+                name=msg.key_data.key,
+                code=msg.key_data.code,
+                location=msg.key_data.location,
+            )
+            handle_event(self.key_handler, KeyEventArguments(sender=self, action=action, modifiers=modifiers, key=key))
+        except Exception:
+            traceback.print_exc()

+ 5 - 5
nicegui/elements/menu_item.py

@@ -1,14 +1,15 @@
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 import justpy as jp
+
+from ..events import ClickEventArguments, handle_event
 from .element import Element
-from ..utils import handle_exceptions, provide_arguments
 
 
 class MenuItem(Element):
 
     def __init__(self,
                  text: str = '',
-                 on_click: Callable = None,
+                 on_click: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Menu Item Element
 
@@ -19,7 +20,6 @@ class MenuItem(Element):
         """
         view = jp.QItem(text=text, clickable=True)
 
-        if on_click is not None:
-            view.on('click', handle_exceptions(provide_arguments(on_click)))
+        view.on('click', lambda *_: handle_event(on_click, ClickEventArguments(sender=self), update_view=True))
 
         super().__init__(view)

+ 2 - 2
nicegui/elements/number.py

@@ -1,5 +1,5 @@
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 from .float_element import FloatElement
 
 class Number(FloatElement):
@@ -10,7 +10,7 @@ class Number(FloatElement):
                  placeholder: str = None,
                  value: float = None,
                  format: str = None,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Number Input Element
 

+ 3 - 3
nicegui/elements/radio.py

@@ -1,14 +1,14 @@
 import justpy as jp
-from typing import Callable, List, Dict, Union
+from typing import Awaitable, Callable, Optional, Union
 from .choice_element import ChoiceElement
 
 class Radio(ChoiceElement):
 
     def __init__(self,
-                 options: Union[List, Dict],
+                 options: Union[list, dict],
                  *,
                  value: any = None,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Radio Selection Element
 

+ 3 - 3
nicegui/elements/scene.py

@@ -1,4 +1,4 @@
-from typing import Callable
+from typing import Callable, Optional
 import traceback
 from .element import Element
 from .custom_view import CustomView
@@ -8,7 +8,7 @@ from ..globals import view_stack
 
 class SceneView(CustomView):
 
-    def __init__(self, *, width: int, height: int, on_click: Callable):
+    def __init__(self, *, width: int, height: int, on_click: Optional[Callable]):
         dependencies = ['three.min.js', 'OrbitControls.js', 'STLLoader.js']
         super().__init__('scene', __file__, dependencies, width=width, height=height)
         self.on_click = on_click
@@ -44,7 +44,7 @@ class Scene(Element):
     from .scene_objects import Curve as curve
     from .scene_objects import Texture as texture
 
-    def __init__(self, width: int = 400, height: int = 300, on_click: Callable = None):
+    def __init__(self, width: int = 400, height: int = 300, on_click: Optional[Callable] = None):
         """3D Scene
 
         Display a 3d scene using `three.js <https://threejs.org/>`_.

+ 3 - 3
nicegui/elements/select.py

@@ -1,14 +1,14 @@
 import justpy as jp
-from typing import Callable, List, Dict, Union
+from typing import Awaitable, Callable, Optional, Union
 from .choice_element import ChoiceElement
 
 class Select(ChoiceElement):
 
     def __init__(self,
-                 options: Union[List, Dict],
+                 options: Union[list, dict],
                  *,
                  value: any = None,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Dropdown Selection Element
 

+ 2 - 2
nicegui/elements/slider.py

@@ -1,4 +1,4 @@
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 import justpy as jp
 from .float_element import FloatElement
 
@@ -10,7 +10,7 @@ class Slider(FloatElement):
                  max: float,
                  step: float = 1,
                  value: float = None,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Slider Element
 

+ 2 - 2
nicegui/elements/string_element.py

@@ -1,5 +1,5 @@
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 from .value_element import ValueElement
 
 class StringElement(ValueElement):
@@ -8,6 +8,6 @@ class StringElement(ValueElement):
                  view: jp.HTMLBaseComponent,
                  *,
                  value: float,
-                 on_change: Callable,
+                 on_change: Optional[Union[Callable, Awaitable]],
                  ):
         super().__init__(view, value=value, on_change=on_change)

+ 2 - 2
nicegui/elements/switch.py

@@ -1,4 +1,4 @@
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 import justpy as jp
 from .bool_element import BoolElement
 
@@ -8,7 +8,7 @@ class Switch(BoolElement):
                  text: str = '',
                  *,
                  value: bool = False,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Switch Element
 

+ 3 - 3
nicegui/elements/toggle.py

@@ -1,14 +1,14 @@
 import justpy as jp
-from typing import Callable, List, Dict, Union
+from typing import Awaitable, Callable, Optional, Union
 from .choice_element import ChoiceElement
 
 class Toggle(ChoiceElement):
 
     def __init__(self,
-                 options: Union[List, Dict],
+                 options: Union[list, dict],
                  *,
                  value: any = None,
-                 on_change: Callable = None,
+                 on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """Toggle Element
 

+ 13 - 7
nicegui/elements/upload.py

@@ -1,22 +1,24 @@
+import traceback
 import justpy as jp
-from typing import Callable
+from typing import Awaitable, Callable, Optional, Union
 import base64
+
+from ..events import UploadEventArguments, handle_event
 from .element import Element
-from ..utils import handle_exceptions
 
 class Upload(Element):
 
     def __init__(self,
                  *,
                  multiple: bool = False,
-                 on_upload: Callable = None,
+                 on_upload: Optional[Union[Callable, Awaitable]] = None,
                  ):
         """File Upload Element
 
         :param multiple: allow uploading multiple files at once (default: False)
         :param on_upload: callback to execute when a file is uploaded (list of bytearrays)
         """
-        self.on_upload = handle_exceptions(on_upload)
+        self.upload_handler = on_upload
         view = jp.Form(enctype='multipart/form-data', classes='flex gap-4 items-center',
                        submit=lambda s, m: self.submit(s, m))
         jp.Input(type='file', multiple=multiple, a=view)
@@ -25,6 +27,10 @@ class Upload(Element):
         super().__init__(view)
 
     def submit(self, _, msg):
-        for form_data in msg.form_data:
-            if form_data.type == 'file':
-                self.on_upload([base64.b64decode(f.file_content) for f in form_data.files])
+        try:
+            for form_data in msg.form_data:
+                if form_data.type == 'file':
+                    files = [base64.b64decode(f.file_content) for f in form_data.files]
+                    handle_event(self.upload_handler, UploadEventArguments(sender=self, files=files))
+        except Exception:
+            traceback.print_exc()

+ 6 - 14
nicegui/elements/value_element.py

@@ -1,8 +1,8 @@
 import justpy as jp
-from typing import Any, Callable
-import traceback
+from typing import Any, Awaitable, Callable, Optional, Union
+
+from ..events import ValueChangeEventArguments, handle_event
 from ..binding import bind_from, bind_to, BindableProperty
-from ..utils import EventArguments
 from .element import Element
 
 class ValueElement(Element):
@@ -12,11 +12,11 @@ class ValueElement(Element):
                  view: jp.HTMLBaseComponent,
                  *,
                  value: Any,
-                 on_change: Callable,
+                 on_change: Optional[Union[Callable, Awaitable]],
                  ):
         super().__init__(view)
 
-        self.on_change = on_change
+        self.change_handler = on_change
         self.value = value
         self.bind_value_to(self.view, 'value', forward=self.value_to_view)
 
@@ -25,15 +25,7 @@ class ValueElement(Element):
 
     def handle_change(self, msg):
         self.value = msg['value']
-
-        if self.on_change is not None:
-            try:
-                try:
-                    self.on_change()
-                except TypeError:
-                    self.on_change(EventArguments(self, **msg))
-            except Exception:
-                traceback.print_exc()
+        handle_event(self.change_handler, ValueChangeEventArguments(sender=self, value=self.value))
 
     def bind_value_to(self, target_object, target_name, *, forward=lambda x: x):
         bind_to(self, 'value', target_object, target_name, forward=forward)

+ 212 - 0
nicegui/events.py

@@ -0,0 +1,212 @@
+import asyncio
+from inspect import signature
+from pydantic import BaseModel
+import traceback
+from typing import Any, Awaitable, Callable, Optional, Union
+
+from .elements.element import Element
+
+class EventArguments(BaseModel):
+    class Config:
+        arbitrary_types_allowed = True
+    sender: Element
+
+class ClickEventArguments(EventArguments):
+    pass
+
+class UploadEventArguments(EventArguments):
+    files: list[bytes]
+
+class ValueChangeEventArguments(EventArguments):
+    value: Any
+
+class KeyboardAction(BaseModel):
+    keypress: bool
+    keydown: bool
+    keyup: bool
+    repeat: bool
+
+class KeyboardModifiers(BaseModel):
+    alt: bool
+    ctrl: bool
+    meta: bool
+    shift: bool
+
+class KeyboardKey(BaseModel):
+    name: str
+    code: str
+    location: int
+
+    def __eq__(self, other: str) -> bool:
+        return self.name == other or self.code == other
+
+    def __repr__(self):
+        return str(self.name)
+
+    @property
+    def is_cursorkey(self):
+        return self.code.startswith('Arrow')
+
+    @property
+    def number(self) -> Optional[int]:
+        """Integer value of a number key."""
+        return int(self.name) if self.code.startswith('Digit') else None
+
+    @property
+    def backspace(self) -> bool:
+        return self.name == 'Backspace'
+
+    @property
+    def tab(self) -> bool:
+        return self.name == 'Tab'
+
+    @property
+    def enter(self) -> bool:
+        return self.name == 'enter'
+
+    @property
+    def shift(self) -> bool:
+        return self.name == 'Shift'
+
+    @property
+    def control(self) -> bool:
+        return self.name == 'Control'
+
+    @property
+    def alt(self) -> bool:
+        return self.name == 'Alt'
+
+    @property
+    def pause(self) -> bool:
+        return self.name == 'Pause'
+
+    @property
+    def caps_lock(self) -> bool:
+        return self.name == 'CapsLock'
+
+    @property
+    def escape(self) -> bool:
+        return self.name == 'Escape'
+
+    @property
+    def space(self) -> bool:
+        return self.name == 'Space'
+
+    @property
+    def page_up(self) -> bool:
+        return self.name == 'PageUp'
+
+    @property
+    def page_down(self) -> bool:
+        return self.name == 'PageDown'
+
+    @property
+    def end(self) -> bool:
+        return self.name == 'End'
+
+    @property
+    def home(self) -> bool:
+        return self.name == 'Home'
+
+    @property
+    def arrow_left(self) -> bool:
+        return self.name == 'ArrowLeft'
+
+    @property
+    def arrow_up(self) -> bool:
+        return self.name == 'ArrowUp'
+
+    @property
+    def arrow_right(self) -> bool:
+        return self.name == 'ArrowRight'
+
+    @property
+    def arrow_down(self) -> bool:
+        return self.name == 'ArrowDown'
+
+    @property
+    def print_screen(self) -> bool:
+        return self.name == 'PrintScreen'
+
+    @property
+    def insert(self) -> bool:
+        return self.name == 'Insert'
+
+    @property
+    def delete(self) -> bool:
+        return self.name == 'Delete'
+
+    @property
+    def meta(self) -> bool:
+        return self.name == 'Meta'
+
+    @property
+    def f1(self) -> bool:
+        return self.name == 'F1'
+
+    @property
+    def f2(self) -> bool:
+        return self.name == 'F2'
+
+    @property
+    def f3(self) -> bool:
+        return self.name == 'F3'
+
+    @property
+    def f4(self) -> bool:
+        return self.name == 'F4'
+
+    @property
+    def f5(self) -> bool:
+        return self.name == 'F5'
+
+    @property
+    def f6(self) -> bool:
+        return self.name == 'F6'
+
+    @property
+    def f7(self) -> bool:
+        return self.name == 'F7'
+
+    @property
+    def f8(self) -> bool:
+        return self.name == 'F8'
+
+    @property
+    def f9(self) -> bool:
+        return self.name == 'F9'
+
+    @property
+    def f10(self) -> bool:
+        return self.name == 'F10'
+
+    @property
+    def f11(self) -> bool:
+        return self.name == 'F11'
+
+    @property
+    def f12(self) -> bool:
+        return self.name == 'F12'
+
+class KeyEventArguments(EventArguments):
+    class Config:
+        arbitrary_types_allowed = True
+    action: KeyboardAction
+    key: KeyboardKey
+    modifiers: KeyboardModifiers
+
+
+def handle_event(handler: Optional[Union[Callable, Awaitable]], arguments: EventArguments, *, update_view: bool = False):
+    async def async_handler():
+        if handler is None:
+            return
+        try:
+            no_arguments = not signature(handler).parameters
+            call = handler() if no_arguments else handler(arguments)
+            if asyncio.iscoroutinefunction(handler):
+                await call
+            if update_view:
+                await arguments.sender.parent_view.update()
+        except Exception:
+            traceback.print_exc()
+    asyncio.get_event_loop().create_task(async_handler())

+ 12 - 3
nicegui/timer.py

@@ -2,9 +2,9 @@ import asyncio
 import time
 import traceback
 from typing import Awaitable, Callable, Union
+
 from .binding import BindableProperty
 from .globals import view_stack
-from .utils import handle_exceptions, handle_awaitable
 
 class Timer:
     tasks = []
@@ -27,9 +27,18 @@ class Timer:
         parent = view_stack[-1]
         self.active = active
 
+        async def do_callback():
+            try:
+                if asyncio.iscoroutinefunction(callback):
+                    return await callback()
+                else:
+                    return callback()
+            except Exception:
+                traceback.print_exc()
+
         async def timeout():
             await asyncio.sleep(interval)
-            await handle_exceptions(handle_awaitable(callback))()
+            await do_callback()
             await parent.update()
 
         async def loop():
@@ -37,7 +46,7 @@ class Timer:
                 try:
                     start = time.time()
                     if self.active:
-                        needs_update = await handle_exceptions(handle_awaitable(callback))()
+                        needs_update = await do_callback()
                         if needs_update != False:
                             await parent.update()
                     dt = time.time() - start

+ 0 - 54
nicegui/utils.py

@@ -1,54 +0,0 @@
-import asyncio
-import traceback
-
-
-class EventArguments:
-    def __init__(self, sender, **kwargs):
-        self.sender = sender
-        for key, value in kwargs.items():
-            setattr(self, key, value)
-
-
-def provide_arguments(func, *keys):
-    def inner_function(sender, event):
-        try:
-            return func()
-        except TypeError:
-            return func(EventArguments(sender, **{key: event[key] for key in keys}))
-
-    return inner_function
-
-
-def handle_exceptions(func):
-    def inner_function(*args, **kwargs):
-        try:
-            return func(*args, **kwargs)
-        except Exception:
-            traceback.print_exc()
-
-    return inner_function
-
-
-def handle_awaitable(func):
-    async def inner_function(*args, **kwargs):
-        if asyncio.iscoroutinefunction(func):
-            return await func(*args, **kwargs)
-        else:
-            return func(*args, **kwargs)
-
-    return inner_function
-
-
-def async_provide_arguments(func, update_function, *keys):
-    def inner_function(sender, event):
-        async def execute_function():
-            try:
-                await func()
-            except TypeError:
-                await func(EventArguments(sender, **{key: event[key] for key in keys}))
-
-            await update_function()
-
-        asyncio.get_event_loop().create_task(execute_function())
-
-    return inner_function