{ "cells": [ { "cell_type": "markdown", "id": "9b95a8a0", "metadata": {}, "source": [ "# Companion plugin — minimal example\n", "\n", "This notebook mirrors **`plugin_action_example.ipynb`** but uses `AbstractCompanion`\n", "instead of calling `viewer.register_action()` directly.\n", "\n", "## What changes compared to the raw API?\n", "\n", "| Raw API | Companion API |\n", "|---|---|\n", "| `viewer.register_action('id', rdown_handler=fn)` | `self._register_action(self._action_id('x'), rdown=self._fn)` |\n", "| `viewer.start_action('id', 'hint')` | `self._start_action(self._action_id('x'), 'hint')` |\n", "| `viewer.end_action(...)` | `self._end_action()` |\n", "| `viewer.unregister_action('id')` | `self.destroy()` — unregisters **all** actions at once |\n", "| `wx.MessageBox(...)` | `self._show_info(...)` / `self._confirm(...)` |\n", "\n", "The companion also:\n", "- **auto-namespaces** every action id (`'{classname}.{local_id}'`) to avoid collisions\n", "- exposes all captured data as plain instance attributes (accessible from the notebook)\n", "- builds the menu declaratively — **no `import wx`** needed in your code\n", "\n", "## Workflow\n", "1. Start wx and create the viewer\n", "2. Define one companion class (state + menu + handlers, all in one place)\n", "3. Instantiate the companion and attach its menu with `menu_build()`\n", "4. Use the menu entries — or call `companion.start()` from the notebook\n", "5. Call `companion.destroy()` to clean up everything at once" ] }, { "cell_type": "markdown", "id": "b2972bf9", "metadata": {}, "source": [ "## 1 — wx startup\n", "\n", "> Always use `%gui wx` in a notebook — never `wx.App()`." ] }, { "cell_type": "code", "execution_count": 1, "id": "d61d1194", "metadata": {}, "outputs": [], "source": [ "import sys\n", "%gui wx" ] }, { "cell_type": "markdown", "id": "b3773124", "metadata": {}, "source": [ "## 2 — Create the MapViewer" ] }, { "cell_type": "code", "execution_count": 2, "id": "630f92b1", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Importing wolfhece modules\n", "INFO:root:wolfhece modules imported\n" ] }, { "data": { "text/plain": [ "False" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from wolfhece.PyDraw import WolfMapViewer\n", "\n", "viewer = WolfMapViewer(None, title=\"Companion — minimal demo\", w=1200, h=800)\n", "viewer.Show()" ] }, { "cell_type": "markdown", "id": "7fb4aff1", "metadata": {}, "source": [ "## 3 — The companion class\n", "\n", "Everything is in **one place**: the list of collected points, the menu,\n", "and all the event handlers.\n", "\n", "### Companion anatomy\n", "\n", "```\n", "class MyCaptureCompanion(AbstractCompanion):\n", "\n", " def __init__(self, viewer): ← constructor: declare state\n", " ...\n", "\n", " def menu_build(self): ← declare the menu and register actions\n", " ... (optional — omit for menu-less companions)\n", "\n", " def start(self) / def stop(self): ← public API — called from the notebook\n", " ... (use self._start_action / _end_action internally)\n", "\n", " def _on_...(self, event): ← menu-item callbacks (wx event, private)\n", " ...\n", "\n", " def _rdown_...(self, viewer, ctx): ← mouse handler (viewer, MouseContext, private)\n", " ...\n", "```\n", "\n", "The `AbstractCompanion` base class provides **protected** building blocks\n", "for use *inside* the companion:\n", "\n", "- `self._action_id('name')` → `'capturcompanion.name'` (namespaced, collision-free)\n", "- `self._register_action(...)` / `self.destroy()` — registration + cleanup\n", "- `self._start_action(...)` / `self._end_action()`\n", "- `self._set_status(msg)` — write to the viewer's status bar\n", "- `self._show_info(msg)` / `self._confirm(msg)` — dialog boxes without wx\n", "\n", "The companion's **public** API (no underscore) is what notebook cells call." ] }, { "cell_type": "code", "execution_count": null, "id": "d0960017", "metadata": {}, "outputs": [], "source": [ "from wolfhece._menu_companion_abc import (\n", " AbstractCompanion,\n", " ActionItem,\n", " SEPARATOR,\n", ")\n", "from wolfhece._viewer_plugin_handlers import MouseContext\n", "\n", "\n", "class CaptureCompanion(AbstractCompanion):\n", " \"\"\"Records right-click positions and exposes them as `self.points`.\"\"\"\n", "\n", " # ---------------------------------------------------------------\n", " # 1 — State: declare every piece of data your companion needs.\n", " # ---------------------------------------------------------------\n", " def __init__(self, viewer):\n", " super().__init__(viewer, namespace='capture')\n", " #: Collected (x, y) world-coordinate pairs — read from the notebook.\n", " self.points: list[tuple[float, float]] = []\n", "\n", " # ---------------------------------------------------------------\n", " # 2 — Menu: declare entries with _build_menu (no wx import needed)\n", " # then register the action handlers.\n", " # ---------------------------------------------------------------\n", " def menu_build(self) -> None:\n", " self._build_menu('Capture', [\n", " ActionItem('Start capture', self._on_start, 'Right-click to record points'),\n", " ActionItem('Stop capture', self._on_stop, 'End the capture action'),\n", " SEPARATOR,\n", " ActionItem('Show points', self._on_show, 'Display collected points'),\n", " ActionItem('Clear points', self._on_clear, 'Delete all collected points'),\n", " ])\n", " # Register the rdown handler — the id is auto-namespaced ('capture.record')\n", " self._register_action(\n", " self._action_id('record'),\n", " rdown=self._rdown,\n", " )\n", "\n", " # ---------------------------------------------------------------\n", " # 3 — Public API — callable directly from the notebook.\n", " # These are the only methods notebook cells should call.\n", " # ---------------------------------------------------------------\n", " def start(self, live_coords: bool = False) -> None:\n", " \"\"\"Begin recording right-click positions.\n", "\n", " Parameters\n", " ----------\n", " live_coords:\n", " When ``True`` the viewer's status bar continuously shows the\n", " current mouse coordinates while the cursor moves over the map.\n", " \"\"\"\n", " if live_coords:\n", " # Re-register the action with the extra motion hook.\n", " self._unregister_action(self._action_id('record'))\n", " self._register_action(\n", " self._action_id('record'),\n", " rdown=self._rdown,\n", " motion=self._motion_coords,\n", " )\n", " self._start_action(\n", " self._action_id('record'),\n", " 'Right-click on the map to capture positions…',\n", " )\n", "\n", " def stop(self) -> None:\n", " \"\"\"Stop recording and print a summary.\"\"\"\n", " self._end_action()\n", " print(f\"{len(self.points)} point(s) recorded.\")\n", "\n", " # ---------------------------------------------------------------\n", " # 4 — Menu-item callbacks (signature: (self, event) → None)\n", " # Private — called by wx, not by notebook cells.\n", " # ---------------------------------------------------------------\n", " def _on_start(self, _event):\n", " self.start()\n", "\n", " def _on_stop(self, _event):\n", " self.stop()\n", "\n", " def _on_show(self, _event):\n", " if not self.points:\n", " self._show_info(\"No points captured yet.\", title=\"Points\")\n", " return\n", " lines = \"\\n\".join(\n", " f\" {i+1:3d}. X={x:.3f} Y={y:.3f}\"\n", " for i, (x, y) in enumerate(self.points)\n", " )\n", " self._show_info(f\"{len(self.points)} point(s):\\n{lines}\", title=\"Captured points\")\n", "\n", " def _on_clear(self, _event):\n", " if not self.points:\n", " self._show_info(\"Nothing to clear.\", title=\"Capture\")\n", " return\n", " if self._confirm(f\"Delete {len(self.points)} point(s)?\", default='no'):\n", " self.points.clear()\n", " print(\"All points cleared.\")\n", "\n", " # ---------------------------------------------------------------\n", " # 5 — Mouse handlers (signature: (self, viewer, ctx) → None)\n", " # Private — called by the viewer's dispatch, not by notebook cells.\n", " # ---------------------------------------------------------------\n", " def _rdown(self, viewer, ctx: MouseContext) -> None:\n", " \"\"\"Called on every right-click while the action is active.\"\"\"\n", " self.points.append((ctx.x_snap, ctx.y_snap))\n", " n = len(self.points)\n", " print(f\"[Capture #{n}] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}\")\n", " if ctx.shift:\n", " print(\" → Shift held: point flagged as reference\")\n", "\n", " def _motion_coords(self, viewer, ctx: MouseContext) -> None:\n", " \"\"\"Updates the status bar with the live cursor position.\"\"\"\n", " self._set_status(f\"X={ctx.x:.2f} Y={ctx.y:.2f}\")" ] }, { "cell_type": "markdown", "id": "663b003b", "metadata": {}, "source": [ "## 4 — Instantiate and attach the menu" ] }, { "cell_type": "code", "execution_count": 4, "id": "1b9fb7fe", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "companion = CaptureCompanion(viewer)\n", "companion.menu_build() # adds 'Capture' menu to the viewer menu bar\n", "\n", "print(companion) # shows namespace, menu status, registered actions" ] }, { "cell_type": "markdown", "id": "d7794a7f", "metadata": {}, "source": [ "## 5 — Activate from the notebook (alternative to using the menu)\n", "\n", "You can also start/stop the action programmatically — useful for scripting." ] }, { "cell_type": "code", "execution_count": null, "id": "55b0c961", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : Right-click on the map to capture positions…\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Action active — right-click on the map.\n" ] } ], "source": [ "companion.start()\n", "print(\"Action active — right-click on the map.\")" ] }, { "cell_type": "markdown", "id": "3a3eef8f", "metadata": {}, "source": [ "## 6 — Inspect the collected points" ] }, { "cell_type": "code", "execution_count": 7, "id": "0c2bb3cf", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3 point(s) captured:\n", " 1. X=13.236 Y=20.826\n", " 2. X=30.812 Y=27.483\n", " 3. X=32.037 Y=18.802\n" ] } ], "source": [ "# Run this cell at any time to see what has been captured.\n", "print(f\"{len(companion.points)} point(s) captured:\")\n", "for i, (x, y) in enumerate(companion.points, 1):\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}\")" ] }, { "cell_type": "markdown", "id": "4801f738", "metadata": {}, "source": [ "## 7 — Live coordinate display\n", "\n", "Pass `live_coords=True` to `start()` to add a motion handler that\n", "continuously refreshes the viewer's status bar with the cursor position.\n", "\n", "The `_motion_coords` handler is defined **inside** the companion class\n", "and uses `self._set_status(msg)` — a helper provided by the base class\n", "that writes to the status bar without needing the viewer reference directly." ] }, { "cell_type": "code", "execution_count": null, "id": "d6c6cc6f", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : \n", "INFO:root:ACTION : Right-click to capture; mouse movement updates the status bar\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Action with live preview active.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[Capture #4] X=17.390 Y=25.033\n", "[Capture #5] X=23.675 Y=20.027\n" ] } ], "source": [ "companion.start(live_coords=True)\n", "print(\"Action with live preview active.\")" ] }, { "cell_type": "markdown", "id": "7dd6b851", "metadata": {}, "source": [ "## 8 — Cleanup" ] }, { "cell_type": "code", "execution_count": null, "id": "25add228", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Companion destroyed — all actions unregistered.\n", "\n" ] } ], "source": [ "# stop() ends the current action.\n", "# destroy() unregisters every action the companion registered — no need\n", "# to track individual ids.\n", "companion.stop()\n", "companion.destroy()\n", "\n", "print(\"Companion destroyed — all actions unregistered.\")\n", "print(companion)" ] }, { "cell_type": "markdown", "id": "608b14f4", "metadata": {}, "source": [ "## Summary — companion vs raw API\n", "\n", "```python\n", "# ── RAW API ──────────────────────────────────────────────────────────────\n", "def _rdown_capture_position(v, ctx):\n", " clicked_points.append((ctx.x, ctx.y))\n", " print(f\"[#{len(clicked_points)}] X={ctx.x:.3f}\")\n", "\n", "viewer.register_action('capture position', rdown_handler=_rdown_capture_position)\n", "viewer.start_action('capture position', 'Right-click…')\n", "# … later:\n", "viewer.end_action('done')\n", "viewer.unregister_action('capture position')\n", "\n", "# ── COMPANION ─────────────────────────────────────────────────────────────\n", "class CaptureCompanion(AbstractCompanion):\n", " def __init__(self, viewer):\n", " super().__init__(viewer, namespace='capture')\n", " self.points = []\n", "\n", " def menu_build(self):\n", " self._build_menu('Capture', [\n", " ActionItem('Start', self._on_start),\n", " ActionItem('Stop', self._on_stop),\n", " ])\n", " self._register_action(self._action_id('record'), rdown=self._rdown)\n", "\n", " # Public API — what the notebook calls (no underscore):\n", " def start(self): self._start_action(self._action_id('record'), '…')\n", " def stop(self): self._end_action()\n", "\n", " # Private — called internally by wx or the viewer dispatch:\n", " def _on_start(self, _e): self.start()\n", " def _on_stop(self, _e): self.stop()\n", " def _rdown(self, v, ctx):\n", " self.points.append((ctx.x, ctx.y))\n", " print(f\"[#{len(self.points)}] X={ctx.x:.3f}\")\n", "\n", "companion = CaptureCompanion(viewer)\n", "companion.menu_build()\n", "\n", "companion.start() # ← public, no underscore\n", "# … later:\n", "companion.stop() # ← public, no underscore\n", "companion.destroy() # ← unregisters everything\n", "```\n", "\n", "### Key advantages\n", "- State (`self.points`) lives inside the companion — no global variables\n", "- Automatic namespace → no id collisions with other plugins\n", "- `destroy()` cleans up all actions at once\n", "- Dialog helpers (`_show_info`, `_confirm`, …) replace manual `wx` calls\n", "- `_build_menu` builds the menu without any `import wx`\n", "- **Public API** (`start`, `stop`, `destroy`) — no `_` methods visible to notebook users" ] } ], "metadata": { "kernelspec": { "display_name": "python311", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 5 }