Source code for simvx.ide.app

"""IDE main application shell -- builds the full UI tree and wires keybindings."""


from __future__ import annotations

import logging
import sys
from pathlib import Path

from simvx.core import (
    Control,
    FileDialog,
    Label,
    MenuBar,
    MenuItem,
    Node,
    Panel,
    ShortcutManager,
    SplitContainer,
    TabContainer,
    Vec2,
)

from .config import IDEConfig
from .debug_controller import DebugController
from .edit_controller import EditCommandController
from .file_controller import FileTabController
from .keybindings import register_keybindings
from .lsp_controller import LSPController
from .state import IDEState

log = logging.getLogger(__name__)

_MENUBAR_H = 28.0
_STATUSBAR_H = 28.0


[docs] class IDERoot(Node): """Root node for the SimVX IDE. Builds the entire UI hierarchy.""" def __init__(self, **kwargs): super().__init__(**kwargs) self.state = IDEState() self.config = IDEConfig() self.config.load() self.shortcuts = ShortcutManager() # Panels (populated in ready) self._menu_bar: MenuBar | None = None self._sidebar_split: SplitContainer | None = None self._sidebar_content: SplitContainer | None = None self._main_split: SplitContainer | None = None self._bottom_tabs: TabContainer | None = None self._status_bar: Control | None = None self._file_dialog: FileDialog | None = None self._command_palette = None self._goto_dialog = None self._confirm_dialog = None self._editor_panel = None self._file_browser = None self._symbol_outline = None self._terminal_panel = None self._output_panel = None self._problems_panel = None self._search_panel = None self._settings_panel = None self._debug_panel_widget = None self._debug_manager = None self._lint_runner = None self._lsp_client = None # Controllers (initialized here, wired in ready) self._file_ctrl = FileTabController(self) self._edit_ctrl = EditCommandController(self) self._lsp_ctrl = LSPController(self) self._debug_ctrl = DebugController(self)
[docs] def ready(self): self._build_layout() self._build_file_dialog() self._build_menu_bar() # Add menu bar AFTER layout so it gets input priority (last child checked first) self._build_overlays() self._wire_keybindings() self._wire_signals() # Apply initial config defaults to editor panel if self._editor_panel: self._editor_panel.default_show_fold_gutter = self.config.show_code_folding self._editor_panel.default_show_indent_guides = self.config.show_indent_guides self._lsp_ctrl.start_lsp() self._lsp_ctrl.start_lint_runner() # Load user keybinding overrides if self.config.keybindings: self.shortcuts.load_bindings(self.config.keybindings) # Register global shortcut handler on the SceneTree so shortcuts work # regardless of which control is focused if self._tree: self._tree._shortcut_handler = self._handle_global_shortcut
def _build_menu_bar(self): self._menu_bar = MenuBar() self._menu_bar.position = Vec2(0, 0) self._menu_bar.size = Vec2(self.config.window_width, _MENUBAR_H) self.add_child(self._menu_bar) # File menu self._menu_bar.add_menu("File", [ MenuItem("New", self._on_file_new, "Ctrl+N"), MenuItem("Open...", self._on_file_open, "Ctrl+O"), MenuItem("Open Folder...", self._on_open_folder), MenuItem(separator=True), MenuItem("Save", self._on_file_save, "Ctrl+S"), MenuItem("Save As...", self._on_file_save_as, "Ctrl+Shift+S"), MenuItem("Close", self._on_file_close, "Ctrl+W"), MenuItem(separator=True), MenuItem("Quit", self._on_quit, "Ctrl+Q"), ]) # Edit menu self._menu_bar.add_menu("Edit", [ MenuItem("Undo", self._on_undo, "Ctrl+Z"), MenuItem("Redo", self._on_redo, "Ctrl+Shift+Z"), MenuItem(separator=True), MenuItem("Cut", self._on_cut, "Ctrl+X"), MenuItem("Copy", self._on_copy, "Ctrl+C"), MenuItem("Paste", self._on_paste, "Ctrl+V"), MenuItem(separator=True), MenuItem("Find", self._on_find, "Ctrl+F"), MenuItem("Replace", self._on_replace, "Ctrl+H"), MenuItem("Find in Files", self._on_find_in_files, "Ctrl+Shift+F"), MenuItem(separator=True), MenuItem("Toggle Comment", self._on_toggle_comment, "Ctrl+/"), MenuItem("Duplicate Line", self._on_duplicate_line, "Ctrl+Shift+D"), MenuItem("Move Line Up", self._on_move_line_up, "Alt+Up"), MenuItem("Move Line Down", self._on_move_line_down, "Alt+Down"), MenuItem(separator=True), MenuItem("Format Document", self._on_format_document, "Ctrl+Shift+L"), ]) # View menu self._menu_bar.add_menu("View", [ MenuItem("Toggle Sidebar", self._on_toggle_sidebar, "Ctrl+B"), MenuItem("Toggle Terminal", self._on_toggle_terminal, "Ctrl+`"), MenuItem("Toggle Bottom Panel", self._on_toggle_bottom_panel, "Ctrl+J"), MenuItem("Toggle Minimap", self._on_toggle_minimap), MenuItem(separator=True), MenuItem("Settings", self._on_show_settings), MenuItem("Command Palette", self._on_command_palette, "Ctrl+Shift+P"), ]) # Go menu self._menu_bar.add_menu("Go", [ MenuItem("Go to Line...", self._on_goto_line, "Ctrl+G"), MenuItem("Go to File...", self._on_goto_file, "Ctrl+P"), MenuItem("Go to Definition", self._on_goto_definition, "F12"), MenuItem("Find References", self._on_find_references, "Shift+F12"), MenuItem(separator=True), MenuItem("Toggle Bookmark", self._on_toggle_bookmark, "Ctrl+F2"), MenuItem("Next Bookmark", self._on_next_bookmark, "F2"), MenuItem("Previous Bookmark", self._on_prev_bookmark, "Shift+F2"), MenuItem(separator=True), MenuItem("Navigate Back", self._on_history_back, "Alt+Left"), MenuItem("Navigate Forward", self._on_history_forward, "Alt+Right"), ]) # Run menu self._menu_bar.add_menu("Run", [ MenuItem("Run File", self._on_run_file, "F5"), MenuItem("Run Without Debug", self._on_run_no_debug, "Ctrl+F5"), MenuItem(separator=True), MenuItem("Toggle Breakpoint", self._on_toggle_breakpoint, "F9"), MenuItem("Step Over", self._on_step_over, "F10"), MenuItem("Step Into", self._on_step_into, "F11"), MenuItem("Step Out", self._on_step_out, "Shift+F11"), MenuItem("Stop", self._on_stop_debug, "Shift+F5"), ]) # Tools menu self._menu_bar.add_menu("Tools", [ MenuItem("Lint Current File", self._on_lint_file), MenuItem("Format Document", self._on_format_document, "Ctrl+Shift+L"), MenuItem(separator=True), MenuItem("Toggle Linting", self._on_toggle_linting), MenuItem("Toggle LSP", self._on_toggle_lsp), MenuItem(separator=True), MenuItem("Restart LSP Server", self._on_restart_lsp), ]) def _build_layout(self): from .embedded import IDEEmbeddedShell content_h = self.config.window_height - _MENUBAR_H - _STATUSBAR_H # Embedded shell provides sidebar + code editor + bottom panels self._shell = IDEEmbeddedShell(state=self.state, config=self.config, name="IDEShell") self._shell.position = Vec2(0, _MENUBAR_H) self.add_child(self._shell) self._shell.build(self.config.window_width, content_h) # Alias shell sub-components for backward compat with the rest of IDERoot self._sidebar_split = self._shell._sidebar_split self._sidebar_content = self._shell._sidebar_content self._main_split = self._shell._main_split self._editor_panel = self._shell.code_panel self._bottom_tabs = self._shell.bottom_tabs self._file_browser = self._shell.file_browser self._symbol_outline = self._shell.symbol_outline self._debug_manager = self._shell.debug_manager # Settings tab — standalone IDE only (not embedded in editor) self._build_settings_panel() # Status bar — standalone IDE only from .widgets.status_bar import StatusBar self._status_bar = StatusBar(self.state, self.config) self._status_bar.position = Vec2(0, self.config.window_height - _STATUSBAR_H) self._status_bar.size = Vec2(self.config.window_width, _STATUSBAR_H) self.add_child(self._status_bar) def _build_settings_panel(self): """Add Settings tab — only used by standalone IDE, not embedded shell.""" if not self._bottom_tabs: return try: from .panels.settings_panel import SettingsPanel self._settings_panel = SettingsPanel(state=self.state, config=self.config) self._settings_panel.settings_changed.connect(self._on_setting_changed) self._bottom_tabs.add_child(self._settings_panel) except Exception as e: log.error("Failed to load SettingsPanel: %s", e) placeholder = Panel(name="Settings") placeholder.bg_colour = (0.08, 0.08, 0.08, 1.0) err_label = placeholder.add_child(Label(f"Error: {e}", name="ErrorLabel")) err_label.text_colour = (0.8, 0.3, 0.3, 1.0) err_label.font_size = 11.0 err_label.position = Vec2(8, 4) self._bottom_tabs.add_child(placeholder) def _build_file_dialog(self): self._file_dialog = FileDialog() self.add_child(self._file_dialog) def _build_overlays(self): from .widgets.command_palette import CommandPalette from .widgets.confirm_dialog import ConfirmDialog from .widgets.goto_line import GotoLineDialog self._command_palette = CommandPalette(self.state, self.config) self.add_child(self._command_palette) self._register_palette_commands() self._goto_dialog = GotoLineDialog(self.state) self.add_child(self._goto_dialog) self._confirm_dialog = ConfirmDialog() self.add_child(self._confirm_dialog) def _register_palette_commands(self): cp = self._command_palette cp.register_command("File: New File", self._on_file_new, "Ctrl+N") cp.register_command("File: Open File", self._on_file_open, "Ctrl+O") cp.register_command("File: Open Folder", self._on_open_folder) cp.register_command("File: Save", self._on_file_save, "Ctrl+S") cp.register_command("File: Save As", self._on_file_save_as, "Ctrl+Shift+S") cp.register_command("File: Close", self._on_file_close, "Ctrl+W") cp.register_command("Edit: Undo", self._on_undo, "Ctrl+Z") cp.register_command("Edit: Redo", self._on_redo, "Ctrl+Shift+Z") cp.register_command("Edit: Toggle Comment", self._on_toggle_comment, "Ctrl+/") cp.register_command("Edit: Format Document", self._on_format_document, "Ctrl+Shift+L") cp.register_command("View: Toggle Sidebar", self._on_toggle_sidebar, "Ctrl+B") cp.register_command("View: Toggle Terminal", self._on_toggle_terminal, "Ctrl+`") cp.register_command("View: Toggle Bottom Panel", self._on_toggle_bottom_panel, "Ctrl+J") cp.register_command("Go: Go to Line", self._on_goto_line, "Ctrl+G") cp.register_command("Go: Go to File", self._on_goto_file, "Ctrl+P") cp.register_command("Go: Go to Definition", self._on_goto_definition, "F12") cp.register_command("Go: Go to Symbol", self._on_goto_symbol, "Ctrl+Shift+O") cp.register_command("Edit: Duplicate Line", self._on_duplicate_line, "Ctrl+Shift+D") cp.register_command("Edit: Move Line Up", self._on_move_line_up, "Alt+Up") cp.register_command("Edit: Move Line Down", self._on_move_line_down, "Alt+Down") cp.register_command("Edit: Fold", self._on_fold, "Ctrl+Shift+[") cp.register_command("Edit: Unfold", self._on_unfold, "Ctrl+Shift+]") cp.register_command("Navigate: Toggle Bookmark", self._on_toggle_bookmark, "Ctrl+F2") cp.register_command("Navigate: Next Bookmark", self._on_next_bookmark, "F2") cp.register_command("Navigate: Previous Bookmark", self._on_prev_bookmark, "Shift+F2") cp.register_command("Navigate: Back", self._on_history_back, "Alt+Left") cp.register_command("Navigate: Forward", self._on_history_forward, "Alt+Right") cp.register_command("View: Toggle Minimap", self._on_toggle_minimap) cp.register_command("View: Settings", self._on_show_settings) cp.register_command("Run: Run File", self._on_run_file, "F5") cp.register_command("Run: Toggle Breakpoint", self._on_toggle_breakpoint, "F9") cp.register_command("Tools: Lint Current File", self._on_lint_file) cp.register_command("Tools: Toggle Linting", self._on_toggle_linting) cp.register_command("Tools: Toggle LSP", self._on_toggle_lsp) cp.register_command("Tools: Restart LSP Server", self._on_restart_lsp) def _wire_keybindings(self): actions = { "file.new": self._on_file_new, "file.open": self._on_file_open, "file.save": self._on_file_save, "file.save_as": self._on_file_save_as, "file.close": self._on_file_close, "file.open_folder": self._on_open_folder, "edit.undo": self._on_undo, "edit.redo": self._on_redo, "edit.cut": self._on_cut, "edit.copy": self._on_copy, "edit.paste": self._on_paste, "edit.select_all": self._on_select_all, "edit.toggle_comment": self._on_toggle_comment, "edit.delete_line": self._on_delete_line, "edit.select_next_occurrence": self._on_select_next, "edit.format_document": self._on_format_document, # Line operations "edit.duplicate_line": self._on_duplicate_line, "edit.move_line_up": self._on_move_line_up, "edit.move_line_down": self._on_move_line_down, # Folding "edit.fold": self._on_fold, "edit.unfold": self._on_unfold, "find.find": self._on_find, "find.replace": self._on_replace, "find.find_in_files": self._on_find_in_files, "navigate.goto_line": self._on_goto_line, "navigate.goto_file": self._on_goto_file, "navigate.command_palette": self._on_command_palette, "navigate.goto_definition": self._on_goto_definition, "navigate.find_references": self._on_find_references, "navigate.goto_symbol": self._on_goto_symbol, # Bookmarks "navigate.toggle_bookmark": self._on_toggle_bookmark, "navigate.next_bookmark": self._on_next_bookmark, "navigate.prev_bookmark": self._on_prev_bookmark, # History "navigate.history_back": self._on_history_back, "navigate.history_forward": self._on_history_forward, "view.toggle_sidebar": self._on_toggle_sidebar, "view.toggle_bottom_panel": self._on_toggle_bottom_panel, "view.toggle_terminal": self._on_toggle_terminal, "run.run": self._on_run_file, "run.run_no_debug": self._on_run_no_debug, "debug.toggle_breakpoint": self._on_toggle_breakpoint, "debug.step_over": self._on_step_over, "debug.step_into": self._on_step_into, "debug.step_out": self._on_step_out, "debug.stop": self._on_stop_debug, "debug.restart": self._on_restart_debug, "view.zoom_in": self._on_zoom_in, "view.zoom_out": self._on_zoom_out, } register_keybindings(self.shortcuts, actions) def _wire_signals(self): self.state.goto_requested.connect(self._on_goto_requested) self.state.run_requested.connect(self._debug_ctrl.on_run_requested) self.state.format_requested.connect(self._lsp_ctrl.on_format_requested) self.state.file_saved.connect(self._lsp_ctrl.on_file_saved_signal) self.state.definition_received.connect(self._on_definition_received) self.state.exception_occurred.connect(self._on_exception_occurred) self.state.file_opened.connect(self._lsp_ctrl.on_file_opened_lint) self.state.references_received.connect(self._lsp_ctrl.on_references_received) # File management from file browser if self._file_browser: self._file_browser.file_deleted.connect(self._file_ctrl.on_file_deleted) if self._file_browser: self._file_browser.file_renamed.connect(self._file_ctrl.on_file_renamed) # Wire CodeEditorPanel signals -> IDEState if self._editor_panel: self._editor_panel.close_requested.connect(self._file_ctrl.on_tab_close_via_button) self._editor_panel.cursor_moved.connect(self.state.set_cursor) self._editor_panel.completion_requested.connect(self.state.completion_requested.emit) self._editor_panel.file_opened.connect(self.state.file_opened.emit) self._editor_panel.file_saved.connect(self.state.file_saved.emit) self._editor_panel.file_closed.connect(self.state.file_closed.emit) self._editor_panel.active_file_changed.connect(lambda p: setattr(self.state, 'active_file', p)) self._editor_panel.active_file_changed.connect(self._update_window_title) # Wire IDEState -> CodeEditorPanel if self._editor_panel: self.state.diagnostics_updated.connect( lambda p, _d: self._editor_panel.refresh_diagnostics(p) if self._editor_panel else None ) self.state.breakpoint_toggled.connect( lambda p, _l: self._editor_panel.refresh_breakpoints(p) if self._editor_panel else None ) self.state.completion_received.connect(self._editor_panel.show_completions) # LSP document lifecycle self.state.file_opened.connect(self._lsp_ctrl.lsp_notify_open) self.state.file_closed.connect(self._lsp_ctrl.lsp_notify_close) self.state.file_saved.connect(self._lsp_ctrl.lsp_notify_save) self.state.completion_requested.connect(self._lsp_ctrl.lsp_request_completion) self.state.rename_edits_received.connect(self._lsp_ctrl.apply_rename_edits) self.state.formatting_edits_received.connect(self._lsp_ctrl.apply_formatting_edits) self.state.hover_received.connect(self._lsp_ctrl.on_hover_received) _close_guard_installed = False def _install_close_guard(self): """Set a GLFW window close callback that checks for unsaved files. Called lazily from process() since the GLFW window must exist first. """ if self._close_guard_installed: return if not self._tree: return handle = getattr(self._tree, '_platform_window', None) if not handle: return try: import glfw def _close_cb(_win): unsaved = self._file_ctrl.get_unsaved_files() if unsaved: glfw.set_window_should_close(_win, False) self._on_quit() glfw.set_window_close_callback(handle, _close_cb) self._close_guard_installed = True except Exception: pass # -- Global shortcuts ------------------------------------------------------ def _handle_global_shortcut(self, key_combo: str) -> bool: """Called by SceneTree before routing key events to focused controls. Converts the key combo string (e.g. 'ctrl+s') to ShortcutManager format and dispatches. Returns True if a shortcut matched (event consumed). """ # Don't intercept shortcuts when command palette or goto dialog is open if (self._command_palette and self._command_palette.visible) or \ (self._goto_dialog and self._goto_dialog.visible): return False # Parse modifier keys from the combo parts = key_combo.lower().split("+") key = parts[-1] if parts else "" modifiers = set(parts[:-1]) if len(parts) > 1 else set() return self.shortcuts.handle_key(key, modifiers) # -- Process loop ----------------------------------------------------------
[docs] def process(self, dt: float): self.shortcuts.tick(dt) self._handle_resize() self._install_close_guard() # Poll LSP client for incoming messages if self._lsp_client: self._lsp_client.poll() # Poll debug manager if self._debug_manager: self._debug_manager.process(dt)
def _handle_resize(self): if not self._tree: return ss = self._tree.screen_size w = ss.x if hasattr(ss, "x") else ss[0] h = ss.y if hasattr(ss, "y") else ss[1] if w == self.config.window_width and h == self.config.window_height: return self.config.window_width = int(w) self.config.window_height = int(h) content_h = h - _MENUBAR_H - _STATUSBAR_H if self._menu_bar: self._menu_bar.size = Vec2(w, _MENUBAR_H) shell = getattr(self, "_shell", None) if shell: shell.resize(w, content_h) if self._status_bar: self._status_bar.position = Vec2(0, h - _STATUSBAR_H) self._status_bar.size = Vec2(w, _STATUSBAR_H) # -- Internal helpers ------------------------------------------------------ def _switch_to_tab(self, name: str): """Switch bottom tabs to the tab with the given name.""" if not self._bottom_tabs: return for i, child in enumerate(self._bottom_tabs.children): if getattr(child, "name", "") == name: self._bottom_tabs.current_tab = i self._bottom_tabs._update_layout() return def _show_bottom_panel(self): if not self.config.bottom_panel_visible: self._on_toggle_bottom_panel() # -- File action delegates ------------------------------------------------- def _on_file_new(self): self._file_ctrl.on_file_new() def _on_file_open(self): self._file_ctrl.on_file_open()
[docs] def open_file(self, path: str): """Open a file in the editor panel.""" self._file_ctrl.open_file(path)
def _on_open_folder(self): self._file_ctrl.on_open_folder() def _on_file_save(self): self._file_ctrl.on_file_save() def _on_file_save_as(self): self._file_ctrl.on_file_save_as() def _on_file_close(self): self._file_ctrl.on_file_close() def _on_quit(self): self._file_ctrl.on_quit() # -- Edit action delegates ------------------------------------------------- def _on_undo(self): self._edit_ctrl.on_undo() def _on_redo(self): self._edit_ctrl.on_redo() def _on_cut(self): self._edit_ctrl.on_cut() def _on_copy(self): self._edit_ctrl.on_copy() def _on_paste(self): self._edit_ctrl.on_paste() def _on_select_all(self): self._edit_ctrl.on_select_all() def _on_toggle_comment(self): self._edit_ctrl.on_toggle_comment() def _on_delete_line(self): self._edit_ctrl.on_delete_line() def _on_select_next(self): self._edit_ctrl.on_select_next() def _on_format_document(self): self._edit_ctrl.on_format_document() def _on_duplicate_line(self): self._edit_ctrl.on_duplicate_line() def _on_move_line_up(self): self._edit_ctrl.on_move_line_up() def _on_move_line_down(self): self._edit_ctrl.on_move_line_down() def _on_fold(self): self._edit_ctrl.on_fold() def _on_unfold(self): self._edit_ctrl.on_unfold() # -- Bookmark delegates ---------------------------------------------------- def _on_toggle_bookmark(self): self._edit_ctrl.on_toggle_bookmark() def _on_next_bookmark(self): self._edit_ctrl.on_next_bookmark() def _on_prev_bookmark(self): self._edit_ctrl.on_prev_bookmark() # -- History delegates ----------------------------------------------------- def _on_history_back(self): self._edit_ctrl.on_history_back() def _on_history_forward(self): self._edit_ctrl.on_history_forward() # -- Find delegates -------------------------------------------------------- def _on_find(self): self._edit_ctrl.on_find() def _on_replace(self): self._edit_ctrl.on_replace() def _on_find_in_files(self): self._edit_ctrl.on_find_in_files() # -- Navigation ------------------------------------------------------------ def _on_goto_line(self): if self._goto_dialog: max_line = 1 if self._editor_panel: editor = self._editor_panel.get_current_editor() if editor: max_line = len(editor._lines) self._goto_dialog.show(max_line) def _on_goto_file(self): if self._command_palette: self._command_palette.show(file_mode=True) def _on_command_palette(self): if self._command_palette: self._command_palette.show() def _on_goto_definition(self): if self.state.active_file and self._lsp_client: self._lsp_client.request_definition( self.state.active_file, self.state.cursor_line, self.state.cursor_col ) def _on_find_references(self): if self.state.active_file and self._lsp_client: self._lsp_client.request_references( self.state.active_file, self.state.cursor_line, self.state.cursor_col ) def _on_goto_symbol(self): if self._command_palette: symbols = [] if self._symbol_outline and hasattr(self._symbol_outline, "get_symbols"): symbols = self._symbol_outline.get_symbols() self._command_palette.show(symbol_mode=True, symbols=symbols) def _update_window_title(self, path: str): """Update window title with the active filename.""" name = Path(path).name if path else "SimVX IDE" if self.app: self.app.title = f"{name} — SimVX IDE" def _on_goto_requested(self, path: str, line: int, col: int): self.state.push_cursor_history(path, line, col) if self._editor_panel: self._editor_panel.open_file(str(path)) self._editor_panel.goto_line(line, col) def _on_definition_received(self, locations: list): if locations: loc = locations[0] from .lsp.protocol import uri_to_path path = uri_to_path(loc.uri) self.state.goto_requested.emit(path, loc.range.start.line, loc.range.start.character) def _on_exception_occurred(self, path: str, line: int, message: str): self.state.status_message.emit(f"Exception: {message}") self.state.goto_requested.emit(path, line, 0) # -- View toggles ---------------------------------------------------------- def _on_zoom_in(self): self.config.font_size = min(32, self.config.font_size + 1) self._apply_font_size() def _on_zoom_out(self): self.config.font_size = max(8, self.config.font_size - 1) self._apply_font_size() def _apply_font_size(self): """Propagate font_size to open editors and save config.""" if self._editor_panel: self._editor_panel.set_all_editors_property("font_size", float(self.config.font_size)) self.config.save() self.state.status_message.emit(f"Font size: {self.config.font_size}") def _on_toggle_sidebar(self): self.config.sidebar_visible = not self.config.sidebar_visible if self._sidebar_split: if self.config.sidebar_visible: self._sidebar_split.split_ratio = self.config.sidebar_width / self.config.window_width else: self._sidebar_split.split_ratio = 0.001 self._sidebar_split._update_layout() self.state.sidebar_toggled.emit(self.config.sidebar_visible) def _on_toggle_terminal(self): self._show_bottom_panel() self._switch_to_tab("Terminal") def _on_toggle_bottom_panel(self): self.config.bottom_panel_visible = not self.config.bottom_panel_visible if self._main_split: if self.config.bottom_panel_visible: content_h = self.config.window_height - _MENUBAR_H - _STATUSBAR_H self._main_split.split_ratio = 1.0 - (self.config.bottom_panel_height / content_h) else: self._main_split.split_ratio = 0.999 self._main_split._update_layout() self.state.bottom_panel_toggled.emit(self.config.bottom_panel_visible) def _on_toggle_minimap(self): self.config.show_minimap = not self.config.show_minimap shell = getattr(self, "_shell", None) if shell and shell._minimap: shell._minimap.visible = self.config.show_minimap status = "shown" if self.config.show_minimap else "hidden" self.state.status_message.emit(f"Minimap {status}") def _on_setting_changed(self, attr: str, value): """Propagate a settings change to affected IDE components.""" if attr == "font_size" and self._editor_panel: self._editor_panel.set_all_editors_property("font_size", float(value)) elif attr == "tab_size" and self._editor_panel: self._editor_panel.set_all_editors_property("tab_size", int(value)) elif attr == "show_line_numbers" and self._editor_panel: self._editor_panel.set_all_editors_property("show_line_numbers", bool(value)) elif attr == "show_code_folding" and self._editor_panel: self._editor_panel.default_show_fold_gutter = bool(value) self._editor_panel.set_all_editors_property("_show_fold_gutter", bool(value)) elif attr == "show_indent_guides" and self._editor_panel: self._editor_panel.default_show_indent_guides = bool(value) self._editor_panel.set_all_editors_property("show_indent_guides", bool(value)) elif attr == "show_minimap": shell = getattr(self, "_shell", None) if shell and shell._minimap: shell._minimap.visible = bool(value) elif attr == "theme_preset": self._refresh_theme() def _refresh_theme(self): """Re-apply theme colours to all IDE panels that cache them.""" shell = getattr(self, "_shell", None) if shell and hasattr(shell, "refresh_theme"): shell.refresh_theme() def _on_show_settings(self): """Show the Settings tab in the bottom panel.""" self._show_bottom_panel() self._switch_to_tab("Settings") # -- Run / Debug delegates ------------------------------------------------- def _on_run_file(self): self._debug_ctrl.on_run_file() def _on_run_no_debug(self): self._debug_ctrl.on_run_no_debug() def _on_toggle_breakpoint(self): self._debug_ctrl.on_toggle_breakpoint() def _on_step_over(self): self._debug_ctrl.on_step_over() def _on_step_into(self): self._debug_ctrl.on_step_into() def _on_step_out(self): self._debug_ctrl.on_step_out() def _on_stop_debug(self): self._debug_ctrl.on_stop_debug() def _on_restart_debug(self): self._debug_ctrl.on_restart_debug() # -- LSP / Lint delegates -------------------------------------------------- def _on_lint_file(self): self._lsp_ctrl.on_lint_file() def _on_toggle_linting(self): self._lsp_ctrl.on_toggle_linting() def _on_toggle_lsp(self): self._lsp_ctrl.on_toggle_lsp() def _on_restart_lsp(self): self._lsp_ctrl.on_restart_lsp()
[docs] def main(argv=None): """Entry point for the SimVX IDE.""" if argv is None: argv = sys.argv[1:] from simvx.graphics import App app = App(title="SimVX IDE", width=1400, height=900, physics_fps=60, vsync=True) ide = IDERoot() # Queue CLI args to open after tree is ready if argv: _pending = list(argv) _orig_ready = ide.ready def _ready_with_args(): _orig_ready() for arg in _pending: p = Path(arg).resolve() if p.is_dir(): ide.state.project_root = str(p) if ide._file_browser: ide._file_browser.set_root(str(p)) ide.config.add_recent_folder(str(p)) elif p.is_file(): ide.open_file(str(p)) ide.ready = _ready_with_args app.run(ide)
if __name__ == "__main__": main()