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 actionsMultiStepAction encapsulates 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()
step: int[source]
reset() None[source]

Reset step and all public mutable attributes to their class defaults.

Attributes whose class-level default is a type (not a value) are skipped so that accidental re-initialisation of class-level type annotations does not cause errors.

advance() int[source]

Increment step and return the new value.

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’s start_action / register_action system.

  • 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 CompanionState to reset automatically when start() and cancel() are called. If not provided a plain CompanionState is 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

action_id: str[source]
step_hints: list[str][source]
state: CompanionState = None[source]
_viewer: wolfhece.PyDraw.WolfMapViewer | None = None[source]
_active: bool = False[source]
_resolve_viewer(viewer: wolfhece.PyDraw.WolfMapViewer | None) wolfhece.PyDraw.WolfMapViewer[source]

Return viewer if provided, else fall back to the bound viewer.

Raises RuntimeError if neither is available.

property is_active: bool[source]

True while the action has been started and not yet finished.

property current_step: int[source]

Current zero-based step index.

property current_hint: str[source]

Status message for the current step, or empty string if out of range.

property total_steps: int[source]
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 True if there are more steps to perform, False when all steps have been completed (the caller should then finalise its logic and call cancel() 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) -> Noneevent can be ignored if not needed.

  • help – Optional help string shown in the status bar.

  • checkable – When True the item renders as a toggle checkbox.

  • enabled – When False the item is greyed out on creation.

label: str[source]
handler: Callable[Ellipsis, None][source]
help: str = ''[source]
checkable: bool = False[source]
enabled: bool = True[source]
class wolfhece._menu_companion_abc.Separator[source]

Singleton sentinel that inserts a separator line in the menu.

Use the pre-built SEPARATOR instance rather than instantiating this class directly.

_instance: Separator | None = None[source]
wolfhece._menu_companion_abc.SEPARATOR: Separator[source]
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 nested SubMenuSpec objects.

  • help – Optional help string shown in the status bar.

label: str[source]
items: list[MenuEntry] = [][source]
help: str = ''[source]
wolfhece._menu_companion_abc.MenuEntry[source]
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

Inheritance diagram of wolfhece._menu_companion_abc.AbstractCompanion

Abstract base class for companion objects that integrate with WolfMapViewer.

A companion is an object that:

  1. Owns the transient state needed by its interactive actions.

  2. Registers (and later cleans up) custom mouse/keyboard handlers in the viewer’s action dispatch tables.

  3. Optionally appends its own wx.Menu to the viewer’s menu bar (override menu_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 WolfMapViewer instance that owns this companion.

_viewer: wolfhece.PyDraw.WolfMapViewer[source]
_dialogs: wolfhece.dialog_provider.DialogProvider = None[source]
_namespace: str = None[source]
_menu: wx.Menu | None = None[source]
_registered_action_ids: list[str] = [][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 calls self.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_id is 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 CompanionState instance.

_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 raw wx.Menu code.

Idempotent — subsequent calls are no-ops once the menu is built.

Parameters:

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 that destroy() can call unregister_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_action additionally resets active_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 True if the viewer has an active array, else warn and return False.

_require(condition: bool, warning: str) bool[source]

Generic guard: log warning and return False when condition is falsy.

Parameters:
  • condition – Pre-evaluated boolean condition.

  • warning – Message logged at WARNING level when the guard fails.

_show_error(message: str, title: str = '') None[source]

Display a modal error dialog.

_show_info(message: str, title: str = '') None[source]

Display a modal information dialog.

_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:

True if 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 None if 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 None if 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 None if 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 None if 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 None if 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 None if cancelled.

_ask_directory(message: str, default_path: str = '') str | None[source]

Open a directory-chooser dialog.

Returns:

Selected directory path, or None if 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 OpenGL is 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 True a 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.