wolfhece._menu_companion_abc
Abstract base class for menu-based companion objects in WolfMapViewer.
Purpose
This module provides the building blocks to author new companion classes that integrate with the viewer’s menu bar and interactive-action system.
Design goals
Encapsulation of state — each companion instance is the natural home for all transient state needed by its actions (picked objects, step counters, intermediate geometry, …). The ABC guarantees a minimal, consistent interface while leaving full freedom to subclasses.
Reduced boilerplate — common operations (menu idempotency, guards, action registration/cleanup, standard dialogs) are provided once and reused by every subclass.
Multi-step actions —
MultiStepActionencapsulates the step-counter, per-step hint messages and state reset for interactions that span several mouse events.
Typical usage
from wolfhece._menu_companion_abc import AbstractCompanion, CompanionState, MultiStepAction
class MyCompanion(AbstractCompanion):
def __init__(self, viewer):
super().__init__(viewer)
# _make_action() creates a namespaced + viewer-bound MultiStepAction.
# The action_id becomes e.g. 'mycompanion.place' automatically.
self._place = self._make_action(
'place',
['Left-click to pick start point',
'Left-click to pick end point — Esc to cancel'],
)
# Typed local state.
self._pts: list = []
def menu_build(self) -> None:
# No wx import needed — _build_menu handles it.
self._build_menu(_('My Feature'), [
ActionItem(_('Place…'), self._on_place, _('Pick two points')),
])
# self._place.action_id is already namespaced ('mycompanion.place').
self._register_action(
self._place.action_id,
ldown=self._ldown_place,
key=self._key_place,
)
# -- event handlers -----------------------------------------------------
def _on_place(self, event):
self._pts.clear()
self._place.start() # viewer already bound
def _ldown_place(self, viewer, ctx):
self._pts.append((ctx.x, ctx.y))
if not self._place.advance(): # viewer already bound
# All steps done — finalise.
logging.info('Two points: %s', self._pts)
self._place.cancel() # viewer already bound
def _key_place(self, viewer, kb):
if kb.key_code == 27: # Esc
self._place.cancel() # viewer already bound
return True
return False
Integration in PyDraw.py
from ._my_feature_manager import MyCompanion
...
self._my_feature = MyCompanion(self)
...
self._my_feature.menu_build()
Module Contents
- class wolfhece._menu_companion_abc.CompanionState[source]
Minimal resettable key/value store for companion state.
Subclass this to get typed attributes:
class _MyState(CompanionState): start_xy: tuple | None = None end_xy: tuple | None = None class MyCompanion(AbstractCompanion): def __init__(self, viewer): super().__init__(viewer) self._state = _MyState()
- class wolfhece._menu_companion_abc.MultiStepAction(action_id: str | wolfhece._action_kind.ActionKind, step_hints: list[str], state: CompanionState | None = None, viewer: wolfhece.PyDraw.WolfMapViewer | None = None)[source]
Wraps a viewer interactive action that spans multiple input steps.
- Parameters:
action_id – The lowercase string (or
ActionKind) used to identify the action in the viewer’sstart_action/register_actionsystem.step_hints – Ordered list of status-bar / log messages, one per step. The message at index n is shown while the user is performing step n.
state – Optional
CompanionStateto reset automatically whenstart()andcancel()are called. If not provided a plainCompanionStateis created.companion (Typical pattern inside a)
-----------------------------------
:: –
- self._pick_action = MultiStepAction(
‘mycompanion pick’, [‘Click first point’, ‘Click second point’], state=self._state,
)
handler:: (In the ldown) –
- def _ldown(self, viewer, ctx):
# store data … done = not self._pick_action.advance() # viewer already bound if done:
self._finalise() self._pick_action.cancel() # resets and ends action
- state: CompanionState = None[source]
- _viewer: wolfhece.PyDraw.WolfMapViewer | None = None[source]
- _resolve_viewer(viewer: wolfhece.PyDraw.WolfMapViewer | None) → wolfhece.PyDraw.WolfMapViewer[source]
Return viewer if provided, else fall back to the bound viewer.
Raises
RuntimeErrorif neither is available.
- property current_hint: str[source]
Status message for the current step, or empty string if out of range.
- start(viewer: wolfhece.PyDraw.WolfMapViewer | None = None, message: str = '') → None[source]
Begin the action at step 0.
- Parameters:
viewer – The WolfMapViewer instance. May be omitted when a viewer was supplied to the constructor.
message – Override for the initial hint (defaults to
step_hints[0]).
- advance(viewer: wolfhece.PyDraw.WolfMapViewer | None = None) → bool[source]
Increment the step counter and refresh the viewer’s action message.
Returns
Trueif there are more steps to perform,Falsewhen all steps have been completed (the caller should then finalise its logic and callcancel()to end the action cleanly).- Parameters:
viewer – May be omitted when a viewer was supplied to the constructor.
- cancel(viewer: wolfhece.PyDraw.WolfMapViewer | None = None, message: str = '') → None[source]
Abort or finish the action and reset all state.
Safe to call even when the action is not active.
- Parameters:
viewer – May be omitted when a viewer was supplied to the constructor.
message – Optional status-bar text forwarded to
WolfMapViewer.end_action().
- class wolfhece._menu_companion_abc.ActionItem[source]
A single clickable (or checkable) menu entry.
- Parameters:
label – Text shown in the menu.
handler – Callable invoked when the item is selected. Signature:
(event) -> None—eventcan be ignored if not needed.help – Optional help string shown in the status bar.
checkable – When
Truethe item renders as a toggle checkbox.enabled – When
Falsethe item is greyed out on creation.
- class wolfhece._menu_companion_abc.Separator[source]
Singleton sentinel that inserts a separator line in the menu.
Use the pre-built
SEPARATORinstance rather than instantiating this class directly.
- class wolfhece._menu_companion_abc.SubMenuSpec[source]
A labelled submenu containing its own list of entries.
- Parameters:
label – Text shown in the parent menu.
items – Ordered list of
ActionItem,SEPARATOR, or nestedSubMenuSpecobjects.help – Optional help string shown in the status bar.
- class wolfhece._menu_companion_abc.AbstractCompanion(viewer: wolfhece.PyDraw.WolfMapViewer, dialogs: wolfhece.dialog_provider.DialogProvider | None = None, namespace: str | None = None)[source]
Bases:
abc.ABC
Abstract base class for companion objects that integrate with WolfMapViewer.
A companion is an object that:
Owns the transient state needed by its interactive actions.
Registers (and later cleans up) custom mouse/keyboard handlers in the viewer’s action dispatch tables.
Optionally appends its own
wx.Menuto the viewer’s menu bar (overridemenu_build()to add one).
Subclasses must implement
start(). All other methods are either concrete helpers or optional overrides. In particular,menu_build()has a no-op default — companions that interact purely via mouse/keyboard events do not need a menu.- Parameters:
viewer – The
WolfMapViewerinstance that owns this companion.
- _dialogs: wolfhece.dialog_provider.DialogProvider = None[source]
- menu_build() → None[source]
Build and attach the companion’s menu to the viewer menubar.
The default implementation does nothing — companions that work purely through mouse/keyboard interactions do not need a menu. Override this method when a menu is desired.
Preferred — wx-free approach using
_build_menu():def menu_build(self) -> None: self._build_menu(_('My Feature'), [ ActionItem(_('Do something'), self._on_do), SEPARATOR, SubMenuSpec(_('Tools'), [ ActionItem(_('Export'), self._on_export), ]), ])
Alternative — full wx control (for advanced cases):
def menu_build(self) -> None: if self._menu is not None: return import wx self._menu = wx.Menu() ...
Implementations must be idempotent.
- abstractmethod start() → None[source]
Activate the companion’s primary interactive action.
Implement this method by calling
_start_action()with the appropriate action id and status-bar hint:def start(self) -> None: self._start_action(self._action_id('record'), 'Right-click to capture…')
This method is intended to be called from notebook cells or scripts. It should not contain wx logic — delegate that to the menu-item callback (
_on_start) which itself callsself.start():def _on_start(self, _event): self.start()
- stop() → None[source]
Deactivate the companion’s primary interactive action.
The default implementation calls
_end_action(). Override to add post-processing (e.g. printing a summary):def stop(self) -> None: self._end_action() print(f"{len(self.points)} item(s) recorded.")
- _action_id(local_id: str) → str[source]
Return a fully-qualified action id:
'{namespace}.{local_id}'.Using this method (instead of bare strings) guarantees that two companions that independently choose the same local_id will never collide in the viewer’s action dispatch tables.
- Parameters:
local_id – Short, human-readable name for the action (e.g.
'pick','place wall').- Returns:
'{namespace}.{local_id}'— e.g.'mycompanion.pick'.
- _make_action(local_id: str, step_hints: list[str], state: CompanionState | None = None) → MultiStepAction[source]
Create a namespaced, viewer-bound
MultiStepAction.Shorthand for:
MultiStepAction( self._action_id(local_id), step_hints, state=state, viewer=self._viewer, )
The resulting
action.action_idis already namespaced, so it can be passed directly to_register_action():self._pick = self._make_action('pick', ['Click a point']) # In menu_build: self._register_action(self._pick.action_id, ldown=self._ldown)
- Parameters:
local_id – Short name scoped to this companion.
step_hints – One hint message per interaction step.
state – Optional shared
CompanionStateinstance.
- _build_menu(title: str, items: list[MenuEntry]) → None[source]
Build and attach the companion’s menu from a declarative spec.
No wx knowledge is required. Call this from
menu_build()instead of writing rawwx.Menucode.Idempotent — subsequent calls are no-ops once the menu is built.
- Parameters:
title – Label shown in the viewer’s menu bar.
items – Ordered list of
ActionItem,SEPARATOR, orSubMenuSpecobjects.
Example:
def menu_build(self) -> None: self._build_menu(_('My Tool'), [ ActionItem(_('Run'), self._on_run, _('Execute the tool')), SEPARATOR, SubMenuSpec(_('Settings'), [ ActionItem(_('Configure…'), self._on_configure), ]), ])
- _fill_wx_menu(menu: wx.Menu, items: list[MenuEntry]) → None[source]
Recursively populate menu from items. Internal use only.
- destroy() → None[source]
Unregister all custom actions and release viewer resources.
Call this when the companion is no longer needed. The method is safe to call multiple times.
- _register_action(action_id: str | wolfhece._action_kind.ActionKind, *, rdown: Callable | None = None, motion: Callable | None = None, ldown: Callable | None = None, key: Callable | None = None, paint: Callable | None = None, overload: bool = False) → None[source]
Register a custom action in the viewer and track it for cleanup.
All parameters are forwarded to
register_action(). The action_id is remembered so thatdestroy()can callunregister_action()automatically.- Parameters:
action_id – Lowercase action identifier.
rdown –
(viewer, MouseContext) → None— right click.motion –
(viewer, MouseContext) → None— mouse move.ldown –
(viewer, MouseContext) → None— left click.key –
(viewer, KeyboardSnapshot) → bool— key press.paint –
(viewer) → None— OpenGL paint hook.overload – Save and restore displaced handlers on cleanup.
- _unregister_action(action_id: str | wolfhece._action_kind.ActionKind) → None[source]
Unregister a single custom action.
- Parameters:
action_id – The id passed to
_register_action().
- _start_action(action_id: str | wolfhece._action_kind.ActionKind, message: str = '') → None[source]
Shortcut for
self._viewer.start_action(action_id, message).
- _end_action(message: str = '') → None[source]
End the current viewer action via
WolfMapViewer.end_action().Prefer this over calling
viewer.start_action('', '')directly:end_actionadditionally resetsactive_vertex,active_cloud_vertex_id, fires the sculpt / assets end-action callbacks, and logs an “End of action” message.- Parameters:
message – Optional status-bar text to display after the action ends (forwarded to
WolfMapViewer.end_action()).
- _require_active_array() → bool[source]
Return
Trueif the viewer has an active array, else warn and returnFalse.
- _require(condition: bool, warning: str) → bool[source]
Generic guard: log warning and return
Falsewhen condition is falsy.- Parameters:
condition – Pre-evaluated boolean condition.
warning – Message logged at WARNING level when the guard fails.
- _confirm(message: str, title: str = '', default: str = 'yes') → bool[source]
Display a Yes/No confirmation dialog.
- Parameters:
message – Question text.
title – Dialog title (defaults to “Confirm”).
default – Which button is the default —
'yes'or'no'.
- Returns:
Trueif the user clicked Yes.
- _ask_text(message: str, title: str = '', default: str = '') → str | None[source]
Prompt the user for a text value.
- Returns:
The entered string, or
Noneif cancelled.
- _ask_float(message: str, title: str = '', default: float | str = '') → float | None[source]
Prompt the user for a floating-point value.
- Returns:
The entered float, or
Noneif cancelled or invalid.
- _ask_integer(message: str, title: str = '', prompt: str = '', default: int = 0, min_value: int = 0, max_value: int = 0) → int | None[source]
Prompt the user for an integer value via a spin-entry dialog.
- Returns:
The entered integer, or
Noneif cancelled.
- _ask_single_choice(message: str, title: str, choices: list[str], preselected: int | None = None) → str | None[source]
Show a single-choice list dialog.
- Returns:
The selected string, or
Noneif cancelled.
- _ask_file_open(message: str, wildcard: str = 'all (*.*)|*.*', default_path: str = '') → str | None[source]
Open a file-open dialog.
- Returns:
Selected file path, or
Noneif cancelled.
- _ask_file_save(message: str, wildcard: str = 'all (*.*)|*.*', default_path: str = '', default_file: str = '') → str | None[source]
Open a file-save dialog.
- Returns:
Selected file path, or
Noneif cancelled.
- _ask_directory(message: str, default_path: str = '') → str | None[source]
Open a directory-chooser dialog.
- Returns:
Selected directory path, or
Noneif cancelled.
- _run_with_progress(title: str, steps: list[tuple[int, str, Callable]]) → None[source]
Run a series of steps under a progress dialog.
Each element of steps is
(percent, label, callable)where the callable is invoked with no arguments. The progress dialog is always closed even if an exception occurs.Example:
self._run_with_progress( _("Processing"), [ (25, _("Loading…"), self._load), (75, _("Computing…"), self._compute), (100, _("Saving…"), self._save), ], )
- Parameters:
title – Dialog title.
steps – Ordered list of
(percent, label, fn)tuples.
- _set_status(message: str) → None[source]
Write message to the viewer’s status bar.
Shorthand for
self._viewer.set_statusbar_text(message). Useful in motion handlers to display live coordinates without needing to know the internal viewer API.- Parameters:
message – Text to display in the status bar.
- _force_redraw() → None[source]
Ask the viewer to repaint immediately.
Shorthand for
self._viewer.Paint(). Call this after modifying data that is drawn by a paint hook so that the screen reflects the change without waiting for the next natural repaint event.
- _viewport_fraction(fraction: float = 0.008) → float[source]
Return fraction × viewport width in world units.
Useful for computing marker sizes that scale naturally with the current zoom level:
def _paint_markers(self, viewer) -> None: half = self._viewport_fraction(0.008) self._draw_crosses(self.points, half)
- Parameters:
fraction – Fraction of the visible viewport width (default 0.008).
- Returns:
World-space distance proportional to the current zoom.
- _draw_crosses(points: Iterable[tuple[float, float]], half_size: float, color: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 1.0), *, selected_idx: int = -1, selected_color: tuple[float, float, float, float] = (1.0, 0.8, 0.0, 1.0), line_width: float = 2.0) → None[source]
Draw a cross marker at each
(x, y)point using OpenGL.No
import OpenGLis needed in the companion — this helper handles it internally. Typical use inside a paint handler:def _paint_markers(self, viewer) -> None: half = self._viewport_fraction(0.008) self._draw_crosses( self.markers, half, selected_idx=self._selected, )
- Parameters:
points – Sequence of
(x, y)world-coordinate pairs.half_size – Half arm-length of each cross, in world units.
color – RGBA color for unselected crosses (each channel 0–1).
selected_idx – Index of the cross to highlight (
-1= none).selected_color – RGBA color for the highlighted cross.
line_width – OpenGL line width in pixels.
- _draw_polyline(points: Iterable[tuple[float, float]], color: tuple[float, float, float, float] = (0.0, 0.5, 1.0, 1.0), *, closed: bool = False, line_width: float = 1.5) → None[source]
Draw a polyline (connected line string) through points using OpenGL.
def _paint_path(self, viewer) -> None: self._draw_polyline(self.path, (0.0, 0.7, 1.0, 1.0))
- Parameters:
points – Sequence of
(x, y)world-coordinate pairs (≥ 2).color – RGBA line color (each channel 0–1).
closed – If
Truea closing segment is added back to the first point.line_width – OpenGL line width in pixels.
- _draw_segments(segments: Iterable[tuple[float, float, float, float]], color: tuple[float, float, float, float] = (1.0, 1.0, 0.0, 1.0), *, line_width: float = 1.5) → None[source]
Draw individual line segments using OpenGL.
Each segment is a
(x1, y1, x2, y2)tuple:def _paint_edges(self, viewer) -> None: self._draw_segments( [(x0, y0, x1, y1) for x0, y0, x1, y1 in self.edges], )
- Parameters:
segments – Sequence of
(x1, y1, x2, y2)tuples.color – RGBA line color (each channel 0–1).
line_width – OpenGL line width in pixels.
- _draw_points(points: Iterable[tuple[float, float]], color: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 1.0), *, point_size: float = 4.0) → None[source]
Draw filled dots at each
(x, y)point using OpenGL.def _paint_nodes(self, viewer) -> None: self._draw_points(self.nodes, (0.2, 0.8, 0.2, 1.0), point_size=6.0)
- Parameters:
points – Sequence of
(x, y)world-coordinate pairs.color – RGBA color (each channel 0–1).
point_size – OpenGL point size in pixels.