Files
2025-08-26 13:07:59 -07:00

635 lines
24 KiB
Python

"""
Settings View - Application configuration and preferences
Manages all application settings, model configurations, and system preferences.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
QSlider, QTextEdit, QFileDialog, QMessageBox, QProgressBar,
QFormLayout, QButtonGroup, QRadioButton
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSettings, QThread
from PySide6.QtGui import QFont, QPixmap
import os
import json
import sys
from pathlib import Path
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
from qt_app_pyside.ui.config_panel import ConfigPanel
from qt_app_pyside.utils.helpers import load_configuration, save_configuration
from qt_app_pyside.utils.helpers import format_timestamp, format_duration
class ModelConfigWidget(QGroupBox):
"""
Widget for configuring AI models and detection parameters.
"""
config_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__("AI Model Configuration", parent)
self.setup_ui()
def setup_ui(self):
"""Setup model configuration UI"""
layout = QFormLayout(self)
# Vehicle detection model
self.vehicle_model_edit = QLineEdit()
self.vehicle_model_edit.setPlaceholderText("Path to vehicle detection model...")
vehicle_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
vehicle_browse_btn.setFixedSize(32, 32)
vehicle_browse_btn.clicked.connect(lambda: self.browse_model("vehicle"))
vehicle_layout = QHBoxLayout()
vehicle_layout.addWidget(self.vehicle_model_edit)
vehicle_layout.addWidget(vehicle_browse_btn)
layout.addRow("Vehicle Model:", vehicle_layout)
# Traffic light detection model
self.traffic_model_edit = QLineEdit()
self.traffic_model_edit.setPlaceholderText("Path to traffic light model...")
traffic_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
traffic_browse_btn.setFixedSize(32, 32)
traffic_browse_btn.clicked.connect(lambda: self.browse_model("traffic"))
traffic_layout = QHBoxLayout()
traffic_layout.addWidget(self.traffic_model_edit)
traffic_layout.addWidget(traffic_browse_btn)
layout.addRow("Traffic Light Model:", traffic_layout)
# Detection parameters
self.confidence_spin = QDoubleSpinBox()
self.confidence_spin.setRange(0.1, 1.0)
self.confidence_spin.setSingleStep(0.05)
self.confidence_spin.setValue(0.3)
self.confidence_spin.setSuffix(" (30%)")
layout.addRow("Confidence Threshold:", self.confidence_spin)
self.nms_spin = QDoubleSpinBox()
self.nms_spin.setRange(0.1, 1.0)
self.nms_spin.setSingleStep(0.05)
self.nms_spin.setValue(0.45)
layout.addRow("NMS Threshold:", self.nms_spin)
self.max_detections_spin = QSpinBox()
self.max_detections_spin.setRange(10, 1000)
self.max_detections_spin.setValue(100)
layout.addRow("Max Detections:", self.max_detections_spin)
# Device selection
self.device_combo = QComboBox()
self.device_combo.addItems(["CPU", "GPU", "AUTO"])
layout.addRow("Device:", self.device_combo)
# Model optimization
self.optimize_check = QCheckBox("Enable Model Optimization")
self.optimize_check.setChecked(True)
layout.addRow(self.optimize_check)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_model(self, model_type):
"""Browse for model file"""
file_path, _ = QFileDialog.getOpenFileName(
self, f"Select {model_type.title()} Model", "",
"Model Files (*.xml *.onnx *.pt *.bin);;All Files (*)"
)
if file_path:
if model_type == "vehicle":
self.vehicle_model_edit.setText(file_path)
elif model_type == "traffic":
self.traffic_model_edit.setText(file_path)
def get_config(self):
"""Get current model configuration"""
return {
'vehicle_model': self.vehicle_model_edit.text(),
'traffic_model': self.traffic_model_edit.text(),
'confidence_threshold': self.confidence_spin.value(),
'nms_threshold': self.nms_spin.value(),
'max_detections': self.max_detections_spin.value(),
'device': self.device_combo.currentText(),
'optimize_model': self.optimize_check.isChecked()
}
def set_config(self, config):
"""Set model configuration"""
self.vehicle_model_edit.setText(config.get('vehicle_model', ''))
self.traffic_model_edit.setText(config.get('traffic_model', ''))
self.confidence_spin.setValue(config.get('confidence_threshold', 0.3))
self.nms_spin.setValue(config.get('nms_threshold', 0.45))
self.max_detections_spin.setValue(config.get('max_detections', 100))
self.device_combo.setCurrentText(config.get('device', 'CPU'))
self.optimize_check.setChecked(config.get('optimize_model', True))
class ViolationConfigWidget(QGroupBox):
"""
Widget for configuring violation detection parameters.
"""
def __init__(self, parent=None):
super().__init__("Violation Detection", parent)
self.setup_ui()
def setup_ui(self):
"""Setup violation configuration UI"""
layout = QFormLayout(self)
# Red light violation
self.red_light_check = QCheckBox("Enable Red Light Detection")
self.red_light_check.setChecked(True)
layout.addRow(self.red_light_check)
self.red_light_sensitivity = QSlider(Qt.Horizontal)
self.red_light_sensitivity.setRange(1, 10)
self.red_light_sensitivity.setValue(5)
layout.addRow("Red Light Sensitivity:", self.red_light_sensitivity)
# Speed violation
self.speed_check = QCheckBox("Enable Speed Detection")
self.speed_check.setChecked(True)
layout.addRow(self.speed_check)
self.speed_limit_spin = QSpinBox()
self.speed_limit_spin.setRange(10, 200)
self.speed_limit_spin.setValue(50)
self.speed_limit_spin.setSuffix(" km/h")
layout.addRow("Speed Limit:", self.speed_limit_spin)
self.speed_tolerance_spin = QSpinBox()
self.speed_tolerance_spin.setRange(0, 20)
self.speed_tolerance_spin.setValue(5)
self.speed_tolerance_spin.setSuffix(" km/h")
layout.addRow("Speed Tolerance:", self.speed_tolerance_spin)
# Wrong lane detection
self.wrong_lane_check = QCheckBox("Enable Wrong Lane Detection")
self.wrong_lane_check.setChecked(True)
layout.addRow(self.wrong_lane_check)
# Helmet detection
self.helmet_check = QCheckBox("Enable Helmet Detection")
self.helmet_check.setChecked(False)
layout.addRow(self.helmet_check)
# Violation zone setup
self.zone_setup_btn = QPushButton(FinaleIcons.get_icon("map"), "Setup Violation Zones")
layout.addRow(self.zone_setup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class UIPreferencesWidget(QGroupBox):
"""
Widget for UI preferences and appearance settings.
"""
theme_changed = Signal(bool) # dark_mode
def __init__(self, parent=None):
super().__init__("User Interface", parent)
self.setup_ui()
def setup_ui(self):
"""Setup UI preferences"""
layout = QFormLayout(self)
# Theme selection
theme_group = QButtonGroup(self)
self.dark_radio = QRadioButton("Dark Theme")
self.light_radio = QRadioButton("Light Theme")
self.auto_radio = QRadioButton("Auto (System)")
self.dark_radio.setChecked(True) # Default to dark
theme_group.addButton(self.dark_radio)
theme_group.addButton(self.light_radio)
theme_group.addButton(self.auto_radio)
theme_layout = QVBoxLayout()
theme_layout.addWidget(self.dark_radio)
theme_layout.addWidget(self.light_radio)
theme_layout.addWidget(self.auto_radio)
layout.addRow("Theme:", theme_layout)
# Language selection
self.language_combo = QComboBox()
self.language_combo.addItems(["English", "Español", "Français", "Deutsch", "العربية"])
layout.addRow("Language:", self.language_combo)
# Font size
self.font_size_spin = QSpinBox()
self.font_size_spin.setRange(8, 16)
self.font_size_spin.setValue(9)
layout.addRow("Font Size:", self.font_size_spin)
# Animations
self.animations_check = QCheckBox("Enable Animations")
self.animations_check.setChecked(True)
layout.addRow(self.animations_check)
# Sound notifications
self.sound_check = QCheckBox("Sound Notifications")
self.sound_check.setChecked(True)
layout.addRow(self.sound_check)
# Auto-save
self.autosave_check = QCheckBox("Auto-save Configuration")
self.autosave_check.setChecked(True)
layout.addRow(self.autosave_check)
# Update interval
self.update_interval_spin = QSpinBox()
self.update_interval_spin.setRange(100, 5000)
self.update_interval_spin.setValue(1000)
self.update_interval_spin.setSuffix(" ms")
layout.addRow("Update Interval:", self.update_interval_spin)
# Connect theme signals
self.dark_radio.toggled.connect(lambda checked: self.theme_changed.emit(True) if checked else None)
self.light_radio.toggled.connect(lambda checked: self.theme_changed.emit(False) if checked else None)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class PerformanceWidget(QGroupBox):
"""
Widget for performance and system settings.
"""
def __init__(self, parent=None):
super().__init__("Performance", parent)
self.setup_ui()
def setup_ui(self):
"""Setup performance settings"""
layout = QFormLayout(self)
# Processing threads
self.threads_spin = QSpinBox()
self.threads_spin.setRange(1, 16)
self.threads_spin.setValue(4)
layout.addRow("Processing Threads:", self.threads_spin)
# Frame buffer size
self.buffer_size_spin = QSpinBox()
self.buffer_size_spin.setRange(1, 100)
self.buffer_size_spin.setValue(10)
layout.addRow("Frame Buffer Size:", self.buffer_size_spin)
# Memory limit
self.memory_limit_spin = QSpinBox()
self.memory_limit_spin.setRange(512, 8192)
self.memory_limit_spin.setValue(2048)
self.memory_limit_spin.setSuffix(" MB")
layout.addRow("Memory Limit:", self.memory_limit_spin)
# GPU acceleration
self.gpu_check = QCheckBox("Enable GPU Acceleration")
self.gpu_check.setChecked(False)
layout.addRow(self.gpu_check)
# Performance mode
self.performance_combo = QComboBox()
self.performance_combo.addItems(["Balanced", "Performance", "Power Save"])
layout.addRow("Performance Mode:", self.performance_combo)
# Logging level
self.logging_combo = QComboBox()
self.logging_combo.addItems(["DEBUG", "INFO", "WARNING", "ERROR"])
self.logging_combo.setCurrentText("INFO")
layout.addRow("Logging Level:", self.logging_combo)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class DataManagementWidget(QGroupBox):
"""
Widget for data storage and export settings.
"""
def __init__(self, parent=None):
super().__init__("Data Management", parent)
self.setup_ui()
def setup_ui(self):
"""Setup data management settings"""
layout = QFormLayout(self)
# Data directory
self.data_dir_edit = QLineEdit()
self.data_dir_edit.setPlaceholderText("Data storage directory...")
data_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
data_browse_btn.setFixedSize(32, 32)
data_browse_btn.clicked.connect(self.browse_data_directory)
data_layout = QHBoxLayout()
data_layout.addWidget(self.data_dir_edit)
data_layout.addWidget(data_browse_btn)
layout.addRow("Data Directory:", data_layout)
# Auto-export
self.auto_export_check = QCheckBox("Auto-export Violations")
layout.addRow(self.auto_export_check)
# Export format
self.export_format_combo = QComboBox()
self.export_format_combo.addItems(["JSON", "CSV", "XML", "PDF"])
layout.addRow("Export Format:", self.export_format_combo)
# Data retention
self.retention_spin = QSpinBox()
self.retention_spin.setRange(1, 365)
self.retention_spin.setValue(30)
self.retention_spin.setSuffix(" days")
layout.addRow("Data Retention:", self.retention_spin)
# Backup settings
self.backup_check = QCheckBox("Enable Automatic Backup")
layout.addRow(self.backup_check)
self.backup_interval_combo = QComboBox()
self.backup_interval_combo.addItems(["Daily", "Weekly", "Monthly"])
layout.addRow("Backup Interval:", self.backup_interval_combo)
# Database cleanup
cleanup_btn = QPushButton(FinaleIcons.get_icon("delete"), "Cleanup Old Data")
layout.addRow(cleanup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_data_directory(self):
"""Browse for data directory"""
directory = QFileDialog.getExistingDirectory(
self, "Select Data Directory", self.data_dir_edit.text()
)
if directory:
self.data_dir_edit.setText(directory)
class SettingsView(QWidget):
"""
Main settings view with tabbed configuration sections.
"""
settings_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.config = load_configuration('config.json')
# Add configuration panel from original
self.config_panel = ConfigPanel()
self.settings = QSettings("Finale", "TrafficMonitoring")
self.setup_ui()
self.load_settings()
def setup_ui(self):
"""Setup the settings view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Header
header_layout = QHBoxLayout()
title_label = QLabel("Settings")
title_label.setFont(QFont("Segoe UI", 18, QFont.Bold))
# Action buttons
self.reset_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Reset to Defaults")
self.reset_btn.clicked.connect(self.reset_to_defaults)
self.export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Settings")
self.export_btn.clicked.connect(self.export_settings)
self.import_btn = QPushButton(FinaleIcons.get_icon("import"), "Import Settings")
self.import_btn.clicked.connect(self.import_settings)
header_layout.addWidget(title_label)
header_layout.addStretch()
header_layout.addWidget(self.reset_btn)
header_layout.addWidget(self.export_btn)
header_layout.addWidget(self.import_btn)
layout.addLayout(header_layout)
# Settings tabs
self.tabs = QTabWidget()
# Create configuration widgets
self.model_config = ModelConfigWidget()
self.violation_config = ViolationConfigWidget()
self.ui_preferences = UIPreferencesWidget()
self.performance_config = PerformanceWidget()
self.data_management = DataManagementWidget()
# Add tabs
self.tabs.addTab(self.model_config, FinaleIcons.get_icon("model"), "AI Models")
self.tabs.addTab(self.violation_config, FinaleIcons.get_icon("warning"), "Violations")
self.tabs.addTab(self.ui_preferences, FinaleIcons.get_icon("palette"), "Interface")
self.tabs.addTab(self.performance_config, FinaleIcons.get_icon("speed"), "Performance")
self.tabs.addTab(self.data_management, FinaleIcons.get_icon("database"), "Data")
# Style tabs
self.tabs.setStyleSheet(FinaleStyles.get_tab_widget_style())
layout.addWidget(self.tabs, 1)
# Bottom action bar
action_layout = QHBoxLayout()
self.apply_btn = QPushButton(FinaleIcons.get_icon("check"), "Apply")
self.apply_btn.clicked.connect(self.apply_settings)
self.save_btn = QPushButton(FinaleIcons.get_icon("save"), "Save")
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton(FinaleIcons.get_icon("close"), "Cancel")
self.cancel_btn.clicked.connect(self.cancel_changes)
action_layout.addStretch()
action_layout.addWidget(self.apply_btn)
action_layout.addWidget(self.save_btn)
action_layout.addWidget(self.cancel_btn)
layout.addLayout(action_layout)
# Connect signals
self.ui_preferences.theme_changed.connect(self.on_theme_changed)
# Apply theme
self.apply_theme(True)
def load_settings(self):
"""Load settings from QSettings"""
# Load model configuration
model_config = {
'vehicle_model': self.settings.value('model/vehicle_model', ''),
'traffic_model': self.settings.value('model/traffic_model', ''),
'confidence_threshold': self.settings.value('model/confidence_threshold', 0.3, float),
'nms_threshold': self.settings.value('model/nms_threshold', 0.45, float),
'max_detections': self.settings.value('model/max_detections', 100, int),
'device': self.settings.value('model/device', 'CPU'),
'optimize_model': self.settings.value('model/optimize_model', True, bool)
}
self.model_config.set_config(model_config)
# Load UI preferences
dark_mode = self.settings.value('ui/dark_mode', True, bool)
if dark_mode:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot()
def apply_settings(self):
"""Apply current settings"""
settings_data = self.get_all_settings()
self.settings_changed.emit(settings_data)
@Slot()
def save_settings(self):
"""Save settings to QSettings"""
# Save model configuration
model_config = self.model_config.get_config()
for key, value in model_config.items():
self.settings.setValue(f'model/{key}', value)
# Save UI preferences
self.settings.setValue('ui/dark_mode', self.ui_preferences.dark_radio.isChecked())
# Sync settings
self.settings.sync()
QMessageBox.information(self, "Settings Saved", "Settings have been saved successfully.")
save_configuration(settings_data, 'config.json')
@Slot()
def cancel_changes(self):
"""Cancel changes and reload settings"""
self.load_settings()
@Slot()
def reset_to_defaults(self):
"""Reset all settings to defaults"""
reply = QMessageBox.question(
self, "Reset Settings",
"Are you sure you want to reset all settings to defaults?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.settings.clear()
self.load_settings()
@Slot()
def export_settings(self):
"""Export settings to file"""
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
settings_data = self.get_all_settings()
try:
with open(file_path, 'w') as f:
json.dump(settings_data, f, indent=2)
QMessageBox.information(self, "Export Successful", "Settings exported successfully.")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Failed to export settings:\n{str(e)}")
@Slot()
def import_settings(self):
"""Import settings from file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Import Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
with open(file_path, 'r') as f:
settings_data = json.load(f)
# Apply imported settings
self.apply_imported_settings(settings_data)
QMessageBox.information(self, "Import Successful", "Settings imported successfully.")
except Exception as e:
QMessageBox.critical(self, "Import Error", f"Failed to import settings:\n{str(e)}")
def get_all_settings(self):
"""Get all current settings as dictionary"""
return {
'model': self.model_config.get_config(),
'ui': {
'dark_mode': self.ui_preferences.dark_radio.isChecked(),
'language': self.ui_preferences.language_combo.currentText(),
'font_size': self.ui_preferences.font_size_spin.value(),
'animations': self.ui_preferences.animations_check.isChecked(),
'sound': self.ui_preferences.sound_check.isChecked()
}
}
def apply_imported_settings(self, settings_data):
"""Apply imported settings data"""
if 'model' in settings_data:
self.model_config.set_config(settings_data['model'])
if 'ui' in settings_data:
ui_settings = settings_data['ui']
if 'dark_mode' in ui_settings:
if ui_settings['dark_mode']:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot(bool)
def on_theme_changed(self, dark_mode):
"""Handle theme change"""
self.apply_theme(dark_mode)
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
""")
def display_timestamp(self, ts):
return format_timestamp(ts)
def display_duration(self, seconds):
return format_duration(seconds)