from __future__ import annotations from pathlib import Path from PySide6.QtGui import QKeySequence from PySide6.QtWidgets import ( QListWidget, QListWidgetItem, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QHBoxLayout, QKeySequenceEdit, QLabel, QLineEdit, QPushButton, QScrollArea, QSpinBox, QStackedWidget, QVBoxLayout, QWidget, ) try: from .localization import localize_dialog_buttons, show_warning from .settings import DEFAULT_SHORTCUTS, SHORTCUT_DEFINITIONS, AppSettings except ImportError: from localization import localize_dialog_buttons, show_warning from settings import DEFAULT_SHORTCUTS, SHORTCUT_DEFINITIONS, AppSettings class SettingsDialog(QDialog): def __init__(self, settings: AppSettings, parent=None) -> None: # noqa: ANN001 super().__init__(parent) self.setWindowTitle("Параметры") self.setMinimumSize(520, 360) self._settings = settings self.theme = QComboBox() self.theme.addItem("Системная", "system") self.theme.addItem("Светлая", "light") self.theme.addItem("Темная", "dark") self.theme.setCurrentIndex(max(0, self.theme.findData(settings.theme))) self.show_grid = QCheckBox("Показывать сетку 34 мм при запуске") self.show_grid.setChecked(settings.show_grid_by_default) self.show_arrows = QCheckBox("Показывать стрелки направления при запуске") self.show_arrows.setChecked(settings.show_direction_arrows_by_default) self.show_led_ids = QCheckBox("Показывать id светодиодов при выборе") self.show_led_ids.setChecked(settings.show_led_ids_by_default) self.zoom_pull = QSpinBox() self.zoom_pull.setRange(0, 100) self.zoom_pull.setSuffix(" %") self.zoom_pull.setValue(settings.zoom_focus_pull) self.pan_mode = QComboBox() self.pan_mode.addItem("СКМ", "middle") self.pan_mode.addItem("Пробел", "space") self.pan_mode.addItem("СКМ + Пробел", "middle_space") self.pan_mode.setCurrentIndex(max(0, self.pan_mode.findData(settings.pan_mode))) self.instant_space_pan = QCheckBox("Сразу перетаскивать на пробел") self.instant_space_pan.setChecked(settings.instant_space_pan) self.brush_radius = QSpinBox() self.brush_radius.setRange(4, 80) self.brush_radius.setSuffix(" px") self.brush_radius.setValue(settings.brush_radius) self.output_dir = QLineEdit(settings.default_output_dir) self.output_dir.setPlaceholderText("Выбирать папку при генерации") self.confirm_partial = QCheckBox("Предупреждать, если назначены не все LED") self.confirm_partial.setChecked(settings.confirm_partial_generation) self.shortcut_edits: dict[str, QKeySequenceEdit] = {} self.section_list = QListWidget() self.section_list.setFixedWidth(170) self.section_stack = QStackedWidget() self._add_section("Внешний вид", self._build_appearance_tab()) self._add_section("Поведение", self._build_behavior_tab()) self._add_section("Инструменты", self._build_tools_tab()) self._add_section("Файлы и генерация", self._build_files_tab()) self._add_section("Комбинации клавиш", self._build_shortcuts_tab()) self.section_list.setCurrentRow(0) self.section_list.currentRowChanged.connect(self.section_stack.setCurrentIndex) content = QWidget() content_layout = QHBoxLayout(content) content_layout.setContentsMargins(0, 0, 0, 0) content_layout.addWidget(self.section_list) content_layout.addWidget(self.section_stack, 1) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) localize_dialog_buttons(buttons) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout = QVBoxLayout(self) layout.addWidget(content) layout.addWidget(buttons) def _add_section(self, title: str, widget: QWidget) -> None: item = QListWidgetItem(title) self.section_list.addItem(item) self.section_stack.addWidget(widget) def settings(self) -> AppSettings: return AppSettings( theme=str(self.theme.currentData()), show_grid_by_default=self.show_grid.isChecked(), show_direction_arrows_by_default=self.show_arrows.isChecked(), show_led_ids_by_default=self.show_led_ids.isChecked(), zoom_focus_pull=self.zoom_pull.value(), pan_mode=str(self.pan_mode.currentData()), instant_space_pan=self.instant_space_pan.isChecked(), brush_radius=self.brush_radius.value(), default_output_dir=self.output_dir.text().strip(), confirm_partial_generation=self.confirm_partial.isChecked(), shortcuts=self._shortcuts(), ) def _build_appearance_tab(self) -> QWidget: page = QWidget() layout = QFormLayout(page) layout.addRow("Тема приложения", self.theme) layout.addRow(self.show_grid) layout.addRow(self.show_arrows) layout.addRow(self.show_led_ids) layout.addRow("Притяжение зума к курсору", self.zoom_pull) layout.addRow(QLabel("0% оставляет центр спокойнее, 100% сильнее тянет схему к курсору.")) return page def _build_behavior_tab(self) -> QWidget: page = QWidget() layout = QFormLayout(page) layout.addRow("Перемещение", self.pan_mode) layout.addRow(self.instant_space_pan) layout.addRow(QLabel("Пробел означает: удерживайте Пробел и перетаскивайте схему ЛКМ.")) return page def _build_tools_tab(self) -> QWidget: page = QWidget() layout = QFormLayout(page) layout.addRow("Радиус кисти", self.brush_radius) layout.addRow(QLabel("Обычный мазок создает новый блок. Shift + ЛКМ добавляет точки к выбранному блоку.")) return page def _build_files_tab(self) -> QWidget: page = QWidget() form = QFormLayout(page) row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) browse = QPushButton("Выбрать") browse.clicked.connect(self._browse_output_dir) row_layout.addWidget(self.output_dir, 1) row_layout.addWidget(browse) form.addRow("Папка вывода", row) form.addRow(self.confirm_partial) return page def _build_shortcuts_tab(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(0, 0, 0, 0) scroll_content = QWidget() form = QFormLayout() form.setContentsMargins(0, 0, 0, 0) for key, title, _default_shortcut in SHORTCUT_DEFINITIONS: edit = QKeySequenceEdit(QKeySequence(self._settings.shortcut_for(key))) edit.setClearButtonEnabled(True) edit.setMinimumWidth(190) self.shortcut_edits[key] = edit form.addRow(title, edit) scroll_content.setLayout(form) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFrameShape(QScrollArea.NoFrame) scroll_area.setWidget(scroll_content) reset = QPushButton("Сбросить по умолчанию") reset.clicked.connect(self._reset_shortcuts) layout.addWidget(scroll_area, 1) layout.addWidget(reset) layout.addWidget(QLabel("Сочетания работают по физическим клавишам и не зависят от раскладки клавиатуры.")) return page def accept(self) -> None: duplicates: dict[str, list[str]] = {} for key, sequence in self._shortcuts().items(): if not sequence: continue duplicates.setdefault(sequence, []).append(key) conflict = next((keys for keys in duplicates.values() if len(keys) > 1), None) if conflict: names = {key: title for key, title, _shortcut in SHORTCUT_DEFINITIONS} show_warning( self, "Конфликт сочетаний", "Одно сочетание назначено нескольким действиям:\n" + "\n".join(names.get(key, key) for key in conflict), ) return super().accept() def _shortcuts(self) -> dict[str, str]: shortcuts: dict[str, str] = {} for key, edit in self.shortcut_edits.items(): shortcuts[key] = edit.keySequence().toString(QKeySequence.SequenceFormat.PortableText) return shortcuts def _reset_shortcuts(self) -> None: for key, edit in self.shortcut_edits.items(): edit.setKeySequence(QKeySequence(DEFAULT_SHORTCUTS.get(key, ""))) def _browse_output_dir(self) -> None: start = self.output_dir.text().strip() if not start or not Path(start).exists(): start = str(Path.home()) selected = QFileDialog.getExistingDirectory(self, "Папка вывода", start) if selected: self.output_dir.setText(selected)