Source code for simvx.ide.panels.settings_panel

"""Settings panel -- IDE configuration editor with grouped settings."""


from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from simvx.core import Signal
from simvx.core.ui.core import Control
from simvx.core.ui.theme import get_theme

if TYPE_CHECKING:
    from ..config import IDEConfig
    from ..state import IDEState

log = logging.getLogger(__name__)

_HEADER_H = 28.0
_ROW_H = 28.0
_SECTION_H = 32.0
_PAD = 12.0


[docs] class SettingsPanel(Control): """IDE settings editor with grouped settings and auto-save.""" def __init__(self, state: IDEState, config: IDEConfig, **kwargs): super().__init__(**kwargs) self.name = "Settings" self._state = state self._config = config self.settings_changed = Signal() self._scroll_y: float = 0.0 self._rows: list[dict] = [] # Each: {"type": "section"|"bool"|"int"|"choice"|"str", ...} # Inline int editing state self._editing_row: dict | None = None self._edit_text: str = "" self._build_rows() def _build_rows(self): """Build the settings row definitions from current config.""" c = self._config self._rows = [ # Font & Editor {"type": "section", "label": "Editor"}, {"type": "int", "label": "Font Size", "attr": "font_size", "min": 8, "max": 32, "value": c.font_size}, {"type": "int", "label": "Tab Size", "attr": "tab_size", "min": 1, "max": 8, "value": c.tab_size}, {"type": "bool", "label": "Insert Spaces", "attr": "insert_spaces", "value": c.insert_spaces}, {"type": "bool", "label": "Line Numbers", "attr": "show_line_numbers", "value": c.show_line_numbers}, {"type": "bool", "label": "Minimap", "attr": "show_minimap", "value": c.show_minimap}, {"type": "bool", "label": "Code Folding", "attr": "show_code_folding", "value": c.show_code_folding}, {"type": "bool", "label": "Indent Guides", "attr": "show_indent_guides", "value": c.show_indent_guides}, {"type": "bool", "label": "Auto Save", "attr": "auto_save", "value": c.auto_save}, {"type": "bool", "label": "Format on Save", "attr": "format_on_save", "value": c.format_on_save}, # Theme {"type": "section", "label": "Appearance"}, {"type": "choice", "label": "Theme", "attr": "theme_preset", "options": ["dark", "abyss", "midnight", "light", "monokai", "solarised_dark", "nord"], "value": c.theme_preset}, # LSP {"type": "section", "label": "Language Server"}, {"type": "bool", "label": "LSP Enabled", "attr": "lsp_enabled", "value": c.lsp_enabled}, {"type": "str", "label": "LSP Command", "attr": "lsp_command", "value": c.lsp_command}, # Linting {"type": "section", "label": "Linting"}, {"type": "bool", "label": "Lint Enabled", "attr": "lint_enabled", "value": c.lint_enabled}, {"type": "bool", "label": "Lint on Save", "attr": "lint_on_save", "value": c.lint_on_save}, ] def _set_value(self, attr: str, value): """Set a config value and save.""" if attr == "theme_preset": self._config.apply_theme(value) else: setattr(self._config, attr, value) self._config.save() self.settings_changed.emit(attr, value) def _commit_edit(self): """Commit the current inline int edit.""" if self._editing_row is not None: try: val = int(self._edit_text) if self._edit_text else self._editing_row["value"] val = max(self._editing_row["min"], min(self._editing_row["max"], val)) self._editing_row["value"] = val self._set_value(self._editing_row["attr"], val) except ValueError: pass self._editing_row = None self._edit_text = "" def _on_gui_input(self, event): if event.key == "scroll_up": self._scroll_y = max(0, self._scroll_y - 30) return if event.key == "scroll_down": total_h = sum(_SECTION_H if r["type"] == "section" else _ROW_H for r in self._rows) + _PAD * 2 max_scroll = max(0, total_h - self.size.y + _HEADER_H) self._scroll_y = min(max_scroll, self._scroll_y + 30) return # Keyboard input for inline int editing if self._editing_row is not None and not event.button: if event.key == "enter" and event.pressed: self._commit_edit() return if event.key == "escape" and event.pressed: self._editing_row = None self._edit_text = "" return if event.key == "backspace" and event.pressed: self._edit_text = self._edit_text[:-1] return if event.char and event.char.isdigit(): self._edit_text += event.char return return # consume other keyboard events while editing # Handle clicks — commit any active edit first if event.button == 1 and event.pressed and self._editing_row is not None: self._commit_edit() # Handle clicks on boolean toggles, int steppers, and choice buttons if event.button == 1 and event.pressed: x, y, w, h = self.get_global_rect() py = event.position.y if hasattr(event.position, "y") else event.position[1] px = event.position.x if hasattr(event.position, "x") else event.position[0] content_y = y + _HEADER_H - self._scroll_y + _PAD for row in self._rows: row_h = _SECTION_H if row["type"] == "section" else _ROW_H if content_y <= py < content_y + row_h: if row["type"] == "bool": new_val = not row["value"] row["value"] = new_val self._set_value(row["attr"], new_val) elif row["type"] == "int": val_x = x + w * 0.6 # Three zones: [<] [value] [>] lt_end = val_x + 16 # "<" button zone # ">" starts after "< NN " text num_w = len(str(row["value"])) * 8 + 16 # approx text width gt_start = val_x + 16 + num_w if px < lt_end: row["value"] = max(row["min"], row["value"] - 1) self._set_value(row["attr"], row["value"]) elif px >= gt_start: row["value"] = min(row["max"], row["value"] + 1) self._set_value(row["attr"], row["value"]) else: # Click on the number — enter edit mode self._editing_row = row self._edit_text = str(row["value"]) elif row["type"] == "choice": opts = row["options"] idx = opts.index(row["value"]) if row["value"] in opts else 0 row["value"] = opts[(idx + 1) % len(opts)] self._set_value(row["attr"], row["value"]) return content_y += row_h # -- Draw ------------------------------------------------------------------
[docs] def draw(self, renderer): theme = get_theme() x, y, w, h = self.get_global_rect() renderer.draw_filled_rect(x, y, w, h, theme.panel_bg) scale = 1.0 # 14px base — readable even at small panel sizes # Header renderer.draw_filled_rect(x, y, w, _HEADER_H, theme.header_bg) renderer.draw_text_coloured("Settings", x + 8, y + (_HEADER_H - 14) / 2, scale, theme.text) renderer.draw_line_coloured(x, y + _HEADER_H, x + w, y + _HEADER_H, theme.border) # Content renderer.push_clip(x, y + _HEADER_H, w, h - _HEADER_H) cy = y + _HEADER_H - self._scroll_y + _PAD for row in self._rows: if row["type"] == "section": if cy + _SECTION_H > y + _HEADER_H and cy < y + h: renderer.draw_text_coloured(row["label"], x + _PAD, cy + 8, scale, theme.accent) renderer.draw_line_coloured( x + _PAD, cy + _SECTION_H - 2, x + w - _PAD, cy + _SECTION_H - 2, theme.border, ) cy += _SECTION_H continue if cy + _ROW_H > y + _HEADER_H and cy < y + h: # Label renderer.draw_text_coloured(row["label"], x + _PAD + 8, cy + 5, scale, theme.text) # Value controls val_x = x + w * 0.6 if row["type"] == "bool": indicator = "[x]" if row["value"] else "[ ]" colour = theme.accent if row["value"] else theme.text_dim renderer.draw_text_coloured(indicator, val_x, cy + 5, scale, colour) elif row["type"] == "int": btn_colour = theme.accent if self._editing_row is row: # Editing mode — show input with cursor renderer.draw_text_coloured("<", val_x, cy + 5, scale, btn_colour) input_x = val_x + renderer.text_width("< ", scale) # Input background renderer.draw_filled_rect(input_x - 2, cy + 2, 60, _ROW_H - 4, theme.bg_input) renderer.draw_rect_coloured(input_x - 2, cy + 2, 60, _ROW_H - 4, btn_colour) renderer.draw_text_coloured(self._edit_text, input_x + 2, cy + 5, scale, theme.text) gt_x = input_x + 64 renderer.draw_text_coloured(">", gt_x, cy + 5, scale, btn_colour) else: # Normal mode — show < value > renderer.draw_text_coloured("<", val_x, cy + 5, scale, btn_colour) num_text = str(row["value"]) num_x = val_x + renderer.text_width("< ", scale) renderer.draw_text_coloured(num_text, num_x, cy + 5, scale, theme.text) gt_x = num_x + renderer.text_width(num_text + " ", scale) renderer.draw_text_coloured(">", gt_x, cy + 5, scale, btn_colour) elif row["type"] == "choice": renderer.draw_text_coloured(str(row["value"]), val_x, cy + 5, scale, theme.accent) elif row["type"] == "str": display = str(row["value"]) if len(display) > 25: display = display[:22] + "..." renderer.draw_text_coloured(display, val_x, cy + 5, scale, theme.text_dim) cy += _ROW_H renderer.pop_clip()