Falko Schindler 2 лет назад
Родитель
Сommit
3fb63059e7

+ 2 - 2
api_docs_and_examples.py

@@ -258,13 +258,13 @@ To overlay an SVG, make the `viewBox` exactly the size of the image and provide
                 </svg>'''
                 </svg>'''
             ui.html(content).style('background:transparent')
             ui.html(content).style('background:transparent')
 
 
-    # @example(ui.interactive_image)
+    @example(ui.interactive_image, skip=False)
     def interactive_image_example():
     def interactive_image_example():
         from nicegui.events import MouseEventArguments
         from nicegui.events import MouseEventArguments
 
 
         def mouse_handler(e: MouseEventArguments):
         def mouse_handler(e: MouseEventArguments):
             color = 'SkyBlue' if e.type == 'mousedown' else 'SteelBlue'
             color = 'SkyBlue' if e.type == 'mousedown' else 'SteelBlue'
-            ii.svg_content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="20" fill="{color}"/>'
+            ii.content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="20" fill="{color}"/>'
             ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
             ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
 
 
         src = 'https://cdn.stocksnap.io/img-thumbs/960w/corn-cob_YSZZZEC59W.jpg'
         src = 'https://cdn.stocksnap.io/img-thumbs/960w/corn-cob_YSZZZEC59W.jpg'

+ 29 - 37
nicegui/elements/old/interactive_image.js → nicegui/elements/interactive_image.js

@@ -1,19 +1,23 @@
-Vue.component("interactive_image", {
+export default {
   template: `
   template: `
-    <div :id="jp_props.id" style="position:relative" :style="jp_props.style" :class="jp_props.classes">
-      <img style="width:100%; height:100%">
+    <div style="position:relative">
+      <img style="width:100%; height:100%" />
       <svg style="position:absolute;top:0;left:0;pointer-events:none">
       <svg style="position:absolute;top:0;left:0;pointer-events:none">
         <g style="display:none">
         <g style="display:none">
           <line x1="100" y1="0" x2="100" y2="100%" stroke="black" />
           <line x1="100" y1="0" x2="100" y2="100%" stroke="black" />
           <line x1="0" y1="100" x2="100%" y2="100" stroke="black" />
           <line x1="0" y1="100" x2="100%" y2="100" stroke="black" />
         </g>
         </g>
-        <g v-html="jp_props.options.svg_content"></g>
+        <g v-html="content"></g>
       </svg>
       </svg>
     </div>
     </div>
   `,
   `,
+  data() {
+    return {
+      content: "",
+    };
+  },
   mounted() {
   mounted() {
-    comp_dict[this.$props.jp_props.id] = this;
-    this.image = document.getElementById(this.$props.jp_props.id).firstChild;
+    this.image = this.$el.firstChild;
     const handle_completion = () => {
     const handle_completion = () => {
       if (this.waiting_source) {
       if (this.waiting_source) {
         this.image.src = this.waiting_source;
         this.image.src = this.waiting_source;
@@ -24,10 +28,10 @@ Vue.component("interactive_image", {
     };
     };
     this.image.addEventListener("load", handle_completion);
     this.image.addEventListener("load", handle_completion);
     this.image.addEventListener("error", handle_completion);
     this.image.addEventListener("error", handle_completion);
-    const svg = document.getElementById(this.$props.jp_props.id).lastChild;
-    const cross = svg.firstChild;
+    this.svg = this.$el.lastChild;
+    const cross = this.svg.firstChild;
     this.image.ondragstart = () => false;
     this.image.ondragstart = () => false;
-    if (this.$props.jp_props.options.cross) {
+    if (this.cross) {
       this.image.style.cursor = "none";
       this.image.style.cursor = "none";
       this.image.addEventListener("mouseenter", (e) => {
       this.image.addEventListener("mouseenter", (e) => {
         cross.style.display = "block";
         cross.style.display = "block";
@@ -46,46 +50,29 @@ Vue.component("interactive_image", {
     }
     }
     this.image.onload = (e) => {
     this.image.onload = (e) => {
       const viewBox = `0 0 ${this.image.naturalWidth} ${this.image.naturalHeight}`;
       const viewBox = `0 0 ${this.image.naturalWidth} ${this.image.naturalHeight}`;
-      svg.setAttribute("viewBox", viewBox);
+      this.svg.setAttribute("viewBox", viewBox);
     };
     };
-    this.image.src = this.$props.jp_props.options.source;
-    for (const type of this.$props.jp_props.options.events) {
+    this.image.src = this.src;
+    for (const type of this.events) {
       this.image.addEventListener(type, (e) => {
       this.image.addEventListener(type, (e) => {
-        const event = {
-          event_type: "onMouse",
+        this.$emit("mouse", {
           mouse_event_type: type,
           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_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
           image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
           image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
-        };
-        send_to_server(event, "event");
+        });
       });
       });
     }
     }
 
 
+    this.is_initialized = false;
     const sendConnectEvent = () => {
     const sendConnectEvent = () => {
-      if (websocket_id === "") return;
-      const event = {
-        event_type: "onConnect",
-        vue_type: this.$props.jp_props.vue_type,
-        id: this.$props.jp_props.id,
-        page_id: page_id,
-        websocket_id: websocket_id,
-      };
-      send_to_server(event, "event");
-      clearInterval(connectInterval);
+      if (!this.is_initialized) this.$emit("connect");
+      else clearInterval(connectInterval);
     };
     };
     const connectInterval = setInterval(sendConnectEvent, 100);
     const connectInterval = setInterval(sendConnectEvent, 100);
   },
   },
-  updated() {
-    if (this.image.src != this.$props.jp_props.options.source) {
-      this.image.src = this.$props.jp_props.options.source;
-    }
-  },
   methods: {
   methods: {
     set_source(source) {
     set_source(source) {
+      this.is_initialized = true;
       if (this.loading) {
       if (this.loading) {
         this.waiting_source = source;
         this.waiting_source = source;
         return;
         return;
@@ -93,8 +80,13 @@ Vue.component("interactive_image", {
       this.loading = true;
       this.loading = true;
       this.image.src = source;
       this.image.src = source;
     },
     },
+    set_content(content) {
+      this.content = content;
+    },
   },
   },
   props: {
   props: {
-    jp_props: Object,
+    src: String,
+    events: Array,
+    cross: Boolean,
   },
   },
-});
+};

+ 52 - 0
nicegui/elements/interactive_image.py

@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from typing import Callable, Dict, List, Optional
+
+from ..events import MouseEventArguments, handle_event
+from ..vue import register_component
+from .mixins.content_element import ContentElement
+from .mixins.source_element import SourceElement
+
+register_component('interactive_image', __file__, 'interactive_image.js')
+
+
+class InteractiveImage(SourceElement, ContentElement):
+
+    def __init__(self, source: str = '', *,
+                 on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
+        """Interactive Image
+
+        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`)
+        """
+        super().__init__(tag='interactive_image', source=source, content='')
+        self._props['events'] = events
+        self._props['cross'] = cross
+
+        def handle_connect(_) -> None:
+            self.run_method('set_source', self.source)
+            self.run_method('set_content', self.content)
+        self.on('connect', handle_connect)
+
+        def handle_mouse(msg: Dict) -> None:
+            if on_mouse is None:
+                return
+            arguments = MouseEventArguments(
+                sender=self,
+                client=self.client,
+                type=msg['args'].get('mouse_event_type'),
+                image_x=msg['args'].get('image_x'),
+                image_y=msg['args'].get('image_y'),
+            )
+            return handle_event(on_mouse, arguments)
+        self.on('mouse', handle_mouse, ['*'])
+
+    def on_source_change(self, source: str) -> None:
+        self.run_method('set_source', source)
+
+    def on_content_change(self, content: str) -> None:
+        self.run_method('set_content', content)

+ 0 - 90
nicegui/elements/old/interactive_image.py

@@ -1,90 +0,0 @@
-from __future__ import annotations
-
-import traceback
-from typing import Any, Callable, Dict, List, Optional
-
-from justpy import WebPage
-
-from ..binding import BindableProperty, BindSourceMixin
-from ..events import MouseEventArguments, handle_event
-from ..routes import add_dependencies
-from .custom_view import CustomView
-from .element import Element
-
-add_dependencies(__file__)
-
-
-class InteractiveImageView(CustomView):
-
-    def __init__(self, source: str, on_mouse: Callable, events: List[str], cross: bool):
-        super().__init__('interactive_image', source=source, events=events, cross=cross, svg_content='')
-        self.allowed_events = ['onMouse', 'onConnect']
-        self.initialize(onMouse=on_mouse, onConnect=self.on_connect)
-        self.sockets = []
-
-    def on_connect(self, msg):
-        self.prune_sockets()
-        self.sockets.append(msg.websocket)
-
-    def prune_sockets(self):
-        page_sockets = [s for page_id in self.pages for s in WebPage.sockets.get(page_id, {}).values()]
-        self.sockets = [s for s in self.sockets if s in page_sockets]
-
-
-def _handle_source_change(sender: Element, source: str) -> None:
-    sender.view.options.source = source
-    sender.update()
-
-
-class InteractiveImage(Element, BindSourceMixin):
-    source = BindableProperty(on_change=_handle_source_change)
-
-    def __init__(self, source: str = '', *,
-                 on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False):
-        """Interactive Image
-
-        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__(InteractiveImageView(source, self.handle_mouse, events, cross))
-
-        self.source = source
-
-    def handle_mouse(self, msg: Dict[str, Any]) -> Optional[bool]:
-        if self.mouse_handler is None:
-            return False
-        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'),
-            )
-            return handle_event(self.mouse_handler, arguments)
-        except:
-            traceback.print_exc()
-
-    async def set_source(self, source: str) -> None:
-        self.view.options.source = source
-        self.view.prune_sockets()
-        for socket in self.view.sockets:
-            await self.view.run_method(f'set_source("{source}")', socket)
-
-    @property
-    def svg_content(self) -> str:
-        return self.view.options.svg_content
-
-    @svg_content.setter
-    def svg_content(self, content: str):
-        if self.view.options.svg_content != content:
-            self.view.options.svg_content = content
-            self.update()
-
-    def set_svg_content(self, svg_content: str) -> None:
-        self.svg_content = svg_content

+ 1 - 0
nicegui/ui.py

@@ -12,6 +12,7 @@ from .elements.html import Html as html
 from .elements.icon import Icon as icon
 from .elements.icon import Icon as icon
 from .elements.image import Image as image
 from .elements.image import Image as image
 from .elements.input import Input as input
 from .elements.input import Input as input
+from .elements.interactive_image import InteractiveImage as interactive_image
 from .elements.joystick import Joystick as joystick
 from .elements.joystick import Joystick as joystick
 from .elements.keyboard import Keyboard as keyboard
 from .elements.keyboard import Keyboard as keyboard
 from .elements.label import Label as label
 from .elements.label import Label as label