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]