Companion plugin — complete reference
This notebook mirrors ``plugin_action_complete.ipynb`` using AbstractCompanion.
All five event hooks are demonstrated:
Hook |
Companion method signature |
Event |
|---|---|---|
|
|
Right mouse-button press |
|
|
Mouse motion |
|
|
Left mouse-button press |
|
|
Key press |
|
|
OpenGL paint hook |
The companion built here lets the user:
Right-click → add a marker at the snapped cursor
Left-click → select / highlight the nearest marker
Mouse motion → show live coordinates in the status bar
Ctrl+Z → undo last marker
Ctrl+C → clear all markers
Paint hook → draw crosses on every marker in OpenGL
What the companion adds vs the raw API
All state is encapsulated —
self.markers,self._selectedMenu is declared once in
menu_build()— no ``import wx`` neededself._set_status(msg)replacesviewer.set_statusbar_text(msg)self._force_redraw()replacesviewer.Paint()self.destroy()unregisters every action automatically
1 — wx startup
[ ]:
import sys
%gui wx
2 — Create the MapViewer
[ ]:
from wolfhece.PyDraw import WolfMapViewer
viewer = WolfMapViewer(None, title="Companion — complete demo", w=1200, h=800)
viewer.Show()
3 — The MarkerCompanion
All state, menu entries, and event handlers live in a single class. Notice that there is no ``import wx`` anywhere — the companion helpers abstract it away.
[ ]:
import numpy as np
from wolfhece._menu_companion_abc import (
AbstractCompanion,
ActionItem,
SEPARATOR,
)
from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot
class MarkerCompanion(AbstractCompanion):
"""Places and manages coloured markers on the map."""
# ---------------------------------------------------------------
# State
# ---------------------------------------------------------------
def __init__(self, viewer):
super().__init__(viewer, namespace='markers')
#: List of (x, y) world-coordinate pairs — read from the notebook.
self.markers: list[tuple[float, float]] = []
#: Index of the currently selected marker (-1 = none).
self._selected: int = -1
@property
def selected(self) -> int:
"""Index of the currently selected marker (-1 = none)."""
return self._selected
# ---------------------------------------------------------------
# Menu
# ---------------------------------------------------------------
def menu_build(self) -> None:
self._build_menu('Markers', [
ActionItem('Start', self._on_start,
'Activate: right-click = add, left-click = select'),
ActionItem('Stop', self._on_stop,
'Deactivate the marker action'),
SEPARATOR,
ActionItem('Show markers', self._on_show, 'Display all markers'),
ActionItem('Clear markers', self._on_clear, 'Delete all markers'),
])
# One register_action call covers all five hooks.
self._register_action(
self._action_id('place'),
rdown = self._rdown_add,
ldown = self._ldown_select,
motion = self._motion_coords,
key = self._key_handler,
paint = self._paint_markers, # ← wires up the OpenGL overlay
)
# ---------------------------------------------------------------
# Public API — callable directly from the notebook.
# ---------------------------------------------------------------
def start(self) -> None:
"""Activate: right-click = add, left-click = select."""
self._start_action(
self._action_id('place'),
'Right-click: add | Left-click: select | Ctrl+Z: undo | Ctrl+C: clear',
)
def stop(self) -> None:
"""Deactivate the marker action."""
self._end_action()
# ---------------------------------------------------------------
# Menu-item callbacks
# ---------------------------------------------------------------
def _on_start(self, _event) -> None:
self.start()
def _on_stop(self, _event) -> None:
self.stop()
def _on_show(self, _event) -> None:
if not self.markers:
self._show_info("No markers yet.", title="Markers")
return
lines = "\n".join(
f" {i+1:3d}. X={x:.3f} Y={y:.3f}"
for i, (x, y) in enumerate(self.markers)
)
self._show_info(f"{len(self.markers)} marker(s):\n{lines}", title="Markers")
def _on_clear(self, _event) -> None:
if not self.markers:
self._show_info("No markers to clear.", title="Markers")
return
if self._confirm(f"Delete all {len(self.markers)} marker(s)?", default='no'):
self.markers.clear()
self._selected = -1
self._force_redraw()
print("All markers cleared.")
# ---------------------------------------------------------------
# rdown handler — right-click adds a marker
# ---------------------------------------------------------------
def _rdown_add(self, viewer, ctx: MouseContext) -> None:
self.markers.append((ctx.x_snap, ctx.y_snap))
n = len(self.markers)
print(f"[Marker #{n}] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}")
self._force_redraw()
# ---------------------------------------------------------------
# ldown handler — left-click selects the nearest marker
# ---------------------------------------------------------------
def _ldown_select(self, viewer, ctx: MouseContext) -> None:
if not self.markers:
return
pts = np.array(self.markers)
dists = np.hypot(pts[:, 0] - ctx.x, pts[:, 1] - ctx.y)
self._selected = int(np.argmin(dists))
x, y = self.markers[self._selected]
print(f"Selected marker #{self._selected + 1} X={x:.3f} Y={y:.3f} "
f"(dist={dists[self._selected]:.1f})")
self._force_redraw()
# ---------------------------------------------------------------
# motion handler — live coordinate display in the status bar
# ---------------------------------------------------------------
def _motion_coords(self, viewer, ctx: MouseContext) -> None:
self._set_status(
f"X={ctx.x:.2f} Y={ctx.y:.2f} | {len(self.markers)} marker(s)"
)
# ---------------------------------------------------------------
# key handler — Ctrl+Z undo, Ctrl+C clear
# Return True to consume the event (prevents default viewer behaviour).
# ---------------------------------------------------------------
def _key_handler(self, viewer, kb: KeyboardSnapshot) -> bool:
if not kb.ctrl:
return False
if kb.key_code == ord('Z'):
if self.markers:
removed = self.markers.pop()
if self._selected >= len(self.markers):
self._selected = len(self.markers) - 1
print(f"Undo — removed X={removed[0]:.3f} Y={removed[1]:.3f}")
self._force_redraw()
return True
if kb.key_code == ord('C'):
self.markers.clear()
self._selected = -1
print("All markers cleared.")
self._force_redraw()
return True
return False
# ---------------------------------------------------------------
# paint hook — draw crosses via the ABC helper (no raw GL needed)
# Called after all data layers, before the viewer's UI overlays.
# ---------------------------------------------------------------
def _paint_markers(self, viewer) -> None:
half = self._viewport_fraction(0.008) # scales with zoom
self._draw_crosses(
self.markers,
half,
selected_idx=self._selected,
)
5 — Inspect the markers
[ ]:
print(f"{len(mc.markers)} marker(s) | selected index: {mc.selected}")
for i, (x, y) in enumerate(mc.markers):
flag = " ← selected" if i == mc.selected else ""
print(f" {i+1:3d}. X={x:.3f} Y={y:.3f}{flag}")
6 — Export markers as a numpy array
[ ]:
if mc.markers:
pts = np.array(mc.markers, dtype=float)
print(f"Shape: {pts.shape}")
print(pts)
else:
print("No markers yet.")
7 — Cleanup
[ ]:
mc.stop()
mc.destroy() # unregisters 'markers.place' automatically
print("Companion destroyed.")
print(mc)
Appendix — helper reference
Public API (call from notebook cells)
Method |
Description |
|---|---|
|
Activate the action |
|
Deactivate the action |
|
Unregister all actions and remove the menu |
Protected helpers (use inside the companion class only)
Companion method |
Equivalent raw call |
When to use |
|---|---|---|
|
|
motion handler, live feedback |
|
|
after changing drawn data |
|
|
inside |
|
|
inside |
OpenGL paint helpers (no import OpenGL required)
Wire the paint hook with paint=self._paint_xxx in _register_action, then call these helpers from inside the method body.
Method |
Description |
|---|---|
|
Returns |
|
Cross markers at each |
|
Connected line string; |
|
Individual segments as |
|
Filled GL dots at each point |
Minimal paint handler example:
def _paint_markers(self, viewer) -> None:
half = self._viewport_fraction(0.008)
self._draw_crosses(self.points, half, selected_idx=self._selected)
Dialog helpers (no import wx required)
Method |
Description |
|---|---|
|
Information dialog |
|
Error dialog |
|
Yes/No confirmation — returns |
|
Free-text prompt — returns |
|
Float prompt — returns |
|
Integer spin dialog |
|
List picker — returns |
|
File-open dialog |
|
File-save dialog |
|
Directory chooser |
Event handler signatures
def _rdown_xxx(self, viewer, ctx: MouseContext) -> None: ...
def _ldown_xxx(self, viewer, ctx: MouseContext) -> None: ...
def _motion_xxx(self, viewer, ctx: MouseContext) -> None: ...
def _key_xxx(self, viewer, kb: KeyboardSnapshot) -> bool: ...
def _paint_xxx(self, viewer) -> None: ... # wire with paint=self._paint_xxx
MouseContext quick reference
Attribute |
Description |
|---|---|
|
Raw world coordinates |
|
Grid-snapped world coordinates |
|
Screen pixel coordinates |
|
Keyboard modifiers |
KeyboardSnapshot quick reference
Attribute |
Description |
|---|---|
|
|
|
Modifiers |
|
|