{ "cells": [ { "cell_type": "markdown", "id": "b44d047a", "metadata": {}, "source": [ "# Companion plugin — overload and restore\n", "\n", "This notebook mirrors **`plugin_action_overload.ipynb`** using `AbstractCompanion`.\n", "\n", "## The overload scenario\n", "\n", "Sometimes a second plugin needs to **temporarily replace** a handler that a first\n", "plugin already owns — for example, adding a diagnostic layer on top of a\n", "production handler. The viewer supports this through the `overload=True` flag.\n", "\n", "With companions, the pattern maps cleanly:\n", "\n", "| Step | What happens |\n", "|---|---|\n", "| `base.menu_build()` | Registers `'demo.click'` → `_rdown_base` |\n", "| `diag.menu_build()` | Registers same id with `overload=True` → `_rdown_diag` installed, `_rdown_base` saved |\n", "| `diag.destroy()` | Unregisters `'demo.click'` → **`_rdown_base` automatically restored** |\n", "\n", "The saving and restoration are handled entirely by the viewer internals —\n", "neither companion needs to know about the other.\n", "\n", "## Overload rules (same as raw API)\n", "\n", "| Scenario | `overload=` | Result |\n", "|---|---|---|\n", "| New action | `False` | Silent registration |\n", "| Occupied slot | `False` | `WARNING` — previous handler lost |\n", "| Occupied slot | `True` | `WARNING` + previous handler **saved** — restored on `unregister` |" ] }, { "cell_type": "markdown", "id": "b070bbc9", "metadata": {}, "source": [ "## 1 — wx startup" ] }, { "cell_type": "code", "execution_count": null, "id": "62cf7b45", "metadata": {}, "outputs": [], "source": [ "import logging, sys\n", "logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')\n", "%gui wx" ] }, { "cell_type": "markdown", "id": "088ddf73", "metadata": {}, "source": [ "## 2 — Viewer" ] }, { "cell_type": "code", "execution_count": null, "id": "7c4cede3", "metadata": {}, "outputs": [], "source": [ "from wolfhece.PyDraw import WolfMapViewer\n", "\n", "viewer = WolfMapViewer(None, title=\"Companion — overload demo\", w=1200, h=800)\n", "viewer.Show()" ] }, { "cell_type": "markdown", "id": "4ef35e49", "metadata": {}, "source": [ "## 3 — Base companion (permanent handler)\n", "\n", "The `DemoCompanion` registers a simple right-click handler under the id\n", "`'demo.click'`. This is the *production* handler that should survive overloads." ] }, { "cell_type": "code", "execution_count": null, "id": "6c4fb066", "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 DemoCompanion(AbstractCompanion):\n", " \"\"\"Base companion — registers the production rdown handler.\"\"\"\n", "\n", " def __init__(self, viewer):\n", " super().__init__(viewer, namespace='demo')\n", "\n", " def menu_build(self) -> None:\n", " self._build_menu('Demo', [\n", " ActionItem('Activate', self._on_activate),\n", " ActionItem('Stop', self._on_stop),\n", " ])\n", " # Standard registration — no overload.\n", " self._register_action(self._action_id('click'), rdown=self._rdown_v1)\n", "\n", " # Public API\n", " def start(self) -> None:\n", " \"\"\"Activate the v1 handler.\"\"\"\n", " self._start_action(self._action_id('click'), 'Right-click → version 1')\n", "\n", " def stop(self) -> None:\n", " \"\"\"Deactivate the action.\"\"\"\n", " self._end_action()\n", "\n", " # Menu callbacks (private — delegate to public methods)\n", " def _on_activate(self, _event) -> None:\n", " self.start()\n", "\n", " def _on_stop(self, _event) -> None:\n", " self.stop()\n", "\n", " def _rdown_v1(self, viewer, ctx: MouseContext) -> None:\n", " print(f\"[v1] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}\")\n", "\n", "\n", "base = DemoCompanion(viewer)\n", "base.menu_build()\n", "base.start() # activate immediately\n", "\n", "print(base)\n", "print(\"Active handler:\", viewer._custom_rdown_handlers.get('demo.click').__name__)\n", "print(\"_saved_handlers:\", viewer._saved_handlers)" ] }, { "cell_type": "markdown", "id": "9c01084c", "metadata": {}, "source": [ "## 4 — Diagnostic companion (temporary overload)\n", "\n", "`DiagCompanion` registers on the **same action id** with `overload=True`.\n", "\n", "- The viewer emits a `WARNING` and **saves** the `v1` handler.\n", "- `v2` becomes active.\n", "- When `diag.destroy()` is called, `v1` is **automatically restored**." ] }, { "cell_type": "code", "execution_count": null, "id": "f80ecc6a", "metadata": {}, "outputs": [], "source": [ "class DiagCompanion(AbstractCompanion):\n", " \"\"\"Temporary diagnostic companion that overloads the base handler.\"\"\"\n", "\n", " def __init__(self, viewer):\n", " super().__init__(viewer, namespace='diag')\n", "\n", " def menu_build(self) -> None:\n", " self._build_menu('Diagnostics', [\n", " ActionItem('Install diagnostic handler', self._on_install),\n", " ActionItem('Remove diagnostic handler', self._on_remove),\n", " ])\n", "\n", " # Public API\n", " def start(self) -> None:\n", " \"\"\"Install the diagnostic handler (alias for install()).\"\"\"\n", " self.install()\n", "\n", " def stop(self) -> None:\n", " \"\"\"Remove the diagnostic handler (alias for remove()).\"\"\"\n", " self.remove()\n", "\n", " def install(self) -> None:\n", " \"\"\"Register the diagnostic handler on top of any existing one.\"\"\"\n", " # overload=True saves the displaced handler and restores it on destroy().\n", " self._register_action(\n", " 'demo.click', # same id as the base companion\n", " rdown=self._rdown_v2,\n", " overload=True,\n", " )\n", " print(\"Diagnostic handler installed.\")\n", "\n", " def remove(self) -> None:\n", " \"\"\"Unregister the diagnostic handler and restore the base handler.\"\"\"\n", " self._unregister_action('demo.click')\n", " print(\"Diagnostic handler removed — base handler restored.\")\n", "\n", " # Menu callbacks (private — delegate to public methods)\n", " def _on_install(self, _event) -> None:\n", " self.install()\n", "\n", " def _on_remove(self, _event) -> None:\n", " self.remove()\n", "\n", " def _rdown_v2(self, viewer, ctx: MouseContext) -> None:\n", " print(f\"[v2 DIAG] pixel=({ctx.x_pixel},{ctx.y_pixel}) \"\n", " f\"world=({ctx.x:.3f},{ctx.y:.3f})\")\n", "\n", "\n", "diag = DiagCompanion(viewer)\n", "diag.menu_build()\n", "\n", "print(diag)" ] }, { "cell_type": "markdown", "id": "5593bad8", "metadata": {}, "source": [ "## 5 — Install the diagnostic handler" ] }, { "cell_type": "code", "execution_count": null, "id": "6bd5a371", "metadata": {}, "outputs": [], "source": [ "diag.install()\n", "\n", "print(\"\\nActive handler:\",\n", " viewer._custom_rdown_handlers.get('demo.click').__name__)\n", "saved = viewer._saved_handlers.get('demo.click', {})\n", "print(\"Saved rdown:\",\n", " saved.get('rdown').__name__ if saved.get('rdown') else '')" ] }, { "cell_type": "markdown", "id": "132a06ba", "metadata": {}, "source": [ "## 6 — Live check\n", "\n", "Do a few **right-clicks** in the viewer window. \n", "You should see `[v2 DIAG]` messages — the diagnostic version is active." ] }, { "cell_type": "code", "execution_count": null, "id": "38e6abe7", "metadata": {}, "outputs": [], "source": [ "print(\"Current action:\", viewer.action)\n", "print(\"→ Right-click in the viewer window to see [v2 DIAG]\")" ] }, { "cell_type": "markdown", "id": "123ae0d7", "metadata": {}, "source": [ "## 7 — Remove the diagnostic handler (restores the base)\n", "\n", "`diag.destroy()` calls `unregister_action('demo.click')` internally,\n", "which triggers the viewer to restore the saved `_rdown_v1`." ] }, { "cell_type": "code", "execution_count": null, "id": "2e9e3115", "metadata": {}, "outputs": [], "source": [ "# Option A — remove only the overloaded action (keeps the diag companion alive):\n", "diag.remove()\n", "\n", "current = viewer._custom_rdown_handlers.get('demo.click')\n", "print(\"Active handler after removal:\",\n", " current.__name__ if current else '')\n", "print(\"_saved_handlers:\", viewer._saved_handlers) # must be empty again\n", "\n", "# v1 is active again — right-click to verify\n", "base.start()" ] }, { "cell_type": "markdown", "id": "a41846d3", "metadata": {}, "source": [ "## 8 — Cascaded overloads (same rules as raw API)\n", "\n", "The save happens **only once** per slot. Stacking two overloads on top of each\n", "other means only the original handler is saved — `destroy()` always unwinds back\n", "to that original, skipping any intermediate handlers." ] }, { "cell_type": "code", "execution_count": null, "id": "df293ad3", "metadata": {}, "outputs": [], "source": [ "# Build two extra companions that both overload 'demo.click'.\n", "class ExtraA(AbstractCompanion):\n", " def menu_build(self):\n", " self._register_action('demo.click',\n", " rdown=lambda v, c: print(f\"[vA] X={c.x:.1f}\"),\n", " overload=True)\n", "\n", "class ExtraB(AbstractCompanion):\n", " def menu_build(self):\n", " self._register_action('demo.click',\n", " rdown=lambda v, c: print(f\"[vB] X={c.x:.1f}\"),\n", " overload=True)\n", "\n", "ea = ExtraA(viewer)\n", "eb = ExtraB(viewer)\n", "\n", "ea.menu_build() # saves v1, installs vA\n", "eb.menu_build() # slot 'rdown' already saved → WARNING, vA lost, vB installed\n", "\n", "print(\"Active:\",\n", " viewer._custom_rdown_handlers.get('demo.click'))\n", "saved_rdown = viewer._saved_handlers.get('demo.click', {}).get('rdown')\n", "print(\"Saved (rdown):\",\n", " saved_rdown.__name__ if saved_rdown else '')\n", "\n", "# Restore v1 (skips vA)\n", "eb.destroy()\n", "ea.destroy() # already unregistered by eb, silently ignored\n", "\n", "current = viewer._custom_rdown_handlers.get('demo.click')\n", "print(\"After both destroys:\",\n", " current.__name__ if current else '')" ] }, { "cell_type": "markdown", "id": "06f0bc76", "metadata": {}, "source": [ "## 9 — Final cleanup" ] }, { "cell_type": "code", "execution_count": null, "id": "67bfe61b", "metadata": {}, "outputs": [], "source": [ "base.stop()\n", "base.destroy() # removes 'demo.click'\n", "diag.destroy() # idempotent — already removed by step 7\n", "\n", "print(\"All companions destroyed.\")\n", "print(\"_custom_rdown_handlers:\", viewer._custom_rdown_handlers)\n", "print(\"_saved_handlers:\", viewer._saved_handlers)" ] }, { "cell_type": "markdown", "id": "74901b4a", "metadata": {}, "source": [ "## Summary — overload with companions\n", "\n", "```python\n", "# ── BASE companion ─────────────────────────────────────────────────────────\n", "class BaseCompanion(AbstractCompanion):\n", " def menu_build(self):\n", " self._register_action('shared.action', rdown=self._handler_v1)\n", "\n", " # start() is abstract in the base class — every companion must implement it:\n", " def start(self): self._start_action('shared.action', '…')\n", " def stop(self): self._end_action() # or override to add post-processing\n", "\n", " def _on_activate(self, _e): self.start()\n", " def _on_stop(self, _e): self.stop()\n", "\n", "# ── DIAGNOSTIC companion ───────────────────────────────────────────────────\n", "class DiagCompanion(AbstractCompanion):\n", " def install(self):\n", " # overload=True → saves _handler_v1, installs _handler_v2\n", " self._register_action('shared.action', rdown=self._handler_v2, overload=True)\n", "\n", " def remove(self):\n", " # Unregister → _handler_v1 automatically restored\n", " self._unregister_action('shared.action')\n", "\n", " # start()/stop() are aliases for the domain-specific names:\n", " def start(self): self.install()\n", " def stop(self): self.remove()\n", "\n", " def _on_install(self, _e): self.install()\n", " def _on_remove(self, _e): self.remove()\n", "\n", "# Usage:\n", "base = BaseCompanion(viewer); base.menu_build()\n", "base.start() # v1 active\n", "diag = DiagCompanion(viewer); diag.install() # v2 active — or: diag.start()\n", "diag.remove() # v1 restored — or: diag.stop()\n", "base.stop()\n", "base.destroy() # removes 'shared.action'\n", "```\n", "\n", "### Rules recap\n", "\n", "- `_register_action(id, overload=True)` → saves the previous handler once; subsequent `overload=True` calls on the same slot emit a `WARNING` but do not replace the save.\n", "- `_unregister_action(id)` → restores the saved handler if one exists.\n", "- `destroy()` → calls `_unregister_action` for every registered id, so restoration is automatic.\n", "- All of this is identical to the raw API behaviour; the companion just **wraps** it cleanly." ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }