from __future__ import annotations from dataclasses import replace from pathlib import Path from PySide6.QtGui import QKeySequence from PySide6.QtWidgets import ( QListWidget, QListWidgetItem, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, 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_GRID_STEP_MM, DEFAULT_LED_INDEX_FONT_SIZE_PX, DEFAULT_LED_POINT_DIAMETER_PX, DEFAULT_SHORTCUTS, MAX_GRID_STEP_MM, MAX_LED_INDEX_FONT_SIZE_PX, MAX_LED_POINT_DIAMETER_PX, MIN_GRID_STEP_MM, MIN_LED_INDEX_FONT_SIZE_PX, MIN_LED_POINT_DIAMETER_PX, SETTINGS_VERSION, SHORTCUT_DEFINITIONS, AppSettings, ) except ImportError: from localization import localize_dialog_buttons, show_warning from settings import ( DEFAULT_GRID_STEP_MM, DEFAULT_LED_INDEX_FONT_SIZE_PX, DEFAULT_LED_POINT_DIAMETER_PX, DEFAULT_SHORTCUTS, MAX_GRID_STEP_MM, MAX_LED_INDEX_FONT_SIZE_PX, MAX_LED_POINT_DIAMETER_PX, MIN_GRID_STEP_MM, MIN_LED_INDEX_FONT_SIZE_PX, MIN_LED_POINT_DIAMETER_PX, SETTINGS_VERSION, 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("Показывать сетку при запуске") 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.grid_step = QDoubleSpinBox() self.grid_step.setRange(MIN_GRID_STEP_MM, MAX_GRID_STEP_MM) self.grid_step.setDecimals(2) self.grid_step.setSingleStep(1.0) self.grid_step.setSuffix(" мм") self.grid_step.setValue(settings.grid_step_mm) self.grid_step_reset = QPushButton("Сбросить") self.grid_step_reset.clicked.connect( lambda: self.grid_step.setValue(DEFAULT_GRID_STEP_MM) ) self.led_point_diameter = QSpinBox() self.led_point_diameter.setRange( MIN_LED_POINT_DIAMETER_PX, MAX_LED_POINT_DIAMETER_PX, ) self.led_point_diameter.setSuffix(" px") self.led_point_diameter.setValue(settings.led_point_diameter_px) self.led_point_diameter_reset = QPushButton("Сбросить") self.led_point_diameter_reset.clicked.connect( lambda: self.led_point_diameter.setValue(DEFAULT_LED_POINT_DIAMETER_PX) ) self.led_index_font_size = QSpinBox() self.led_index_font_size.setRange( MIN_LED_INDEX_FONT_SIZE_PX, MAX_LED_INDEX_FONT_SIZE_PX, ) self.led_index_font_size.setSuffix(" px") self.led_index_font_size.setValue(settings.led_index_font_size_px) self.led_index_font_size_reset = QPushButton("Сбросить") self.led_index_font_size_reset.clicked.connect( lambda: self.led_index_font_size.setValue(DEFAULT_LED_INDEX_FONT_SIZE_PX) ) 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 replace( self._settings, version=SETTINGS_VERSION, 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(), grid_step_mm=self.grid_step.value(), led_point_diameter_px=self.led_point_diameter.value(), led_index_font_size_px=self.led_index_font_size.value(), 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._setting_with_reset(self.grid_step, self.grid_step_reset)) layout.addRow( "Диаметр LED-точек", self._setting_with_reset(self.led_point_diameter, self.led_point_diameter_reset), ) layout.addRow( "Размер текста индексов LED", self._setting_with_reset(self.led_index_font_size, self.led_index_font_size_reset), ) layout.addRow("Притяжение зума к курсору", self.zoom_pull) layout.addRow(QLabel("0% оставляет центр спокойнее, 100% сильнее тянет схему к курсору.")) return page @staticmethod def _setting_with_reset(editor: QWidget, reset: QPushButton) -> QWidget: row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.addWidget(editor, 1) row_layout.addWidget(reset) return row 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)