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