"""Save Preview Dialog -- Modal code preview for CST ambiguity resolution.
When the CST codegen cannot fully automate a scene-to-Python save (e.g.,
procedural code, complex expressions), this dialog shows the generated
source for the user to review, edit, and accept before writing to disk.
"""
import logging
from simvx.core import Button, CodeTextEdit, Label, Panel, Signal, Vec2
log = logging.getLogger(__name__)
__all__ = ["SavePreviewDialog"]
[docs]
class SavePreviewDialog(Panel):
"""Modal dialog showing generated Python code before saving.
Shows a diff-style preview of changes. User can:
- Accept (save as-is)
- Edit (modify the code in an inline editor)
- Cancel (don't save)
Signals:
accepted: emits (final_source: str,) when user accepts the code.
cancelled: emitted when user cancels the save.
"""
DIALOG_WIDTH = 700.0
DIALOG_HEIGHT = 500.0
HEADER_HEIGHT = 36.0
BUTTON_HEIGHT = 32.0
BUTTON_GAP = 8.0
MARGIN = 16.0
def __init__(self, original_source: str, generated_source: str, **kwargs):
super().__init__(name="SavePreviewDialog", **kwargs)
self.accepted = Signal()
self.cancelled = Signal()
self.bg_colour = (0.0, 0.0, 0.0, 0.6)
self.border_width = 0
self.visible = False
self._original_source = original_source
self._generated_source = generated_source
self._editing = False
self._dialog_panel: Panel | None = None
self._code_editor: CodeTextEdit | None = None
self._edit_btn: Button | None = None
self._accept_btn: Button | None = None
self._build()
def _build(self):
m = self.MARGIN
dw, dh = self.DIALOG_WIDTH, self.DIALOG_HEIGHT
inner = Panel(name="DialogInner")
inner.bg_colour = (0.18, 0.18, 0.20, 1.0)
inner.border_colour = (0.35, 0.35, 0.4, 1.0)
inner.border_width = 1.0
inner.size = Vec2(dw, dh)
self._dialog_panel = inner
# Title
title = Label("Save Preview", name="Title")
title.font_size = 15.0
title.text_colour = (0.9, 0.9, 0.9, 1.0)
title.position = Vec2(m, 10)
inner.add_child(title)
# Subtitle / description
desc = Label("Review the generated Python source before saving.", name="Description")
desc.font_size = 11.0
desc.text_colour = (0.6, 0.6, 0.6, 1.0)
desc.position = Vec2(m, 30)
inner.add_child(desc)
# Code editor area
code_top = self.HEADER_HEIGHT + m
code_h = dh - code_top - self.BUTTON_HEIGHT - m * 2
editor = CodeTextEdit(self._generated_source, name="CodePreview")
editor.position = Vec2(m, code_top)
editor.size = Vec2(dw - m * 2, code_h)
editor.read_only = True
self._code_editor = editor
inner.add_child(editor)
# Buttons row at bottom
btn_y = dh - self.BUTTON_HEIGHT - m
btn_w = (dw - m * 2 - self.BUTTON_GAP * 2) / 3
accept_btn = Button("Accept", name="AcceptBtn")
accept_btn.size = Vec2(btn_w, self.BUTTON_HEIGHT)
accept_btn.position = Vec2(m, btn_y)
accept_btn.bg_colour = (0.20, 0.57, 0.92, 1.0)
accept_btn.pressed.connect(self._on_accept)
self._accept_btn = accept_btn
inner.add_child(accept_btn)
edit_btn = Button("Edit", name="EditBtn")
edit_btn.size = Vec2(btn_w, self.BUTTON_HEIGHT)
edit_btn.position = Vec2(m + btn_w + self.BUTTON_GAP, btn_y)
edit_btn.bg_colour = (0.30, 0.30, 0.33, 1.0)
edit_btn.pressed.connect(self._on_edit)
self._edit_btn = edit_btn
inner.add_child(edit_btn)
cancel_btn = Button("Cancel", name="CancelBtn")
cancel_btn.size = Vec2(btn_w, self.BUTTON_HEIGHT)
cancel_btn.position = Vec2(m + (btn_w + self.BUTTON_GAP) * 2, btn_y)
cancel_btn.bg_colour = (0.30, 0.30, 0.33, 1.0)
cancel_btn.pressed.connect(self._on_cancel)
inner.add_child(cancel_btn)
self.add_child(inner)
@property
def source(self) -> str:
"""Current source text (may have been edited by the user)."""
return self._code_editor.text if self._code_editor else self._generated_source
@property
def editing(self) -> bool:
"""Whether the user is in edit mode."""
return self._editing
[docs]
def show(self, parent_size: Vec2 | None = None):
"""Show the dialog, centering within parent_size if given."""
self.visible = True
if parent_size:
self.size = parent_size
if self._dialog_panel:
pw = self.size.x if self.size.x > 0 else 800
ph = self.size.y if self.size.y > 0 else 600
dw, dh = self._dialog_panel.size.x, self._dialog_panel.size.y
self._dialog_panel.position = Vec2((pw - dw) / 2, (ph - dh) / 2)
def _on_accept(self):
self.visible = False
self.accepted.emit(self.source)
def _on_edit(self):
if not self._editing:
self._editing = True
if self._code_editor:
self._code_editor.read_only = False
if self._edit_btn:
self._edit_btn.text = "Editing..."
self._edit_btn.bg_colour = (0.20, 0.57, 0.92, 0.6)
def _on_cancel(self):
self.visible = False
self.cancelled.emit()
def _on_gui_input(self, event):
"""Handle escape to cancel."""
if hasattr(event, "key") and event.key == "escape" and event.pressed:
self._on_cancel()
return
super()._on_gui_input(event)