Companion plugin — complete reference
This notebook mirrors ``plugin_action_complete.ipynb`` using AbstractUICompanion.
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_spec()— no ``import wx`` neededself.proxy.set_status(msg)replacesviewer.set_statusbar_text(msg)self.proxy._viewer.Refresh()replacesviewer.Paint()self.destroy()unregisters every action automatically
1 — wx startup
[1]:
import sys
%gui wx
2 — Create the MapViewer
[2]:
from wolfhece.PyDraw import WolfMapViewer
viewer = WolfMapViewer(None, title="Companion — complete demo", w=1200, h=800)
viewer.Show()
INFO:root:Importing wolfhece modules
INFO:root:wolfhece modules imported
INFO:root:Plugin manager init: skipping auto-discovery because no global configuration is available yet.
[2]:
False
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.
[3]:
from wolfhece.plugins.abc import (
AbstractUICompanion,
ActionSpec,
MenuItem,
SEPARATOR,
)
from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot
from OpenGL.GL import glBegin, glEnd, glVertex2f, glColor4f, glLineWidth, GL_LINES
class MarkerCompanion(AbstractUICompanion):
"""Places and manages coloured markers on the map."""
def get_namespace(self) -> str:
return 'markers'
def __init__(self):
super().__init__()
self.markers: list[tuple[float, float]] = []
self._selected: int = -1
@property
def selected(self) -> int:
return self._selected
def menu_spec(self):
return ('Markers', [
MenuItem('Start', self.on_start, 'Right-click=add left-click=select'),
MenuItem('Stop', self.on_stop, 'Deactivate'),
SEPARATOR,
MenuItem('Show markers', self.on_show),
MenuItem('Clear markers', self.on_clear),
])
def actions_spec(self):
return [
ActionSpec(
'place',
rdown=self.rdown_add,
ldown=self.ldown_select,
motion=self.motion_coords,
key=self.key_handler,
paint=self.paint_markers,
),
]
def start(self) -> None:
self.proxy.start_action(
'place',
'Right-click: add | Left-click: select | Ctrl+Z: undo | Ctrl+C: clear',
)
def stop(self) -> None:
self.proxy.end_action()
def on_start(self, _ctx):
self.start()
def on_stop(self, _ctx):
self.stop()
def on_show(self, _ctx):
if not self.markers:
self.proxy.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.proxy.show_info(f'{len(self.markers)} marker(s):\n{lines}', title='Markers')
def on_clear(self, _ctx):
if not self.markers:
self.proxy.show_info('No markers.', title='Markers')
return
if self.proxy.confirm(f'Delete all {len(self.markers)} marker(s)?', default='no'):
self.markers.clear()
self._selected = -1
self.proxy._viewer.Refresh()
print('All markers cleared.')
def rdown_add(self, ctx: MouseContext) -> None:
self.markers.append((ctx.x_snap, ctx.y_snap))
print(f'[Marker #{len(self.markers)}] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}')
self.proxy._viewer.Refresh()
def ldown_select(self, ctx: MouseContext) -> None:
if not self.markers:
return
d2 = [(x - ctx.x) ** 2 + (y - ctx.y) ** 2 for (x, y) in self.markers]
self._selected = int(min(range(len(d2)), key=d2.__getitem__))
x, y = self.markers[self._selected]
self.proxy.set_status(f'Selected marker #{self._selected + 1} at ({x:.3f}, {y:.3f})')
self.proxy._viewer.Refresh()
def motion_coords(self, ctx: MouseContext) -> None:
self.proxy.set_status(f'X={ctx.x:.2f} Y={ctx.y:.2f}')
def key_handler(self, kb: KeyboardSnapshot) -> bool:
if kb.key_code == 27:
self.stop()
return True
if kb.ctrl and kb.key_code in (ord('Z'), ord('z')):
if self.markers:
removed = self.markers.pop()
self._selected = min(self._selected, len(self.markers) - 1)
self.proxy._viewer.Refresh()
print(f'Removed marker at ({removed[0]:.3f}, {removed[1]:.3f})')
return True
if kb.ctrl and kb.key_code in (ord('C'), ord('c')):
self.markers.clear()
self._selected = -1
self.proxy._viewer.Refresh()
print('All markers cleared.')
return True
return False
def paint_markers(self) -> None:
if not self.markers:
return
dx = max(1e-9, float(self.proxy._viewer.xmax - self.proxy._viewer.xmin))
h = 0.01 * dx
for i, (x, y) in enumerate(self.markers):
if i == self._selected:
glColor4f(1.0, 0.9, 0.1, 1.0)
lw = 3.0
else:
glColor4f(1.0, 0.2, 0.2, 1.0)
lw = 2.0
glLineWidth(lw)
glBegin(GL_LINES)
glVertex2f(x - h, y)
glVertex2f(x + h, y)
glVertex2f(x, y - h)
glVertex2f(x, y + h)
glEnd()
5 — Inspect the markers
[5]:
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}")
3 marker(s) | selected index: -1
1. X=6.312 Y=31.052
2. X=23.462 Y=28.229
3. X=11.958 Y=16.192
6 — Export markers as a numpy array
[7]:
import numpy as np
if mc.markers:
pts = np.array(mc.markers, dtype=float)
print(f"Shape: {pts.shape}")
print(pts)
else:
print("No markers yet.")
Shape: (3, 2)
[[ 6.31183086 31.05173189]
[23.4619883 28.22887989]
[11.95753486 16.19181287]]
7 — Cleanup
[8]:
mc.stop()
viewer.detach_companion(mc) # destroys and detaches the companion from the viewer
print("Companion destroyed.")
print(mc)
INFO:root:ACTION : End of action
Companion destroyed.
<MarkerCompanion ns='markers' menu=not built actions=[—]>
Appendix — helper reference
Public API (call from notebook cells)
Method |
Description |
|---|---|
|
Activate the action |
|
Deactivate the action |
|
Unregister all actions and remove the menu |
Proxy helpers (inside companion methods)
Helper |
Equivalent raw call |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenGL paint pattern
def paint_markers(self) -> None:
half = (self.proxy._viewer.xmax - self.proxy._viewer.xmin) * 0.008
# draw with raw OpenGL calls (glBegin/glVertex/glEnd)
Event handler signatures
def rdown_xxx(self, ctx: MouseContext) -> None: ...
def ldown_xxx(self, ctx: MouseContext) -> None: ...
def motion_xxx(self, ctx: MouseContext) -> None: ...
def key_xxx(self, kb: KeyboardSnapshot) -> bool: ...
def paint_xxx(self) -> None: ...