wolfhece.plugins.viewer_proxy ============================= .. py:module:: wolfhece.plugins.viewer_proxy .. autoapi-nested-parse:: Technical runtime proxy used by companion plugins. This proxy sits between companions and the viewer API. Companion code should prefer accessing viewer facilities through this object. Temporary migration escape hatch -------------------------------- The underlying viewer remains reachable through the private ``_viewer`` attribute of this proxy so legacy companions can still access raw viewer features while dedicated helper methods are progressively added. Module Contents --------------- .. py:class:: ViewerProxy(name: str, viewer: WolfMapViewer | None = None, dialogs: wolfhece.dialog_provider.DialogProvider | None = None, namespace: str = '') Bridge object that hosts technical viewer/wx/dialog/OpenGL routines. Companion subclasses can focus on business logic while this proxy owns the low-level plumbing and viewer integration details. The proxy is the **sole owner** of both the viewer reference and the dialog provider. Companion code should access the viewer only through ``self.proxy._viewer`` (escape hatch) or dedicated helper methods. .. py:attribute:: _name :type: str .. py:attribute:: _viewer :type: WolfMapViewer | None :value: None .. py:attribute:: _dialogs :type: wolfhece.dialog_provider.DialogProvider :value: None .. py:attribute:: _namespace :type: str :value: '' .. py:attribute:: _menu :type: wx.Menu | None :value: None .. py:attribute:: _menu_host :type: str :value: 'top_level' .. py:attribute:: _menu_parent :type: wx.Menu | None :value: None .. py:attribute:: _menu_parent_item :type: wx.MenuItem | None :value: None .. py:attribute:: _registered_action_ids :type: list[str] :value: [] .. py:attribute:: _appended_items :type: list[tuple[wx.Menu, wx.MenuItem]] :value: [] .. py:attribute:: _multistep_specs :type: dict[str, wolfhece.plugins.types.MultiStepSpec] .. py:attribute:: _multistep_step_index :type: dict[str, int] .. py:attribute:: _active_multistep_action_id :type: str | None :value: None .. py:method:: attach(viewer: wolfhece.PyDraw.WolfMapViewer, dialogs: wolfhece.dialog_provider.DialogProvider | None = None) -> None Attach (or re-attach) the proxy to a viewer after construction. Useful when a companion is created without a viewer and the viewer becomes available later:: companion = MyCompanion() ... companion.runtime.attach(viewer) companion.runtime.activate() :param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` to bind. :param dialogs: Optional dialog provider; when omitted the current provider is kept (or a default one is used if none was set yet). :raises RuntimeError: If a *different* viewer is already attached. .. py:method:: detach() -> None Detach the proxy from the current viewer. After calling this the companion is inert: all viewer-dependent helpers will fail until :meth:`attach` is called again. The dialog provider is kept so that standalone dialogs (e.g. file pickers) still work. .. py:method:: _proxy_registry_key() -> str Return the key used to de-duplicate proxy instances on a viewer. .. py:method:: _get_proxy_registry() -> dict[str, ViewerProxy] Return viewer-scoped proxy registry, creating it if needed. .. py:method:: _replace_previous_proxy_instance() -> None Replace a previous attached proxy with the same identity. Notebook cells are often re-executed, creating a new companion instance while the old one is still attached. We proactively destroy and detach the previous proxy to keep registration/menu state idempotent. .. py:method:: action_id(local_id: str) -> str Return ``'{namespace}.{local_id}'`` — the companion's namespaced action id. Guarantees uniqueness across companions that independently pick the same *local_id*. Use this instead of building the string manually:: self.proxy.register_action('pick', ldown=self._ldown) :param local_id: Short, human-readable name (e.g. ``'pick'``, ``'place wall'``). :returns: ``'{namespace}.{local_id}'`` — e.g. ``'mycompanion.pick'``. .. py:method:: build_menu(title: str, items: list[wolfhece.plugins.types.MenuEntry], *, host: str = 'top_level', companions_root_label: str | None = None) -> None Build a companion menu either as top-level or under "Companions". Menu item callbacks are always called with a synthetic :class:`~wolfhece._viewer_plugin_handlers.MouseContext` instead of a raw wx event. This keeps companion logic framework-agnostic. .. py:method:: _menu_registry_key(*, title: str, host: str) -> tuple[str, str, str] Return a stable key for de-duplicating companion menus per viewer. .. py:method:: _safe_isinstance(obj: object, type_or_tuple: object) -> bool :staticmethod: Like isinstance(), but tolerant when test doubles replace wx classes. .. py:method:: _get_companion_menu_registry() -> dict[tuple[str, str, str], dict] Return viewer-scoped companion menu registry, creating it if needed. .. py:method:: _replace_existing_companion_menu(*, title: str, host: str) -> None Remove an existing menu built by a previous companion instance. This makes notebook re-execution idempotent: rebuilding a companion with the same identity (host + namespace + title) replaces the old menu instead of appending duplicates. .. py:method:: _register_companion_menu(*, title: str, host: str) -> None Store bookkeeping for later de-duplication and safe teardown. .. py:method:: _ensure_companions_root_menu(label: str) -> wx.Menu Return the shared top-level root menu used by companions. .. py:method:: _cleanup_companions_root_menu_if_empty() -> None Remove the shared companions root when no child submenus remain. .. py:method:: fill_wx_menu(menu: wx.Menu, items: list[wolfhece.plugins.types.MenuEntry]) -> None Populate a wx menu from declarative entries. The proxy intercepts ``wx.EVT_MENU`` and forwards a synthetic mouse context to handlers. .. py:method:: _wrap_menu_handler(handler: Callable[Ellipsis, None]) -> Callable Adapt a wx menu callback to a MouseContext callback. Companion handlers stay independent from wx by receiving a synthetic :class:`~wolfhece._viewer_plugin_handlers.MouseContext`. .. py:method:: _menu_event_to_mouse_context(event) -> wolfhece._viewer_plugin_handlers.MouseContext Build a synthetic :class:`MouseContext` from a wx menu event. Strategy: 1. Reuse the viewer's latest cached mouse context when available. 2. Fall back to a neutral origin context when no cache exists. .. py:method:: append_to_menu(menu: wx.Menu, items: list[wolfhece.plugins.types.MenuEntry]) -> None Append declarative entries to an existing wx menu. The proxy records the appended items so :meth:`menu_destroy` can later remove them cleanly. .. py:method:: append_to_create_menu(items: list[wolfhece.plugins.types.MenuEntry]) -> None Append entries to the viewer's create-object menu. .. py:method:: append_to_add_menu(items: list[wolfhece.plugins.types.MenuEntry]) -> None Append entries to the viewer's add-object menu. .. py:method:: _require_viewer() -> wolfhece.PyDraw.WolfMapViewer Return the attached viewer or raise when the proxy is detached. :raises RuntimeError: If no viewer is currently attached. .. py:property:: active_array Return the viewer's active array, or ``None`` when unset. .. py:property:: active_matrix Alias for :attr:`active_array`. The viewer code historically uses "array" while some companions and docs still talk about matrices. .. py:property:: active_zones Return the viewer's active zones, or ``None`` when unset. .. py:property:: active_triangulation Return the viewer's active triangulation, or ``None`` when unset. .. py:method:: get_array(id: str | None = None) Return the active array or a named array from the viewer. .. py:method:: get_matrix(id: str | None = None) Alias for :meth:`get_array`. Kept for companions / docs that still use the matrix wording. .. py:method:: get_zones(id: str | None = None) Return the active zones or a named zones object from the viewer. .. py:method:: get_triangulation(id: str | None = None) Return the active triangulation or a named triangulation. .. py:method:: add_array(newobj=None, *, filename: str = '', ToCheck: bool = True, id: str = '') -> int Add an array to the viewer and return the viewer's add result. .. py:method:: add_matrix(newobj=None, *, filename: str = '', ToCheck: bool = True, id: str = '') -> int Alias for :meth:`add_array`. .. py:method:: add_zones(newobj=None, *, filename: str = '', ToCheck: bool = True, id: str = '') -> int Add a zones object to the viewer and return the add result. .. py:method:: add_triangulation(newobj=None, *, filename: str = '', ToCheck: bool = True, id: str = '') -> int Add a triangulation to the viewer and return the add result. .. py:method:: destroy() -> None Unregister actions, remove menus, and detach the proxy. This is the main teardown hook used by the plugin manager and by the notebook re-execution cleanup path. .. py:method:: menu_destroy() -> None Remove menus and appended items that were registered by the proxy. .. py:method:: _resolve_action_id(action_id: str | ActionKind) -> str Return a fully-qualified (namespaced) action id string. If *action_id* is an :class:`ActionKind` enum it is converted to its value. If the resulting string contains no ``'.'``, it is treated as a *local* id and the companion namespace is prepended automatically:: rt._resolve_action_id('pick') # → 'mycompanion.pick' rt._resolve_action_id('mycompanion.pick') # → 'mycompanion.pick' rt._resolve_action_id(ActionKind.SCULPT) # → 'sculpt' (enum value kept) This means companion code can simply write ``self.proxy.register_action('pick', …)`` without a preceding ``self.proxy.action_id('pick')`` call. .. py:method:: resolve_action_id(action_id: str | ActionKind) -> str Public resolver for local/namespaced action ids. Declarative objects (e.g. ``MultiStepAction``) should use this method instead of duplicating namespacing logic. .. py:method:: _accepts_n_or_more_positional(fn: Callable, n: int) -> bool :staticmethod: Return True when *fn* can consume at least *n* positional args. .. py:method:: _adapt_mouse_handler(handler: Optional[Callable]) -> Optional[Callable] Adapt mouse handlers to support both ``(viewer, ctx)`` and ``(ctx)``. .. py:method:: _adapt_key_handler(handler: Optional[Callable]) -> Optional[Callable] Adapt key handlers to support both ``(viewer, kb)`` and ``(kb)``. .. py:method:: _adapt_paint_handler(handler: Optional[Callable]) -> Optional[Callable] Adapt paint handlers to support both ``(viewer)`` and ``()``. .. py:method:: register_action(action_id: str | wolfhece._action_kind.ActionKind, *, rdown: Optional[Callable] = None, motion: Optional[Callable] = None, ldown: Optional[Callable] = None, key: Optional[Callable] = None, paint: Optional[Callable] = None, overload: bool = False) -> None Register interactive handlers for an action id. Supported callback signatures: - ``rdown/motion/ldown``: ``(viewer, ctx)`` or ``(ctx)`` - ``key``: ``(viewer, kb)`` or ``(kb)`` - ``paint``: ``(viewer)`` or ``()`` This allows companion code to ignore direct viewer access and rely on ``self.proxy`` for viewer interactions. .. py:method:: register_actions(specs: Iterable[ActionSpec | MultiStepSpec]) -> None Register a declarative batch of ActionSpec and/or MultiStepSpec. .. py:method:: register_multistep_action(spec: wolfhece.plugins.types.MultiStepSpec) -> None Register one declarative multi-step action with runtime step dispatch. .. py:method:: _current_multistep_step(action_id: str) Return the currently active step object for a multi-step action. .. py:method:: _coerce_transition(result) -> object Normalize a handler return value into a :class:`StepTransition`. .. py:method:: _apply_multistep_transition(action_id: str, result) -> None Apply a handler-driven transition to a multi-step action. .. py:method:: _advance_multistep(action_id: str) -> None Advance a multi-step action to its next step when possible. .. py:method:: _finish_multistep(action_id: str) -> None Finish a multi-step action and reset its runtime state. .. py:method:: _cancel_multistep(action_id: str) -> None Cancel a multi-step action and clear its runtime state. .. py:method:: _build_multistep_mouse_dispatch(action_id: str, event_name: str) -> Callable Build a mouse-event dispatcher for one step-driven action. .. py:method:: _build_multistep_key_dispatch(action_id: str) -> Callable Build a keyboard-event dispatcher for one step-driven action. .. py:method:: _build_multistep_paint_dispatch(action_id: str) -> Callable Build a paint dispatcher for one step-driven action. .. py:method:: unregister_action(action_id: str | wolfhece._action_kind.ActionKind) -> None Unregister a previously registered action id. .. py:method:: start_action(action_id: str | wolfhece._action_kind.ActionKind, message: str = '') -> None Start an interaction on the viewer, honoring multi-step actions. .. py:method:: end_action(message: str = '') -> None End the current viewer interaction. .. py:method:: require_active_array() -> bool Warn and return ``False`` when no active array is selected. .. py:method:: require(condition: bool, warning: str) -> bool :staticmethod: Emit a warning and return the truth value of *condition*. .. py:method:: show_error(message: str, title: str = '') -> None Show an error dialog. :param message: Message body shown to the user. :param title: Optional dialog title. .. py:method:: show_info(message: str, title: str = '') -> None Show an informational dialog. .. py:method:: confirm(message: str, title: str = '', default: str = 'yes') -> bool Ask the user for a yes/no style confirmation. .. py:method:: ask_text(message: str, title: str = '', default: str = '') -> str | None Ask the user for free-form text. .. py:method:: ask_float(message: str, title: str = '', default: float | str = '') -> float | None Ask the user for a floating-point value. .. py:method:: ask_integer(message: str, title: str = '', prompt: str = '', default: int = 0, min_value: int = 0, max_value: int = 0) -> int | None Ask the user for an integer value. .. py:method:: ask_single_choice(message: str, title: str, choices: list[str], preselected: int | None = None) -> str | None Ask the user to pick one value from a finite list of choices. .. py:method:: ask_file_open(message: str, wildcard: str = 'all (*.*)|*.*', default_path: str = '') -> str | None Open a file-selection dialog and return the chosen path. .. py:method:: ask_file_save(message: str, wildcard: str = 'all (*.*)|*.*', default_path: str = '', default_file: str = '') -> str | None Open a save-file dialog and return the chosen path. .. py:method:: ask_directory(message: str, default_path: str = '') -> str | None Open a directory picker and return the chosen path. .. py:method:: run_with_progress(title: str, steps: list[tuple[int, str, Callable]]) -> None Run a list of callables while updating a progress dialog. .. py:method:: set_status(message: str) -> None Set the viewer status bar text. .. py:method:: force_redraw() -> None Trigger a repaint of the viewer canvas. .. py:method:: viewport_fraction(fraction: float = 0.008) -> float Return a fraction of the current viewport width. .. py:method:: 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 :staticmethod: Draw cross markers at the given points using OpenGL primitives. .. py:method:: 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 :staticmethod: Draw a polyline or closed loop using OpenGL primitives. .. py:method:: 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 :staticmethod: Draw independent line segments using OpenGL primitives. .. py:method:: 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 :staticmethod: Draw points using OpenGL point primitives.