Refactor SettingsDialog for improved readability and maintainability

- Cleaned up whitespace and formatting throughout the settings_dialog.py file.
- Enhanced type hints for better clarity and type checking.
- Consolidated URL mapping handling in get_config_data method.
- Improved error handling and logging for configuration operations.
- Added comments for better understanding of the code structure and functionality.
This commit is contained in:
claudi 2026-02-25 13:26:46 +01:00
parent 03991fdea5
commit 986793632e
4 changed files with 468 additions and 480 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,11 @@
import logging
from pathlib import Path
from typing import List, Optional
from typing import Any, Dict, List, Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
class SettingsDialog(QDialog):
"""Dialog for managing application settings and configuration.
Provides tabs for:
- Paths: Manage allowed root directories
- URLs: Manage allowed web URLs
@ -41,9 +42,9 @@ class SettingsDialog(QDialog):
- Profiles: Save/load/delete configuration profiles
"""
def __init__(self, config: Config, parent=None):
def __init__(self, config: Config, parent: Optional[QWidget] = None):
"""Initialize the settings dialog.
Args:
config: Current application configuration
parent: Parent widget
@ -53,16 +54,16 @@ class SettingsDialog(QDialog):
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_web_source_tab(), "Web Source")
self.tabs.addTab(self._create_paths_tab(), "Paths")
@ -70,9 +71,9 @@ class SettingsDialog(QDialog):
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
@ -80,12 +81,12 @@ class SettingsDialog(QDialog):
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def accept(self) -> None:
"""Handle OK button - save configuration changes to file.
Validates configuration and saves to the default config path.
Applies log level changes immediately in the running application.
If validation or save fails, shows error and stays in dialog.
@ -93,50 +94,49 @@ class SettingsDialog(QDialog):
try:
# Get updated configuration data from UI
config_data = self.get_config_data()
# Convert URL mappings from dict to URLMapping objects
from webdrop_bridge.config import URLMapping
url_mappings = [
URLMapping(
url_prefix=m["url_prefix"],
local_path=m["local_path"]
)
URLMapping(url_prefix=m["url_prefix"], local_path=m["local_path"])
for m in config_data["url_mappings"]
]
# Update the config object with new values
old_log_level = self.config.log_level
self.config.log_level = config_data["log_level"]
self.config.log_file = Path(config_data["log_file"]) if config_data["log_file"] else None
self.config.log_file = (
Path(config_data["log_file"]) if config_data["log_file"] else None
)
self.config.allowed_roots = [Path(r).resolve() for r in config_data["allowed_roots"]]
self.config.allowed_urls = config_data["allowed_urls"]
self.config.webapp_url = config_data["webapp_url"]
self.config.url_mappings = url_mappings
self.config.window_width = config_data["window_width"]
self.config.window_height = config_data["window_height"]
# Save to file (creates parent dirs if needed)
config_path = Config.get_default_config_path()
self.config.to_file(config_path)
logger.info(f"Configuration saved to {config_path}")
logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})")
logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}")
# Apply log level change immediately to running application
if old_log_level != self.config.log_level:
logger.info(f"🔄 Updating log level: {old_log_level}{self.config.log_level}")
reconfigure_logging(
logger_name="webdrop_bridge",
level=self.config.log_level,
log_file=self.config.log_file
log_file=self.config.log_file,
)
logger.info(f"✅ Log level updated to {self.config.log_level}")
# Call parent accept to close dialog
super().accept()
except ConfigurationError as e:
logger.error(f"Configuration error: {e}")
self._show_error(f"Configuration Error:\n\n{e}")
@ -147,67 +147,70 @@ class SettingsDialog(QDialog):
def _create_web_source_tab(self) -> QWidget:
"""Create web source configuration tab."""
from PySide6.QtWidgets import QTableWidget, QTableWidgetItem
widget = QWidget()
layout = QVBoxLayout()
# Webapp URL configuration
layout.addWidget(QLabel("Web Application URL:"))
url_layout = QHBoxLayout()
self.webapp_url_input = QLineEdit()
self.webapp_url_input.setText(self.config.webapp_url)
self.webapp_url_input.setPlaceholderText("e.g., http://localhost:8080 or file:///./webapp/index.html")
self.webapp_url_input.setPlaceholderText(
"e.g., http://localhost:8080 or file:///./webapp/index.html"
)
url_layout.addWidget(self.webapp_url_input)
open_btn = QPushButton("Open")
open_btn.clicked.connect(self._open_webapp_url)
url_layout.addWidget(open_btn)
layout.addLayout(url_layout)
# URL Mappings (Azure Blob URL → Local Path)
layout.addWidget(QLabel("URL Mappings (Azure Blob Storage → Local Paths):"))
# Create table for URL mappings
self.url_mappings_table = QTableWidget()
self.url_mappings_table.setColumnCount(2)
self.url_mappings_table.setHorizontalHeaderLabels(["URL Prefix", "Local Path"])
self.url_mappings_table.horizontalHeader().setStretchLastSection(True)
# Populate from config
for mapping in self.config.url_mappings:
row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(mapping.url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(mapping.local_path))
layout.addWidget(self.url_mappings_table)
# Buttons for URL mapping management
button_layout = QHBoxLayout()
add_mapping_btn = QPushButton("Add Mapping")
add_mapping_btn.clicked.connect(self._add_url_mapping)
button_layout.addWidget(add_mapping_btn)
edit_mapping_btn = QPushButton("Edit Selected")
edit_mapping_btn.clicked.connect(self._edit_url_mapping)
button_layout.addWidget(edit_mapping_btn)
remove_mapping_btn = QPushButton("Remove Selected")
remove_mapping_btn.clicked.connect(self._remove_url_mapping)
button_layout.addWidget(remove_mapping_btn)
layout.addLayout(button_layout)
layout.addStretch()
widget.setLayout(layout)
return widget
def _open_webapp_url(self) -> None:
"""Open the webapp URL in the default browser."""
import webbrowser
url = self.webapp_url_input.text().strip()
if url:
# Handle file:// URLs
@ -216,61 +219,55 @@ class SettingsDialog(QDialog):
except Exception as e:
logger.error(f"Failed to open URL: {e}")
self._show_error(f"Failed to open URL:\n\n{e}")
def _add_url_mapping(self) -> None:
"""Add new URL mapping."""
from PySide6.QtWidgets import QInputDialog
url_prefix, ok1 = QInputDialog.getText(
self,
"Add URL Mapping",
"Enter Azure Blob Storage URL prefix:\n(e.g., https://myblob.blob.core.windows.net/container/)"
"Enter Azure Blob Storage URL prefix:\n(e.g., https://myblob.blob.core.windows.net/container/)",
)
if ok1 and url_prefix:
local_path, ok2 = QInputDialog.getText(
self,
"Add URL Mapping",
"Enter local file system path:\n(e.g., C:\\Share or /mnt/share)"
"Enter local file system path:\n(e.g., C:\\Share or /mnt/share)",
)
if ok2 and local_path:
row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(local_path))
def _edit_url_mapping(self) -> None:
"""Edit selected URL mapping."""
from PySide6.QtWidgets import QInputDialog
current_row = self.url_mappings_table.currentRow()
if current_row < 0:
self._show_error("Please select a mapping to edit")
return
url_prefix = self.url_mappings_table.item(current_row, 0).text()
local_path = self.url_mappings_table.item(current_row, 1).text()
url_prefix = self.url_mappings_table.item(current_row, 0).text() # type: ignore
local_path = self.url_mappings_table.item(current_row, 1).text() # type: ignore
new_url_prefix, ok1 = QInputDialog.getText(
self,
"Edit URL Mapping",
"Enter Azure Blob Storage URL prefix:",
text=url_prefix
self, "Edit URL Mapping", "Enter Azure Blob Storage URL prefix:", text=url_prefix
)
if ok1 and new_url_prefix:
new_local_path, ok2 = QInputDialog.getText(
self,
"Edit URL Mapping",
"Enter local file system path:",
text=local_path
self, "Edit URL Mapping", "Enter local file system path:", text=local_path
)
if ok2 and new_local_path:
self.url_mappings_table.setItem(current_row, 0, QTableWidgetItem(new_url_prefix))
self.url_mappings_table.setItem(current_row, 1, QTableWidgetItem(new_local_path))
def _remove_url_mapping(self) -> None:
"""Remove selected URL mapping."""
current_row = self.url_mappings_table.currentRow()
@ -281,29 +278,29 @@ class SettingsDialog(QDialog):
"""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
@ -311,29 +308,29 @@ class SettingsDialog(QDialog):
"""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
@ -341,27 +338,28 @@ class SettingsDialog(QDialog):
"""Create logging configuration tab."""
widget = QWidget()
layout = QVBoxLayout()
# Log level selection
layout.addWidget(QLabel("Log Level:"))
from PySide6.QtWidgets import QComboBox
self.log_level_combo: QComboBox = 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
@ -370,7 +368,7 @@ class SettingsDialog(QDialog):
"""Create window settings tab."""
widget = QWidget()
layout = QVBoxLayout()
# Window width
width_layout = QHBoxLayout()
width_layout.addWidget(QLabel("Window Width:"))
@ -381,7 +379,7 @@ class SettingsDialog(QDialog):
width_layout.addWidget(self.width_spin)
width_layout.addStretch()
layout.addLayout(width_layout)
# Window height
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("Window Height:"))
@ -392,7 +390,7 @@ class SettingsDialog(QDialog):
height_layout.addWidget(self.height_spin)
height_layout.addStretch()
layout.addLayout(height_layout)
layout.addStretch()
widget.setLayout(layout)
return widget
@ -401,52 +399,50 @@ class SettingsDialog(QDialog):
"""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):
def _create_log_level_widget(self) -> QComboBox:
"""Create log level selection widget."""
from PySide6.QtWidgets import QComboBox
combo = QComboBox()
levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
combo.addItems(levels)
@ -467,11 +463,9 @@ class SettingsDialog(QDialog):
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):"
self, "Add URL", "Enter URL pattern (e.g., http://example.com or http://*.example.com):"
)
if ok and url:
self.urls_list.addItem(url)
@ -484,10 +478,7 @@ class SettingsDialog(QDialog):
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 (*)"
self, "Select Log File", str(Path.home()), "Log Files (*.log);;All Files (*)"
)
if file_path:
self.log_file_input.setText(file_path)
@ -501,13 +492,11 @@ class SettingsDialog(QDialog):
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):"
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)
@ -521,7 +510,7 @@ class SettingsDialog(QDialog):
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)
@ -535,7 +524,7 @@ class SettingsDialog(QDialog):
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)
@ -546,12 +535,9 @@ class SettingsDialog(QDialog):
def _export_config(self) -> None:
"""Export configuration to file."""
file_path, _ = QFileDialog.getSaveFileName(
self,
"Export Configuration",
str(Path.home()),
"JSON Files (*.json);;All Files (*)"
self, "Export Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
ConfigExporter.export_to_json(self.config, Path(file_path))
@ -561,12 +547,9 @@ class SettingsDialog(QDialog):
def _import_config(self) -> None:
"""Import configuration from file."""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Import Configuration",
str(Path.home()),
"JSON Files (*.json);;All Files (*)"
self, "Import Configuration", str(Path.home()), "JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
config_data = ConfigExporter.import_from_json(Path(file_path))
@ -574,9 +557,9 @@ class SettingsDialog(QDialog):
except ConfigurationError as e:
self._show_error(f"Failed to import configuration: {e}")
def _apply_config_data(self, config_data: dict) -> None:
def _apply_config_data(self, config_data: Dict[str, Any]) -> None:
"""Apply imported configuration data to UI.
Args:
config_data: Configuration dictionary
"""
@ -584,60 +567,67 @@ class SettingsDialog(QDialog):
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:
def get_config_data(self) -> Dict[str, Any]:
"""Get updated configuration data from dialog.
Returns:
Configuration dictionary
Raises:
ConfigurationError: If configuration is invalid
"""
if self.url_mappings_table:
url_mappings_table_count = self.url_mappings_table.rowCount() or 0
else:
url_mappings_table_count = 0
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_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.webapp_url_input.text().strip(),
"url_mappings": [
{
"url_prefix": self.url_mappings_table.item(i, 0).text(),
"local_path": self.url_mappings_table.item(i, 1).text()
"url_prefix": self.url_mappings_table.item(i, 0).text() if self.url_mappings_table.item(i, 0) else "", # type: ignore
"local_path": self.url_mappings_table.item(i, 1).text() if self.url_mappings_table.item(i, 1) else "", # type: ignore
}
for i in range(self.url_mappings_table.rowCount())
for i in range(url_mappings_table_count)
],
"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)