- Implement tests for ConfigValidator to ensure proper validation of configuration settings, including valid configurations, required fields, type checks, and error handling. - Create tests for ConfigProfile to verify profile management functionalities such as saving, loading, listing, and deleting profiles. - Add tests for ConfigExporter to validate JSON export and import processes, including error handling for non-existent files and invalid JSON. - Introduce tests for SettingsDialog to confirm proper initialization, tab existence, and configuration data retrieval and application.
434 lines
15 KiB
Python
434 lines
15 KiB
Python
"""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)
|