Explorar o código

Merge branch 'main' into feature/jp-upgrade

Christoph Trappe %!s(int64=3) %!d(string=hai) anos
pai
achega
74e2ec77d5

+ 13 - 1
main.py

@@ -131,6 +131,18 @@ with example(overlay):
             </svg>'''
         ui.svg(svg_content).style('background:transparent')
 
+with example(ui.annotation_tool):
+    from nicegui.events import MouseEventArguments
+
+    def mouse_handler(e: MouseEventArguments):
+        color = 'green' if e.type == 'mousedown' else 'red'
+        at.svg_content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="10" fill="{color}"/>'
+        ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
+
+    at = ui.annotation_tool('http://placeimg.com/640/360/geometry',
+                            on_mouse=mouse_handler,
+                            events=['mousedown', 'mouseup'], cross=True)
+
 with example(ui.markdown):
     ui.markdown('### Headline\nWith hyperlink to [GitHub](https://github.com/zauberzeug/nicegui).')
 
@@ -335,7 +347,7 @@ with example(clear):
 
 binding = '''### Bindings
 
-With help of the [binding](https://pypi.org/project/binding/) package NiceGUI is able to directly bind UI elements to models.
+NiceGUI is able to directly bind UI elements to models.
 Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
 
 Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.

+ 60 - 0
nicegui/elements/annotation_tool.js

@@ -0,0 +1,60 @@
+Vue.component("annotation_tool", {
+  template: `
+    <div :id="jp_props.id" style="position:relative;display:inline-block">
+      <img style="max-width:100%">
+      <svg style="position:absolute;top:0;left:0;pointer-events:none">
+        <g style="display:none">
+          <line x1="100" y1="0" x2="100" y2="100%" stroke="black" />
+          <line x1="0" y1="100" x2="100%" y2="100" stroke="black" />
+        </g>
+        <g v-html="jp_props.options.svg_content"></g>
+      </svg>
+    </div>
+  `,
+  mounted() {
+    const image = document.getElementById(this.$props.jp_props.id).firstChild;
+    const svg = document.getElementById(this.$props.jp_props.id).lastChild;
+    const cross = svg.firstChild;
+    image.ondragstart = () => false;
+    if (this.$props.jp_props.options.cross) {
+      image.style.cursor = "none";
+      image.addEventListener("mouseenter", (e) => {
+        cross.style.display = "block";
+      });
+      image.addEventListener("mouseleave", (e) => {
+        cross.style.display = "none";
+      });
+      image.addEventListener("mousemove", (e) => {
+        const x = (e.offsetX * e.target.naturalWidth) / e.target.clientWidth;
+        const y = (e.offsetY * e.target.naturalHeight) / e.target.clientHeight;
+        cross.firstChild.setAttribute("x1", x);
+        cross.firstChild.setAttribute("x2", x);
+        cross.lastChild.setAttribute("y1", y);
+        cross.lastChild.setAttribute("y2", y);
+      });
+    }
+    image.onload = (e) => {
+      const viewBox = `0 0 ${image.naturalWidth} ${image.naturalHeight}`;
+      svg.setAttribute("viewBox", viewBox);
+    };
+    image.src = this.$props.jp_props.options.source;
+    for (const type of this.$props.jp_props.options.events) {
+      image.addEventListener(type, (e) => {
+        const event = {
+          event_type: "onMouse",
+          mouse_event_type: type,
+          vue_type: this.$props.jp_props.vue_type,
+          id: this.$props.jp_props.id,
+          page_id: page_id,
+          websocket_id: websocket_id,
+          image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
+          image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
+        };
+        send_to_server(event, "event");
+      });
+    }
+  },
+  props: {
+    jp_props: Object,
+  },
+});

+ 51 - 0
nicegui/elements/annotation_tool.py

@@ -0,0 +1,51 @@
+from __future__ import annotations
+from typing import Callable
+import traceback
+from ..events import MouseEventArguments, handle_event
+from .custom_view import CustomView
+from .element import Element
+
+CustomView.use(__file__)
+
+class AnnotationToolView(CustomView):
+
+    def __init__(self, source: str, on_mouse: Callable, events: list[str], cross: bool):
+        super().__init__('annotation_tool', source=source, events=events, cross=cross, svg_content='')
+        self.allowed_events = ['onMouse']
+        self.initialize(onMouse=on_mouse)
+
+class AnnotationTool(Element):
+
+    def __init__(self, source: str, on_mouse: Callable, *, events: list[str] = ['click'], cross: bool = False):
+        """Annotation Tool
+
+        Create an image with an SVG overlay that handles mouse events and yields image coordinates.
+
+        :param source: the source of the image; can be an url or a base64 string
+        :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
+        :param events: list of JavaScript events to subscribe to (default: `['click']`)
+        :param cross: whether to show crosshairs (default: `False`)
+        """
+        self.mouse_handler = on_mouse
+        super().__init__(AnnotationToolView(source, self.handle_mouse, events, cross))
+
+    def handle_mouse(self, msg):
+        try:
+            arguments = MouseEventArguments(
+                sender=self,
+                socket=msg.get('websocket'),
+                type=msg.get('mouse_event_type'),
+                image_x=msg.get('image_x'),
+                image_y=msg.get('image_y'),
+            )
+            handle_event(self.mouse_handler, arguments)
+        except:
+            traceback.print_exc()
+
+    @property
+    def svg_content(self) -> str:
+        return self.view.options.svg_content
+
+    @svg_content.setter
+    def svg_content(self, content: str):
+        self.view.options.svg_content = content

+ 1 - 1
nicegui/elements/checkbox.py

@@ -13,7 +13,7 @@ class Checkbox(BoolElement):
         """Checkbox Element
 
         :param text: the label to display next to the checkbox
-        :param value: set to `True` if it should be checked initally; default is `False`
+        :param value: whether it should be checked initally (default: `False`)
         :param on_change: callback to execute when value changes
         """
         view = jp.QCheckbox(text=text, input=self.handle_change)

+ 1 - 1
nicegui/elements/dialog.py

@@ -11,7 +11,7 @@ class Dialog(Group):
 
         Creates a modal dialog.
 
-        :param value: whether the dialog is already opened (default: False)
+        :param value: whether the dialog is already opened (default: `False`)
         """
         view = jp.QDialog(
             value=value,

+ 1 - 1
nicegui/elements/joystick.py

@@ -51,7 +51,7 @@ class Joystick(Element):
 
         Create a joystick based on `nipple.js <https://yoannmoi.net/nipplejs/>`_.
 
-        :param on_start: callback for when the user toches the joystick
+        :param on_start: callback for when the user touches the joystick
         :param on_move: callback for when the user moves the joystick
         :param on_end: callback for when the user releases the joystick
         :param options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_

+ 2 - 2
nicegui/elements/keyboard.py

@@ -30,8 +30,8 @@ class Keyboard(Element):
         Adds global keyboard event tracking.
 
         :param handle_keys: callback to be executed when keyboard events occur.
-        :param active: boolean flag indicating whether the callback should be executed or not (default: True)
-        :param repeating: boolean flag indicating whether held keys should be sent repeatedly (default: True)
+        :param active: boolean flag indicating whether the callback should be executed or not (default: `True`)
+        :param repeating: boolean flag indicating whether held keys should be sent repeatedly (default: `True`)
         """
         super().__init__(KeyboardView(on_key=self.handle_key, repeating=repeating))
         self.active = active

+ 1 - 1
nicegui/elements/line_plot.py

@@ -18,7 +18,7 @@ class LinePlot(Plot):
         :param n: number of lines
         :param limit: maximum number of datapoints per line (new points will displace the oldest)
         :param update_every: update plot only after pushing new data multiple times to save CPU and bandwidth
-        :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
+        :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later (default: `True`)
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         """
         super().__init__(close=close, **kwargs)

+ 1 - 1
nicegui/elements/log.js

@@ -17,7 +17,7 @@ Vue.component("log", {
       send_to_server(event, "event");
       clearInterval(connectInterval);
     };
-    connectInterval = setInterval(sendConnectEvent, 100);
+    const connectInterval = setInterval(sendConnectEvent, 100);
   },
   methods: {
     push(line) {

+ 1 - 1
nicegui/elements/log.py

@@ -35,7 +35,7 @@ class Log(Element):
 
         Create a log view that allows to add new lines without re-transmitting the whole history to the client.
 
-        :param max_lines: maximum number of lines before dropping oldest ones (default: None)
+        :param max_lines: maximum number of lines before dropping oldest ones (default: `None`)
         """
         self.lines = deque(maxlen=max_lines)
         super().__init__(LogView(lines=self.lines, max_lines=max_lines))

+ 1 - 1
nicegui/elements/menu.py

@@ -11,7 +11,7 @@ class Menu(Group):
 
         Creates a menu.
 
-        :param value: whether the menu is already opened (default: False)
+        :param value: whether the menu is already opened (default: `False`)
         """
         view = jp.QMenu(value=value)
 

+ 1 - 1
nicegui/elements/menu_item.py

@@ -19,7 +19,7 @@ class MenuItem(Element):
 
         :param text: label of the menu item
         :param on_click: callback to be executed when selecting the menu item
-        :param auto_close: whether the menu should be closed after a click event (default: True)
+        :param auto_close: whether the menu should be closed after a click event (default: `True`)
         """
         view = jp.QItem(text=text, clickable=True)
 

+ 1 - 1
nicegui/elements/notify.py

@@ -17,7 +17,7 @@ class Notify(Element):
 
         :param message: content of the notification
         :param position: position on the screen ("top-left", "top-right", "bottom-left","bottom-right, "top", "bottom", "left", "right" or "center", default: "bottom")
-        :param close_button: optional label of a button to dismiss the notification (default: None)
+        :param close_button: optional label of a button to dismiss the notification (default: `None`)
         """
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
 

+ 1 - 1
nicegui/elements/plot.py

@@ -13,7 +13,7 @@ class Plot(Element):
 
         Create a context to configure a `Matplotlib <https://matplotlib.org/>`_ plot.
 
-        :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later, default is `True`
+        :param close: whether the figure should be closed after exiting the context; set to `False` if you want to update it later (default: `True`)
         :param kwargs: arguments like `figsize` which should be passed to `pyplot.figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html>`_
         """
         self.close = close

+ 1 - 1
nicegui/elements/scene.js

@@ -98,7 +98,7 @@ Vue.component("scene", {
       send_to_server(event, "event");
       clearInterval(connectInterval);
     };
-    connectInterval = setInterval(sendConnectEvent, 100);
+    const connectInterval = setInterval(sendConnectEvent, 100);
   },
 
   updated() {},

+ 1 - 1
nicegui/elements/switch.py

@@ -13,7 +13,7 @@ class Switch(BoolElement):
         """Switch Element
 
         :param text: the label to display next to the switch
-        :param value: set to `True` if initally it should be active; default is `False`
+        :param value: whether it should be active initally (default: `False`)
         :param on_click: callback which is invoked when state is changed by the user
         """
         view = jp.QToggle(text=text, input=self.handle_change)

+ 1 - 1
nicegui/elements/upload.py

@@ -15,7 +15,7 @@ class Upload(Element):
                  ):
         """File Upload Element
 
-        :param multiple: allow uploading multiple files at once (default: False)
+        :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.upload_handler = on_upload

+ 5 - 0
nicegui/events.py

@@ -18,6 +18,11 @@ class EventArguments(BaseModel):
 class ClickEventArguments(EventArguments):
     pass
 
+class MouseEventArguments(EventArguments):
+    type: str
+    image_x: float
+    image_y: float
+
 class UploadEventArguments(EventArguments):
     files: List[bytes]
 

+ 0 - 1
nicegui/nicegui.py

@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 from typing import Awaitable, Callable
-import asyncio
 
 from .ui import Ui  # NOTE: before justpy
 import justpy as jp

+ 1 - 1
nicegui/timer.py

@@ -25,7 +25,7 @@ class Timer:
         :param interval: the interval in which the timer is called
         :param callback: function or coroutine to execute when interval elapses (can return `False` to prevent view update)
         :param active: whether the callback should be executed or not
-        :param once: whether the callback is only executed once after a delay specified by `interval`; default is `False`
+        :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
         """
 
         parent = view_stack[-1]

+ 1 - 0
nicegui/ui.py

@@ -2,6 +2,7 @@ class Ui:
     from .config import config  # NOTE: before run
     from .run import run  # NOTE: before justpy
 
+    from .elements.annotation_tool import AnnotationTool as annotation_tool
     from .elements.button import Button as button
     from .elements.checkbox import Checkbox as checkbox
     from .elements.colors import Colors as colors