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()