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