Source code for simvx.editor.save_dialog

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