Jelajahi Sumber

Merge pull request #12 from zauberzeug/feature/clicky_image

add ui.anntation_tool element with a click handler that provides image coordinates
Rodja Trappe 3 tahun lalu
induk
melakukan
e15c7c64c5
5 mengubah file dengan 129 tambahan dan 0 penghapusan
  1. 12 0
      main.py
  2. 60 0
      nicegui/elements/annotation_tool.js
  3. 51 0
      nicegui/elements/annotation_tool.py
  4. 5 0
      nicegui/events.py
  5. 1 0
      nicegui/ui.py

+ 12 - 0
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).')
 

+ 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

+ 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]
 

+ 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