Source code for simvx.editor.unsaved_class_warning_dialog

"""Warning dialog shown when saving a scene that references unsaved classes.

A scene file may reference user classes that only exist in unsaved Untitled
scratch buffers. The .py file is still syntactically valid, but the import
will fail at runtime until the buffer is written to disk. The user can
choose to save anyway (and fix the import later) or cancel.

Per the design rule the dialog warns but allows the save through.
"""

from __future__ import annotations

from collections.abc import Callable

from simvx.core import (
    Button,
    HBoxContainer,
    Label,
    Panel,
    Signal,
    VBoxContainer,
    Vec2,
)

__all__ = ["UnsavedClassWarningDialog"]


_DIALOG_W = 460.0
_DIALOG_H = 220.0
_PAD = 18.0
_ROW_H = 30.0

_OVERLAY = (0.0, 0.0, 0.0, 0.55)
_BG = (0.16, 0.16, 0.18, 1.0)
_BORDER = (0.35, 0.35, 0.40, 1.0)
_TITLE = (0.95, 0.95, 0.97, 1.0)
_TEXT = (0.78, 0.78, 0.82, 1.0)
_HINT = (0.55, 0.55, 0.58, 1.0)


[docs] class UnsavedClassWarningDialog(Panel): """Modal "scene references unsaved class" confirmation overlay. Construct once per editor; call :meth:`show_for` with the offending class names + a confirm callback. On "Save anyway" the callback fires; on "Cancel" the dialog hides and emits :attr:`cancelled`. """ confirmed = Signal() cancelled = Signal() DIALOG_W = _DIALOG_W DIALOG_H = _DIALOG_H def __init__(self, **kwargs): super().__init__(**kwargs) self.bg_colour = _OVERLAY self.border_width = 0 self.visible = False self.z_index = 1800 self._inner: Panel | None = None self._title_label: Label | None = None self._classes_label: Label | None = None self._hint_label: Label | None = None self._confirm_btn: Button | None = None self._cancel_btn: Button | None = None self._on_confirm: Callable[[], None] | None = None self._build() def _build(self) -> None: inner = Panel(name="UnsavedClassWarningInner") inner.bg_colour = _BG inner.border_colour = _BORDER inner.border_width = 1.0 inner.size = Vec2(_DIALOG_W, _DIALOG_H) self._inner = inner body = VBoxContainer(name="UnsavedClassWarningBody") body.separation = 10 body.position = Vec2(_PAD, _PAD) body.size = Vec2(_DIALOG_W - 2 * _PAD, _DIALOG_H - 2 * _PAD) inner.add_child(body) self._title_label = Label("Unsaved Class Reference", name="Title") self._title_label.font_size = 16.0 self._title_label.text_colour = _TITLE self._title_label.size = Vec2(_DIALOG_W - 2 * _PAD, 22) body.add_child(self._title_label) self._classes_label = Label("", name="Classes") self._classes_label.font_size = 12.0 self._classes_label.text_colour = _TEXT self._classes_label.size = Vec2(_DIALOG_W - 2 * _PAD, 18) body.add_child(self._classes_label) self._hint_label = Label( "These classes are only defined in unsaved Untitled buffers.\n" "The scene file will save, but the import will fail at runtime\n" "until the buffers are written to disk.", name="Hint", ) self._hint_label.font_size = 11.0 self._hint_label.text_colour = _HINT self._hint_label.size = Vec2(_DIALOG_W - 2 * _PAD, 60) body.add_child(self._hint_label) button_row = HBoxContainer(name="ButtonRow") button_row.separation = 8 button_row.size = Vec2(_DIALOG_W - 2 * _PAD, _ROW_H + 4) self._cancel_btn = Button("Cancel", name="CancelBtn") self._cancel_btn.size = Vec2(120, _ROW_H) self._cancel_btn.pressed.connect(self._on_cancel) button_row.add_child(self._cancel_btn) spacer = Label("", name="Spacer") spacer.size = Vec2(_DIALOG_W - 2 * _PAD - 120 - 150 - 16, _ROW_H) button_row.add_child(spacer) self._confirm_btn = Button("Save Anyway", name="SaveAnywayBtn") self._confirm_btn.size = Vec2(150, _ROW_H) self._confirm_btn.pressed.connect(self._on_confirm_pressed) button_row.add_child(self._confirm_btn) body.add_child(button_row) self.add_child(inner) # ------------------------------------------------------------ public API
[docs] def show_for( self, class_names: list[str], on_confirm: Callable[[], None] | None = None, parent_size: Vec2 | None = None, ) -> None: """Open the dialog listing *class_names*; *on_confirm* fires on Save Anyway.""" joined = ", ".join(class_names) if class_names else "(none)" if self._classes_label is not None: self._classes_label.text = f"Scene references unsaved class(es): {joined}" self._on_confirm = on_confirm if parent_size is not None: self.size = parent_size elif self.size.x <= 0 or self.size.y <= 0: # Fall back to the actual parent rect (root_panel) so the dialog # is sized to cover the editor window. try: ps = self._get_parent_size() if ps.x > 0 and ps.y > 0: self.size = ps except Exception: pass if self.size.x > 0 and self.size.y > 0 and self._inner is not None: self._inner.position = Vec2( (self.size.x - _DIALOG_W) / 2, (self.size.y - _DIALOG_H) / 2, ) self.visible = True
[docs] def hide_dialog(self) -> None: self.visible = False self._on_confirm = None
# --------------------------------------------------------------- buttons def _on_confirm_pressed(self) -> None: cb = self._on_confirm self.hide_dialog() self.confirmed.emit() if cb is not None: cb() def _on_cancel(self) -> None: self.hide_dialog() self.cancelled.emit()
[docs] def gui_input(self, event): """Escape cancels the warning.""" if hasattr(event, "key") and event.key == "escape" and event.pressed: self._on_cancel() return True return super().gui_input(event) if hasattr(super(), "gui_input") else False