Source code for simvx.editor.modal_dialog

"""Shared modal-dialog scaffolding for the SimVX editor.

The editor hosts a family of small modal dialogs (About, Preferences, Duplicate
Node, Rename Class, …) that all register on the engine's overlay layer in exactly
the same way: a ``"blocking"`` overlay (dimmed scrim, input scoped to the dialog,
the editor stays live behind it), outside-click dismissal, the ``cancel_requested``
router signal, and ``show_overlay()`` / ``close_overlay()`` to drive the registry.

:class:`BaseModalDialog` centralises that boilerplate so each dialog only
declares its own geometry, contents, and result signals. It is a thin
:class:`~simvx.core.Panel` subclass: the panel background starts fully
transparent (so a dialog that paints itself via ``on_draw`` or an inner panel is
unaffected), and the overlay layer draws the dimmed scrim behind it.

Subclasses typically:

* set ``z_index`` (overlay stacking) and, for full-rect overlays,
  ``set_anchor_preset(AnchorPreset.FULL_RECT)``;
* call :meth:`open_modal` to show and :meth:`dismiss` to hide;
* override :meth:`_on_cancel` when an outside click / Escape should emit a
  ``cancelled`` result signal (the default just hides the dialog).
"""

from __future__ import annotations

from simvx.core import Panel

__all__ = ["BaseModalDialog"]


[docs] class BaseModalDialog(Panel): """Base class for the editor's modal-router dialogs. Centralises the overlay-layer setup (a ``"blocking"`` overlay with outside-click dismissal and the ``cancel_requested`` wiring) and the show/hide lifecycle (:meth:`open_modal` / :meth:`dismiss`). The panel background is transparent by default; subclasses paint their own card via ``on_draw`` or an inner :class:`~simvx.core.Panel`. The overlay dims the editor and scopes input to the dialog, but the editor stays live behind it (``inert=False``). Set :attr:`PAUSE_TREE_WHEN_MODAL` on the subclass to opt into an inert (paused) backdrop instead. """ #: Whether the running scene tree pauses while this dialog is open. Overlay #: dialogs that want an inert (paused) backdrop set this to ``True``. PAUSE_TREE_WHEN_MODAL = False def __init__(self, **kwargs): super().__init__(**kwargs) # Transparent panel background: subclasses draw their own card. self.bg_colour = (0.0, 0.0, 0.0, 0.0) self.border_width = 0 self.visible = False self.cancel_requested.connect(self._on_router_cancel) # --------------------------------------------------------------- lifecycle
[docs] def open_modal(self, *, initial_focus=None) -> None: """Show the dialog and register it as a blocking overlay (dimmed, world live). ``initial_focus`` is the dialog's primary field to focus on open (``None`` = the first focusable descendant). """ self.show_overlay("blocking", inert=self.PAUSE_TREE_WHEN_MODAL, initial_focus=initial_focus)
[docs] def dismiss(self) -> None: """Hide the dialog without emitting a result signal. Idempotent.""" if not self.visible: return self.close_overlay() self.visible = False
# ----------------------------------------------------------------- router def _on_router_cancel(self) -> None: """Router fired ``cancel_requested`` (Escape / outside click).""" if self.visible: self._on_cancel() def _on_cancel(self) -> None: """Handle Escape / outside-click / Cancel-button dismissal. Defaults to a plain :meth:`dismiss`. Override to additionally emit a ``cancelled`` result signal. """ self.dismiss()