Source code for simvx.editor.scene_file_ops

"""Scene file operations mixin for EditorState."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from simvx.core import Node, SceneTree, load_scene, save_scene

from .workspace_tabs import SceneTabState

if TYPE_CHECKING:
    from .state import EditorState


[docs] class SceneFileOps: """Mixin providing scene lifecycle and file dialog operations. Methods in this class are designed to be mixed into EditorState, which provides the workspace, signals, and delegating properties they depend on. """ # Typed self for IDE support — at runtime this is always EditorState. if TYPE_CHECKING: self: EditorState # type: ignore[assignment]
[docs] def new_scene(self, root_type: type = Node, *, populate: bool = False): """Create a new scene tab with the given root type.""" name = root_type.__name__ if root_type is not Node else "Root" tab = SceneTabState.create(root_type=root_type, name=name) if populate: from .default_scenes import populate_default_scene populate_default_scene(tab.scene_tree.root) self.workspace.add_scene_tab(tab) self.scene_changed.emit()
[docs] def open_scene(self, path: str | Path): """Load a scene from disk into the active tab, or a new tab.""" path = Path(path) if not path.exists(): return root = load_scene(str(path)) if not root: return # Check if already open — reload in place existing = self.workspace.find_scene_tab(path) if existing is not None: self.workspace.set_active(existing) # Load into the active scene tab (overwrite its state) tab = self.workspace.get_active_scene() if tab: tab.scene_tree.set_root(root) tab.scene_path = path tab.tab_name = path.stem tab.selection.clear() tab.undo_stack.clear() tab.modified = False else: new_tab = SceneTabState.create(root_type=type(root), name=root.name) new_tab.scene_tree.set_root(root) new_tab.scene_path = path new_tab.tab_name = path.stem self.workspace.add_scene_tab(new_tab) self._add_recent(str(path)) self.scene_changed.emit()
[docs] def save_scene(self, path: str | Path | None = None): """Save the current scene.""" save_path = Path(path) if path else self.current_scene_path if not save_path: self._show_save_as_dialog() return False root = self.edited_scene.root if self.edited_scene else None if not root: return False save_scene(root, str(save_path)) self.current_scene_path = save_path self._modified = False self._add_recent(str(save_path)) self.scene_modified.emit() return True
def _show_open_dialog(self): if not self._file_dialog: return self._file_dialog.file_selected.clear() self._file_dialog.file_selected.connect(self.open_scene) start = str(self.current_scene_path.parent) if self.current_scene_path else None self._file_dialog.show(mode="open", path=start, filter="*.json") def _show_save_as_dialog(self): if not self._file_dialog: return self._file_dialog.file_selected.clear() self._file_dialog.file_selected.connect(lambda p: self.save_scene(p)) start = str(self.current_scene_path) if self.current_scene_path else None self._file_dialog.show(mode="save", path=start, filter="*.json") def _add_recent(self, path: str): if path in self.recent_files: self.recent_files.remove(path) self.recent_files.insert(0, path) self.recent_files = self.recent_files[:10]