Source code for simvx.editor.menus

"""Editor Menus — Menu bar setup and keyboard shortcuts."""

from simvx.core import (
    MenuBar,
    MenuItem,
)

from .state import State


[docs] def build_menu_bar(state: State) -> MenuBar: """Create the editor's menu bar with all menus wired to state.""" bar = MenuBar(name="MenuBar") bar.size_y = 28.0 # ---- File menu ---- bar.add_menu( "File", [ MenuItem("New Scene", callback=lambda: state.new_scene_requested.emit(), shortcut="Ctrl+N"), MenuItem("New Untitled", callback=lambda: _new_untitled(state)), MenuItem("Open Scene...", callback=lambda: _open_scene(state), shortcut="Ctrl+O"), MenuItem("Close Scene", callback=lambda: _close_scene(state), shortcut="Ctrl+W"), MenuItem(separator=True), MenuItem("Save", callback=lambda: state.save_scene(), shortcut="Ctrl+S"), MenuItem("Save As...", callback=lambda: _save_scene_as(state), shortcut="Ctrl+Shift+S"), MenuItem(separator=True), MenuItem("New Project...", callback=lambda: _new_project(state)), MenuItem("Open Project...", callback=lambda: _open_project(state)), MenuItem("Recent Projects", submenu=_build_recent_submenu(state)), MenuItem(separator=True), MenuItem("Export Project...", callback=lambda: state.export_requested.emit(), shortcut="Ctrl+E"), MenuItem(separator=True), MenuItem("Quit", callback=lambda: _quit(state), shortcut="Ctrl+Q"), ], ) # ---- Edit menu ---- bar.add_menu( "Edit", [ MenuItem("Undo", callback=state.undo_stack.undo, shortcut="Ctrl+Z"), MenuItem("Redo", callback=state.undo_stack.redo, shortcut="Ctrl+Shift+Z"), MenuItem(separator=True), MenuItem("Cut", callback=lambda: _cut(state), shortcut="Ctrl+X"), MenuItem("Copy", callback=lambda: _copy(state), shortcut="Ctrl+C"), MenuItem("Paste", callback=lambda: _paste(state), shortcut="Ctrl+V"), MenuItem("Delete", callback=lambda: _delete(state), shortcut="Delete"), MenuItem(separator=True), MenuItem("Select All", callback=lambda: _select_all(state), shortcut="Ctrl+A"), MenuItem(separator=True), MenuItem("Preferences...", callback=lambda: state.preferences_requested.emit()), MenuItem("Project Settings...", callback=lambda: state.project_settings_requested.emit()), MenuItem("Input Map...", callback=lambda: state.input_map_requested.emit()), ], ) # ---- Scene menu ---- bar.add_menu( "Scene", [ MenuItem("Add Node...", callback=lambda: _add_node(state)), MenuItem("Instance Scene...", callback=lambda: _instance_scene(state)), MenuItem(separator=True), MenuItem("Run Scene", callback=state.play_scene, shortcut="F5"), MenuItem("Stop", callback=state.stop_scene, shortcut="F6"), MenuItem("Pause", callback=state.pause_scene, shortcut="F7"), ], ) # ---- View menu ---- bar.add_menu( "View", [ MenuItem("3D Viewport", callback=lambda: _set_viewport(state, "3d")), MenuItem("2D Viewport", callback=lambda: _set_viewport(state, "2d")), MenuItem("Script Editor", callback=lambda: _set_viewport(state, "code")), MenuItem(separator=True), MenuItem("Reset Layout", callback=lambda: _reset_layout(state)), ], ) # ---- Help menu ---- bar.add_menu( "Help", [ MenuItem("About SimVX", callback=lambda: state.about_requested.emit()), ], ) # Store reference so plugins can add menu items later state._tools_menu_bar = bar return bar
[docs] def register_shortcuts(state: State): """Register all editor keyboard shortcuts.""" s = state.shortcuts # File s.register("new_scene", "Ctrl+N", lambda: state.new_scene_requested.emit()) s.register("open_scene", "Ctrl+O", lambda: _open_scene(state)) s.register("save_scene", "Ctrl+S", lambda: state.save_scene()) s.register("save_scene_as", "Ctrl+Shift+S", lambda: _save_scene_as(state)) s.register("close_scene", "Ctrl+W", lambda: _close_scene(state)) s.register("export_project", "Ctrl+E", lambda: state.export_requested.emit()) # Edit s.register("undo", "Ctrl+Z", state.undo_stack.undo) s.register("redo", "Ctrl+Shift+Z", state.undo_stack.redo) s.register("delete", "Delete", lambda: _delete(state)) # Scene s.register("play", "F5", state.play_scene) s.register("stop", "F6", state.stop_scene) s.register("pause", "F7", state.pause_scene) # Gizmo s.register("gizmo_cycle", "Q", state.gizmo.cycle_mode)
# ---- Action implementations ---- def _open_scene(state: State): """Open scene file dialog.""" state._show_open_dialog() def _save_scene_as(state: State): """Save scene with file dialog.""" state._show_save_as_dialog() def _quit(state: State): """Quit the editor cleanly. SystemExit is caught by the try/finally in Engine.run(), which ensures proper Vulkan cleanup before process exit. """ raise SystemExit(0) def _cut(state: State): """Cut selected node.""" node = state.selection.primary if node: state.clipboard.copy_node(node) state.remove_node(node) def _copy(state: State): """Copy selected node.""" node = state.selection.primary if node: state.clipboard.copy_node(node) def _paste(state: State): """Paste node from clipboard.""" if state.clipboard.has_node(): parent = state.selection.primary or (state.edited_scene.root if state.edited_scene else None) if parent: node = state.clipboard.paste_node() if node: state.add_node(node, parent) def _delete(state: State): """Delete selected node.""" node = state.selection.primary if node and node.parent: state.remove_node(node) state.selection.clear() def _select_all(state: State): """Select all nodes in the scene.""" from simvx.core import Node root = state.edited_scene.root if state.edited_scene else None if root: all_nodes = [root] + root.find_all(Node) state.selection.select_all(all_nodes) def _add_node(state: State): """Request the Add Node type dialog via the editor state signal.""" state.add_node_requested.emit() def _instance_scene(state: State): """Open file dialog to instance a scene.""" state._show_open_dialog() def _set_viewport(state: State, mode: str): """Switch viewport mode and notify the editor shell.""" state.viewport_mode = mode state.viewport_mode_changed.emit() def _close_scene(state: State): """Close the currently active scene/script tab.""" state.workspace.close_current_tab() def _new_untitled(state: State): """Open a fresh Untitled scratch buffer (VS Code "Untitled-N" pattern). The buffer lives purely in editor memory — it is **not** persisted to any Node attribute, scene file, or session file. Save (Ctrl+S) prompts for a destination path; once written the tab becomes a regular file-backed tab. """ state.workspace.new_untitled() def _new_project(state: State): """Request transition to WelcomeScreen for creating a new project.""" state.new_project_requested.emit() def _open_project(state: State): """Request transition to WelcomeScreen for opening a project.""" state.open_project_requested.emit() def _build_recent_submenu(state: State) -> list[MenuItem]: """Build the Recent Projects submenu items from the project registry.""" from .project_registry import ProjectRegistry registry = ProjectRegistry() registry.load() items = [] for entry in registry.recent[:5]: path = entry.path items.append(MenuItem(entry.name, callback=lambda p=path: _switch_project(state, p))) if not items: items.append(MenuItem("(No recent projects)")) return items def _switch_project(state: State, project_path: str): """Signal the shell to switch to a specific project.""" state._pending_switch_project = project_path state.open_project_requested.emit() def _reset_layout(state: State): """Reset dock layout to default split ratios.""" from simvx.core import SplitContainer dock = getattr(state, "_dock_container", None) if dock is None: return def _reset(node): for child in getattr(node, "children", []): if isinstance(child, SplitContainer): child.split_ratio = 0.25 if child.vertical else 0.75 child._update_layout() _reset(child) _reset(dock)