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