|
|
@@ -0,0 +1,618 @@
|
|
|
+"""
|
|
|
+Settings Dialog for UI configuration.
|
|
|
+
|
|
|
+This module provides the settings dialog for configuring the application
|
|
|
+(Story 7.10).
|
|
|
+"""
|
|
|
+
|
|
|
+import json
|
|
|
+from pathlib import Path
|
|
|
+from typing import Optional, Dict, Any
|
|
|
+
|
|
|
+from PyQt6.QtWidgets import (
|
|
|
+ QDialog,
|
|
|
+ QVBoxLayout,
|
|
|
+ QHBoxLayout,
|
|
|
+ QTabWidget,
|
|
|
+ QWidget,
|
|
|
+ QGroupBox,
|
|
|
+ QLabel,
|
|
|
+ QLineEdit,
|
|
|
+ QSpinBox,
|
|
|
+ QComboBox,
|
|
|
+ QPushButton,
|
|
|
+ QCheckBox,
|
|
|
+ QFileDialog,
|
|
|
+ QMessageBox,
|
|
|
+ QScrollArea,
|
|
|
+)
|
|
|
+from PyQt6.QtCore import Qt, pyqtSignal
|
|
|
+from PyQt6.QtGui import QFont
|
|
|
+
|
|
|
+
|
|
|
+class SettingsDialog(QDialog):
|
|
|
+ """
|
|
|
+ Settings dialog for application configuration (Story 7.10).
|
|
|
+
|
|
|
+ Features:
|
|
|
+ - GPU settings (device selection)
|
|
|
+ - Translation model settings (model path)
|
|
|
+ - General settings (working directory, languages)
|
|
|
+ - Save/load configuration to file
|
|
|
+ """
|
|
|
+
|
|
|
+ # Signal emitted when settings are applied
|
|
|
+ settings_changed = pyqtSignal(dict) # settings dictionary
|
|
|
+
|
|
|
+ # Configuration file path
|
|
|
+ DEFAULT_CONFIG_PATH = Path.home() / ".bmad_translator" / "settings.json"
|
|
|
+
|
|
|
+ # Default settings
|
|
|
+ DEFAULT_SETTINGS = {
|
|
|
+ "gpu": {
|
|
|
+ "use_gpu": False,
|
|
|
+ "device_index": 0,
|
|
|
+ },
|
|
|
+ "model": {
|
|
|
+ "model_path": "models/m2m100",
|
|
|
+ "model_name": "facebook/m2m100_418M",
|
|
|
+ "max_length": 512,
|
|
|
+ },
|
|
|
+ "general": {
|
|
|
+ "work_dir": "data/work",
|
|
|
+ "source_language": "Chinese",
|
|
|
+ "target_language": "English",
|
|
|
+ "auto_save": True,
|
|
|
+ "checkpoint_interval": 5,
|
|
|
+ },
|
|
|
+ "ui": {
|
|
|
+ "theme": "default",
|
|
|
+ "show_tooltips": True,
|
|
|
+ "confirm_on_exit": True,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ # Language options
|
|
|
+ LANGUAGE_OPTIONS = [
|
|
|
+ "Chinese",
|
|
|
+ "English",
|
|
|
+ "Japanese",
|
|
|
+ "Korean",
|
|
|
+ "French",
|
|
|
+ "German",
|
|
|
+ "Spanish",
|
|
|
+ "Russian",
|
|
|
+ "Portuguese",
|
|
|
+ "Italian",
|
|
|
+ ]
|
|
|
+
|
|
|
+ # Theme options
|
|
|
+ THEME_OPTIONS = ["default", "light", "dark"]
|
|
|
+
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ settings: Optional[Dict[str, Any]] = None,
|
|
|
+ config_path: Optional[Path] = None,
|
|
|
+ parent: Optional[QWidget] = None,
|
|
|
+ ) -> None:
|
|
|
+ """
|
|
|
+ Initialize the settings dialog.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ settings: Initial settings dictionary
|
|
|
+ config_path: Path to save configuration file
|
|
|
+ parent: Parent widget
|
|
|
+ """
|
|
|
+ super().__init__(parent)
|
|
|
+
|
|
|
+ self._settings = settings or self.DEFAULT_SETTINGS.copy()
|
|
|
+ self._config_path = config_path or self.DEFAULT_CONFIG_PATH
|
|
|
+ self._original_settings = self._settings.copy()
|
|
|
+
|
|
|
+ self._setup_ui()
|
|
|
+ self._load_settings_to_ui()
|
|
|
+
|
|
|
+ def _setup_ui(self) -> None:
|
|
|
+ """Set up the dialog UI."""
|
|
|
+ self.setWindowTitle("Settings")
|
|
|
+ self.setMinimumSize(600, 500)
|
|
|
+
|
|
|
+ layout = QVBoxLayout(self)
|
|
|
+
|
|
|
+ # Create tab widget
|
|
|
+ self._tab_widget = QTabWidget()
|
|
|
+ layout.addWidget(self._tab_widget)
|
|
|
+
|
|
|
+ # Add tabs
|
|
|
+ self._create_gpu_tab()
|
|
|
+ self._create_model_tab()
|
|
|
+ self._create_general_tab()
|
|
|
+ self._create_ui_tab()
|
|
|
+
|
|
|
+ # Button box
|
|
|
+ button_layout = QHBoxLayout()
|
|
|
+ button_layout.addStretch()
|
|
|
+
|
|
|
+ self._reset_btn = QPushButton("Reset to Defaults")
|
|
|
+ self._reset_btn.clicked.connect(self._on_reset)
|
|
|
+ button_layout.addWidget(self._reset_btn)
|
|
|
+
|
|
|
+ self._apply_btn = QPushButton("Apply")
|
|
|
+ self._apply_btn.clicked.connect(self._on_apply)
|
|
|
+ button_layout.addWidget(self._apply_btn)
|
|
|
+
|
|
|
+ self._ok_btn = QPushButton("OK")
|
|
|
+ self._ok_btn.clicked.connect(self._on_ok)
|
|
|
+ button_layout.addWidget(self._ok_btn)
|
|
|
+
|
|
|
+ self._cancel_btn = QPushButton("Cancel")
|
|
|
+ self._cancel_btn.clicked.connect(self._on_cancel)
|
|
|
+ button_layout.addWidget(self._cancel_btn)
|
|
|
+
|
|
|
+ layout.addLayout(button_layout)
|
|
|
+
|
|
|
+ def _create_gpu_tab(self) -> None:
|
|
|
+ """Create the GPU settings tab."""
|
|
|
+ tab = QWidget()
|
|
|
+ layout = QVBoxLayout(tab)
|
|
|
+ layout.setSpacing(12)
|
|
|
+
|
|
|
+ # GPU Settings Group
|
|
|
+ gpu_group = QGroupBox("GPU Settings")
|
|
|
+ gpu_layout = QVBoxLayout(gpu_group)
|
|
|
+ gpu_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Use GPU checkbox
|
|
|
+ use_gpu_layout = QHBoxLayout()
|
|
|
+ self._use_gpu_checkbox = QCheckBox("Use GPU for translation")
|
|
|
+ self._use_gpu_checkbox.setToolTip(
|
|
|
+ "Enable GPU acceleration for faster translation. "
|
|
|
+ "Requires CUDA-compatible GPU."
|
|
|
+ )
|
|
|
+ use_gpu_layout.addWidget(self._use_gpu_checkbox)
|
|
|
+ use_gpu_layout.addStretch()
|
|
|
+ gpu_layout.addLayout(use_gpu_layout)
|
|
|
+
|
|
|
+ # Device index
|
|
|
+ device_layout = QHBoxLayout()
|
|
|
+ device_label = QLabel("GPU Device Index:")
|
|
|
+ device_label.setMinimumWidth(150)
|
|
|
+ device_layout.addWidget(device_label)
|
|
|
+
|
|
|
+ self._device_index_spin = QSpinBox()
|
|
|
+ self._device_index_spin.setRange(0, 7)
|
|
|
+ self._device_index_spin.setValue(0)
|
|
|
+ self._device_index_spin.setToolTip(
|
|
|
+ "Select which GPU to use (0-7). "
|
|
|
+ "Use nvidia-smi to see available GPUs."
|
|
|
+ )
|
|
|
+ device_layout.addWidget(self._device_index_spin)
|
|
|
+ device_layout.addStretch()
|
|
|
+ gpu_layout.addLayout(device_layout)
|
|
|
+
|
|
|
+ layout.addWidget(gpu_group)
|
|
|
+
|
|
|
+ # GPU Info Group
|
|
|
+ info_group = QGroupBox("GPU Information")
|
|
|
+ info_layout = QVBoxLayout(info_group)
|
|
|
+
|
|
|
+ info_label = QLabel(
|
|
|
+ "GPU acceleration requires:\n"
|
|
|
+ "• NVIDIA GPU with CUDA support\n"
|
|
|
+ "• PyTorch with CUDA enabled\n"
|
|
|
+ "• CUDA toolkit installed\n\n"
|
|
|
+ "Check GPU availability with: python -c 'import torch; print(torch.cuda.is_available())'"
|
|
|
+ )
|
|
|
+ info_label.setWordWrap(True)
|
|
|
+ info_label.setStyleSheet("color: gray; font-size: 11px;")
|
|
|
+ info_layout.addWidget(info_label)
|
|
|
+
|
|
|
+ layout.addWidget(info_group)
|
|
|
+ layout.addStretch()
|
|
|
+
|
|
|
+ self._tab_widget.addTab(tab, "GPU")
|
|
|
+
|
|
|
+ def _create_model_tab(self) -> None:
|
|
|
+ """Create the model settings tab."""
|
|
|
+ tab = QWidget()
|
|
|
+ layout = QVBoxLayout(tab)
|
|
|
+ layout.setSpacing(12)
|
|
|
+
|
|
|
+ # Model Path Group
|
|
|
+ model_group = QGroupBox("Translation Model")
|
|
|
+ model_layout = QVBoxLayout(model_group)
|
|
|
+ model_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Model path
|
|
|
+ path_layout = QHBoxLayout()
|
|
|
+ path_label = QLabel("Model Path:")
|
|
|
+ path_label.setMinimumWidth(150)
|
|
|
+ path_layout.addWidget(path_label)
|
|
|
+
|
|
|
+ self._model_path_input = QLineEdit()
|
|
|
+ self._model_path_input.setPlaceholderText("models/m2m100")
|
|
|
+ path_layout.addWidget(self._model_path_input)
|
|
|
+
|
|
|
+ browse_btn = QPushButton("Browse...")
|
|
|
+ browse_btn.clicked.connect(self._on_browse_model_path)
|
|
|
+ path_layout.addWidget(browse_btn)
|
|
|
+ model_layout.addLayout(path_layout)
|
|
|
+
|
|
|
+ # Model name
|
|
|
+ name_layout = QHBoxLayout()
|
|
|
+ name_label = QLabel("Model Name:")
|
|
|
+ name_label.setMinimumWidth(150)
|
|
|
+ name_layout.addWidget(name_label)
|
|
|
+
|
|
|
+ self._model_name_input = QLineEdit()
|
|
|
+ self._model_name_input.setPlaceholderText("facebook/m2m100_418M")
|
|
|
+ self._model_name_input.setToolTip(
|
|
|
+ "Hugging Face model identifier. "
|
|
|
+ "Common options: facebook/m2m100_418M, facebook/m2m100_1.2B"
|
|
|
+ )
|
|
|
+ name_layout.addWidget(self._model_name_input)
|
|
|
+ model_layout.addLayout(name_layout)
|
|
|
+
|
|
|
+ # Max length
|
|
|
+ length_layout = QHBoxLayout()
|
|
|
+ length_label = QLabel("Max Sequence Length:")
|
|
|
+ length_label.setMinimumWidth(150)
|
|
|
+ length_layout.addWidget(length_label)
|
|
|
+
|
|
|
+ self._max_length_spin = QSpinBox()
|
|
|
+ self._max_length_spin.setRange(128, 2048)
|
|
|
+ self._max_length_spin.setValue(512)
|
|
|
+ self._max_length_spin.setSingleStep(64)
|
|
|
+ self._max_length_spin.setToolTip(
|
|
|
+ "Maximum sequence length for translation. "
|
|
|
+ "Longer sequences use more memory."
|
|
|
+ )
|
|
|
+ length_layout.addWidget(self._max_length_spin)
|
|
|
+ length_layout.addStretch()
|
|
|
+ model_layout.addLayout(length_layout)
|
|
|
+
|
|
|
+ layout.addWidget(model_group)
|
|
|
+
|
|
|
+ # Model Info Group
|
|
|
+ info_group = QGroupBox("Model Information")
|
|
|
+ info_layout = QVBoxLayout(info_group)
|
|
|
+
|
|
|
+ info_label = QLabel(
|
|
|
+ "Available m2m100 models:\n"
|
|
|
+ "• facebook/m2m100_418M (~1.9GB) - Recommended\n"
|
|
|
+ "• facebook/m2m100_1.2B (~4.8GB) - Higher quality\n\n"
|
|
|
+ "Models will be downloaded automatically if not found."
|
|
|
+ )
|
|
|
+ info_label.setWordWrap(True)
|
|
|
+ info_label.setStyleSheet("color: gray; font-size: 11px;")
|
|
|
+ info_layout.addWidget(info_label)
|
|
|
+
|
|
|
+ layout.addWidget(info_group)
|
|
|
+ layout.addStretch()
|
|
|
+
|
|
|
+ self._tab_widget.addTab(tab, "Model")
|
|
|
+
|
|
|
+ def _create_general_tab(self) -> None:
|
|
|
+ """Create the general settings tab."""
|
|
|
+ tab = QWidget()
|
|
|
+ layout = QVBoxLayout(tab)
|
|
|
+ layout.setSpacing(12)
|
|
|
+
|
|
|
+ # Work Directory Group
|
|
|
+ work_group = QGroupBox("Working Directory")
|
|
|
+ work_layout = QVBoxLayout(work_group)
|
|
|
+
|
|
|
+ path_layout = QHBoxLayout()
|
|
|
+ path_label = QLabel("Work Directory:")
|
|
|
+ path_label.setMinimumWidth(150)
|
|
|
+ path_layout.addWidget(path_label)
|
|
|
+
|
|
|
+ self._work_dir_input = QLineEdit()
|
|
|
+ path_layout.addWidget(self._work_dir_input)
|
|
|
+
|
|
|
+ browse_btn = QPushButton("Browse...")
|
|
|
+ browse_btn.clicked.connect(self._on_browse_work_dir)
|
|
|
+ path_layout.addWidget(browse_btn)
|
|
|
+ work_layout.addLayout(path_layout)
|
|
|
+
|
|
|
+ layout.addWidget(work_group)
|
|
|
+
|
|
|
+ # Language Settings Group
|
|
|
+ lang_group = QGroupBox("Language Settings")
|
|
|
+ lang_layout = QVBoxLayout(lang_group)
|
|
|
+ lang_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Source language
|
|
|
+ source_layout = QHBoxLayout()
|
|
|
+ source_label = QLabel("Source Language:")
|
|
|
+ source_label.setMinimumWidth(150)
|
|
|
+ source_layout.addWidget(source_label)
|
|
|
+
|
|
|
+ self._source_lang_combo = QComboBox()
|
|
|
+ self._source_lang_combo.addItems(self.LANGUAGE_OPTIONS)
|
|
|
+ source_layout.addWidget(self._source_lang_combo)
|
|
|
+ lang_layout.addLayout(source_layout)
|
|
|
+
|
|
|
+ # Target language
|
|
|
+ target_layout = QHBoxLayout()
|
|
|
+ target_label = QLabel("Target Language:")
|
|
|
+ target_label.setMinimumWidth(150)
|
|
|
+ target_layout.addWidget(target_label)
|
|
|
+
|
|
|
+ self._target_lang_combo = QComboBox()
|
|
|
+ self._target_lang_combo.addItems(self.LANGUAGE_OPTIONS)
|
|
|
+ target_layout.addWidget(self._target_lang_combo)
|
|
|
+ lang_layout.addLayout(target_layout)
|
|
|
+
|
|
|
+ layout.addWidget(lang_group)
|
|
|
+
|
|
|
+ # Auto-save Settings Group
|
|
|
+ autosave_group = QGroupBox("Auto-save")
|
|
|
+ autosave_layout = QVBoxLayout(autosave_group)
|
|
|
+ autosave_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Enable auto-save
|
|
|
+ autosave_check_layout = QHBoxLayout()
|
|
|
+ self._auto_save_checkbox = QCheckBox("Enable automatic saving")
|
|
|
+ autosave_check_layout.addWidget(self._auto_save_checkbox)
|
|
|
+ autosave_check_layout.addStretch()
|
|
|
+ autosave_layout.addLayout(autosave_check_layout)
|
|
|
+
|
|
|
+ # Checkpoint interval
|
|
|
+ checkpoint_layout = QHBoxLayout()
|
|
|
+ checkpoint_label = QLabel("Checkpoint Interval:")
|
|
|
+ checkpoint_label.setMinimumWidth(150)
|
|
|
+ checkpoint_layout.addWidget(checkpoint_label)
|
|
|
+
|
|
|
+ self._checkpoint_spin = QSpinBox()
|
|
|
+ self._checkpoint_spin.setRange(1, 100)
|
|
|
+ self._checkpoint_spin.setValue(5)
|
|
|
+ self._checkpoint_spin.setSuffix(" chapters")
|
|
|
+ self._checkpoint_spin.setToolTip(
|
|
|
+ "Save checkpoint every N chapters. "
|
|
|
+ "Used for crash recovery."
|
|
|
+ )
|
|
|
+ checkpoint_layout.addWidget(self._checkpoint_spin)
|
|
|
+ checkpoint_layout.addStretch()
|
|
|
+ autosave_layout.addLayout(checkpoint_layout)
|
|
|
+
|
|
|
+ layout.addWidget(autosave_group)
|
|
|
+ layout.addStretch()
|
|
|
+
|
|
|
+ self._tab_widget.addTab(tab, "General")
|
|
|
+
|
|
|
+ def _create_ui_tab(self) -> None:
|
|
|
+ """Create the UI settings tab."""
|
|
|
+ tab = QWidget()
|
|
|
+ layout = QVBoxLayout(tab)
|
|
|
+ layout.setSpacing(12)
|
|
|
+
|
|
|
+ # Appearance Group
|
|
|
+ appearance_group = QGroupBox("Appearance")
|
|
|
+ appearance_layout = QVBoxLayout(appearance_group)
|
|
|
+ appearance_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Theme
|
|
|
+ theme_layout = QHBoxLayout()
|
|
|
+ theme_label = QLabel("Theme:")
|
|
|
+ theme_label.setMinimumWidth(150)
|
|
|
+ theme_layout.addWidget(theme_label)
|
|
|
+
|
|
|
+ self._theme_combo = QComboBox()
|
|
|
+ self._theme_combo.addItems(self.THEME_OPTIONS)
|
|
|
+ theme_layout.addWidget(self._theme_combo)
|
|
|
+ theme_layout.addStretch()
|
|
|
+ appearance_layout.addLayout(theme_layout)
|
|
|
+
|
|
|
+ layout.addWidget(appearance_group)
|
|
|
+
|
|
|
+ # Behavior Group
|
|
|
+ behavior_group = QGroupBox("Behavior")
|
|
|
+ behavior_layout = QVBoxLayout(behavior_group)
|
|
|
+ behavior_layout.setSpacing(8)
|
|
|
+
|
|
|
+ # Show tooltips
|
|
|
+ self._show_tooltips_checkbox = QCheckBox("Show tooltips")
|
|
|
+ behavior_layout.addWidget(self._show_tooltips_checkbox)
|
|
|
+
|
|
|
+ # Confirm on exit
|
|
|
+ self._confirm_exit_checkbox = QCheckBox("Confirm before exiting")
|
|
|
+ behavior_layout.addWidget(self._confirm_exit_checkbox)
|
|
|
+
|
|
|
+ layout.addWidget(behavior_group)
|
|
|
+ layout.addStretch()
|
|
|
+
|
|
|
+ self._tab_widget.addTab(tab, "Interface")
|
|
|
+
|
|
|
+ def _load_settings_to_ui(self) -> None:
|
|
|
+ """Load settings values into UI controls."""
|
|
|
+ settings = self._settings
|
|
|
+
|
|
|
+ # GPU settings
|
|
|
+ gpu = settings.get("gpu", {})
|
|
|
+ self._use_gpu_checkbox.setChecked(gpu.get("use_gpu", False))
|
|
|
+ self._device_index_spin.setValue(gpu.get("device_index", 0))
|
|
|
+
|
|
|
+ # Model settings
|
|
|
+ model = settings.get("model", {})
|
|
|
+ self._model_path_input.setText(model.get("model_path", "models/m2m100"))
|
|
|
+ self._model_name_input.setText(model.get("model_name", "facebook/m2m100_418M"))
|
|
|
+ self._max_length_spin.setValue(model.get("max_length", 512))
|
|
|
+
|
|
|
+ # General settings
|
|
|
+ general = settings.get("general", {})
|
|
|
+ self._work_dir_input.setText(general.get("work_dir", "data/work"))
|
|
|
+
|
|
|
+ source_lang = general.get("source_language", "Chinese")
|
|
|
+ target_lang = general.get("target_language", "English")
|
|
|
+ self._source_lang_combo.setCurrentText(source_lang)
|
|
|
+ self._target_lang_combo.setCurrentText(target_lang)
|
|
|
+
|
|
|
+ self._auto_save_checkbox.setChecked(general.get("auto_save", True))
|
|
|
+ self._checkpoint_spin.setValue(general.get("checkpoint_interval", 5))
|
|
|
+
|
|
|
+ # UI settings
|
|
|
+ ui = settings.get("ui", {})
|
|
|
+ self._theme_combo.setCurrentText(ui.get("theme", "default"))
|
|
|
+ self._show_tooltips_checkbox.setChecked(ui.get("show_tooltips", True))
|
|
|
+ self._confirm_exit_checkbox.setChecked(ui.get("confirm_on_exit", True))
|
|
|
+
|
|
|
+ def _get_settings_from_ui(self) -> Dict[str, Any]:
|
|
|
+ """Get current settings values from UI controls."""
|
|
|
+ return {
|
|
|
+ "gpu": {
|
|
|
+ "use_gpu": self._use_gpu_checkbox.isChecked(),
|
|
|
+ "device_index": self._device_index_spin.value(),
|
|
|
+ },
|
|
|
+ "model": {
|
|
|
+ "model_path": self._model_path_input.text().strip() or "models/m2m100",
|
|
|
+ "model_name": self._model_name_input.text().strip() or "facebook/m2m100_418M",
|
|
|
+ "max_length": self._max_length_spin.value(),
|
|
|
+ },
|
|
|
+ "general": {
|
|
|
+ "work_dir": self._work_dir_input.text().strip() or "data/work",
|
|
|
+ "source_language": self._source_lang_combo.currentText(),
|
|
|
+ "target_language": self._target_lang_combo.currentText(),
|
|
|
+ "auto_save": self._auto_save_checkbox.isChecked(),
|
|
|
+ "checkpoint_interval": self._checkpoint_spin.value(),
|
|
|
+ },
|
|
|
+ "ui": {
|
|
|
+ "theme": self._theme_combo.currentText(),
|
|
|
+ "show_tooltips": self._show_tooltips_checkbox.isChecked(),
|
|
|
+ "confirm_on_exit": self._confirm_exit_checkbox.isChecked(),
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ def _on_browse_model_path(self) -> None:
|
|
|
+ """Handle browse model path button click."""
|
|
|
+ dialog = QFileDialog(self)
|
|
|
+ dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
|
+ dialog.setDirectory(self._model_path_input.text())
|
|
|
+
|
|
|
+ if dialog.exec():
|
|
|
+ self._model_path_input.setText(dialog.selectedFiles()[0])
|
|
|
+
|
|
|
+ def _on_browse_work_dir(self) -> None:
|
|
|
+ """Handle browse work directory button click."""
|
|
|
+ dialog = QFileDialog(self)
|
|
|
+ dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
|
+ dialog.setDirectory(self._work_dir_input.text())
|
|
|
+
|
|
|
+ if dialog.exec():
|
|
|
+ self._work_dir_input.setText(dialog.selectedFiles()[0])
|
|
|
+
|
|
|
+ def _on_reset(self) -> None:
|
|
|
+ """Handle reset button click."""
|
|
|
+ reply = QMessageBox.question(
|
|
|
+ self,
|
|
|
+ "Reset Settings",
|
|
|
+ "Are you sure you want to reset all settings to defaults?",
|
|
|
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
+ QMessageBox.StandardButton.No,
|
|
|
+ )
|
|
|
+
|
|
|
+ if reply == QMessageBox.StandardButton.Yes:
|
|
|
+ self._settings = self.DEFAULT_SETTINGS.copy()
|
|
|
+ self._load_settings_to_ui()
|
|
|
+
|
|
|
+ def _on_apply(self) -> None:
|
|
|
+ """Handle apply button click."""
|
|
|
+ self._settings = self._get_settings_from_ui()
|
|
|
+ self.settings_changed.emit(self._settings)
|
|
|
+
|
|
|
+ def _on_ok(self) -> None:
|
|
|
+ """Handle OK button click."""
|
|
|
+ self._settings = self._get_settings_from_ui()
|
|
|
+ self._save_settings()
|
|
|
+ self.accept()
|
|
|
+
|
|
|
+ def _on_cancel(self) -> None:
|
|
|
+ """Handle cancel button click."""
|
|
|
+ self._settings = self._original_settings.copy()
|
|
|
+ self.reject()
|
|
|
+
|
|
|
+ def _save_settings(self) -> None:
|
|
|
+ """Save settings to configuration file."""
|
|
|
+ try:
|
|
|
+ # Ensure directory exists
|
|
|
+ self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+
|
|
|
+ # Save to file
|
|
|
+ with open(self._config_path, "w", encoding="utf-8") as f:
|
|
|
+ json.dump(self._settings, f, indent=2)
|
|
|
+
|
|
|
+ self.settings_changed.emit(self._settings)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ QMessageBox.warning(
|
|
|
+ self,
|
|
|
+ "Save Failed",
|
|
|
+ f"Could not save settings:\n{e}"
|
|
|
+ )
|
|
|
+
|
|
|
+ def load_settings(self, path: Optional[Path] = None) -> bool:
|
|
|
+ """
|
|
|
+ Load settings from configuration file.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ path: Path to configuration file (uses default if None)
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ True if settings were loaded successfully
|
|
|
+ """
|
|
|
+ config_path = path or self._config_path
|
|
|
+
|
|
|
+ if not config_path.exists():
|
|
|
+ return False
|
|
|
+
|
|
|
+ try:
|
|
|
+ with open(config_path, "r", encoding="utf-8") as f:
|
|
|
+ loaded = json.load(f)
|
|
|
+
|
|
|
+ # Merge with defaults to ensure all keys exist
|
|
|
+ self._settings = self._merge_settings(self.DEFAULT_SETTINGS, loaded)
|
|
|
+ self._original_settings = self._settings.copy()
|
|
|
+ self._load_settings_to_ui()
|
|
|
+ return True
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ QMessageBox.warning(
|
|
|
+ self,
|
|
|
+ "Load Failed",
|
|
|
+ f"Could not load settings:\n{e}"
|
|
|
+ )
|
|
|
+ return False
|
|
|
+
|
|
|
+ def _merge_settings(
|
|
|
+ self,
|
|
|
+ defaults: Dict[str, Any],
|
|
|
+ loaded: Dict[str, Any]
|
|
|
+ ) -> Dict[str, Any]:
|
|
|
+ """Merge loaded settings with defaults."""
|
|
|
+ result = defaults.copy()
|
|
|
+
|
|
|
+ for key, value in loaded.items():
|
|
|
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
|
+ result[key] = self._merge_settings(result[key], value)
|
|
|
+ else:
|
|
|
+ result[key] = value
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ @property
|
|
|
+ def settings(self) -> Dict[str, Any]:
|
|
|
+ """Get current settings."""
|
|
|
+ return self._settings.copy()
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def load_default_settings() -> Dict[str, Any]:
|
|
|
+ """
|
|
|
+ Load settings from default configuration file.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Settings dictionary or default settings if file doesn't exist
|
|
|
+ """
|
|
|
+ dialog = SettingsDialog()
|
|
|
+ if dialog.load_settings():
|
|
|
+ return dialog.settings
|
|
|
+ return dialog.DEFAULT_SETTINGS.copy()
|