"""Settings dialog for configuration management.""" from pathlib import Path from typing import List, Optional from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSpinBox, QTabWidget, QVBoxLayout, QWidget, ) from webdrop_bridge.config import Config, ConfigurationError from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator class SettingsDialog(QDialog): """Dialog for managing application settings and configuration. Provides tabs for: - Paths: Manage allowed root directories - URLs: Manage allowed web URLs - Logging: Configure logging settings - Window: Manage window size and behavior - Profiles: Save/load/delete configuration profiles """ def __init__(self, config: Config, parent=None): """Initialize the settings dialog. Args: config: Current application configuration parent: Parent widget """ super().__init__(parent) self.config = config self.profile_manager = ConfigProfile() self.setWindowTitle("Settings") self.setGeometry(100, 100, 600, 500) self.setup_ui() def setup_ui(self) -> None: """Set up the dialog UI with tabs.""" layout = QVBoxLayout() # Create tab widget self.tabs = QTabWidget() # Add tabs self.tabs.addTab(self._create_paths_tab(), "Paths") self.tabs.addTab(self._create_urls_tab(), "URLs") self.tabs.addTab(self._create_logging_tab(), "Logging") self.tabs.addTab(self._create_window_tab(), "Window") self.tabs.addTab(self._create_profiles_tab(), "Profiles") layout.addWidget(self.tabs) # Add buttons button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) self.setLayout(layout) def _create_paths_tab(self) -> QWidget: """Create paths configuration tab.""" widget = QWidget() layout = QVBoxLayout() layout.addWidget(QLabel("Allowed root directories for file access:")) # List widget for paths self.paths_list = QListWidget() for path in self.config.allowed_roots: self.paths_list.addItem(str(path)) layout.addWidget(self.paths_list) # Buttons for path management button_layout = QHBoxLayout() add_path_btn = QPushButton("Add Path") add_path_btn.clicked.connect(self._add_path) button_layout.addWidget(add_path_btn) remove_path_btn = QPushButton("Remove Selected") remove_path_btn.clicked.connect(self._remove_path) button_layout.addWidget(remove_path_btn) layout.addLayout(button_layout) layout.addStretch() widget.setLayout(layout) return widget def _create_urls_tab(self) -> QWidget: """Create URLs configuration tab.""" widget = QWidget() layout = QVBoxLayout() layout.addWidget(QLabel("Allowed web URLs (supports wildcards like http://*.example.com):")) # List widget for URLs self.urls_list = QListWidget() for url in self.config.allowed_urls: self.urls_list.addItem(url) layout.addWidget(self.urls_list) # Buttons for URL management button_layout = QHBoxLayout() add_url_btn = QPushButton("Add URL") add_url_btn.clicked.connect(self._add_url) button_layout.addWidget(add_url_btn) remove_url_btn = QPushButton("Remove Selected") remove_url_btn.clicked.connect(self._remove_url) button_layout.addWidget(remove_url_btn) layout.addLayout(button_layout) layout.addStretch() widget.setLayout(layout) return widget def _create_logging_tab(self) -> QWidget: """Create logging configuration tab.""" widget = QWidget() layout = QVBoxLayout() # Log level selection layout.addWidget(QLabel("Log Level:")) self.log_level_combo = self._create_log_level_widget() layout.addWidget(self.log_level_combo) # Log file path layout.addWidget(QLabel("Log File (optional):")) log_file_layout = QHBoxLayout() self.log_file_input = QLineEdit() self.log_file_input.setText(str(self.config.log_file) if self.config.log_file else "") log_file_layout.addWidget(self.log_file_input) browse_btn = QPushButton("Browse...") browse_btn.clicked.connect(self._browse_log_file) log_file_layout.addWidget(browse_btn) layout.addLayout(log_file_layout) layout.addStretch() widget.setLayout(layout) return widget def _create_window_tab(self) -> QWidget: """Create window settings tab.""" widget = QWidget() layout = QVBoxLayout() # Window width width_layout = QHBoxLayout() width_layout.addWidget(QLabel("Window Width:")) self.width_spin = QSpinBox() self.width_spin.setMinimum(400) self.width_spin.setMaximum(5000) self.width_spin.setValue(self.config.window_width) width_layout.addWidget(self.width_spin) width_layout.addStretch() layout.addLayout(width_layout) # Window height height_layout = QHBoxLayout() height_layout.addWidget(QLabel("Window Height:")) self.height_spin = QSpinBox() self.height_spin.setMinimum(300) self.height_spin.setMaximum(5000) self.height_spin.setValue(self.config.window_height) height_layout.addWidget(self.height_spin) height_layout.addStretch() layout.addLayout(height_layout) layout.addStretch() widget.setLayout(layout) return widget def _create_profiles_tab(self) -> QWidget: """Create profiles management tab.""" widget = QWidget() layout = QVBoxLayout() layout.addWidget(QLabel("Saved Configuration Profiles:")) # List of profiles self.profiles_list = QListWidget() self._refresh_profiles_list() layout.addWidget(self.profiles_list) # Profile management buttons button_layout = QHBoxLayout() save_profile_btn = QPushButton("Save as Profile") save_profile_btn.clicked.connect(self._save_profile) button_layout.addWidget(save_profile_btn) load_profile_btn = QPushButton("Load Profile") load_profile_btn.clicked.connect(self._load_profile) button_layout.addWidget(load_profile_btn) delete_profile_btn = QPushButton("Delete Profile") delete_profile_btn.clicked.connect(self._delete_profile) button_layout.addWidget(delete_profile_btn) layout.addLayout(button_layout) # Export/Import buttons export_layout = QHBoxLayout() export_btn = QPushButton("Export Configuration") export_btn.clicked.connect(self._export_config) export_layout.addWidget(export_btn) import_btn = QPushButton("Import Configuration") import_btn.clicked.connect(self._import_config) export_layout.addWidget(import_btn) layout.addLayout(export_layout) layout.addStretch() widget.setLayout(layout) return widget def _create_log_level_widget(self) -> QWidget: """Create log level selection widget.""" from PySide6.QtWidgets import QComboBox combo = QComboBox() levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] combo.addItems(levels) combo.setCurrentText(self.config.log_level) return combo def _add_path(self) -> None: """Add a new allowed path.""" path = QFileDialog.getExistingDirectory(self, "Select Directory to Allow") if path: self.paths_list.addItem(path) def _remove_path(self) -> None: """Remove selected path.""" if self.paths_list.currentItem(): self.paths_list.takeItem(self.paths_list.row(self.paths_list.currentItem())) def _add_url(self) -> None: """Add a new allowed URL.""" from PySide6.QtWidgets import QInputDialog url, ok = QInputDialog.getText( self, "Add URL", "Enter URL pattern (e.g., http://example.com or http://*.example.com):" ) if ok and url: self.urls_list.addItem(url) def _remove_url(self) -> None: """Remove selected URL.""" if self.urls_list.currentItem(): self.urls_list.takeItem(self.urls_list.row(self.urls_list.currentItem())) def _browse_log_file(self) -> None: """Browse for log file location.""" file_path, _ = QFileDialog.getSaveFileName( self, "Select Log File", str(Path.home()), "Log Files (*.log);;All Files (*)" ) if file_path: self.log_file_input.setText(file_path) def _refresh_profiles_list(self) -> None: """Refresh the list of available profiles.""" self.profiles_list.clear() for profile_name in self.profile_manager.list_profiles(): self.profiles_list.addItem(profile_name) def _save_profile(self) -> None: """Save current configuration as a profile.""" from PySide6.QtWidgets import QInputDialog profile_name, ok = QInputDialog.getText( self, "Save Profile", "Enter profile name (e.g., work, personal):" ) if ok and profile_name: try: self.profile_manager.save_profile(profile_name, self.config) self._refresh_profiles_list() except ConfigurationError as e: self._show_error(f"Failed to save profile: {e}") def _load_profile(self) -> None: """Load a saved profile.""" current_item = self.profiles_list.currentItem() if not current_item: self._show_error("Please select a profile to load") return profile_name = current_item.text() try: config_data = self.profile_manager.load_profile(profile_name) self._apply_config_data(config_data) except ConfigurationError as e: self._show_error(f"Failed to load profile: {e}") def _delete_profile(self) -> None: """Delete a saved profile.""" current_item = self.profiles_list.currentItem() if not current_item: self._show_error("Please select a profile to delete") return profile_name = current_item.text() try: self.profile_manager.delete_profile(profile_name) self._refresh_profiles_list() except ConfigurationError as e: self._show_error(f"Failed to delete profile: {e}") def _export_config(self) -> None: """Export configuration to file.""" file_path, _ = QFileDialog.getSaveFileName( self, "Export Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)" ) if file_path: try: ConfigExporter.export_to_json(self.config, Path(file_path)) except ConfigurationError as e: self._show_error(f"Failed to export configuration: {e}") def _import_config(self) -> None: """Import configuration from file.""" file_path, _ = QFileDialog.getOpenFileName( self, "Import Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)" ) if file_path: try: config_data = ConfigExporter.import_from_json(Path(file_path)) self._apply_config_data(config_data) except ConfigurationError as e: self._show_error(f"Failed to import configuration: {e}") def _apply_config_data(self, config_data: dict) -> None: """Apply imported configuration data to UI. Args: config_data: Configuration dictionary """ # Apply paths self.paths_list.clear() for path in config_data.get("allowed_roots", []): self.paths_list.addItem(str(path)) # Apply URLs self.urls_list.clear() for url in config_data.get("allowed_urls", []): self.urls_list.addItem(url) # Apply logging settings self.log_level_combo.setCurrentText(config_data.get("log_level", "INFO")) log_file = config_data.get("log_file") self.log_file_input.setText(str(log_file) if log_file else "") # Apply window settings self.width_spin.setValue(config_data.get("window_width", 800)) self.height_spin.setValue(config_data.get("window_height", 600)) def get_config_data(self) -> dict: """Get updated configuration data from dialog. Returns: Configuration dictionary Raises: ConfigurationError: If configuration is invalid """ config_data = { "app_name": self.config.app_name, "app_version": self.config.app_version, "log_level": self.log_level_combo.currentText(), "log_file": self.log_file_input.text() or None, "allowed_roots": [self.paths_list.item(i).text() for i in range(self.paths_list.count())], "allowed_urls": [self.urls_list.item(i).text() for i in range(self.urls_list.count())], "webapp_url": self.config.webapp_url, "window_width": self.width_spin.value(), "window_height": self.height_spin.value(), "enable_logging": self.config.enable_logging, } # Validate ConfigValidator.validate_or_raise(config_data) return config_data def _show_error(self, message: str) -> None: """Show error message to user. Args: message: Error message """ from PySide6.QtWidgets import QMessageBox QMessageBox.critical(self, "Error", message)