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