Companion plugin — overload and restore
Advanced user only
This notebook mirrors ``plugin_action_overload.ipynb`` using AbstractUICompanion.
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
[1]:
import logging, sys
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')
%gui wx
2 — Viewer
[2]:
from wolfhece.PyDraw import WolfMapViewer
viewer = WolfMapViewer(None, title="Companion — overload demo", w=1200, h=800)
viewer.Show()
DEBUG Searching MKL in c:\Users\pierre\Documents\Gitlab\python311\Scripts\Library\bin
DEBUG Searching MKL in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Found MKL in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Searching FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Scripts\Library\bin
DEBUG Searching FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Found FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
INFO Executable paths loaded from cache
DEBUG Searching MKL in c:\Users\pierre\Documents\Gitlab\python311\Scripts\Library\bin
DEBUG Searching MKL in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Found MKL in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Searching FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Scripts\Library\bin
DEBUG Searching FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Found FORTRAN in c:\Users\pierre\Documents\Gitlab\python311\Library\bin
DEBUG Detected MKL DLL version: 2
DEBUG OpenGL_accelerate module loaded
DEBUG Using accelerated ArrayDatatype
INFO Using NT-specific GLUT calls with exit callbacks
INFO Importing wolfhece modules
INFO wolfhece modules imported
INFO Plugin manager init: skipping auto-discovery because no global configuration is available yet.
[2]:
False
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.
[3]:
from wolfhece.plugins.abc import AbstractUICompanion, ActionSpec, MenuItem
from wolfhece._viewer_plugin_handlers import MouseContext
class DemoCompanion(AbstractUICompanion):
"""Base companion — registers the production rdown handler."""
def get_namespace(self) -> str:
return 'demo'
def menu_spec(self):
return ('Demo', [
MenuItem('Activate', self.on_activate),
MenuItem('Stop', self.on_stop),
])
def actions_spec(self):
return [ActionSpec('click', rdown=self.rdown_v1)]
def start(self) -> None:
self.proxy.start_action('click', 'Right-click -> version 1')
def stop(self) -> None:
self.proxy.end_action()
def on_activate(self, _ctx):
self.start()
def on_stop(self, _ctx):
self.stop()
def rdown_v1(self, ctx: MouseContext) -> None:
print(f'[v1] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}')
base = DemoCompanion()
viewer.attach_companion(base)
base.start()
print(base)
INFO ACTION : Right-click -> version 1
<DemoCompanion ns='demo' menu=attached actions=[demo.click]>
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(AbstractUICompanion):
"""Temporary diagnostic companion that overloads the base handler."""
def get_namespace(self) -> str:
return 'diag'
def menu_spec(self):
return ('Diagnostics', [
MenuItem('Install diagnostic handler', self.on_install),
MenuItem('Remove diagnostic handler', self.on_remove),
])
def actions_spec(self):
return None # installed on demand
def start(self) -> None:
self.install()
def stop(self) -> None:
self.remove()
def install(self) -> None:
self.proxy.register_action(
'demo.click', rdown=self.rdown_v2, overload=True,
)
print('Diagnostic handler installed.')
def remove(self) -> None:
self.proxy.unregister_action('demo.click')
print('Diagnostic handler removed — base handler restored.')
def on_install(self, _ctx):
self.install()
def on_remove(self, _ctx):
self.remove()
def rdown_v2(self, 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.attach_companion(diag)
print(diag)
<DiagCompanion ns='diag' menu=attached actions=[—]>
5 — Install the diagnostic handler
[5]:
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>')
WARNING register_action: 'demo.click' — rdown handler overloaded (previous '_wrapped' saved and will be restored on unregister).
Diagnostic handler installed.
Active handler: _wrapped
Saved rdown: _wrapped
6 — Live check
[v2 DIAG] messages — the diagnostic version is active.[6]:
print("Current action:", viewer.action)
print("→ Right-click in the viewer window to see [v2 DIAG]")
Current action: demo.click
→ 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.
[7]:
# 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()
INFO unregister_action: 'demo.click' — rdown handler restored to '_wrapped'.
INFO ACTION : Right-click -> version 1
Diagnostic handler removed — base handler restored.
Active handler after removal: _wrapped
_saved_handlers: {}
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.
[8]:
# Build two extra companions that both overload 'demo.click'.
class ExtraA(AbstractUICompanion):
def actions_spec(self):
return [
ActionSpec(
'demo.click',
rdown=lambda ctx: print(f'[vA] X={ctx.x:.1f}'),
overload=True,
),
]
def start(self):
self.proxy.start_action('demo.click', '')
class ExtraB(AbstractUICompanion):
def actions_spec(self):
return [
ActionSpec(
'demo.click',
rdown=lambda ctx: print(f'[vB] X={ctx.x:.1f}'),
overload=True,
),
]
def start(self):
self.proxy.start_action('demo.click', '')
ea = ExtraA()
viewer.attach_companion(ea)
eb = ExtraB()
viewer.attach_companion(eb)
print('Active:', viewer._custom_rdown_handlers.get('demo.click'))
saved = viewer._saved_handlers.get('demo.click', {}).get('rdown')
print('Saved (rdown):', saved.__name__ if saved else '<none>')
WARNING register_action: 'demo.click' — rdown handler overloaded (previous '_wrapped' saved and will be restored on unregister).
WARNING register_action: 'demo.click' — rdown handler overloaded (previous '_wrapped' saved and will be restored on unregister).
Active: <function ViewerProxy._adapt_mouse_handler.<locals>._wrapped at 0x0000026C85112020>
Saved (rdown): _wrapped
9 — Final cleanup
[9]:
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)
INFO ACTION : End of action
INFO unregister_action: 'demo.click' — rdown handler restored to '_wrapped'.
All companions destroyed.
_custom_rdown_handlers: {'demo.click': <function ViewerProxy._adapt_mouse_handler.<locals>._wrapped at 0x0000026C85112160>}
_saved_handlers: {}
Summary — overload with companions
# -- BASE companion ----------------------------------------------------------
class BaseCompanion(AbstractUICompanion):
def actions_spec(self):
return [ActionSpec('shared.action', rdown=self._handler_v1)]
def start(self): self.proxy.start_action('shared.action', '...')
def stop(self): self.proxy.end_action() # override if you need post-processing
def on_activate(self, _ctx): self.start()
def on_stop(self, _ctx): self.stop()
# -- DIAGNOSTIC companion ----------------------------------------------------
class DiagCompanion(AbstractUICompanion):
def install(self):
# overload=True -> saves _handler_v1, installs _handler_v2
self.proxy.register_action('shared.action', rdown=self._handler_v2, overload=True)
def remove(self):
# unregister -> _handler_v1 automatically restored
self.proxy.unregister_action('shared.action')
# start()/stop() can alias domain-specific names.
def start(self): self.install()
def stop(self): self.remove()
def on_install(self, _ctx): self.install()
def on_remove(self, _ctx): self.remove()
# Usage:
base = BaseCompanion(); base.proxy.attach(viewer); base.build()
base.start() # v1 active
diag = DiagCompanion(); diag.proxy.attach(viewer); diag.install() # v2 active (or diag.start())
diag.remove() # v1 restored (or diag.stop())
base.stop()
base.destroy() # removes 'shared.action'
Rules recap
proxy.register_action(id, overload=True)saves the previous handler once.Subsequent overloads on the same slot emit a warning but do not replace the saved base handler.
proxy.unregister_action(id)restores the saved handler if one exists.destroy()unregisters companion actions, so restoration is automatic.