interactive_image.py 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. from __future__ import annotations
  2. import traceback
  3. from typing import Any, Callable, Dict, List, Optional
  4. from justpy import WebPage
  5. from ..binding import BindableProperty, BindSourceMixin
  6. from ..events import MouseEventArguments, handle_event
  7. from ..routes import add_dependencies
  8. from .custom_view import CustomView
  9. from .element import Element
  10. add_dependencies(__file__)
  11. class InteractiveImageView(CustomView):
  12. def __init__(self, source: str, on_mouse: Callable, events: List[str], cross: bool):
  13. super().__init__('interactive_image', source=source, events=events, cross=cross, svg_content='')
  14. self.allowed_events = ['onMouse', 'onConnect']
  15. self.initialize(onMouse=on_mouse, onConnect=self.on_connect)
  16. self.sockets = []
  17. def on_connect(self, msg):
  18. self.prune_sockets()
  19. self.sockets.append(msg.websocket)
  20. def prune_sockets(self):
  21. page_sockets = [s for page_id in self.pages for s in WebPage.sockets.get(page_id, {}).values()]
  22. self.sockets = [s for s in self.sockets if s in page_sockets]
  23. def _handle_source_change(sender: Element, source: str) -> None:
  24. sender.view.options.source = source
  25. sender.update()
  26. class InteractiveImage(Element, BindSourceMixin):
  27. source = BindableProperty(on_change=_handle_source_change)
  28. def __init__(self, source: str = '', *,
  29. on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False):
  30. """Interactive Image
  31. Create an image with an SVG overlay that handles mouse events and yields image coordinates.
  32. :param source: the source of the image; can be an URL or a base64 string
  33. :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
  34. :param events: list of JavaScript events to subscribe to (default: `['click']`)
  35. :param cross: whether to show crosshairs (default: `False`)
  36. """
  37. self.mouse_handler = on_mouse
  38. super().__init__(InteractiveImageView(source, self.handle_mouse, events, cross))
  39. self.source = source
  40. def handle_mouse(self, msg: Dict[str, Any]) -> Optional[bool]:
  41. if self.mouse_handler is None:
  42. return False
  43. try:
  44. arguments = MouseEventArguments(
  45. sender=self,
  46. socket=msg.get('websocket'),
  47. type=msg.get('mouse_event_type'),
  48. image_x=msg.get('image_x'),
  49. image_y=msg.get('image_y'),
  50. )
  51. return handle_event(self.mouse_handler, arguments)
  52. except:
  53. traceback.print_exc()
  54. async def set_source(self, source: str):
  55. self.view.options.source = source
  56. self.view.prune_sockets()
  57. for socket in self.view.sockets:
  58. await self.view.run_method(f'set_source("{source}")', socket)
  59. @property
  60. def svg_content(self) -> str:
  61. return self.view.options.svg_content
  62. @svg_content.setter
  63. def svg_content(self, content: str):
  64. self.view.options.svg_content = content