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