from __future__ import annotations import json from dataclasses import asdict, dataclass from pathlib import Path from PySide6.QtCore import QStandardPaths from PySide6.QtGui import QKeySequence SHORTCUT_DEFINITIONS: list[tuple[str, str, str]] = [ ("open", "Открыть", "Ctrl+O"), ("save", "Сохранить", "Ctrl+S"), ("save_as", "Сохранить как", "Ctrl+Shift+S"), ("generate_script", "Сгенерировать скрипт", "Ctrl+G"), ("settings", "Параметры", "Ctrl+,"), ("undo", "Отменить", "Ctrl+Z"), ("redo", "Повторить", "Ctrl+Y"), ("delete", "Удалить фиксацию", "Del"), ("clear_all", "Удалить все", "Ctrl+Shift+Del"), ("clear_selection", "Очистить выбор", "Esc"), ("fit_view", "Показать всё", "Ctrl+0"), ("grid", "Сетка 34 мм", "G"), ("direction_arrows", "Стрелки направления", "Ctrl+Shift+A"), ("toggle_sidebar", "Скрыть боковую панель", "F9"), ("add_line", "Новая линия", "Ctrl+L"), ("auto_blocks", "Автоблоки", "Ctrl+Shift+B"), ("brush", "Кисть", "B"), ] DEFAULT_SHORTCUTS = {key: shortcut for key, _title, shortcut in SHORTCUT_DEFINITIONS} @dataclass class AppSettings: theme: str = "system" show_grid_by_default: bool = False show_direction_arrows_by_default: bool = True zoom_focus_pull: int = 35 pan_mode: str = "middle_space" instant_space_pan: bool = False brush_radius: int = 18 default_output_dir: str = "" confirm_partial_generation: bool = True shortcuts: dict[str, str] | None = None @classmethod def load(cls) -> "AppSettings": path = settings_path() if not path.exists(): return cls() try: data = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return cls() settings = cls() for key, value in data.items(): if hasattr(settings, key): setattr(settings, key, value) settings.zoom_focus_pull = clamp_int(settings.zoom_focus_pull, 0, 100, cls.zoom_focus_pull) settings.brush_radius = clamp_int(settings.brush_radius, 4, 80, cls.brush_radius) if settings.theme not in {"system", "light", "dark"}: settings.theme = cls.theme if settings.pan_mode not in {"middle", "space", "middle_space"}: settings.pan_mode = cls.pan_mode settings.instant_space_pan = bool(settings.instant_space_pan) settings.show_grid_by_default = bool(settings.show_grid_by_default) settings.show_direction_arrows_by_default = bool(settings.show_direction_arrows_by_default) settings.confirm_partial_generation = bool(settings.confirm_partial_generation) settings.default_output_dir = str(settings.default_output_dir or "") settings.shortcuts = normalize_shortcuts(settings.shortcuts) return settings def save(self) -> None: path = settings_path() path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(asdict(self), ensure_ascii=False, indent=2), encoding="utf-8") def shortcut_for(self, key: str) -> str: if self.shortcuts is None: return DEFAULT_SHORTCUTS.get(key, "") return self.shortcuts.get(key, DEFAULT_SHORTCUTS.get(key, "")) def settings_path() -> Path: config_dir = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation) if not config_dir: config_dir = str(Path.home() / ".led_mapper") return Path(config_dir) / "settings.json" def clamp_int(value, minimum: int, maximum: int, fallback: int) -> int: # noqa: ANN001 try: number = int(value) except (TypeError, ValueError): return fallback return max(minimum, min(maximum, number)) def normalize_shortcuts(value) -> dict[str, str]: # noqa: ANN001 source = value if isinstance(value, dict) else {} shortcuts: dict[str, str] = {} for key, default in DEFAULT_SHORTCUTS.items(): raw = source.get(key, default) if raw is None: raw = "" sequence = QKeySequence(str(raw)) shortcuts[key] = sequence.toString(QKeySequence.SequenceFormat.PortableText) return shortcuts