Clean push: Removed heavy files & added only latest snapshot
This commit is contained in:
203
qt_app_pyside1/finale/UI.py
Normal file
203
qt_app_pyside1/finale/UI.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
Finale UI - Main Entry Point
|
||||
Modern traffic monitoring interface entry point.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QPalette, QColor
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Import finale components
|
||||
try:
|
||||
# Try relative imports first (when running as a package)
|
||||
from .main_window import FinaleMainWindow
|
||||
from .splash import FinaleSplashScreen
|
||||
from .styles import FinaleStyles, MaterialColors
|
||||
from .icons import FinaleIcons
|
||||
except ImportError:
|
||||
# Fallback to direct imports (when running as script)
|
||||
try:
|
||||
from main_window import FinaleMainWindow
|
||||
from splash import FinaleSplashScreen
|
||||
from styles import FinaleStyles, MaterialColors
|
||||
from icons import FinaleIcons
|
||||
except ImportError:
|
||||
print('Error importing main components')
|
||||
|
||||
# Add Qt message handler from original main.py
|
||||
def qt_message_handler(mode, context, message):
|
||||
print(f"Qt Message: {message} (Mode: {mode})")
|
||||
# Install custom handler for Qt messages
|
||||
from PySide6.QtCore import Qt
|
||||
if hasattr(Qt, 'qInstallMessageHandler'):
|
||||
Qt.qInstallMessageHandler(qt_message_handler)
|
||||
|
||||
class FinaleUI:
|
||||
"""
|
||||
Main Finale UI application class.
|
||||
Handles application initialization, theme setup, and window management.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.app = None
|
||||
self.main_window = None
|
||||
self.splash = None
|
||||
|
||||
def initialize_application(self, sys_argv=None):
|
||||
"""
|
||||
Initialize the QApplication with proper settings.
|
||||
|
||||
Args:
|
||||
sys_argv: System arguments (defaults to sys.argv)
|
||||
"""
|
||||
if sys_argv is None:
|
||||
sys_argv = sys.argv
|
||||
|
||||
# Create or get existing application instance
|
||||
self.app = QApplication.instance() or QApplication(sys_argv)
|
||||
|
||||
# Set application properties
|
||||
self.app.setApplicationName("Finale Traffic Monitoring")
|
||||
self.app.setApplicationVersion("1.0.0")
|
||||
self.app.setOrganizationName("Finale Systems")
|
||||
self.app.setOrganizationDomain("finale.traffic")
|
||||
|
||||
# Set application icon
|
||||
self.app.setWindowIcon(FinaleIcons.get_icon("traffic_monitoring"))
|
||||
|
||||
# Enable high DPI scaling
|
||||
self.app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
self.app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
# Set font
|
||||
self.setup_fonts()
|
||||
|
||||
# Set global theme
|
||||
self.setup_theme()
|
||||
|
||||
return self.app
|
||||
|
||||
def setup_fonts(self):
|
||||
"""Setup application fonts"""
|
||||
# Set default font
|
||||
font = QFont("Segoe UI", 9)
|
||||
font.setHintingPreference(QFont.PreferDefaultHinting)
|
||||
self.app.setFont(font)
|
||||
|
||||
def setup_theme(self):
|
||||
"""Setup global application theme"""
|
||||
# Apply dark theme by default
|
||||
MaterialColors.apply_dark_theme()
|
||||
|
||||
# Set global stylesheet
|
||||
self.app.setStyleSheet(FinaleStyles.get_global_style())
|
||||
|
||||
def show_splash_screen(self):
|
||||
"""Show splash screen during initialization"""
|
||||
try:
|
||||
self.splash = FinaleSplashScreen()
|
||||
self.splash.show()
|
||||
|
||||
# Process events to show splash
|
||||
self.app.processEvents()
|
||||
|
||||
return self.splash
|
||||
except Exception as e:
|
||||
print(f"Could not show splash screen: {e}")
|
||||
return None
|
||||
|
||||
def create_main_window(self):
|
||||
"""Create and initialize the main window"""
|
||||
try:
|
||||
self.main_window = FinaleMainWindow()
|
||||
return self.main_window
|
||||
except Exception as e:
|
||||
print(f"Error creating main window: {e}")
|
||||
raise
|
||||
|
||||
def run(self, show_splash=True):
|
||||
"""
|
||||
Run the complete Finale UI application.
|
||||
|
||||
Args:
|
||||
show_splash: Whether to show splash screen
|
||||
|
||||
Returns:
|
||||
Application exit code
|
||||
"""
|
||||
try:
|
||||
# Initialize application
|
||||
if not self.app:
|
||||
self.initialize_application()
|
||||
|
||||
# Show splash screen
|
||||
if show_splash:
|
||||
splash = self.show_splash_screen()
|
||||
if splash:
|
||||
splash.update_progress(20, "Initializing UI components...")
|
||||
self.app.processEvents()
|
||||
|
||||
# Create main window
|
||||
if splash:
|
||||
splash.update_progress(50, "Loading detection models...")
|
||||
self.app.processEvents()
|
||||
|
||||
self.main_window = self.create_main_window()
|
||||
|
||||
if splash:
|
||||
splash.update_progress(80, "Connecting to backend...")
|
||||
self.app.processEvents()
|
||||
|
||||
# Finish splash and show main window
|
||||
if splash:
|
||||
splash.update_progress(100, "Ready!")
|
||||
self.app.processEvents()
|
||||
splash.finish(self.main_window)
|
||||
|
||||
# Show main window
|
||||
self.main_window.show()
|
||||
|
||||
# Start event loop
|
||||
return self.app.exec()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error running Finale UI: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
def create_finale_app(sys_argv=None):
|
||||
"""
|
||||
Create and return a Finale UI application instance.
|
||||
|
||||
Args:
|
||||
sys_argv: System arguments
|
||||
|
||||
Returns:
|
||||
FinaleUI instance
|
||||
"""
|
||||
finale_ui = FinaleUI()
|
||||
finale_ui.initialize_application(sys_argv)
|
||||
return finale_ui
|
||||
|
||||
def run_finale_ui(sys_argv=None, show_splash=True):
|
||||
"""
|
||||
Convenience function to run the Finale UI.
|
||||
|
||||
Args:
|
||||
sys_argv: System arguments
|
||||
show_splash: Whether to show splash screen
|
||||
|
||||
Returns:
|
||||
Application exit code
|
||||
"""
|
||||
finale_ui = create_finale_app(sys_argv)
|
||||
return finale_ui.run(show_splash)
|
||||
|
||||
# Main execution
|
||||
if __name__ == "__main__":
|
||||
exit_code = run_finale_ui()
|
||||
sys.exit(exit_code)
|
||||
1
qt_app_pyside1/finale/__init__.py
Normal file
1
qt_app_pyside1/finale/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Finale module for traffic monitoring system
|
||||
432
qt_app_pyside1/finale/icons.py
Normal file
432
qt_app_pyside1/finale/icons.py
Normal file
@@ -0,0 +1,432 @@
|
||||
"""
|
||||
Icon Management System
|
||||
=====================
|
||||
|
||||
Comprehensive icon system with SVG icons, Material Design icons,
|
||||
and utility functions for the Traffic Monitoring Application.
|
||||
|
||||
Features:
|
||||
- Material Design icon set
|
||||
- SVG icon generation
|
||||
- Icon theming and colorization
|
||||
- Size variants and scaling
|
||||
- Custom icon registration
|
||||
"""
|
||||
|
||||
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QBrush, QPen
|
||||
from PySide6.QtCore import Qt, QSize
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from typing import Dict, Optional, Tuple
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
class IconTheme:
|
||||
"""Icon theme management"""
|
||||
|
||||
# Icon colors for dark theme
|
||||
PRIMARY = "#FFFFFF"
|
||||
SECONDARY = "#B0B0B0"
|
||||
ACCENT = "#00BCD4"
|
||||
SUCCESS = "#4CAF50"
|
||||
WARNING = "#FF9800"
|
||||
ERROR = "#F44336"
|
||||
INFO = "#2196F3"
|
||||
|
||||
class SVGIcons:
|
||||
"""Collection of SVG icons as base64 encoded strings"""
|
||||
|
||||
# Navigation icons
|
||||
HOME = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2l7 7v11h-4v-7h-6v7H5V9l7-7z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
PLAY = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
PAUSE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
STOP = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 6h12v12H6z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
RECORD = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
# Detection and monitoring icons
|
||||
CAMERA = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 8.8c-2.1 0-3.8 1.7-3.8 3.8s1.7 3.8 3.8 3.8 3.8-1.7 3.8-3.8-1.7-3.8-3.8-3.8z"/>
|
||||
<path d="M21 7h-3.4l-1.9-2.6c-.4-.5-.9-.8-1.6-.8H9.9c-.7 0-1.2.3-1.6.8L6.4 7H3c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
MONITOR = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 3H3c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h6l-2 3v1h8v-1l-2-3h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 11H3V5h18v9z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
TRAFFIC_LIGHT = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="8" y="2" width="8" height="20" rx="4" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<circle cx="12" cy="7" r="2" fill="#F44336"/>
|
||||
<circle cx="12" cy="12" r="2" fill="#FF9800"/>
|
||||
<circle cx="12" cy="17" r="2" fill="#4CAF50"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
VIOLATION = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2L1 21h22L12 2zm0 3.99L19.53 19H4.47L12 5.99zM11 16h2v2h-2v-2zm0-6h2v4h-2v-4z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
# Analytics and statistics icons
|
||||
CHART_BAR = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M5 9v6h4V9H5zm6-4v10h4V5h-4zm6 6v4h4v-4h-4z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
CHART_LINE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L3 16.99l.5 1.5z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
CHART_PIE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11 2v20c-5.07-.5-9-4.79-9-10s3.93-9.5 9-10zm2.03 0v8.99H22c-.47-4.74-4.24-8.52-8.97-8.99zm0 11.01V22c4.74-.47 8.5-4.25 8.97-8.99h-8.97z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
DASHBOARD = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
# System and settings icons
|
||||
SETTINGS = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
EXPORT = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2L8 8h3v8h2V8h3l-4-6zm7 7h-2v10H7V9H5v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
IMPORT = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 22L8 16h3V8h2v8h3l-4 6zm7-15h-2V5H7v2H5V5c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2v2z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
SAVE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V6h10v3z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
# Status and alert icons
|
||||
CHECK_CIRCLE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
WARNING_CIRCLE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
ERROR_CIRCLE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
INFO_CIRCLE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
# Action icons
|
||||
REFRESH = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
DELETE = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
EDIT = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
FILTER = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
SEARCH = """
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
class IconManager:
|
||||
"""Manages icons for the application"""
|
||||
|
||||
def __init__(self):
|
||||
self._icon_cache: Dict[str, QIcon] = {}
|
||||
self.theme = IconTheme()
|
||||
|
||||
def get_icon(self, name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
|
||||
"""Get an icon by name with specified color and size"""
|
||||
cache_key = f"{name}_{color}_{size}"
|
||||
|
||||
if cache_key in self._icon_cache:
|
||||
return self._icon_cache[cache_key]
|
||||
|
||||
# Get SVG content
|
||||
svg_content = getattr(SVGIcons, name.upper(), None)
|
||||
if not svg_content:
|
||||
return QIcon() # Return empty icon if not found
|
||||
|
||||
# Replace currentColor with specified color
|
||||
svg_content = svg_content.replace('currentColor', color)
|
||||
|
||||
# Create icon from SVG
|
||||
icon = self._create_icon_from_svg(svg_content, size)
|
||||
self._icon_cache[cache_key] = icon
|
||||
|
||||
return icon
|
||||
|
||||
def _create_icon_from_svg(self, svg_content: str, size: int) -> QIcon:
|
||||
"""Create QIcon from SVG content"""
|
||||
# Create QSvgRenderer from SVG content
|
||||
svg_bytes = svg_content.encode('utf-8')
|
||||
renderer = QSvgRenderer(svg_bytes)
|
||||
|
||||
# Create pixmap
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.transparent)
|
||||
|
||||
# Paint SVG onto pixmap
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
def get_status_icon(self, status: str, size: int = 16) -> QIcon:
|
||||
"""Get icon for specific status"""
|
||||
status_map = {
|
||||
'success': ('CHECK_CIRCLE', IconTheme.SUCCESS),
|
||||
'warning': ('WARNING_CIRCLE', IconTheme.WARNING),
|
||||
'error': ('ERROR_CIRCLE', IconTheme.ERROR),
|
||||
'info': ('INFO_CIRCLE', IconTheme.INFO),
|
||||
'violation': ('VIOLATION', IconTheme.ERROR),
|
||||
'active': ('PLAY', IconTheme.SUCCESS),
|
||||
'inactive': ('PAUSE', IconTheme.SECONDARY),
|
||||
'recording': ('RECORD', IconTheme.ERROR)
|
||||
}
|
||||
|
||||
icon_name, color = status_map.get(status, ('INFO_CIRCLE', IconTheme.INFO))
|
||||
return self.get_icon(icon_name, color, size)
|
||||
|
||||
def get_action_icon(self, action: str, size: int = 20) -> QIcon:
|
||||
"""Get icon for specific action"""
|
||||
action_map = {
|
||||
'play': 'PLAY',
|
||||
'pause': 'PAUSE',
|
||||
'stop': 'STOP',
|
||||
'record': 'RECORD',
|
||||
'settings': 'SETTINGS',
|
||||
'export': 'EXPORT',
|
||||
'import': 'IMPORT',
|
||||
'save': 'SAVE',
|
||||
'refresh': 'REFRESH',
|
||||
'delete': 'DELETE',
|
||||
'edit': 'EDIT',
|
||||
'filter': 'FILTER',
|
||||
'search': 'SEARCH'
|
||||
}
|
||||
|
||||
icon_name = action_map.get(action, 'INFO_CIRCLE')
|
||||
return self.get_icon(icon_name, IconTheme.PRIMARY, size)
|
||||
|
||||
def get_navigation_icon(self, view: str, size: int = 24) -> QIcon:
|
||||
"""Get icon for navigation views"""
|
||||
nav_map = {
|
||||
'home': 'HOME',
|
||||
'detection': 'CAMERA',
|
||||
'violations': 'VIOLATION',
|
||||
'analytics': 'DASHBOARD',
|
||||
'export': 'EXPORT',
|
||||
'monitor': 'MONITOR',
|
||||
'chart': 'CHART_BAR'
|
||||
}
|
||||
|
||||
icon_name = nav_map.get(view, 'HOME')
|
||||
return self.get_icon(icon_name, IconTheme.ACCENT, size)
|
||||
|
||||
def create_colored_icon(self, base_icon: str, color: str, size: int = 24) -> QIcon:
|
||||
"""Create a colored version of an icon"""
|
||||
return self.get_icon(base_icon, color, size)
|
||||
|
||||
def set_theme_color(self, color: str):
|
||||
"""Set the theme accent color"""
|
||||
self.theme.ACCENT = color
|
||||
# Clear cache to regenerate icons with new color
|
||||
self._icon_cache.clear()
|
||||
|
||||
# Global icon manager instance
|
||||
icon_manager = IconManager()
|
||||
|
||||
# Convenience functions
|
||||
def get_icon(name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
|
||||
"""Get an icon - convenience function"""
|
||||
return icon_manager.get_icon(name, color, size)
|
||||
|
||||
def get_status_icon(status: str, size: int = 16) -> QIcon:
|
||||
"""Get status icon - convenience function"""
|
||||
return icon_manager.get_status_icon(status, size)
|
||||
|
||||
def get_action_icon(action: str, size: int = 20) -> QIcon:
|
||||
"""Get action icon - convenience function"""
|
||||
return icon_manager.get_action_icon(action, size)
|
||||
|
||||
def get_navigation_icon(view: str, size: int = 24) -> QIcon:
|
||||
"""Get navigation icon - convenience function"""
|
||||
return icon_manager.get_navigation_icon(view, size)
|
||||
|
||||
# Common icon sets for easy access
|
||||
class CommonIcons:
|
||||
"""Commonly used icon combinations"""
|
||||
|
||||
@staticmethod
|
||||
def toolbar_icons() -> Dict[str, QIcon]:
|
||||
"""Get all toolbar icons"""
|
||||
return {
|
||||
'play': get_action_icon('play'),
|
||||
'pause': get_action_icon('pause'),
|
||||
'stop': get_action_icon('stop'),
|
||||
'record': get_action_icon('record'),
|
||||
'settings': get_action_icon('settings'),
|
||||
'export': get_action_icon('export'),
|
||||
'refresh': get_action_icon('refresh')
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def status_icons() -> Dict[str, QIcon]:
|
||||
"""Get all status icons"""
|
||||
return {
|
||||
'success': get_status_icon('success'),
|
||||
'warning': get_status_icon('warning'),
|
||||
'error': get_status_icon('error'),
|
||||
'info': get_status_icon('info'),
|
||||
'violation': get_status_icon('violation'),
|
||||
'active': get_status_icon('active'),
|
||||
'inactive': get_status_icon('inactive'),
|
||||
'recording': get_status_icon('recording')
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def navigation_icons() -> Dict[str, QIcon]:
|
||||
"""Get all navigation icons"""
|
||||
return {
|
||||
'detection': get_navigation_icon('detection'),
|
||||
'violations': get_navigation_icon('violations'),
|
||||
'analytics': get_navigation_icon('analytics'),
|
||||
'export': get_navigation_icon('export'),
|
||||
'monitor': get_navigation_icon('monitor')
|
||||
}
|
||||
|
||||
# Traffic light specific icons
|
||||
def create_traffic_light_icon(red_on: bool = False, yellow_on: bool = False, green_on: bool = False, size: int = 32) -> QIcon:
|
||||
"""Create a traffic light icon with specific lights on/off"""
|
||||
svg_template = f"""
|
||||
<svg viewBox="0 0 24 24" width="{size}" height="{size}">
|
||||
<rect x="8" y="2" width="8" height="20" rx="4" stroke="#424242" stroke-width="2" fill="#2C2C2C"/>
|
||||
<circle cx="12" cy="7" r="2" fill="{'#F44336' if red_on else '#5D4037'}"/>
|
||||
<circle cx="12" cy="12" r="2" fill="{'#FF9800' if yellow_on else '#5D4037'}"/>
|
||||
<circle cx="12" cy="17" r="2" fill="{'#4CAF50' if green_on else '#5D4037'}"/>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
svg_bytes = svg_template.encode('utf-8')
|
||||
renderer = QSvgRenderer(svg_bytes)
|
||||
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
# New FinaleIcons class to wrap the existing functionality
|
||||
class FinaleIcons:
|
||||
"""
|
||||
Wrapper class for icon management to maintain compatibility
|
||||
with existing code that references FinaleIcons.get_icon() etc.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_icon(name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
|
||||
"""Get an icon by name"""
|
||||
return get_icon(name, color, size)
|
||||
|
||||
@staticmethod
|
||||
def get_status_icon(status: str, size: int = 16) -> QIcon:
|
||||
"""Get a status icon"""
|
||||
return get_status_icon(status, size)
|
||||
|
||||
@staticmethod
|
||||
def get_action_icon(action: str, size: int = 20) -> QIcon:
|
||||
"""Get an action icon"""
|
||||
return get_action_icon(action, size)
|
||||
|
||||
@staticmethod
|
||||
def get_navigation_icon(view: str, size: int = 24) -> QIcon:
|
||||
"""Get a navigation icon"""
|
||||
return get_navigation_icon(view, size)
|
||||
|
||||
@staticmethod
|
||||
def create_colored_icon(base_icon: str, color: str, size: int = 24) -> QIcon:
|
||||
"""Create a colored version of an icon"""
|
||||
return get_icon(base_icon, color, size)
|
||||
|
||||
@staticmethod
|
||||
def traffic_light_icon(red_on: bool = False, yellow_on: bool = False, green_on: bool = False, size: int = 32) -> QIcon:
|
||||
"""Create a traffic light icon with specific lights on/off"""
|
||||
return create_traffic_light_icon(red_on, yellow_on, green_on, size)
|
||||
51
qt_app_pyside1/finale/main.py
Normal file
51
qt_app_pyside1/finale/main.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from PySide6.QtWidgets import QApplication
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
def main():
|
||||
# Create application instance first
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
# Show splash screen if available
|
||||
splash = None
|
||||
try:
|
||||
from splash import show_splash
|
||||
splash, app = show_splash(app)
|
||||
except Exception as e:
|
||||
print(f"Could not show splash screen: {e}")
|
||||
|
||||
# Add a short delay to show the splash screen
|
||||
if splash:
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
# Try to use enhanced version with traffic light detection
|
||||
from ..ui.main_window import MainWindow
|
||||
print("✅ Using standard MainWindow")
|
||||
except Exception as e:
|
||||
# Fall back to standard version
|
||||
print(f"⚠️ Could not load MainWindow: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Initialize main window
|
||||
window = MainWindow()
|
||||
|
||||
# Close splash if it exists
|
||||
if splash:
|
||||
splash.finish(window)
|
||||
|
||||
# Show main window
|
||||
window.show()
|
||||
|
||||
# Start application event loop
|
||||
sys.exit(app.exec())
|
||||
except Exception as e:
|
||||
print(f"❌ Error starting application: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
558
qt_app_pyside1/finale/main_window.py
Normal file
558
qt_app_pyside1/finale/main_window.py
Normal file
@@ -0,0 +1,558 @@
|
||||
"""
|
||||
Finale UI - Modern Main Window
|
||||
Advanced traffic monitoring interface with Material Design and dark theme.
|
||||
Connects to existing detection/violation logic from qt_app_pyside.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
|
||||
QDockWidget, QSplitter, QFrame, QMessageBox, QApplication,
|
||||
QFileDialog, QStatusBar, QMenuBar, QMenu, QToolBar
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, Signal, Slot, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QIcon, QPixmap, QAction, QPainter, QBrush, QColor
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
# Import finale UI components
|
||||
try:
|
||||
# Try relative imports first (when running as a package)
|
||||
from .styles import FinaleStyles, MaterialColors
|
||||
from .icons import FinaleIcons
|
||||
from .toolbar import FinaleToolbar
|
||||
from .components.stats_widgets import StatsWidget, MetricsWidget, SystemResourceWidget
|
||||
from .views import LiveView, AnalyticsView, ViolationsView, SettingsView
|
||||
except ImportError:
|
||||
# Fallback to direct imports (when running as script)
|
||||
try:
|
||||
from styles import FinaleStyles, MaterialColors
|
||||
from icons import FinaleIcons
|
||||
from toolbar import FinaleToolbar
|
||||
from components.stats_widgets import StatsWidget, MetricsWidget, SystemResourceWidget
|
||||
from views import LiveView, AnalyticsView, ViolationsView, SettingsView
|
||||
except ImportError:
|
||||
print('Error importing main window components')
|
||||
|
||||
# Import existing detection/violation logic from qt_app_pyside
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
try:
|
||||
from controllers.model_manager import ModelManager
|
||||
from controllers.video_controller_new import VideoController
|
||||
from controllers.analytics_controller import AnalyticsController
|
||||
from controllers.performance_overlay import PerformanceOverlay
|
||||
# Import detection_openvino for advanced detection logic
|
||||
from detection_openvino import OpenVINOVehicleDetector
|
||||
from red_light_violation_pipeline import RedLightViolationPipeline
|
||||
from utils.helpers import load_configuration, save_configuration
|
||||
from utils.annotation_utils import draw_detections, convert_cv_to_pixmap
|
||||
from utils.enhanced_annotation_utils import enhanced_draw_detections
|
||||
from utils.traffic_light_utils import detect_traffic_light_color
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import some dependencies: {e}")
|
||||
# Fallback imports
|
||||
from controllers.model_manager import ModelManager
|
||||
VideoController = None
|
||||
def load_configuration(path): return {}
|
||||
def save_configuration(config, path): pass
|
||||
|
||||
class FinaleMainWindow(QMainWindow):
|
||||
"""
|
||||
Modern main window for traffic monitoring with advanced UI.
|
||||
Connects to existing detection/violation logic without modifying it.
|
||||
"""
|
||||
|
||||
# Signals for UI updates
|
||||
theme_changed = Signal(bool) # dark_mode
|
||||
view_changed = Signal(str) # view_name
|
||||
fullscreen_toggled = Signal(bool)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Initialize settings and configuration
|
||||
self.settings = QSettings("Finale", "TrafficMonitoring")
|
||||
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "qt_app_pyside", "config.json")
|
||||
self.config = load_configuration(self.config_file)
|
||||
|
||||
# UI state
|
||||
self.dark_mode = True
|
||||
self.current_view = "live"
|
||||
self.is_fullscreen = False
|
||||
|
||||
# Animation system
|
||||
self.animations = {}
|
||||
|
||||
# Initialize UI
|
||||
self.setup_ui()
|
||||
|
||||
# Initialize backend controllers (existing logic)
|
||||
self.setup_controllers()
|
||||
|
||||
# Connect signals
|
||||
self.connect_signals()
|
||||
|
||||
# Apply theme and restore settings
|
||||
self.apply_theme()
|
||||
self.restore_settings()
|
||||
|
||||
# Show ready message
|
||||
self.statusBar().showMessage("Finale UI Ready", 3000)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the modern user interface"""
|
||||
# Window properties with advanced styling
|
||||
self.setWindowTitle("Finale Traffic Monitoring System")
|
||||
self.setMinimumSize(1400, 900)
|
||||
self.resize(1600, 1000)
|
||||
|
||||
# Set window icon
|
||||
self.setWindowIcon(FinaleIcons.get_icon("traffic_monitoring"))
|
||||
|
||||
# Create central widget with modern layout
|
||||
self.setup_central_widget()
|
||||
|
||||
# Create modern toolbar
|
||||
self.setup_toolbar()
|
||||
|
||||
# Create docked widgets
|
||||
self.setup_dock_widgets()
|
||||
|
||||
# Create status bar
|
||||
self.setup_status_bar()
|
||||
|
||||
# Create menu bar
|
||||
self.setup_menu_bar()
|
||||
|
||||
# Apply initial styling
|
||||
self.setStyleSheet(FinaleStyles.get_main_window_style())
|
||||
|
||||
def setup_central_widget(self):
|
||||
"""Create the central widget with modern tabbed interface"""
|
||||
# Create main splitter for flexible layout
|
||||
self.main_splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Create left panel for main content
|
||||
self.content_widget = QWidget()
|
||||
self.content_layout = QVBoxLayout(self.content_widget)
|
||||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.content_layout.setSpacing(0)
|
||||
|
||||
# Create modern tab widget
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setTabPosition(QTabWidget.North)
|
||||
self.tabs.setMovable(True)
|
||||
self.tabs.setTabsClosable(False)
|
||||
|
||||
# Create views (these will be implemented next)
|
||||
self.live_view = LiveView()
|
||||
self.analytics_view = AnalyticsView()
|
||||
self.violations_view = ViolationsView()
|
||||
self.settings_view = SettingsView()
|
||||
|
||||
# Add tabs with icons
|
||||
self.tabs.addTab(self.live_view, FinaleIcons.get_icon("live"), "Live Detection")
|
||||
self.tabs.addTab(self.analytics_view, FinaleIcons.get_icon("analytics"), "Analytics")
|
||||
self.tabs.addTab(self.violations_view, FinaleIcons.get_icon("warning"), "Violations")
|
||||
self.tabs.addTab(self.settings_view, FinaleIcons.get_icon("settings"), "Settings")
|
||||
|
||||
# Style the tab widget
|
||||
self.tabs.setStyleSheet(FinaleStyles.get_tab_widget_style())
|
||||
|
||||
# Add to layout
|
||||
self.content_layout.addWidget(self.tabs)
|
||||
self.main_splitter.addWidget(self.content_widget)
|
||||
|
||||
# Set as central widget
|
||||
self.setCentralWidget(self.main_splitter)
|
||||
|
||||
def setup_toolbar(self):
|
||||
"""Create the modern toolbar"""
|
||||
self.toolbar = FinaleToolbar(self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
|
||||
|
||||
# Connect toolbar signals
|
||||
self.toolbar.play_clicked.connect(self.on_play_clicked)
|
||||
self.toolbar.pause_clicked.connect(self.on_pause_clicked)
|
||||
self.toolbar.stop_clicked.connect(self.on_stop_clicked)
|
||||
self.toolbar.record_clicked.connect(self.on_record_clicked)
|
||||
self.toolbar.snapshot_clicked.connect(self.on_snapshot_clicked)
|
||||
self.toolbar.settings_clicked.connect(self.show_settings)
|
||||
self.toolbar.fullscreen_clicked.connect(self.toggle_fullscreen)
|
||||
self.toolbar.theme_changed.connect(self.set_dark_mode)
|
||||
|
||||
def setup_dock_widgets(self):
|
||||
"""Create docked widgets for statistics and controls"""
|
||||
# Stats dock widget
|
||||
self.stats_dock = QDockWidget("Statistics", self)
|
||||
self.stats_dock.setObjectName("StatsDock")
|
||||
self.stats_widget = StatsWidget()
|
||||
self.stats_dock.setWidget(self.stats_widget)
|
||||
self.stats_dock.setFeatures(
|
||||
QDockWidget.DockWidgetMovable |
|
||||
QDockWidget.DockWidgetClosable |
|
||||
QDockWidget.DockWidgetFloatable
|
||||
)
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.stats_dock)
|
||||
|
||||
# Metrics dock widget
|
||||
self.metrics_dock = QDockWidget("Performance", self)
|
||||
self.metrics_dock.setObjectName("MetricsDock")
|
||||
self.metrics_widget = MetricsWidget()
|
||||
self.metrics_dock.setWidget(self.metrics_widget)
|
||||
self.metrics_dock.setFeatures(
|
||||
QDockWidget.DockWidgetMovable |
|
||||
QDockWidget.DockWidgetClosable |
|
||||
QDockWidget.DockWidgetFloatable
|
||||
)
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.metrics_dock)
|
||||
|
||||
# System resources dock widget
|
||||
self.system_dock = QDockWidget("System", self)
|
||||
self.system_dock.setObjectName("SystemDock")
|
||||
self.system_widget = SystemResourceWidget()
|
||||
self.system_dock.setWidget(self.system_widget)
|
||||
self.system_dock.setFeatures(
|
||||
QDockWidget.DockWidgetMovable |
|
||||
QDockWidget.DockWidgetClosable |
|
||||
QDockWidget.DockWidgetFloatable
|
||||
)
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.system_dock)
|
||||
|
||||
# Tabify dock widgets for space efficiency
|
||||
self.tabifyDockWidget(self.stats_dock, self.metrics_dock)
|
||||
self.tabifyDockWidget(self.metrics_dock, self.system_dock)
|
||||
|
||||
# Show stats dock by default
|
||||
self.stats_dock.raise_()
|
||||
|
||||
# Apply dock widget styling
|
||||
for dock in [self.stats_dock, self.metrics_dock, self.system_dock]:
|
||||
dock.setStyleSheet(FinaleStyles.get_dock_widget_style())
|
||||
|
||||
def setup_status_bar(self):
|
||||
"""Create modern status bar"""
|
||||
self.status_bar = QStatusBar()
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
# Add permanent widgets to status bar
|
||||
self.fps_label = QWidget()
|
||||
self.connection_label = QWidget()
|
||||
self.model_label = QWidget()
|
||||
|
||||
self.status_bar.addPermanentWidget(self.fps_label)
|
||||
self.status_bar.addPermanentWidget(self.connection_label)
|
||||
self.status_bar.addPermanentWidget(self.model_label)
|
||||
|
||||
# Style status bar
|
||||
self.status_bar.setStyleSheet(FinaleStyles.get_status_bar_style())
|
||||
|
||||
def setup_menu_bar(self):
|
||||
"""Create modern menu bar"""
|
||||
self.menu_bar = self.menuBar()
|
||||
|
||||
# File menu
|
||||
file_menu = self.menu_bar.addMenu("&File")
|
||||
|
||||
open_action = QAction(FinaleIcons.get_icon("folder"), "&Open Video", self)
|
||||
open_action.setShortcut("Ctrl+O")
|
||||
open_action.triggered.connect(self.open_file)
|
||||
file_menu.addAction(open_action)
|
||||
|
||||
save_action = QAction(FinaleIcons.get_icon("save"), "&Save Config", self)
|
||||
save_action.setShortcut("Ctrl+S")
|
||||
save_action.triggered.connect(self.save_config)
|
||||
file_menu.addAction(save_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
exit_action = QAction(FinaleIcons.get_icon("exit"), "E&xit", self)
|
||||
exit_action.setShortcut("Ctrl+Q")
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# View menu
|
||||
view_menu = self.menu_bar.addMenu("&View")
|
||||
|
||||
fullscreen_action = QAction(FinaleIcons.get_icon("fullscreen"), "&Fullscreen", self)
|
||||
fullscreen_action.setShortcut("F11")
|
||||
fullscreen_action.setCheckable(True)
|
||||
fullscreen_action.triggered.connect(self.toggle_fullscreen)
|
||||
view_menu.addAction(fullscreen_action)
|
||||
|
||||
theme_action = QAction(FinaleIcons.get_icon("theme"), "&Dark Theme", self)
|
||||
theme_action.setCheckable(True)
|
||||
theme_action.setChecked(self.dark_mode)
|
||||
theme_action.triggered.connect(self.toggle_theme)
|
||||
view_menu.addAction(theme_action)
|
||||
|
||||
# Tools menu
|
||||
tools_menu = self.menu_bar.addMenu("&Tools")
|
||||
|
||||
settings_action = QAction(FinaleIcons.get_icon("settings"), "&Settings", self)
|
||||
settings_action.setShortcut("Ctrl+,")
|
||||
settings_action.triggered.connect(self.show_settings)
|
||||
tools_menu.addAction(settings_action)
|
||||
|
||||
# Help menu
|
||||
help_menu = self.menu_bar.addMenu("&Help")
|
||||
|
||||
about_action = QAction(FinaleIcons.get_icon("info"), "&About", self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
# Style menu bar
|
||||
self.menu_bar.setStyleSheet(FinaleStyles.get_menu_bar_style())
|
||||
|
||||
def setup_controllers(self):
|
||||
"""Initialize backend controllers (existing logic)"""
|
||||
try:
|
||||
# Initialize model manager (existing from qt_app_pyside)
|
||||
self.model_manager = ModelManager(self.config_file)
|
||||
|
||||
# Initialize video controller (existing from qt_app_pyside)
|
||||
self.video_controller = VideoController(self.model_manager)
|
||||
|
||||
# Initialize analytics controller (existing from qt_app_pyside)
|
||||
self.analytics_controller = AnalyticsController()
|
||||
|
||||
# Initialize performance overlay (existing from qt_app_pyside)
|
||||
self.performance_overlay = PerformanceOverlay()
|
||||
|
||||
print("✅ Backend controllers initialized successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error initializing controllers: {e}")
|
||||
QMessageBox.critical(self, "Initialization Error",
|
||||
f"Failed to initialize backend controllers:\n{str(e)}")
|
||||
|
||||
def connect_signals(self):
|
||||
"""Connect signals between UI and backend"""
|
||||
try:
|
||||
# Connect video controller signals to UI updates
|
||||
if hasattr(self.video_controller, 'frame_ready'):
|
||||
self.video_controller.frame_ready.connect(self.on_frame_ready)
|
||||
|
||||
if hasattr(self.video_controller, 'stats_ready'):
|
||||
self.video_controller.stats_ready.connect(self.on_stats_ready)
|
||||
|
||||
if hasattr(self.video_controller, 'violation_detected'):
|
||||
self.video_controller.violation_detected.connect(self.on_violation_detected)
|
||||
|
||||
# Connect tab change signal
|
||||
self.tabs.currentChanged.connect(self.on_tab_changed)
|
||||
|
||||
# Connect view signals to backend
|
||||
self.live_view.source_changed.connect(self.on_source_changed)
|
||||
|
||||
print("✅ Signals connected successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error connecting signals: {e}")
|
||||
|
||||
# Event handlers for UI interactions
|
||||
@Slot()
|
||||
def on_play_clicked(self):
|
||||
"""Handle play button click"""
|
||||
if hasattr(self.video_controller, 'start'):
|
||||
self.video_controller.start()
|
||||
self.toolbar.set_playback_state("playing")
|
||||
|
||||
@Slot()
|
||||
def on_pause_clicked(self):
|
||||
"""Handle pause button click"""
|
||||
if hasattr(self.video_controller, 'pause'):
|
||||
self.video_controller.pause()
|
||||
self.toolbar.set_playback_state("paused")
|
||||
|
||||
@Slot()
|
||||
def on_stop_clicked(self):
|
||||
"""Handle stop button click"""
|
||||
if hasattr(self.video_controller, 'stop'):
|
||||
self.video_controller.stop()
|
||||
self.toolbar.set_playback_state("stopped")
|
||||
|
||||
@Slot()
|
||||
def on_record_clicked(self):
|
||||
"""Handle record button click"""
|
||||
# Implementation depends on existing recording logic
|
||||
pass
|
||||
|
||||
@Slot()
|
||||
def on_snapshot_clicked(self):
|
||||
"""Handle snapshot button click"""
|
||||
# Implementation depends on existing snapshot logic
|
||||
pass
|
||||
|
||||
# Backend signal handlers
|
||||
@Slot(object, object, dict)
|
||||
def on_frame_ready(self, pixmap, detections, metrics):
|
||||
"""Handle frame ready signal from video controller"""
|
||||
# Update live view
|
||||
if self.current_view == "live":
|
||||
self.live_view.update_frame(pixmap, detections)
|
||||
|
||||
# Update toolbar status
|
||||
self.toolbar.update_status("processing", True)
|
||||
|
||||
@Slot(dict)
|
||||
def on_stats_ready(self, stats):
|
||||
"""Handle stats ready signal from video controller"""
|
||||
# Update stats widgets
|
||||
self.stats_widget.update_stats(stats)
|
||||
self.metrics_widget.update_metrics(stats)
|
||||
|
||||
# Update toolbar FPS
|
||||
if 'fps' in stats:
|
||||
self.toolbar.update_fps(stats['fps'])
|
||||
|
||||
@Slot(dict)
|
||||
def on_violation_detected(self, violation_data):
|
||||
"""Handle violation detected signal"""
|
||||
# Update violations view
|
||||
self.violations_view.add_violation(violation_data)
|
||||
|
||||
# Update toolbar status
|
||||
self.toolbar.update_status("violation", True)
|
||||
|
||||
# Play notification sound/animation if enabled
|
||||
self.play_violation_notification()
|
||||
|
||||
@Slot(str)
|
||||
def on_source_changed(self, source_path):
|
||||
"""Handle source change from live view"""
|
||||
if hasattr(self.video_controller, 'set_source'):
|
||||
self.video_controller.set_source(source_path)
|
||||
|
||||
@Slot(int)
|
||||
def on_tab_changed(self, index):
|
||||
"""Handle tab change"""
|
||||
tab_names = ["live", "analytics", "violations", "settings"]
|
||||
if 0 <= index < len(tab_names):
|
||||
self.current_view = tab_names[index]
|
||||
self.view_changed.emit(self.current_view)
|
||||
|
||||
# UI control methods
|
||||
def toggle_fullscreen(self):
|
||||
"""Toggle fullscreen mode"""
|
||||
if self.isFullScreen():
|
||||
self.showNormal()
|
||||
self.is_fullscreen = False
|
||||
else:
|
||||
self.showFullScreen()
|
||||
self.is_fullscreen = True
|
||||
|
||||
self.fullscreen_toggled.emit(self.is_fullscreen)
|
||||
|
||||
def toggle_theme(self):
|
||||
"""Toggle between dark and light theme"""
|
||||
self.set_dark_mode(not self.dark_mode)
|
||||
|
||||
def set_dark_mode(self, dark_mode):
|
||||
"""Set theme mode"""
|
||||
self.dark_mode = dark_mode
|
||||
self.apply_theme()
|
||||
self.theme_changed.emit(self.dark_mode)
|
||||
|
||||
def apply_theme(self):
|
||||
"""Apply current theme to all UI elements"""
|
||||
# Apply main styles
|
||||
self.setStyleSheet(FinaleStyles.get_main_window_style(self.dark_mode))
|
||||
|
||||
# Update all child widgets
|
||||
for child in self.findChildren(QWidget):
|
||||
if hasattr(child, 'apply_theme'):
|
||||
child.apply_theme(self.dark_mode)
|
||||
|
||||
# Update color scheme
|
||||
if self.dark_mode:
|
||||
MaterialColors.apply_dark_theme()
|
||||
else:
|
||||
MaterialColors.apply_light_theme()
|
||||
|
||||
def show_settings(self):
|
||||
"""Show settings view"""
|
||||
self.tabs.setCurrentWidget(self.settings_view)
|
||||
|
||||
def show_about(self):
|
||||
"""Show about dialog"""
|
||||
QMessageBox.about(self, "About Finale UI",
|
||||
"Finale Traffic Monitoring System\n"
|
||||
"Modern UI for OpenVINO-based traffic detection\n"
|
||||
"Built with PySide6 and Material Design")
|
||||
|
||||
def open_file(self):
|
||||
"""Open file dialog for video source"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Open Video File", "",
|
||||
"Video Files (*.mp4 *.avi *.mov *.mkv);;All Files (*)"
|
||||
)
|
||||
if file_path:
|
||||
self.on_source_changed(file_path)
|
||||
|
||||
def save_config(self):
|
||||
"""Save current configuration"""
|
||||
try:
|
||||
save_configuration(self.config, self.config_file)
|
||||
self.statusBar().showMessage("Configuration saved", 3000)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Save Error", f"Failed to save configuration:\n{str(e)}")
|
||||
|
||||
def play_violation_notification(self):
|
||||
"""Play violation notification (visual/audio)"""
|
||||
# Create a brief red flash animation
|
||||
self.create_violation_flash()
|
||||
|
||||
def create_violation_flash(self):
|
||||
"""Create a red flash effect for violations"""
|
||||
# Create a semi-transparent red overlay
|
||||
overlay = QWidget(self)
|
||||
overlay.setStyleSheet("background-color: rgba(244, 67, 54, 0.3);")
|
||||
overlay.resize(self.size())
|
||||
overlay.show()
|
||||
|
||||
# Animate the overlay
|
||||
self.flash_animation = QPropertyAnimation(overlay, b"windowOpacity")
|
||||
self.flash_animation.setDuration(500)
|
||||
self.flash_animation.setStartValue(0.3)
|
||||
self.flash_animation.setEndValue(0.0)
|
||||
self.flash_animation.setEasingCurve(QEasingCurve.OutCubic)
|
||||
self.flash_animation.finished.connect(overlay.deleteLater)
|
||||
self.flash_animation.start()
|
||||
|
||||
# Settings persistence
|
||||
def save_settings(self):
|
||||
"""Save window settings"""
|
||||
self.settings.setValue("geometry", self.saveGeometry())
|
||||
self.settings.setValue("windowState", self.saveState())
|
||||
self.settings.setValue("dark_mode", self.dark_mode)
|
||||
self.settings.setValue("current_view", self.current_view)
|
||||
|
||||
def restore_settings(self):
|
||||
"""Restore window settings"""
|
||||
if self.settings.contains("geometry"):
|
||||
self.restoreGeometry(self.settings.value("geometry"))
|
||||
if self.settings.contains("windowState"):
|
||||
self.restoreState(self.settings.value("windowState"))
|
||||
if self.settings.contains("dark_mode"):
|
||||
self.dark_mode = self.settings.value("dark_mode", True, bool)
|
||||
if self.settings.contains("current_view"):
|
||||
view_name = self.settings.value("current_view", "live")
|
||||
view_index = {"live": 0, "analytics": 1, "violations": 2, "settings": 3}.get(view_name, 0)
|
||||
self.tabs.setCurrentIndex(view_index)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event"""
|
||||
# Save settings
|
||||
self.save_settings()
|
||||
|
||||
# Stop video controller
|
||||
if hasattr(self.video_controller, 'stop'):
|
||||
self.video_controller.stop()
|
||||
|
||||
# Accept close event
|
||||
event.accept()
|
||||
641
qt_app_pyside1/finale/main_window_old.py
Normal file
641
qt_app_pyside1/finale/main_window_old.py
Normal file
@@ -0,0 +1,641 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QTabWidget, QDockWidget, QMessageBox,
|
||||
QApplication, QFileDialog, QSplashScreen
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, Slot
|
||||
from PySide6.QtGui import QIcon, QPixmap, QAction
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
# Custom exception handler for Qt
|
||||
def qt_message_handler(mode, context, message):
|
||||
print(f"Qt Message: {message} (Mode: {mode})")
|
||||
|
||||
# Install custom handler for Qt messages
|
||||
if hasattr(Qt, 'qInstallMessageHandler'):
|
||||
Qt.qInstallMessageHandler(qt_message_handler)
|
||||
|
||||
# Import UI components
|
||||
from ..ui.fixed_live_tab import LiveTab # Using fixed version
|
||||
from ..ui.analytics_tab import AnalyticsTab
|
||||
from ..ui.violations_tab import ViolationsTab
|
||||
from ..ui.export_tab import ExportTab
|
||||
from ..ui.config_panel import ConfigPanel
|
||||
|
||||
# Import controllers
|
||||
from ..controllers.video_controller_new import VideoController
|
||||
from ..controllers.analytics_controller import AnalyticsController
|
||||
from ..controllers.performance_overlay import PerformanceOverlay
|
||||
from ..controllers.model_manager import ModelManager
|
||||
|
||||
# Import utilities
|
||||
from ..utils.helpers import load_configuration, save_configuration, save_snapshot
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main application window."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Initialize settings and configuration
|
||||
self.settings = QSettings("OpenVINO", "TrafficMonitoring")
|
||||
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
||||
self.config = load_configuration(self.config_file)
|
||||
|
||||
# Set up UI
|
||||
self.setupUI()
|
||||
|
||||
# Initialize controllers
|
||||
self.setupControllers()
|
||||
|
||||
# Connect signals and slots
|
||||
self.connectSignals()
|
||||
|
||||
# Restore settings
|
||||
self.restoreSettings()
|
||||
|
||||
# Apply theme
|
||||
self.applyTheme(True) # Start with dark theme
|
||||
|
||||
# Show ready message
|
||||
self.statusBar().showMessage("Ready")
|
||||
|
||||
def setupUI(self):
|
||||
"""Set up the user interface"""
|
||||
# Window properties
|
||||
self.setWindowTitle("Traffic Monitoring System (OpenVINO PySide6)")
|
||||
self.setMinimumSize(1200, 800)
|
||||
self.resize(1400, 900)
|
||||
|
||||
# Set up central widget with tabs
|
||||
self.tabs = QTabWidget()
|
||||
|
||||
# Create tabs
|
||||
self.live_tab = LiveTab()
|
||||
self.analytics_tab = AnalyticsTab()
|
||||
self.violations_tab = ViolationsTab()
|
||||
self.export_tab = ExportTab()
|
||||
|
||||
# Add tabs to tab widget
|
||||
self.tabs.addTab(self.live_tab, "Live Detection")
|
||||
self.tabs.addTab(self.analytics_tab, "Analytics")
|
||||
self.tabs.addTab(self.violations_tab, "Violations")
|
||||
self.tabs.addTab(self.export_tab, "Export & Config")
|
||||
|
||||
# Set central widget
|
||||
self.setCentralWidget(self.tabs)
|
||||
# Create config panel in dock widget
|
||||
self.config_panel = ConfigPanel()
|
||||
dock = QDockWidget("Settings", self)
|
||||
dock.setObjectName("SettingsDock") # Set object name to avoid warning
|
||||
dock.setWidget(self.config_panel)
|
||||
dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
|
||||
dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, dock)
|
||||
|
||||
# Create status bar
|
||||
self.statusBar().showMessage("Initializing...")
|
||||
|
||||
# Create menu bar
|
||||
self.setupMenus()
|
||||
|
||||
# Create performance overlay
|
||||
self.performance_overlay = PerformanceOverlay()
|
||||
|
||||
def setupControllers(self):
|
||||
"""Set up controllers and models"""
|
||||
# Load config from file
|
||||
try:
|
||||
# Initialize model manager
|
||||
self.model_manager = ModelManager(self.config_file)
|
||||
|
||||
# Create video controller
|
||||
self.video_controller = VideoController(self.model_manager)
|
||||
|
||||
# Create analytics controller
|
||||
self.analytics_controller = AnalyticsController()
|
||||
|
||||
# Setup update timer for performance overlay
|
||||
self.perf_timer = QTimer()
|
||||
self.perf_timer.timeout.connect(self.performance_overlay.update_stats)
|
||||
self.perf_timer.start(1000) # Update every second
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Initialization Error",
|
||||
f"Error initializing controllers: {str(e)}"
|
||||
)
|
||||
print(f"Error details: {e}")
|
||||
|
||||
|
||||
def connectSignals(self):
|
||||
"""Connect signals and slots between components""" # Video controller connections - With extra debug
|
||||
print("🔌 Connecting video controller signals...")
|
||||
try:
|
||||
# Connect for UI frame updates (QPixmap-based)
|
||||
self.video_controller.frame_ready.connect(self.live_tab.update_display, Qt.QueuedConnection)
|
||||
print("✅ Connected frame_ready signal") # Connect for direct NumPy frame display (critical for live video)
|
||||
try:
|
||||
self.video_controller.frame_np_ready.connect(self.live_tab.update_display_np, Qt.QueuedConnection)
|
||||
print("✅ Connected frame_np_ready signal")
|
||||
# PySide6 doesn't have isConnected method, so let's just confirm the connection works
|
||||
print("🔌 frame_np_ready connection should be established")
|
||||
except Exception as e:
|
||||
print(f"❌ Error connecting frame_np_ready signal: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Connect stats signal
|
||||
self.video_controller.stats_ready.connect(self.live_tab.update_stats, Qt.QueuedConnection)
|
||||
# Also connect stats signal to update traffic light status in main window
|
||||
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
|
||||
print("✅ Connected stats_ready signals")
|
||||
# Connect raw frame data for analytics
|
||||
self.video_controller.raw_frame_ready.connect(self.analytics_controller.process_frame_data)
|
||||
print("✅ Connected raw_frame_ready signal")
|
||||
|
||||
# Connect for traffic light status updates
|
||||
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
|
||||
print("✅ Connected stats_ready signal to update_traffic_light_status")
|
||||
|
||||
# Connect violation detection signal
|
||||
try:
|
||||
self.video_controller.violation_detected.connect(self.handle_violation_detected, Qt.QueuedConnection)
|
||||
print("✅ Connected violation_detected signal")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not connect violation signal: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error connecting signals: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Live tab connections
|
||||
self.live_tab.source_changed.connect(self.video_controller.set_source)
|
||||
self.live_tab.video_dropped.connect(self.video_controller.set_source)
|
||||
self.live_tab.snapshot_requested.connect(self.take_snapshot)
|
||||
self.live_tab.run_requested.connect(self.toggle_video_processing)
|
||||
|
||||
# Config panel connections
|
||||
self.config_panel.config_changed.connect(self.apply_config)
|
||||
self.config_panel.theme_toggled.connect(self.applyTheme)
|
||||
|
||||
# Analytics controller connections
|
||||
self.analytics_controller.analytics_updated.connect(self.analytics_tab.update_analytics)
|
||||
self.analytics_controller.analytics_updated.connect(self.export_tab.update_export_preview)
|
||||
|
||||
# Tab-specific connections
|
||||
self.violations_tab.clear_btn.clicked.connect(self.analytics_controller.clear_statistics)
|
||||
self.export_tab.reset_btn.clicked.connect(self.config_panel.reset_config)
|
||||
self.export_tab.save_config_btn.clicked.connect(self.save_config)
|
||||
self.export_tab.reload_config_btn.clicked.connect(self.load_config)
|
||||
self.export_tab.export_btn.clicked.connect(self.export_data)
|
||||
|
||||
def setupMenus(self):
|
||||
"""Set up application menus"""
|
||||
# File menu
|
||||
file_menu = self.menuBar().addMenu("&File")
|
||||
|
||||
open_action = QAction("&Open Video...", self)
|
||||
open_action.setShortcut("Ctrl+O")
|
||||
open_action.triggered.connect(self.open_video_file)
|
||||
file_menu.addAction(open_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
snapshot_action = QAction("Take &Snapshot", self)
|
||||
snapshot_action.setShortcut("Ctrl+S")
|
||||
snapshot_action.triggered.connect(self.take_snapshot)
|
||||
file_menu.addAction(snapshot_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
exit_action = QAction("E&xit", self)
|
||||
exit_action.setShortcut("Alt+F4")
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# View menu
|
||||
view_menu = self.menuBar().addMenu("&View")
|
||||
|
||||
toggle_config_action = QAction("Show/Hide &Settings Panel", self)
|
||||
toggle_config_action.setShortcut("F4")
|
||||
toggle_config_action.triggered.connect(self.toggle_config_panel)
|
||||
view_menu.addAction(toggle_config_action)
|
||||
|
||||
toggle_perf_action = QAction("Show/Hide &Performance Overlay", self)
|
||||
toggle_perf_action.setShortcut("F5")
|
||||
toggle_perf_action.triggered.connect(self.toggle_performance_overlay)
|
||||
view_menu.addAction(toggle_perf_action)
|
||||
|
||||
# Help menu
|
||||
help_menu = self.menuBar().addMenu("&Help")
|
||||
|
||||
about_action = QAction("&About", self)
|
||||
about_action.triggered.connect(self.show_about_dialog)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
@Slot(dict)
|
||||
def apply_config(self, config):
|
||||
"""
|
||||
Apply configuration changes.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
"""
|
||||
# Update configuration
|
||||
if not config:
|
||||
return
|
||||
|
||||
# Update config
|
||||
for section in config:
|
||||
if section in self.config:
|
||||
self.config[section].update(config[section])
|
||||
else:
|
||||
self.config[section] = config[section]
|
||||
|
||||
# Update model manager
|
||||
if self.model_manager:
|
||||
self.model_manager.update_config(self.config)
|
||||
|
||||
# Save config to file
|
||||
save_configuration(self.config, self.config_file)
|
||||
|
||||
# Update export tab
|
||||
self.export_tab.update_config_display(self.config)
|
||||
|
||||
# Update status
|
||||
self.statusBar().showMessage("Configuration applied", 2000)
|
||||
|
||||
@Slot()
|
||||
def load_config(self):
|
||||
"""Load configuration from file"""
|
||||
# Ask for confirmation if needed
|
||||
if self.video_controller and self.video_controller._running:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Reload Configuration",
|
||||
"Reloading configuration will stop current processing. Continue?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.No:
|
||||
return
|
||||
|
||||
# Stop processing
|
||||
self.video_controller.stop()
|
||||
|
||||
# Load config
|
||||
self.config = load_configuration(self.config_file)
|
||||
|
||||
# Update UI
|
||||
self.config_panel.set_config(self.config)
|
||||
self.export_tab.update_config_display(self.config)
|
||||
|
||||
# Update model manager
|
||||
if self.model_manager:
|
||||
self.model_manager.update_config(self.config)
|
||||
|
||||
# Update status
|
||||
self.statusBar().showMessage("Configuration loaded", 2000)
|
||||
|
||||
@Slot()
|
||||
def save_config(self):
|
||||
"""Save configuration to file"""
|
||||
# Get config from UI
|
||||
ui_config = self.export_tab.get_config_from_ui()
|
||||
|
||||
# Update config
|
||||
for section in ui_config:
|
||||
if section in self.config:
|
||||
self.config[section].update(ui_config[section])
|
||||
else:
|
||||
self.config[section] = ui_config[section]
|
||||
|
||||
# Save to file
|
||||
if save_configuration(self.config, self.config_file):
|
||||
self.statusBar().showMessage("Configuration saved", 2000)
|
||||
else:
|
||||
self.statusBar().showMessage("Error saving configuration", 2000)
|
||||
|
||||
# Update model manager
|
||||
if self.model_manager:
|
||||
self.model_manager.update_config(self.config)
|
||||
|
||||
@Slot()
|
||||
def open_video_file(self):
|
||||
"""Open video file dialog"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Open Video File",
|
||||
"",
|
||||
"Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
# Update live tab
|
||||
self.live_tab.source_changed.emit(file_path)
|
||||
|
||||
# Update status
|
||||
self.statusBar().showMessage(f"Loaded video: {os.path.basename(file_path)}")
|
||||
|
||||
@Slot()
|
||||
def take_snapshot(self):
|
||||
"""Take snapshot of current frame"""
|
||||
if self.video_controller:
|
||||
# Get current frame
|
||||
frame = self.video_controller.capture_snapshot()
|
||||
|
||||
if frame is not None:
|
||||
# Save frame to file
|
||||
save_dir = self.settings.value("snapshot_dir", ".")
|
||||
file_path = os.path.join(save_dir, "snapshot_" +
|
||||
str(int(time.time())) + ".jpg")
|
||||
|
||||
saved_path = save_snapshot(frame, file_path)
|
||||
|
||||
if saved_path:
|
||||
self.statusBar().showMessage(f"Snapshot saved: {saved_path}", 3000)
|
||||
else:
|
||||
self.statusBar().showMessage("Error saving snapshot", 3000)
|
||||
else:
|
||||
self.statusBar().showMessage("No frame to capture", 3000)
|
||||
|
||||
@Slot()
|
||||
def toggle_config_panel(self):
|
||||
"""Toggle configuration panel visibility"""
|
||||
dock_widgets = self.findChildren(QDockWidget)
|
||||
for dock in dock_widgets:
|
||||
dock.setVisible(not dock.isVisible())
|
||||
|
||||
@Slot()
|
||||
def toggle_performance_overlay(self):
|
||||
"""Toggle performance overlay visibility"""
|
||||
if self.performance_overlay.isVisible():
|
||||
self.performance_overlay.hide()
|
||||
else:
|
||||
# Position in the corner
|
||||
self.performance_overlay.move(self.pos().x() + 10, self.pos().y() + 30)
|
||||
self.performance_overlay.show()
|
||||
|
||||
@Slot(bool)
|
||||
def applyTheme(self, dark_theme):
|
||||
"""
|
||||
Apply light or dark theme.
|
||||
|
||||
Args:
|
||||
dark_theme: True for dark theme, False for light theme
|
||||
"""
|
||||
if dark_theme:
|
||||
# Load dark theme stylesheet
|
||||
theme_file = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"resources", "themes", "dark.qss"
|
||||
)
|
||||
else:
|
||||
# Load light theme stylesheet
|
||||
theme_file = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"resources", "themes", "light.qss"
|
||||
)
|
||||
|
||||
# Apply theme if file exists
|
||||
if os.path.exists(theme_file):
|
||||
with open(theme_file, "r") as f:
|
||||
self.setStyleSheet(f.read())
|
||||
else:
|
||||
# Fallback to built-in style
|
||||
self.setStyleSheet("")
|
||||
|
||||
@Slot()
|
||||
def export_data(self):
|
||||
"""Export data to file"""
|
||||
export_format = self.export_tab.export_format_combo.currentText()
|
||||
export_data = self.export_tab.export_data_combo.currentText()
|
||||
|
||||
# Get file type filter based on format
|
||||
if export_format == "CSV":
|
||||
file_filter = "CSV Files (*.csv)"
|
||||
default_ext = ".csv"
|
||||
elif export_format == "JSON":
|
||||
file_filter = "JSON Files (*.json)"
|
||||
default_ext = ".json"
|
||||
elif export_format == "Excel":
|
||||
file_filter = "Excel Files (*.xlsx)"
|
||||
default_ext = ".xlsx"
|
||||
elif export_format == "PDF Report":
|
||||
file_filter = "PDF Files (*.pdf)"
|
||||
default_ext = ".pdf"
|
||||
else:
|
||||
file_filter = "All Files (*)"
|
||||
default_ext = ".txt"
|
||||
|
||||
# Get save path
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Export Data",
|
||||
f"traffic_data{default_ext}",
|
||||
file_filter
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get analytics data
|
||||
analytics = self.analytics_controller.get_analytics()
|
||||
|
||||
# Export based on format
|
||||
if export_format == "CSV":
|
||||
from ..utils.helpers import create_export_csv
|
||||
result = create_export_csv(analytics['detection_counts'], file_path)
|
||||
elif export_format == "JSON":
|
||||
from ..utils.helpers import create_export_json
|
||||
result = create_export_json(analytics, file_path)
|
||||
elif export_format == "Excel":
|
||||
# Requires openpyxl
|
||||
try:
|
||||
import pandas as pd
|
||||
df = pd.DataFrame({
|
||||
'Class': list(analytics['detection_counts'].keys()),
|
||||
'Count': list(analytics['detection_counts'].values())
|
||||
})
|
||||
df.to_excel(file_path, index=False)
|
||||
result = True
|
||||
except Exception as e:
|
||||
print(f"Excel export error: {e}")
|
||||
result = False
|
||||
else:
|
||||
# Not implemented
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Not Implemented",
|
||||
f"Export to {export_format} is not yet implemented."
|
||||
)
|
||||
return
|
||||
|
||||
if result:
|
||||
self.statusBar().showMessage(f"Data exported to {file_path}", 3000)
|
||||
else:
|
||||
self.statusBar().showMessage("Error exporting data", 3000)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Export Error",
|
||||
f"Error exporting data: {str(e)}"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def show_about_dialog(self):
|
||||
"""Show about dialog"""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About Traffic Monitoring System",
|
||||
"<h3>Traffic Monitoring System</h3>"
|
||||
"<p>Based on OpenVINO™ and PySide6</p>"
|
||||
"<p>Version 1.0.0</p>"
|
||||
"<p>© 2025 GSOC Project</p>"
|
||||
)
|
||||
@Slot(bool)
|
||||
def toggle_video_processing(self, start):
|
||||
"""
|
||||
Start or stop video processing.
|
||||
|
||||
Args:
|
||||
start: True to start processing, False to stop
|
||||
"""
|
||||
if self.video_controller:
|
||||
if start:
|
||||
try:
|
||||
# Make sure the source is correctly set to what the LiveTab has
|
||||
current_source = self.live_tab.current_source
|
||||
print(f"DEBUG: MainWindow toggle_processing with source: {current_source} (type: {type(current_source)})")
|
||||
|
||||
# Validate source
|
||||
if current_source is None:
|
||||
self.statusBar().showMessage("Error: No valid source selected")
|
||||
return
|
||||
|
||||
# For file sources, verify file exists
|
||||
if isinstance(current_source, str) and not current_source.isdigit():
|
||||
if not os.path.exists(current_source):
|
||||
self.statusBar().showMessage(f"Error: File not found: {current_source}")
|
||||
return
|
||||
|
||||
# Ensure the source is set before starting
|
||||
print(f"🎥 Setting video controller source to: {current_source}")
|
||||
self.video_controller.set_source(current_source)
|
||||
|
||||
# Now start processing after a short delay to ensure source is set
|
||||
print("⏱️ Scheduling video processing start after 200ms delay...")
|
||||
QTimer.singleShot(200, lambda: self._start_video_processing())
|
||||
|
||||
source_desc = f"file: {os.path.basename(current_source)}" if isinstance(current_source, str) and os.path.exists(current_source) else f"camera: {current_source}"
|
||||
self.statusBar().showMessage(f"Video processing started with {source_desc}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error starting video: {e}")
|
||||
traceback.print_exc()
|
||||
self.statusBar().showMessage(f"Error: {str(e)}")
|
||||
else:
|
||||
try:
|
||||
print("🛑 Stopping video processing...")
|
||||
self.video_controller.stop()
|
||||
print("✅ Video controller stopped")
|
||||
self.statusBar().showMessage("Video processing stopped")
|
||||
except Exception as e:
|
||||
print(f"❌ Error stopping video: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
def _start_video_processing(self):
|
||||
"""Actual video processing start with extra error handling"""
|
||||
try:
|
||||
print("🚀 Starting video controller...")
|
||||
self.video_controller.start()
|
||||
print("✅ Video controller started successfully")
|
||||
except Exception as e:
|
||||
print(f"❌ Error in video processing start: {e}")
|
||||
traceback.print_exc()
|
||||
self.statusBar().showMessage(f"Video processing error: {str(e)}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event"""
|
||||
# Stop processing
|
||||
if self.video_controller and self.video_controller._running:
|
||||
self.video_controller.stop()
|
||||
|
||||
# Save settings
|
||||
self.saveSettings()
|
||||
|
||||
# Accept close event
|
||||
event.accept()
|
||||
|
||||
def restoreSettings(self):
|
||||
"""Restore application settings"""
|
||||
# Restore window geometry
|
||||
geometry = self.settings.value("geometry")
|
||||
if geometry:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
# Restore window state
|
||||
state = self.settings.value("windowState")
|
||||
if state:
|
||||
self.restoreState(state)
|
||||
|
||||
def saveSettings(self):
|
||||
"""Save application settings"""
|
||||
# Save window geometry
|
||||
self.settings.setValue("geometry", self.saveGeometry())
|
||||
|
||||
# Save window state
|
||||
self.settings.setValue("windowState", self.saveState())
|
||||
|
||||
# Save current directory as snapshot directory
|
||||
self.settings.setValue("snapshot_dir", os.getcwd())
|
||||
@Slot(dict)
|
||||
def update_traffic_light_status(self, stats):
|
||||
"""Update status bar with traffic light information if detected"""
|
||||
traffic_light_info = stats.get('traffic_light_color', 'unknown')
|
||||
|
||||
# Handle both string and dictionary return formats
|
||||
if isinstance(traffic_light_info, dict):
|
||||
traffic_light_color = traffic_light_info.get('color', 'unknown')
|
||||
confidence = traffic_light_info.get('confidence', 0.0)
|
||||
confidence_str = f" (Confidence: {confidence:.2f})" if confidence > 0 else ""
|
||||
else:
|
||||
traffic_light_color = traffic_light_info
|
||||
confidence_str = ""
|
||||
|
||||
if traffic_light_color != 'unknown':
|
||||
current_message = self.statusBar().currentMessage()
|
||||
if not current_message or "Traffic Light" not in current_message:
|
||||
# Handle both dictionary and string formats
|
||||
if isinstance(traffic_light_color, dict):
|
||||
color_text = traffic_light_color.get("color", "unknown").upper()
|
||||
else:
|
||||
color_text = str(traffic_light_color).upper()
|
||||
self.statusBar().showMessage(f"Traffic Light: {color_text}{confidence_str}")
|
||||
@Slot(dict)
|
||||
def handle_violation_detected(self, violation):
|
||||
"""Handle a detected traffic violation"""
|
||||
try:
|
||||
# Flash red status message
|
||||
self.statusBar().showMessage(f"🚨 RED LIGHT VIOLATION DETECTED - Vehicle ID: {violation['track_id']}", 5000)
|
||||
|
||||
# Add to violations tab
|
||||
self.violations_tab.add_violation(violation)
|
||||
|
||||
# Update analytics
|
||||
if self.analytics_controller:
|
||||
self.analytics_controller.register_violation(violation)
|
||||
|
||||
print(f"🚨 Violation processed: {violation['id']} at {violation['timestamp']}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error handling violation: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
41
qt_app_pyside1/finale/splash.py
Normal file
41
qt_app_pyside1/finale/splash.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from PySide6.QtWidgets import QApplication, QSplashScreen
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QPixmap
|
||||
import sys
|
||||
import os
|
||||
|
||||
def show_splash(existing_app=None):
|
||||
# Use existing app if provided, otherwise create a new one
|
||||
app = existing_app or QApplication(sys.argv)
|
||||
|
||||
# Get the directory of the executable or script
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as compiled executable
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# Running as script
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Look for splash image
|
||||
splash_image = os.path.join(app_dir, 'resources', 'splash.png')
|
||||
if not os.path.exists(splash_image):
|
||||
splash_image = os.path.join(app_dir, 'splash.png')
|
||||
if not os.path.exists(splash_image):
|
||||
return None
|
||||
|
||||
# Create splash screen
|
||||
pixmap = QPixmap(splash_image)
|
||||
splash = QSplashScreen(pixmap, Qt.WindowStaysOnTopHint)
|
||||
splash.show()
|
||||
app.processEvents()
|
||||
|
||||
return splash, app
|
||||
|
||||
if __name__ == "__main__":
|
||||
# This is for testing the splash screen independently
|
||||
splash, app = show_splash()
|
||||
|
||||
# Close the splash after 3 seconds
|
||||
QTimer.singleShot(3000, splash.close)
|
||||
|
||||
sys.exit(app.exec())
|
||||
677
qt_app_pyside1/finale/styles.py
Normal file
677
qt_app_pyside1/finale/styles.py
Normal file
@@ -0,0 +1,677 @@
|
||||
"""
|
||||
Modern Dark Theme and Styling System
|
||||
===================================
|
||||
|
||||
Complete styling system with Material Design 3.0 principles, dark theme,
|
||||
animations, and responsive design for the Traffic Monitoring Application.
|
||||
|
||||
Features:
|
||||
- Material Design 3.0 dark theme
|
||||
- Animated transitions and hover effects
|
||||
- Responsive typography and spacing
|
||||
- Custom widget styling
|
||||
- Accent color system
|
||||
- Professional gradients and shadows
|
||||
"""
|
||||
|
||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QRect, QTimer
|
||||
from PySide6.QtGui import QFont, QColor, QPalette, QLinearGradient, QBrush
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
from typing import Dict, Optional
|
||||
import json
|
||||
|
||||
class Colors:
|
||||
"""Material Design 3.0 Color Palette - Dark Theme"""
|
||||
|
||||
# Primary colors
|
||||
PRIMARY_BACKGROUND = "#121212"
|
||||
SECONDARY_BACKGROUND = "#1E1E1E"
|
||||
SURFACE = "#2C2C2C"
|
||||
SURFACE_VARIANT = "#383838"
|
||||
|
||||
# Accent colors
|
||||
ACCENT_CYAN = "#00BCD4"
|
||||
ACCENT_GREEN = "#4CAF50"
|
||||
ACCENT_RED = "#FF5722"
|
||||
ACCENT_YELLOW = "#FFC107"
|
||||
ACCENT_BLUE = "#2196F3"
|
||||
ACCENT_PURPLE = "#9C27B0"
|
||||
|
||||
# Text colors
|
||||
TEXT_PRIMARY = "#FFFFFF"
|
||||
TEXT_SECONDARY = "#B0B0B0"
|
||||
TEXT_DISABLED = "#757575"
|
||||
|
||||
# State colors
|
||||
SUCCESS = "#4CAF50"
|
||||
WARNING = "#FF9800"
|
||||
ERROR = "#F44336"
|
||||
INFO = "#2196F3"
|
||||
|
||||
# Border and divider
|
||||
BORDER = "#424242"
|
||||
DIVIDER = "#2C2C2C"
|
||||
|
||||
# Interactive states
|
||||
HOVER = "#404040"
|
||||
PRESSED = "#505050"
|
||||
SELECTED = "#1976D2"
|
||||
FOCUS = "#03DAC6"
|
||||
|
||||
class Fonts:
|
||||
"""Typography system with hierarchy"""
|
||||
|
||||
@staticmethod
|
||||
def get_font(size: int = 10, weight: str = "normal", family: str = "Segoe UI") -> QFont:
|
||||
"""Get a font with specified parameters"""
|
||||
font = QFont(family, size)
|
||||
|
||||
weight_map = {
|
||||
"light": QFont.Weight.Light,
|
||||
"normal": QFont.Weight.Normal,
|
||||
"medium": QFont.Weight.Medium,
|
||||
"semibold": QFont.Weight.DemiBold,
|
||||
"bold": QFont.Weight.Bold
|
||||
}
|
||||
|
||||
font.setWeight(weight_map.get(weight, QFont.Weight.Normal))
|
||||
return font
|
||||
|
||||
@staticmethod
|
||||
def heading_1() -> QFont:
|
||||
return Fonts.get_font(24, "bold")
|
||||
|
||||
@staticmethod
|
||||
def heading_2() -> QFont:
|
||||
return Fonts.get_font(20, "semibold")
|
||||
|
||||
@staticmethod
|
||||
def heading_3() -> QFont:
|
||||
return Fonts.get_font(16, "semibold")
|
||||
|
||||
@staticmethod
|
||||
def body_large() -> QFont:
|
||||
return Fonts.get_font(14, "normal")
|
||||
|
||||
@staticmethod
|
||||
def body_medium() -> QFont:
|
||||
return Fonts.get_font(12, "normal")
|
||||
|
||||
@staticmethod
|
||||
def body_small() -> QFont:
|
||||
return Fonts.get_font(10, "normal")
|
||||
|
||||
@staticmethod
|
||||
def caption() -> QFont:
|
||||
return Fonts.get_font(9, "normal")
|
||||
|
||||
@staticmethod
|
||||
def button() -> QFont:
|
||||
return Fonts.get_font(12, "medium")
|
||||
|
||||
class Spacing:
|
||||
"""Consistent spacing system"""
|
||||
XS = 4
|
||||
SM = 8
|
||||
MD = 16
|
||||
LG = 24
|
||||
XL = 32
|
||||
XXL = 48
|
||||
|
||||
class BorderRadius:
|
||||
"""Border radius system"""
|
||||
SM = 4
|
||||
MD = 8
|
||||
LG = 12
|
||||
XL = 16
|
||||
PILL = 9999
|
||||
|
||||
class ThemeManager:
|
||||
"""Manages application theme and styling"""
|
||||
|
||||
def __init__(self, accent_color: str = Colors.ACCENT_CYAN):
|
||||
self.accent_color = accent_color
|
||||
self._setup_palette()
|
||||
|
||||
def _setup_palette(self):
|
||||
"""Setup Qt application palette"""
|
||||
palette = QPalette()
|
||||
|
||||
# Window colors
|
||||
palette.setColor(QPalette.Window, QColor(Colors.PRIMARY_BACKGROUND))
|
||||
palette.setColor(QPalette.WindowText, QColor(Colors.TEXT_PRIMARY))
|
||||
|
||||
# Base colors (input fields)
|
||||
palette.setColor(QPalette.Base, QColor(Colors.SURFACE))
|
||||
palette.setColor(QPalette.Text, QColor(Colors.TEXT_PRIMARY))
|
||||
|
||||
# Button colors
|
||||
palette.setColor(QPalette.Button, QColor(Colors.SURFACE))
|
||||
palette.setColor(QPalette.ButtonText, QColor(Colors.TEXT_PRIMARY))
|
||||
|
||||
# Highlight colors
|
||||
palette.setColor(QPalette.Highlight, QColor(self.accent_color))
|
||||
palette.setColor(QPalette.HighlightedText, QColor(Colors.TEXT_PRIMARY))
|
||||
|
||||
# Apply palette
|
||||
if QApplication.instance():
|
||||
QApplication.instance().setPalette(palette)
|
||||
|
||||
def set_accent_color(self, color: str):
|
||||
"""Change the accent color"""
|
||||
self.accent_color = color
|
||||
self._setup_palette()
|
||||
|
||||
class StyleSheets:
|
||||
"""Collection of Qt StyleSheets for various components"""
|
||||
|
||||
@staticmethod
|
||||
def main_window() -> str:
|
||||
return f"""
|
||||
QMainWindow {{
|
||||
background-color: {Colors.PRIMARY_BACKGROUND};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
}}
|
||||
|
||||
QMainWindow::separator {{
|
||||
background-color: {Colors.BORDER};
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def tab_widget() -> str:
|
||||
return f"""
|
||||
QTabWidget::pane {{
|
||||
border: 1px solid {Colors.BORDER};
|
||||
background-color: {Colors.SECONDARY_BACKGROUND};
|
||||
border-radius: {BorderRadius.MD}px;
|
||||
}}
|
||||
|
||||
QTabBar::tab {{
|
||||
background-color: {Colors.SURFACE};
|
||||
color: {Colors.TEXT_SECONDARY};
|
||||
padding: {Spacing.SM}px {Spacing.MD}px;
|
||||
margin-right: 2px;
|
||||
border-top-left-radius: {BorderRadius.SM}px;
|
||||
border-top-right-radius: {BorderRadius.SM}px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}}
|
||||
|
||||
QTabBar::tab:selected {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
}}
|
||||
|
||||
QTabBar::tab:hover:!selected {{
|
||||
background-color: {Colors.HOVER};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def button_primary() -> str:
|
||||
return f"""
|
||||
QPushButton {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
border: none;
|
||||
padding: {Spacing.SM}px {Spacing.MD}px;
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
font-weight: 500;
|
||||
min-height: 32px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: #00ACC1;
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: #0097A7;
|
||||
}}
|
||||
|
||||
QPushButton:disabled {{
|
||||
background-color: {Colors.SURFACE};
|
||||
color: {Colors.TEXT_DISABLED};
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def button_secondary() -> str:
|
||||
return f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {Colors.ACCENT_CYAN};
|
||||
border: 2px solid {Colors.ACCENT_CYAN};
|
||||
padding: {Spacing.SM}px {Spacing.MD}px;
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
font-weight: 500;
|
||||
min-height: 32px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(0, 188, 212, 0.1);
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: rgba(0, 188, 212, 0.2);
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def card() -> str:
|
||||
return f"""
|
||||
QWidget {{
|
||||
background-color: {Colors.SURFACE};
|
||||
border: 1px solid {Colors.BORDER};
|
||||
border-radius: {BorderRadius.MD}px;
|
||||
padding: {Spacing.MD}px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def input_field() -> str:
|
||||
return f"""
|
||||
QLineEdit, QTextEdit, QSpinBox, QDoubleSpinBox, QComboBox {{
|
||||
background-color: {Colors.SURFACE};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
border: 2px solid {Colors.BORDER};
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
padding: {Spacing.SM}px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
|
||||
QLineEdit:focus, QTextEdit:focus, QSpinBox:focus,
|
||||
QDoubleSpinBox:focus, QComboBox:focus {{
|
||||
border-color: {Colors.ACCENT_CYAN};
|
||||
}}
|
||||
|
||||
QLineEdit:hover, QTextEdit:hover, QSpinBox:hover,
|
||||
QDoubleSpinBox:hover, QComboBox:hover {{
|
||||
border-color: {Colors.HOVER};
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def table() -> str:
|
||||
return f"""
|
||||
QTableWidget {{
|
||||
background-color: {Colors.SURFACE};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
gridline-color: {Colors.BORDER};
|
||||
border: 1px solid {Colors.BORDER};
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
}}
|
||||
|
||||
QTableWidget::item {{
|
||||
padding: {Spacing.SM}px;
|
||||
border-bottom: 1px solid {Colors.BORDER};
|
||||
}}
|
||||
|
||||
QTableWidget::item:selected {{
|
||||
background-color: {Colors.SELECTED};
|
||||
}}
|
||||
|
||||
QTableWidget::item:hover {{
|
||||
background-color: {Colors.HOVER};
|
||||
}}
|
||||
|
||||
QHeaderView::section {{
|
||||
background-color: {Colors.SURFACE_VARIANT};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
padding: {Spacing.SM}px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def scroll_bar() -> str:
|
||||
return f"""
|
||||
QScrollBar:vertical {{
|
||||
background-color: {Colors.SURFACE};
|
||||
width: 12px;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical {{
|
||||
background-color: {Colors.BORDER};
|
||||
border-radius: 6px;
|
||||
min-height: 20px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical:hover {{
|
||||
background-color: {Colors.HOVER};
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
||||
height: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar:horizontal {{
|
||||
background-color: {Colors.SURFACE};
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal {{
|
||||
background-color: {Colors.BORDER};
|
||||
border-radius: 6px;
|
||||
min-width: 20px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {{
|
||||
background-color: {Colors.HOVER};
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
|
||||
width: 0px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def progress_bar() -> str:
|
||||
return f"""
|
||||
QProgressBar {{
|
||||
background-color: {Colors.SURFACE};
|
||||
border: none;
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
text-align: center;
|
||||
height: 8px;
|
||||
}}
|
||||
|
||||
QProgressBar::chunk {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def status_bar() -> str:
|
||||
return f"""
|
||||
QStatusBar {{
|
||||
background-color: {Colors.SURFACE_VARIANT};
|
||||
color: {Colors.TEXT_SECONDARY};
|
||||
border-top: 1px solid {Colors.BORDER};
|
||||
padding: {Spacing.SM}px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def toolbar() -> str:
|
||||
return f"""
|
||||
QToolBar {{
|
||||
background-color: {Colors.SURFACE_VARIANT};
|
||||
border: none;
|
||||
spacing: {Spacing.SM}px;
|
||||
padding: {Spacing.SM}px;
|
||||
}}
|
||||
|
||||
QToolButton {{
|
||||
background-color: transparent;
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
border: none;
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
padding: {Spacing.SM}px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}}
|
||||
|
||||
QToolButton:hover {{
|
||||
background-color: {Colors.HOVER};
|
||||
}}
|
||||
|
||||
QToolButton:pressed {{
|
||||
background-color: {Colors.PRESSED};
|
||||
}}
|
||||
|
||||
QToolButton:checked {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def dock_widget() -> str:
|
||||
return f"""
|
||||
QDockWidget {{
|
||||
background-color: {Colors.SECONDARY_BACKGROUND};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
titlebar-close-icon: none;
|
||||
titlebar-normal-icon: none;
|
||||
}}
|
||||
|
||||
QDockWidget::title {{
|
||||
background-color: {Colors.SURFACE_VARIANT};
|
||||
padding: {Spacing.SM}px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
"""
|
||||
|
||||
class AnimationManager:
|
||||
"""Manages UI animations and transitions"""
|
||||
|
||||
@staticmethod
|
||||
def create_fade_animation(widget: QWidget, duration: int = 300) -> QPropertyAnimation:
|
||||
"""Create a fade in/out animation"""
|
||||
animation = QPropertyAnimation(widget, b"windowOpacity")
|
||||
animation.setDuration(duration)
|
||||
animation.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
return animation
|
||||
|
||||
@staticmethod
|
||||
def create_slide_animation(widget: QWidget, start_pos: QRect, end_pos: QRect, duration: int = 300) -> QPropertyAnimation:
|
||||
"""Create a slide animation"""
|
||||
animation = QPropertyAnimation(widget, b"geometry")
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(start_pos)
|
||||
animation.setEndValue(end_pos)
|
||||
animation.setEasingCurve(QEasingCurve.OutCubic)
|
||||
return animation
|
||||
|
||||
@staticmethod
|
||||
def pulse_widget(widget: QWidget, duration: int = 1000):
|
||||
"""Create a pulsing effect on a widget"""
|
||||
animation = QPropertyAnimation(widget, b"windowOpacity")
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(1.0)
|
||||
animation.setKeyValueAt(0.5, 0.5)
|
||||
animation.setEndValue(1.0)
|
||||
animation.setEasingCurve(QEasingCurve.InOutSine)
|
||||
animation.setLoopCount(-1) # Infinite loop
|
||||
animation.start()
|
||||
return animation
|
||||
|
||||
def apply_theme(app: QApplication, theme_manager: Optional[ThemeManager] = None):
|
||||
"""Apply the complete theme to the application"""
|
||||
if not theme_manager:
|
||||
theme_manager = ThemeManager()
|
||||
|
||||
# Set application style
|
||||
app.setStyle("Fusion")
|
||||
|
||||
# Apply global stylesheet
|
||||
global_style = f"""
|
||||
* {{
|
||||
font-family: "Segoe UI", "Inter", "Roboto", sans-serif;
|
||||
}}
|
||||
|
||||
{StyleSheets.main_window()}
|
||||
{StyleSheets.tab_widget()}
|
||||
{StyleSheets.input_field()}
|
||||
{StyleSheets.table()}
|
||||
{StyleSheets.scroll_bar()}
|
||||
{StyleSheets.progress_bar()}
|
||||
{StyleSheets.status_bar()}
|
||||
{StyleSheets.toolbar()}
|
||||
{StyleSheets.dock_widget()}
|
||||
|
||||
QWidget {{
|
||||
background-color: {Colors.PRIMARY_BACKGROUND};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
}}
|
||||
|
||||
QGroupBox {{
|
||||
background-color: {Colors.SURFACE};
|
||||
border: 1px solid {Colors.BORDER};
|
||||
border-radius: {BorderRadius.MD}px;
|
||||
margin-top: {Spacing.MD}px;
|
||||
padding-top: {Spacing.SM}px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QGroupBox::title {{
|
||||
subcontrol-origin: margin;
|
||||
left: {Spacing.MD}px;
|
||||
padding: 0 {Spacing.SM}px 0 {Spacing.SM}px;
|
||||
}}
|
||||
|
||||
QCheckBox, QRadioButton {{
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
spacing: {Spacing.SM}px;
|
||||
}}
|
||||
|
||||
QCheckBox::indicator, QRadioButton::indicator {{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid {Colors.BORDER};
|
||||
border-radius: 4px;
|
||||
background-color: {Colors.SURFACE};
|
||||
}}
|
||||
|
||||
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
border-color: {Colors.ACCENT_CYAN};
|
||||
}}
|
||||
|
||||
QSlider::groove:horizontal {{
|
||||
height: 6px;
|
||||
background-color: {Colors.SURFACE};
|
||||
border-radius: 3px;
|
||||
}}
|
||||
|
||||
QSlider::handle:horizontal {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
border: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
margin: -6px 0;
|
||||
}}
|
||||
|
||||
QSlider::sub-page:horizontal {{
|
||||
background-color: {Colors.ACCENT_CYAN};
|
||||
border-radius: 3px;
|
||||
}}
|
||||
|
||||
QMenu {{
|
||||
background-color: {Colors.SURFACE};
|
||||
color: {Colors.TEXT_PRIMARY};
|
||||
border: 1px solid {Colors.BORDER};
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
padding: {Spacing.SM}px;
|
||||
}}
|
||||
|
||||
QMenu::item {{
|
||||
padding: {Spacing.SM}px {Spacing.MD}px;
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
}}
|
||||
|
||||
QMenu::item:selected {{
|
||||
background-color: {Colors.HOVER};
|
||||
}}
|
||||
|
||||
QMenu::separator {{
|
||||
height: 1px;
|
||||
background-color: {Colors.BORDER};
|
||||
margin: {Spacing.SM}px;
|
||||
}}
|
||||
|
||||
QSplitter::handle {{
|
||||
background-color: {Colors.BORDER};
|
||||
}}
|
||||
|
||||
QSplitter::handle:horizontal {{
|
||||
width: 2px;
|
||||
}}
|
||||
|
||||
QSplitter::handle:vertical {{
|
||||
height: 2px;
|
||||
}}
|
||||
"""
|
||||
|
||||
app.setStyleSheet(global_style)
|
||||
|
||||
# Utility functions for common styling patterns
|
||||
def create_stat_card_style(accent_color: str = Colors.ACCENT_CYAN) -> str:
|
||||
"""Create a styled card for statistics display"""
|
||||
return f"""
|
||||
QWidget {{
|
||||
background-color: {Colors.SURFACE};
|
||||
border: 1px solid {Colors.BORDER};
|
||||
border-left: 4px solid {accent_color};
|
||||
border-radius: {BorderRadius.MD}px;
|
||||
padding: {Spacing.MD}px;
|
||||
}}
|
||||
|
||||
QLabel {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}}
|
||||
"""
|
||||
|
||||
def create_alert_style(alert_type: str = "info") -> str:
|
||||
"""Create styled alert components"""
|
||||
color_map = {
|
||||
"success": Colors.SUCCESS,
|
||||
"warning": Colors.WARNING,
|
||||
"error": Colors.ERROR,
|
||||
"info": Colors.INFO
|
||||
}
|
||||
|
||||
color = color_map.get(alert_type, Colors.INFO)
|
||||
|
||||
return f"""
|
||||
QWidget {{
|
||||
background-color: rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.1);
|
||||
border: 1px solid {color};
|
||||
border-radius: {BorderRadius.SM}px;
|
||||
padding: {Spacing.MD}px;
|
||||
}}
|
||||
|
||||
QLabel {{
|
||||
color: {color};
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
}}
|
||||
"""
|
||||
|
||||
class MaterialColors:
|
||||
"""Alias for Colors for compatibility with old code."""
|
||||
primary = Colors.ACCENT_CYAN
|
||||
primary_variant = Colors.ACCENT_BLUE
|
||||
secondary = Colors.ACCENT_GREEN
|
||||
surface = Colors.SURFACE
|
||||
text_primary = Colors.TEXT_PRIMARY
|
||||
text_on_primary = Colors.TEXT_PRIMARY
|
||||
|
||||
class FinaleStyles:
|
||||
"""Basic style helpers for compatibility with old code."""
|
||||
@staticmethod
|
||||
def get_group_box_style():
|
||||
return """
|
||||
QGroupBox {
|
||||
border: 1px solid #424242;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
background-color: #232323;
|
||||
}
|
||||
QGroupBox:title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 3px 0 3px;
|
||||
color: #B0B0B0;
|
||||
}
|
||||
"""
|
||||
476
qt_app_pyside1/finale/views/analytics_view.py
Normal file
476
qt_app_pyside1/finale/views/analytics_view.py
Normal file
@@ -0,0 +1,476 @@
|
||||
"""
|
||||
Analytics View - Traffic analytics and reporting
|
||||
Displays charts, statistics, and historical data.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
|
||||
QComboBox, QSpinBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate
|
||||
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
# Import finale components
|
||||
try:
|
||||
# Try relative imports first (when running as a package)
|
||||
from ..styles import FinaleStyles, MaterialColors
|
||||
from ..icons import FinaleIcons
|
||||
# Import advanced chart components from original analytics_tab
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import from qt_app_pyside
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
from qt_app_pyside.ui.analytics_tab import ChartWidget, TimeSeriesChart, DetectionPieChart, ViolationBarChart
|
||||
from qt_app_pyside.controllers.analytics_controller import AnalyticsController
|
||||
from qt_app_pyside.utils.helpers import load_configuration, format_timestamp, format_duration
|
||||
except ImportError:
|
||||
# Fallback for direct execution
|
||||
try:
|
||||
from styles import FinaleStyles, MaterialColors
|
||||
from icons import FinaleIcons
|
||||
# Create simplified chart widgets if advanced ones not available
|
||||
except ImportError:
|
||||
print("Error importing analytics components")
|
||||
class ChartWidget(QWidget):
|
||||
def __init__(self, title="Chart"):
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.data = []
|
||||
self.chart_type = "line" # line, bar, pie
|
||||
self.setMinimumSize(400, 300)
|
||||
|
||||
def set_data(self, data, chart_type="line"):
|
||||
"""Set chart data and type"""
|
||||
self.data = data
|
||||
self.chart_type = chart_type
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint the chart"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# Background
|
||||
painter.fillRect(self.rect(), QColor(MaterialColors.surface))
|
||||
|
||||
# Border
|
||||
painter.setPen(QColor(MaterialColors.outline))
|
||||
painter.drawRect(self.rect().adjusted(0, 0, -1, -1))
|
||||
|
||||
# Title
|
||||
painter.setPen(QColor(MaterialColors.text_primary))
|
||||
painter.setFont(QFont("Segoe UI", 12, QFont.Bold))
|
||||
title_rect = self.rect().adjusted(10, 10, -10, -10)
|
||||
painter.drawText(title_rect, Qt.AlignTop | Qt.AlignLeft, self.title)
|
||||
|
||||
# Chart area
|
||||
chart_rect = self.rect().adjusted(50, 50, -20, -50)
|
||||
|
||||
if not self.data:
|
||||
# No data message
|
||||
painter.setPen(QColor(MaterialColors.text_secondary))
|
||||
painter.setFont(QFont("Segoe UI", 10))
|
||||
painter.drawText(chart_rect, Qt.AlignCenter, "No data available")
|
||||
return
|
||||
|
||||
# Draw chart based on type
|
||||
if self.chart_type == "line":
|
||||
self.draw_line_chart(painter, chart_rect)
|
||||
elif self.chart_type == "bar":
|
||||
self.draw_bar_chart(painter, chart_rect)
|
||||
elif self.chart_type == "pie":
|
||||
self.draw_pie_chart(painter, chart_rect)
|
||||
|
||||
def draw_line_chart(self, painter, rect):
|
||||
"""Draw a line chart"""
|
||||
if len(self.data) < 2:
|
||||
return
|
||||
|
||||
# Find min/max values
|
||||
values = [item.get('value', 0) for item in self.data]
|
||||
min_val, max_val = min(values), max(values)
|
||||
|
||||
if max_val == min_val:
|
||||
max_val = min_val + 1
|
||||
|
||||
# Calculate points
|
||||
points = []
|
||||
for i, item in enumerate(self.data):
|
||||
x = rect.left() + (i / (len(self.data) - 1)) * rect.width()
|
||||
y = rect.bottom() - ((item.get('value', 0) - min_val) / (max_val - min_val)) * rect.height()
|
||||
points.append((x, y))
|
||||
|
||||
# Draw grid lines
|
||||
painter.setPen(QColor(MaterialColors.outline_variant))
|
||||
for i in range(5):
|
||||
y = rect.top() + (i / 4) * rect.height()
|
||||
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||
|
||||
# Draw line
|
||||
painter.setPen(QColor(MaterialColors.primary))
|
||||
for i in range(len(points) - 1):
|
||||
painter.drawLine(points[i][0], points[i][1], points[i+1][0], points[i+1][1])
|
||||
|
||||
# Draw points
|
||||
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
|
||||
for x, y in points:
|
||||
painter.drawEllipse(x-3, y-3, 6, 6)
|
||||
|
||||
def draw_bar_chart(self, painter, rect):
|
||||
"""Draw a bar chart"""
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
values = [item.get('value', 0) for item in self.data]
|
||||
max_val = max(values) if values else 1
|
||||
|
||||
bar_width = rect.width() / len(self.data) * 0.8
|
||||
spacing = rect.width() / len(self.data) * 0.2
|
||||
|
||||
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
|
||||
|
||||
for i, item in enumerate(self.data):
|
||||
value = item.get('value', 0)
|
||||
height = (value / max_val) * rect.height()
|
||||
|
||||
x = rect.left() + i * (bar_width + spacing) + spacing / 2
|
||||
y = rect.bottom() - height
|
||||
|
||||
painter.drawRect(x, y, bar_width, height)
|
||||
|
||||
def draw_pie_chart(self, painter, rect):
|
||||
"""Draw a pie chart"""
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
total = sum(item.get('value', 0) for item in self.data)
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
# Calculate center and radius
|
||||
center = rect.center()
|
||||
radius = min(rect.width(), rect.height()) // 2 - 20
|
||||
|
||||
# Colors for pie slices
|
||||
colors = [MaterialColors.primary, MaterialColors.secondary, MaterialColors.tertiary,
|
||||
MaterialColors.error, MaterialColors.success, MaterialColors.warning]
|
||||
|
||||
start_angle = 0
|
||||
for i, item in enumerate(self.data):
|
||||
value = item.get('value', 0)
|
||||
angle = (value / total) * 360 * 16 # Qt uses 16ths of a degree
|
||||
|
||||
color = QColor(colors[i % len(colors)])
|
||||
painter.setBrush(QBrush(color))
|
||||
painter.setPen(QColor(MaterialColors.outline))
|
||||
|
||||
painter.drawPie(center.x() - radius, center.y() - radius,
|
||||
radius * 2, radius * 2, start_angle, angle)
|
||||
|
||||
start_angle += angle
|
||||
|
||||
class TrafficSummaryWidget(QGroupBox):
|
||||
"""
|
||||
Widget showing traffic summary statistics.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("Traffic Summary", parent)
|
||||
self.setup_ui()
|
||||
self.reset_stats()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup summary UI"""
|
||||
layout = QGridLayout(self)
|
||||
|
||||
# Create stat labels
|
||||
self.total_vehicles_label = QLabel("0")
|
||||
self.total_violations_label = QLabel("0")
|
||||
self.avg_speed_label = QLabel("0.0 km/h")
|
||||
self.peak_hour_label = QLabel("N/A")
|
||||
|
||||
# Style the stat values
|
||||
for label in [self.total_vehicles_label, self.total_violations_label,
|
||||
self.avg_speed_label, self.peak_hour_label]:
|
||||
label.setFont(QFont("Segoe UI", 16, QFont.Bold))
|
||||
label.setStyleSheet(f"color: {MaterialColors.primary};")
|
||||
|
||||
# Add to layout
|
||||
layout.addWidget(QLabel("Total Vehicles:"), 0, 0)
|
||||
layout.addWidget(self.total_vehicles_label, 0, 1)
|
||||
|
||||
layout.addWidget(QLabel("Total Violations:"), 1, 0)
|
||||
layout.addWidget(self.total_violations_label, 1, 1)
|
||||
|
||||
layout.addWidget(QLabel("Average Speed:"), 2, 0)
|
||||
layout.addWidget(self.avg_speed_label, 2, 1)
|
||||
|
||||
layout.addWidget(QLabel("Peak Hour:"), 3, 0)
|
||||
layout.addWidget(self.peak_hour_label, 3, 1)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
|
||||
def reset_stats(self):
|
||||
"""Reset all statistics"""
|
||||
self.total_vehicles_label.setText("0")
|
||||
self.total_violations_label.setText("0")
|
||||
self.avg_speed_label.setText("0.0 km/h")
|
||||
self.peak_hour_label.setText("N/A")
|
||||
|
||||
def update_stats(self, stats):
|
||||
"""Update statistics display"""
|
||||
if 'total_vehicles' in stats:
|
||||
self.total_vehicles_label.setText(str(stats['total_vehicles']))
|
||||
|
||||
if 'total_violations' in stats:
|
||||
self.total_violations_label.setText(str(stats['total_violations']))
|
||||
|
||||
if 'avg_speed' in stats:
|
||||
self.avg_speed_label.setText(f"{stats['avg_speed']:.1f} km/h")
|
||||
|
||||
if 'peak_hour' in stats:
|
||||
self.peak_hour_label.setText(stats['peak_hour'])
|
||||
|
||||
class ViolationsTableWidget(QTableWidget):
|
||||
"""
|
||||
Table widget for displaying violation records.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_table()
|
||||
|
||||
def setup_table(self):
|
||||
"""Setup the violations table"""
|
||||
# Set columns
|
||||
columns = ["Time", "Type", "Vehicle", "Location", "Confidence", "Actions"]
|
||||
self.setColumnCount(len(columns))
|
||||
self.setHorizontalHeaderLabels(columns)
|
||||
|
||||
# Configure table
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
self.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_table_style())
|
||||
|
||||
def add_violation(self, violation_data):
|
||||
"""Add a violation record to the table"""
|
||||
row = self.rowCount()
|
||||
self.insertRow(row)
|
||||
|
||||
# Populate row data
|
||||
time_str = violation_data.get('timestamp', datetime.now().strftime('%H:%M:%S'))
|
||||
violation_type = violation_data.get('type', 'Red Light')
|
||||
vehicle_id = violation_data.get('vehicle_id', 'Unknown')
|
||||
location = violation_data.get('location', 'Intersection 1')
|
||||
confidence = violation_data.get('confidence', 0.0)
|
||||
|
||||
self.setItem(row, 0, QTableWidgetItem(time_str))
|
||||
self.setItem(row, 1, QTableWidgetItem(violation_type))
|
||||
self.setItem(row, 2, QTableWidgetItem(vehicle_id))
|
||||
self.setItem(row, 3, QTableWidgetItem(location))
|
||||
self.setItem(row, 4, QTableWidgetItem(f"{confidence:.2f}"))
|
||||
|
||||
# Actions button
|
||||
actions_btn = QPushButton("View Details")
|
||||
actions_btn.clicked.connect(lambda: self.view_violation_details(violation_data))
|
||||
self.setCellWidget(row, 5, actions_btn)
|
||||
|
||||
# Auto-scroll to new violation
|
||||
self.scrollToBottom()
|
||||
|
||||
def view_violation_details(self, violation_data):
|
||||
"""View detailed violation information"""
|
||||
# This could open a detailed dialog
|
||||
print(f"Viewing violation details: {violation_data}")
|
||||
|
||||
class AnalyticsView(QWidget):
|
||||
"""
|
||||
Main analytics view with charts, statistics, and violation history.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.analytics_controller = AnalyticsController()
|
||||
self.setup_ui()
|
||||
self.analytics_controller.data_updated.connect(self.refresh_analytics)
|
||||
# Load config if needed
|
||||
self.config = load_configuration('config.json')
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the analytics view UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Top controls
|
||||
controls_layout = QHBoxLayout()
|
||||
|
||||
# Date range selection
|
||||
controls_layout.addWidget(QLabel("Date Range:"))
|
||||
|
||||
self.start_date = QDateEdit()
|
||||
self.start_date.setDate(QDate.currentDate().addDays(-7))
|
||||
self.start_date.setCalendarPopup(True)
|
||||
controls_layout.addWidget(self.start_date)
|
||||
|
||||
controls_layout.addWidget(QLabel("to"))
|
||||
|
||||
self.end_date = QDateEdit()
|
||||
self.end_date.setDate(QDate.currentDate())
|
||||
self.end_date.setCalendarPopup(True)
|
||||
controls_layout.addWidget(self.end_date)
|
||||
|
||||
# Time interval
|
||||
controls_layout.addWidget(QLabel("Interval:"))
|
||||
self.interval_combo = QComboBox()
|
||||
self.interval_combo.addItems(["Hourly", "Daily", "Weekly"])
|
||||
controls_layout.addWidget(self.interval_combo)
|
||||
|
||||
# Refresh button
|
||||
self.refresh_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Refresh")
|
||||
self.refresh_btn.clicked.connect(self.refresh_data)
|
||||
controls_layout.addWidget(self.refresh_btn)
|
||||
|
||||
controls_layout.addStretch()
|
||||
layout.addLayout(controls_layout)
|
||||
|
||||
# Main content area
|
||||
content_layout = QHBoxLayout()
|
||||
|
||||
# Left panel - Charts
|
||||
charts_widget = QWidget()
|
||||
charts_layout = QVBoxLayout(charts_widget)
|
||||
|
||||
# Traffic flow chart
|
||||
self.traffic_chart = AnalyticsChartWidget("Traffic Flow Over Time")
|
||||
charts_layout.addWidget(self.traffic_chart)
|
||||
|
||||
# Violation types chart
|
||||
self.violations_chart = AnalyticsChartWidget("Violation Types")
|
||||
charts_layout.addWidget(self.violations_chart)
|
||||
|
||||
content_layout.addWidget(charts_widget, 2)
|
||||
|
||||
# Right panel - Statistics and table
|
||||
right_panel = QVBoxLayout()
|
||||
|
||||
# Summary statistics
|
||||
self.summary_widget = TrafficSummaryWidget()
|
||||
right_panel.addWidget(self.summary_widget)
|
||||
|
||||
# Recent violations table
|
||||
violations_group = QGroupBox("Recent Violations")
|
||||
violations_layout = QVBoxLayout(violations_group)
|
||||
|
||||
self.violations_table = ViolationsTableWidget()
|
||||
violations_layout.addWidget(self.violations_table)
|
||||
|
||||
violations_group.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
right_panel.addWidget(violations_group, 1)
|
||||
|
||||
content_layout.addLayout(right_panel, 1)
|
||||
layout.addLayout(content_layout, 1)
|
||||
|
||||
# Apply theme
|
||||
self.apply_theme(True)
|
||||
|
||||
# Load initial data
|
||||
self.refresh_data()
|
||||
|
||||
@Slot()
|
||||
def refresh_data(self):
|
||||
"""Refresh analytics data"""
|
||||
print("Refreshing analytics data...")
|
||||
|
||||
# Update traffic flow chart (sample data)
|
||||
traffic_data = [
|
||||
{'label': '08:00', 'value': 45},
|
||||
{'label': '09:00', 'value': 67},
|
||||
{'label': '10:00', 'value': 89},
|
||||
{'label': '11:00', 'value': 76},
|
||||
{'label': '12:00', 'value': 92},
|
||||
{'label': '13:00', 'value': 84},
|
||||
{'label': '14:00', 'value': 71}
|
||||
]
|
||||
self.traffic_chart.set_data(traffic_data, "line")
|
||||
|
||||
# Update violations chart
|
||||
violations_data = [
|
||||
{'label': 'Red Light', 'value': 12},
|
||||
{'label': 'Speed', 'value': 8},
|
||||
{'label': 'Wrong Lane', 'value': 5},
|
||||
{'label': 'No Helmet', 'value': 3}
|
||||
]
|
||||
self.violations_chart.set_data(violations_data, "pie")
|
||||
|
||||
# Update summary
|
||||
summary_stats = {
|
||||
'total_vehicles': 1247,
|
||||
'total_violations': 28,
|
||||
'avg_speed': 35.2,
|
||||
'peak_hour': '12:00-13:00'
|
||||
}
|
||||
self.summary_widget.update_stats(summary_stats)
|
||||
|
||||
def refresh_analytics(self):
|
||||
"""Refresh analytics data from controller"""
|
||||
data = self.analytics_controller.get_analytics_data()
|
||||
# Use format_timestamp, format_duration for display
|
||||
# ... update charts and stats with new data ...
|
||||
|
||||
def update_demo_data(self):
|
||||
"""Update with demo data for demonstration"""
|
||||
import random
|
||||
|
||||
# Simulate new violation
|
||||
if random.random() < 0.3: # 30% chance
|
||||
violation = {
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S'),
|
||||
'type': random.choice(['Red Light', 'Speed', 'Wrong Lane']),
|
||||
'vehicle_id': f"VH{random.randint(1000, 9999)}",
|
||||
'location': f"Intersection {random.randint(1, 5)}",
|
||||
'confidence': random.uniform(0.7, 0.95)
|
||||
}
|
||||
self.violations_table.add_violation(violation)
|
||||
|
||||
def add_violation(self, violation_data):
|
||||
"""Add a new violation (called from main window)"""
|
||||
self.violations_table.add_violation(violation_data)
|
||||
|
||||
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};
|
||||
}}
|
||||
QDateEdit, QComboBox {{
|
||||
background-color: {MaterialColors.surface_variant};
|
||||
border: 1px solid {MaterialColors.outline};
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}}
|
||||
""")
|
||||
421
qt_app_pyside1/finale/views/live_view.py
Normal file
421
qt_app_pyside1/finale/views/live_view.py
Normal file
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
Live View - Real-time detection and monitoring
|
||||
Connects to existing video controller and live detection logic.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFileDialog, QComboBox, QSlider, QSpinBox, QGroupBox,
|
||||
QGridLayout, QFrame, QSizePolicy, QScrollArea
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSize
|
||||
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# Import finale components
|
||||
from ..styles import FinaleStyles, MaterialColors
|
||||
from ..icons import FinaleIcons
|
||||
|
||||
class VideoDisplayWidget(QLabel):
|
||||
"""
|
||||
Advanced video display widget with overlays and interactions.
|
||||
"""
|
||||
|
||||
frame_clicked = Signal(int, int) # x, y coordinates
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(640, 480)
|
||||
self.setScaledContents(True)
|
||||
self.setAlignment(Qt.AlignCenter)
|
||||
self.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 2px solid #424242;
|
||||
border-radius: 8px;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
""")
|
||||
|
||||
# State
|
||||
self.current_pixmap = None
|
||||
self.overlay_enabled = True
|
||||
|
||||
# Default placeholder
|
||||
self.set_placeholder()
|
||||
|
||||
def set_placeholder(self):
|
||||
"""Set placeholder image when no video is loaded"""
|
||||
placeholder = QPixmap(640, 480)
|
||||
placeholder.fill(QColor(26, 26, 26))
|
||||
|
||||
painter = QPainter(placeholder)
|
||||
painter.setPen(QColor(117, 117, 117))
|
||||
painter.setFont(QFont("Segoe UI", 16))
|
||||
painter.drawText(placeholder.rect(), Qt.AlignCenter, "No Video Source\nClick to select a file")
|
||||
painter.end()
|
||||
|
||||
self.setPixmap(placeholder)
|
||||
|
||||
def update_frame(self, pixmap, detections=None):
|
||||
"""Update frame with detections overlay"""
|
||||
if pixmap is None:
|
||||
return
|
||||
|
||||
self.current_pixmap = pixmap
|
||||
|
||||
if self.overlay_enabled and detections:
|
||||
# Draw detection overlays
|
||||
pixmap = self.add_detection_overlay(pixmap, detections)
|
||||
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
def add_detection_overlay(self, pixmap, detections):
|
||||
"""Add detection overlays to pixmap"""
|
||||
if not detections:
|
||||
return pixmap
|
||||
|
||||
# Create a copy to draw on
|
||||
overlay_pixmap = QPixmap(pixmap)
|
||||
painter = QPainter(overlay_pixmap)
|
||||
|
||||
# Draw detection boxes
|
||||
for detection in detections:
|
||||
# Extract detection info (format depends on backend)
|
||||
if isinstance(detection, dict):
|
||||
bbox = detection.get('bbox', [])
|
||||
confidence = detection.get('confidence', 0.0)
|
||||
class_name = detection.get('class', 'unknown')
|
||||
else:
|
||||
# Handle other detection formats
|
||||
continue
|
||||
|
||||
if len(bbox) >= 4:
|
||||
x1, y1, x2, y2 = bbox[:4]
|
||||
|
||||
# Draw bounding box
|
||||
painter.setPen(QColor(MaterialColors.primary))
|
||||
painter.drawRect(int(x1), int(y1), int(x2-x1), int(y2-y1))
|
||||
|
||||
# Draw label
|
||||
label = f"{class_name}: {confidence:.2f}"
|
||||
painter.setPen(QColor(MaterialColors.text_primary))
|
||||
painter.drawText(int(x1), int(y1-5), label)
|
||||
|
||||
painter.end()
|
||||
return overlay_pixmap
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Handle mouse click events"""
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.frame_clicked.emit(event.x(), event.y())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
class SourceControlWidget(QGroupBox):
|
||||
"""
|
||||
Widget for controlling video source (file, camera, stream).
|
||||
"""
|
||||
|
||||
source_changed = Signal(str) # source path/url
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("Video Source", parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the source control UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Source type selection
|
||||
source_layout = QHBoxLayout()
|
||||
|
||||
self.source_combo = QComboBox()
|
||||
self.source_combo.addItems(["Select Source", "Video File", "Camera", "RTSP Stream"])
|
||||
self.source_combo.currentTextChanged.connect(self.on_source_type_changed)
|
||||
|
||||
self.browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "Browse")
|
||||
self.browse_btn.clicked.connect(self.browse_file)
|
||||
self.browse_btn.setEnabled(False)
|
||||
|
||||
source_layout.addWidget(QLabel("Type:"))
|
||||
source_layout.addWidget(self.source_combo)
|
||||
source_layout.addWidget(self.browse_btn)
|
||||
|
||||
layout.addLayout(source_layout)
|
||||
|
||||
# Source path/URL input
|
||||
path_layout = QHBoxLayout()
|
||||
|
||||
self.path_label = QLabel("Path/URL:")
|
||||
self.path_display = QLabel("No source selected")
|
||||
self.path_display.setStyleSheet("QLabel { color: #757575; font-style: italic; }")
|
||||
|
||||
path_layout.addWidget(self.path_label)
|
||||
path_layout.addWidget(self.path_display, 1)
|
||||
|
||||
layout.addLayout(path_layout)
|
||||
|
||||
# Camera settings (initially hidden)
|
||||
self.camera_widget = QWidget()
|
||||
camera_layout = QHBoxLayout(self.camera_widget)
|
||||
|
||||
camera_layout.addWidget(QLabel("Camera ID:"))
|
||||
self.camera_spin = QSpinBox()
|
||||
self.camera_spin.setRange(0, 10)
|
||||
camera_layout.addWidget(self.camera_spin)
|
||||
|
||||
camera_layout.addStretch()
|
||||
self.camera_widget.hide()
|
||||
|
||||
layout.addWidget(self.camera_widget)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
|
||||
@Slot(str)
|
||||
def on_source_type_changed(self, source_type):
|
||||
"""Handle source type change"""
|
||||
if source_type == "Video File":
|
||||
self.browse_btn.setEnabled(True)
|
||||
self.camera_widget.hide()
|
||||
elif source_type == "Camera":
|
||||
self.browse_btn.setEnabled(False)
|
||||
self.camera_widget.show()
|
||||
self.path_display.setText(f"Camera {self.camera_spin.value()}")
|
||||
self.source_changed.emit(str(self.camera_spin.value()))
|
||||
elif source_type == "RTSP Stream":
|
||||
self.browse_btn.setEnabled(False)
|
||||
self.camera_widget.hide()
|
||||
# Could add RTSP URL input here
|
||||
else:
|
||||
self.browse_btn.setEnabled(False)
|
||||
self.camera_widget.hide()
|
||||
|
||||
@Slot()
|
||||
def browse_file(self):
|
||||
"""Browse for video file"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Select Video File", "",
|
||||
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv);;All Files (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.path_display.setText(file_path)
|
||||
self.source_changed.emit(file_path)
|
||||
|
||||
class DetectionControlWidget(QGroupBox):
|
||||
"""
|
||||
Widget for controlling detection parameters.
|
||||
"""
|
||||
|
||||
confidence_changed = Signal(float)
|
||||
nms_threshold_changed = Signal(float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("Detection Settings", parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup detection control UI"""
|
||||
layout = QGridLayout(self)
|
||||
|
||||
# Confidence threshold
|
||||
layout.addWidget(QLabel("Confidence:"), 0, 0)
|
||||
|
||||
self.confidence_slider = QSlider(Qt.Horizontal)
|
||||
self.confidence_slider.setRange(1, 100)
|
||||
self.confidence_slider.setValue(30)
|
||||
self.confidence_slider.valueChanged.connect(self.on_confidence_changed)
|
||||
|
||||
self.confidence_label = QLabel("0.30")
|
||||
self.confidence_label.setMinimumWidth(40)
|
||||
|
||||
layout.addWidget(self.confidence_slider, 0, 1)
|
||||
layout.addWidget(self.confidence_label, 0, 2)
|
||||
|
||||
# NMS threshold
|
||||
layout.addWidget(QLabel("NMS Threshold:"), 1, 0)
|
||||
|
||||
self.nms_slider = QSlider(Qt.Horizontal)
|
||||
self.nms_slider.setRange(1, 100)
|
||||
self.nms_slider.setValue(45)
|
||||
self.nms_slider.valueChanged.connect(self.on_nms_changed)
|
||||
|
||||
self.nms_label = QLabel("0.45")
|
||||
self.nms_label.setMinimumWidth(40)
|
||||
|
||||
layout.addWidget(self.nms_slider, 1, 1)
|
||||
layout.addWidget(self.nms_label, 1, 2)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
|
||||
@Slot(int)
|
||||
def on_confidence_changed(self, value):
|
||||
"""Handle confidence threshold change"""
|
||||
confidence = value / 100.0
|
||||
self.confidence_label.setText(f"{confidence:.2f}")
|
||||
self.confidence_changed.emit(confidence)
|
||||
|
||||
@Slot(int)
|
||||
def on_nms_changed(self, value):
|
||||
"""Handle NMS threshold change"""
|
||||
nms = value / 100.0
|
||||
self.nms_label.setText(f"{nms:.2f}")
|
||||
self.nms_threshold_changed.emit(nms)
|
||||
|
||||
class LiveView(QWidget):
|
||||
"""
|
||||
Main live detection view.
|
||||
Displays real-time video with detection overlays and controls.
|
||||
"""
|
||||
|
||||
source_changed = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
self.current_detections = []
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the live view UI"""
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Main video display area
|
||||
video_layout = QVBoxLayout()
|
||||
|
||||
self.video_widget = VideoDisplayWidget()
|
||||
self.video_widget.frame_clicked.connect(self.on_frame_clicked)
|
||||
|
||||
video_layout.addWidget(self.video_widget, 1)
|
||||
|
||||
# Video controls
|
||||
controls_layout = QHBoxLayout()
|
||||
|
||||
self.play_btn = QPushButton(FinaleIcons.get_icon("play"), "")
|
||||
self.play_btn.setToolTip("Play/Pause")
|
||||
self.play_btn.setFixedSize(40, 40)
|
||||
|
||||
self.stop_btn = QPushButton(FinaleIcons.get_icon("stop"), "")
|
||||
self.stop_btn.setToolTip("Stop")
|
||||
self.stop_btn.setFixedSize(40, 40)
|
||||
|
||||
self.record_btn = QPushButton(FinaleIcons.get_icon("record"), "")
|
||||
self.record_btn.setToolTip("Record")
|
||||
self.record_btn.setFixedSize(40, 40)
|
||||
self.record_btn.setCheckable(True)
|
||||
|
||||
self.snapshot_btn = QPushButton(FinaleIcons.get_icon("camera"), "")
|
||||
self.snapshot_btn.setToolTip("Take Snapshot")
|
||||
self.snapshot_btn.setFixedSize(40, 40)
|
||||
|
||||
controls_layout.addWidget(self.play_btn)
|
||||
controls_layout.addWidget(self.stop_btn)
|
||||
controls_layout.addWidget(self.record_btn)
|
||||
controls_layout.addWidget(self.snapshot_btn)
|
||||
controls_layout.addStretch()
|
||||
|
||||
# Overlay toggle
|
||||
self.overlay_btn = QPushButton(FinaleIcons.get_icon("visibility"), "Overlays")
|
||||
self.overlay_btn.setCheckable(True)
|
||||
self.overlay_btn.setChecked(True)
|
||||
self.overlay_btn.toggled.connect(self.toggle_overlays)
|
||||
|
||||
controls_layout.addWidget(self.overlay_btn)
|
||||
|
||||
video_layout.addLayout(controls_layout)
|
||||
layout.addLayout(video_layout, 3)
|
||||
|
||||
# Right panel for controls
|
||||
right_panel = QVBoxLayout()
|
||||
|
||||
# Source control
|
||||
self.source_control = SourceControlWidget()
|
||||
self.source_control.source_changed.connect(self.source_changed.emit)
|
||||
right_panel.addWidget(self.source_control)
|
||||
|
||||
# Detection control
|
||||
self.detection_control = DetectionControlWidget()
|
||||
right_panel.addWidget(self.detection_control)
|
||||
|
||||
# Detection info
|
||||
self.info_widget = QGroupBox("Detection Info")
|
||||
info_layout = QVBoxLayout(self.info_widget)
|
||||
|
||||
self.detection_count_label = QLabel("Detections: 0")
|
||||
self.fps_label = QLabel("FPS: 0.0")
|
||||
self.resolution_label = QLabel("Resolution: N/A")
|
||||
|
||||
info_layout.addWidget(self.detection_count_label)
|
||||
info_layout.addWidget(self.fps_label)
|
||||
info_layout.addWidget(self.resolution_label)
|
||||
|
||||
self.info_widget.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
right_panel.addWidget(self.info_widget)
|
||||
|
||||
right_panel.addStretch()
|
||||
|
||||
layout.addLayout(right_panel, 1)
|
||||
|
||||
# Apply theme
|
||||
self.apply_theme(True)
|
||||
|
||||
def update_frame(self, pixmap, detections=None):
|
||||
"""Update the video frame with detections"""
|
||||
if pixmap is None:
|
||||
return
|
||||
|
||||
self.current_detections = detections or []
|
||||
self.video_widget.update_frame(pixmap, self.current_detections)
|
||||
|
||||
# Update detection info
|
||||
self.detection_count_label.setText(f"Detections: {len(self.current_detections)}")
|
||||
|
||||
if pixmap:
|
||||
size = pixmap.size()
|
||||
self.resolution_label.setText(f"Resolution: {size.width()}x{size.height()}")
|
||||
|
||||
def update_fps(self, fps):
|
||||
"""Update FPS display"""
|
||||
self.fps_label.setText(f"FPS: {fps:.1f}")
|
||||
|
||||
@Slot(bool)
|
||||
def toggle_overlays(self, enabled):
|
||||
"""Toggle detection overlays"""
|
||||
self.video_widget.overlay_enabled = enabled
|
||||
# Refresh current frame
|
||||
if self.video_widget.current_pixmap:
|
||||
self.video_widget.update_frame(self.video_widget.current_pixmap, self.current_detections)
|
||||
|
||||
@Slot(int, int)
|
||||
def on_frame_clicked(self, x, y):
|
||||
"""Handle frame click for interaction"""
|
||||
print(f"Frame clicked at ({x}, {y})")
|
||||
# Could be used for region selection, etc.
|
||||
|
||||
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: 20px;
|
||||
padding: 8px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: {MaterialColors.primary_variant};
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background-color: {MaterialColors.secondary};
|
||||
}}
|
||||
""")
|
||||
634
qt_app_pyside1/finale/views/settings_view.py
Normal file
634
qt_app_pyside1/finale/views/settings_view.py
Normal file
@@ -0,0 +1,634 @@
|
||||
"""
|
||||
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, pyqtSignal
|
||||
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)
|
||||
609
qt_app_pyside1/finale/views/violations_view.py
Normal file
609
qt_app_pyside1/finale/views/violations_view.py
Normal file
@@ -0,0 +1,609 @@
|
||||
"""
|
||||
Violations View - Violation management and history
|
||||
Displays violation records, details, and management tools.
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
|
||||
QComboBox, QSpinBox, QLineEdit, QTextEdit, QDialog,
|
||||
QDialogButtonBox, QSplitter, QListWidget, QListWidgetItem
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate, QSize
|
||||
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont, QIcon
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
|
||||
# Import finale components
|
||||
from ..styles import FinaleStyles, MaterialColors
|
||||
from ..icons import FinaleIcons
|
||||
from qt_app_pyside.utils.helpers import save_configuration, create_export_csv, create_export_json
|
||||
from qt_app_pyside.utils.annotation_utils import draw_detections
|
||||
from qt_app_pyside.utils.enhanced_annotation_utils import enhanced_draw_detections
|
||||
from qt_app_pyside.ui.export_tab import ExportTab
|
||||
from qt_app_pyside.ui.violations_tab import ViolationsTab as OriginalViolationsTab
|
||||
|
||||
class ViolationDetailDialog(QDialog):
|
||||
"""
|
||||
Dialog for viewing detailed violation information.
|
||||
"""
|
||||
|
||||
def __init__(self, violation_data, parent=None):
|
||||
super().__init__(parent)
|
||||
self.violation_data = violation_data
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the detail dialog UI"""
|
||||
self.setWindowTitle("Violation Details")
|
||||
self.setMinimumSize(600, 500)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Header with violation type and timestamp
|
||||
header_frame = QFrame()
|
||||
header_frame.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {MaterialColors.primary};
|
||||
color: {MaterialColors.text_on_primary};
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}}
|
||||
""")
|
||||
|
||||
header_layout = QHBoxLayout(header_frame)
|
||||
|
||||
violation_type = self.violation_data.get('type', 'Unknown')
|
||||
timestamp = self.violation_data.get('timestamp', 'Unknown')
|
||||
|
||||
type_label = QLabel(violation_type)
|
||||
type_label.setFont(QFont("Segoe UI", 16, QFont.Bold))
|
||||
|
||||
time_label = QLabel(timestamp)
|
||||
time_label.setFont(QFont("Segoe UI", 12))
|
||||
|
||||
header_layout.addWidget(type_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(time_label)
|
||||
|
||||
layout.addWidget(header_frame)
|
||||
|
||||
# Main content area
|
||||
content_splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Left side - Image/Video
|
||||
image_group = QGroupBox("Evidence")
|
||||
image_layout = QVBoxLayout(image_group)
|
||||
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setMinimumSize(300, 200)
|
||||
self.image_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 2px solid #424242;
|
||||
border-radius: 8px;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
""")
|
||||
self.image_label.setAlignment(Qt.AlignCenter)
|
||||
self.image_label.setText("No image available")
|
||||
|
||||
# Load image if available
|
||||
image_path = self.violation_data.get('image_path')
|
||||
if image_path and os.path.exists(image_path):
|
||||
pixmap = QPixmap(image_path)
|
||||
if not pixmap.isNull():
|
||||
scaled_pixmap = pixmap.scaled(300, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.image_label.setPixmap(scaled_pixmap)
|
||||
|
||||
image_layout.addWidget(self.image_label)
|
||||
|
||||
# Image controls
|
||||
image_controls = QHBoxLayout()
|
||||
|
||||
save_image_btn = QPushButton(FinaleIcons.get_icon("save"), "Save Image")
|
||||
view_full_btn = QPushButton(FinaleIcons.get_icon("zoom_in"), "View Full")
|
||||
|
||||
image_controls.addWidget(save_image_btn)
|
||||
image_controls.addWidget(view_full_btn)
|
||||
image_controls.addStretch()
|
||||
|
||||
image_layout.addLayout(image_controls)
|
||||
|
||||
content_splitter.addWidget(image_group)
|
||||
|
||||
# Right side - Details
|
||||
details_group = QGroupBox("Details")
|
||||
details_layout = QGridLayout(details_group)
|
||||
|
||||
# Violation details
|
||||
details = [
|
||||
("Vehicle ID:", self.violation_data.get('vehicle_id', 'Unknown')),
|
||||
("Location:", self.violation_data.get('location', 'Unknown')),
|
||||
("Confidence:", f"{self.violation_data.get('confidence', 0.0):.2f}"),
|
||||
("Speed:", f"{self.violation_data.get('speed', 0.0):.1f} km/h"),
|
||||
("Lane:", self.violation_data.get('lane', 'Unknown')),
|
||||
("Weather:", self.violation_data.get('weather', 'Unknown')),
|
||||
("Officer ID:", self.violation_data.get('officer_id', 'N/A')),
|
||||
("Status:", self.violation_data.get('status', 'Pending'))
|
||||
]
|
||||
|
||||
for i, (label, value) in enumerate(details):
|
||||
label_widget = QLabel(label)
|
||||
label_widget.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
||||
|
||||
value_widget = QLabel(str(value))
|
||||
value_widget.setStyleSheet(f"color: {MaterialColors.text_secondary};")
|
||||
|
||||
details_layout.addWidget(label_widget, i, 0)
|
||||
details_layout.addWidget(value_widget, i, 1)
|
||||
|
||||
# Notes section
|
||||
notes_label = QLabel("Notes:")
|
||||
notes_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
||||
details_layout.addWidget(notes_label, len(details), 0, 1, 2)
|
||||
|
||||
self.notes_edit = QTextEdit()
|
||||
self.notes_edit.setMaximumHeight(100)
|
||||
self.notes_edit.setPlainText(self.violation_data.get('notes', ''))
|
||||
details_layout.addWidget(self.notes_edit, len(details) + 1, 0, 1, 2)
|
||||
|
||||
content_splitter.addWidget(details_group)
|
||||
layout.addWidget(content_splitter)
|
||||
|
||||
# Action buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Report")
|
||||
delete_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete")
|
||||
delete_btn.setStyleSheet(f"background-color: {MaterialColors.error};")
|
||||
|
||||
button_layout.addWidget(export_btn)
|
||||
button_layout.addWidget(delete_btn)
|
||||
button_layout.addStretch()
|
||||
|
||||
# Standard dialog buttons
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
|
||||
button_box.accepted.connect(self.save_changes)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(button_box)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_dialog_style())
|
||||
|
||||
@Slot()
|
||||
def save_changes(self):
|
||||
"""Save changes to violation data"""
|
||||
# Update notes
|
||||
self.violation_data['notes'] = self.notes_edit.toPlainText()
|
||||
# Here you would save to database/file
|
||||
self.accept()
|
||||
|
||||
class ViolationFilterWidget(QGroupBox):
|
||||
"""
|
||||
Widget for filtering violations by various criteria.
|
||||
"""
|
||||
|
||||
filter_changed = Signal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("Filter Violations", parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup filter UI"""
|
||||
layout = QGridLayout(self)
|
||||
|
||||
# Date range
|
||||
layout.addWidget(QLabel("Date From:"), 0, 0)
|
||||
self.date_from = QDateEdit()
|
||||
self.date_from.setDate(QDate.currentDate().addDays(-30))
|
||||
self.date_from.setCalendarPopup(True)
|
||||
layout.addWidget(self.date_from, 0, 1)
|
||||
|
||||
layout.addWidget(QLabel("Date To:"), 0, 2)
|
||||
self.date_to = QDateEdit()
|
||||
self.date_to.setDate(QDate.currentDate())
|
||||
self.date_to.setCalendarPopup(True)
|
||||
layout.addWidget(self.date_to, 0, 3)
|
||||
|
||||
# Violation type
|
||||
layout.addWidget(QLabel("Type:"), 1, 0)
|
||||
self.type_combo = QComboBox()
|
||||
self.type_combo.addItems(["All Types", "Red Light", "Speed", "Wrong Lane", "No Helmet", "Other"])
|
||||
layout.addWidget(self.type_combo, 1, 1)
|
||||
|
||||
# Status
|
||||
layout.addWidget(QLabel("Status:"), 1, 2)
|
||||
self.status_combo = QComboBox()
|
||||
self.status_combo.addItems(["All Status", "Pending", "Reviewed", "Closed", "Disputed"])
|
||||
layout.addWidget(self.status_combo, 1, 3)
|
||||
|
||||
# Location
|
||||
layout.addWidget(QLabel("Location:"), 2, 0)
|
||||
self.location_edit = QLineEdit()
|
||||
self.location_edit.setPlaceholderText("Enter location...")
|
||||
layout.addWidget(self.location_edit, 2, 1)
|
||||
|
||||
# Confidence threshold
|
||||
layout.addWidget(QLabel("Min Confidence:"), 2, 2)
|
||||
self.confidence_spin = QSpinBox()
|
||||
self.confidence_spin.setRange(0, 100)
|
||||
self.confidence_spin.setValue(50)
|
||||
self.confidence_spin.setSuffix("%")
|
||||
layout.addWidget(self.confidence_spin, 2, 3)
|
||||
|
||||
# Apply button
|
||||
self.apply_btn = QPushButton(FinaleIcons.get_icon("filter"), "Apply Filter")
|
||||
self.apply_btn.clicked.connect(self.apply_filter)
|
||||
layout.addWidget(self.apply_btn, 3, 0, 1, 4)
|
||||
|
||||
# Connect signals for auto-update
|
||||
self.date_from.dateChanged.connect(self.on_filter_changed)
|
||||
self.date_to.dateChanged.connect(self.on_filter_changed)
|
||||
self.type_combo.currentTextChanged.connect(self.on_filter_changed)
|
||||
self.status_combo.currentTextChanged.connect(self.on_filter_changed)
|
||||
|
||||
# Apply styling
|
||||
self.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
|
||||
@Slot()
|
||||
def apply_filter(self):
|
||||
"""Apply current filter settings"""
|
||||
self.on_filter_changed()
|
||||
|
||||
def on_filter_changed(self):
|
||||
"""Emit filter changed signal with current settings"""
|
||||
filter_data = {
|
||||
'date_from': self.date_from.date().toPython(),
|
||||
'date_to': self.date_to.date().toPython(),
|
||||
'type': self.type_combo.currentText(),
|
||||
'status': self.status_combo.currentText(),
|
||||
'location': self.location_edit.text(),
|
||||
'min_confidence': self.confidence_spin.value() / 100.0
|
||||
}
|
||||
self.filter_changed.emit(filter_data)
|
||||
|
||||
class ViolationListWidget(QWidget):
|
||||
"""
|
||||
Widget displaying violation list with thumbnails and quick info.
|
||||
"""
|
||||
|
||||
violation_selected = Signal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.violations = []
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup violation list UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Header
|
||||
header_layout = QHBoxLayout()
|
||||
|
||||
self.count_label = QLabel("0 violations")
|
||||
self.count_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
|
||||
|
||||
self.sort_combo = QComboBox()
|
||||
self.sort_combo.addItems(["Sort by Time", "Sort by Type", "Sort by Confidence", "Sort by Status"])
|
||||
self.sort_combo.currentTextChanged.connect(self.sort_violations)
|
||||
|
||||
header_layout.addWidget(self.count_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(QLabel("Sort:"))
|
||||
header_layout.addWidget(self.sort_combo)
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# Violations list
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.itemClicked.connect(self.on_item_clicked)
|
||||
self.list_widget.setStyleSheet(FinaleStyles.get_list_style())
|
||||
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
def add_violation(self, violation_data):
|
||||
"""Add a violation to the list"""
|
||||
self.violations.append(violation_data)
|
||||
self.update_list()
|
||||
|
||||
def set_violations(self, violations):
|
||||
"""Set the complete list of violations"""
|
||||
self.violations = violations
|
||||
self.update_list()
|
||||
|
||||
def update_list(self):
|
||||
"""Update the violation list display"""
|
||||
self.list_widget.clear()
|
||||
|
||||
for violation in self.violations:
|
||||
item = QListWidgetItem()
|
||||
|
||||
# Create custom widget for violation item
|
||||
item_widget = self.create_violation_item_widget(violation)
|
||||
|
||||
item.setSizeHint(item_widget.sizeHint())
|
||||
self.list_widget.addItem(item)
|
||||
self.list_widget.setItemWidget(item, item_widget)
|
||||
|
||||
# Update count
|
||||
self.count_label.setText(f"{len(self.violations)} violations")
|
||||
|
||||
def create_violation_item_widget(self, violation):
|
||||
"""Create a custom widget for a violation list item"""
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
# Thumbnail (placeholder for now)
|
||||
thumbnail = QLabel()
|
||||
thumbnail.setFixedSize(80, 60)
|
||||
thumbnail.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 1px solid #424242;
|
||||
border-radius: 4px;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
""")
|
||||
thumbnail.setAlignment(Qt.AlignCenter)
|
||||
thumbnail.setText("IMG")
|
||||
layout.addWidget(thumbnail)
|
||||
|
||||
# Violation info
|
||||
info_layout = QVBoxLayout()
|
||||
|
||||
# Title line
|
||||
title_layout = QHBoxLayout()
|
||||
|
||||
type_label = QLabel(violation.get('type', 'Unknown'))
|
||||
type_label.setFont(QFont("Segoe UI", 11, QFont.Bold))
|
||||
|
||||
time_label = QLabel(violation.get('timestamp', ''))
|
||||
time_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 10px;")
|
||||
|
||||
title_layout.addWidget(type_label)
|
||||
title_layout.addStretch()
|
||||
title_layout.addWidget(time_label)
|
||||
|
||||
info_layout.addLayout(title_layout)
|
||||
|
||||
# Details line
|
||||
details = f"Vehicle: {violation.get('vehicle_id', 'Unknown')} | Location: {violation.get('location', 'Unknown')}"
|
||||
details_label = QLabel(details)
|
||||
details_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 9px;")
|
||||
info_layout.addWidget(details_label)
|
||||
|
||||
# Confidence and status
|
||||
status_layout = QHBoxLayout()
|
||||
|
||||
confidence = violation.get('confidence', 0.0)
|
||||
confidence_label = QLabel(f"Confidence: {confidence:.2f}")
|
||||
confidence_label.setStyleSheet(f"color: {MaterialColors.primary}; font-size: 9px;")
|
||||
|
||||
status = violation.get('status', 'Pending')
|
||||
status_label = QLabel(status)
|
||||
status_color = {
|
||||
'Pending': MaterialColors.warning,
|
||||
'Reviewed': MaterialColors.primary,
|
||||
'Closed': MaterialColors.success,
|
||||
'Disputed': MaterialColors.error
|
||||
}.get(status, MaterialColors.text_secondary)
|
||||
status_label.setStyleSheet(f"color: {status_color}; font-size: 9px; font-weight: bold;")
|
||||
|
||||
status_layout.addWidget(confidence_label)
|
||||
status_layout.addStretch()
|
||||
status_layout.addWidget(status_label)
|
||||
|
||||
info_layout.addLayout(status_layout)
|
||||
layout.addLayout(info_layout, 1)
|
||||
|
||||
# Store violation data in widget
|
||||
widget.violation_data = violation
|
||||
|
||||
return widget
|
||||
|
||||
def sort_violations(self, sort_by):
|
||||
"""Sort violations by the specified criteria"""
|
||||
if sort_by == "Sort by Time":
|
||||
self.violations.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
||||
elif sort_by == "Sort by Type":
|
||||
self.violations.sort(key=lambda x: x.get('type', ''))
|
||||
elif sort_by == "Sort by Confidence":
|
||||
self.violations.sort(key=lambda x: x.get('confidence', 0.0), reverse=True)
|
||||
elif sort_by == "Sort by Status":
|
||||
self.violations.sort(key=lambda x: x.get('status', ''))
|
||||
|
||||
self.update_list()
|
||||
|
||||
@Slot(QListWidgetItem)
|
||||
def on_item_clicked(self, item):
|
||||
"""Handle violation item click"""
|
||||
item_widget = self.list_widget.itemWidget(item)
|
||||
if hasattr(item_widget, 'violation_data'):
|
||||
self.violation_selected.emit(item_widget.violation_data)
|
||||
|
||||
class ViolationsView(QWidget):
|
||||
"""
|
||||
Main violations view with filtering, list, and detail management.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
self.load_sample_data()
|
||||
|
||||
self.save_config = save_configuration
|
||||
self.export_csv = create_export_csv
|
||||
self.export_json = create_export_json
|
||||
self.draw_detections = draw_detections
|
||||
self.enhanced_draw_detections = enhanced_draw_detections
|
||||
# Add export functionality from original export_tab
|
||||
self.export_handler = ExportTab()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the violations view UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Filter widget
|
||||
self.filter_widget = ViolationFilterWidget()
|
||||
self.filter_widget.filter_changed.connect(self.apply_filter)
|
||||
layout.addWidget(self.filter_widget)
|
||||
|
||||
# Main content area
|
||||
content_splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Left side - Violation list
|
||||
self.violation_list = ViolationListWidget()
|
||||
self.violation_list.violation_selected.connect(self.show_violation_details)
|
||||
content_splitter.addWidget(self.violation_list)
|
||||
|
||||
# Right side - Quick actions and summary
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
|
||||
# Quick actions
|
||||
actions_group = QGroupBox("Quick Actions")
|
||||
actions_layout = QVBoxLayout(actions_group)
|
||||
|
||||
export_all_btn = QPushButton(FinaleIcons.get_icon("export"), "Export All")
|
||||
export_filtered_btn = QPushButton(FinaleIcons.get_icon("filter"), "Export Filtered")
|
||||
delete_selected_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete Selected")
|
||||
mark_reviewed_btn = QPushButton(FinaleIcons.get_icon("check"), "Mark as Reviewed")
|
||||
|
||||
actions_layout.addWidget(export_all_btn)
|
||||
actions_layout.addWidget(export_filtered_btn)
|
||||
actions_layout.addWidget(delete_selected_btn)
|
||||
actions_layout.addWidget(mark_reviewed_btn)
|
||||
|
||||
actions_group.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
right_layout.addWidget(actions_group)
|
||||
|
||||
# Summary statistics
|
||||
summary_group = QGroupBox("Summary")
|
||||
summary_layout = QGridLayout(summary_group)
|
||||
|
||||
self.total_label = QLabel("Total: 0")
|
||||
self.pending_label = QLabel("Pending: 0")
|
||||
self.reviewed_label = QLabel("Reviewed: 0")
|
||||
self.closed_label = QLabel("Closed: 0")
|
||||
|
||||
summary_layout.addWidget(self.total_label, 0, 0)
|
||||
summary_layout.addWidget(self.pending_label, 0, 1)
|
||||
summary_layout.addWidget(self.reviewed_label, 1, 0)
|
||||
summary_layout.addWidget(self.closed_label, 1, 1)
|
||||
|
||||
summary_group.setStyleSheet(FinaleStyles.get_group_box_style())
|
||||
right_layout.addWidget(summary_group)
|
||||
|
||||
right_layout.addStretch()
|
||||
content_splitter.addWidget(right_panel)
|
||||
|
||||
# Set splitter proportions
|
||||
content_splitter.setSizes([700, 300])
|
||||
|
||||
layout.addWidget(content_splitter, 1)
|
||||
|
||||
# Apply theme
|
||||
self.apply_theme(True)
|
||||
|
||||
def load_sample_data(self):
|
||||
"""Load sample violation data for demonstration"""
|
||||
sample_violations = [
|
||||
{
|
||||
'timestamp': '14:23:15',
|
||||
'type': 'Red Light',
|
||||
'vehicle_id': 'VH1234',
|
||||
'location': 'Main St & 1st Ave',
|
||||
'confidence': 0.92,
|
||||
'status': 'Pending',
|
||||
'speed': 45.2,
|
||||
'lane': 'Left Turn',
|
||||
'notes': 'Clear violation captured on camera.'
|
||||
},
|
||||
{
|
||||
'timestamp': '13:45:32',
|
||||
'type': 'Speed',
|
||||
'vehicle_id': 'VH5678',
|
||||
'location': 'Highway 101',
|
||||
'confidence': 0.87,
|
||||
'status': 'Reviewed',
|
||||
'speed': 78.5,
|
||||
'lane': 'Right',
|
||||
'notes': 'Speed limit 60 km/h, vehicle traveling at 78.5 km/h.'
|
||||
},
|
||||
{
|
||||
'timestamp': '12:15:48',
|
||||
'type': 'Wrong Lane',
|
||||
'vehicle_id': 'VH9012',
|
||||
'location': 'Oak St Bridge',
|
||||
'confidence': 0.76,
|
||||
'status': 'Closed',
|
||||
'speed': 32.1,
|
||||
'lane': 'Bus Lane',
|
||||
'notes': 'Vehicle in bus-only lane during restricted hours.'
|
||||
}
|
||||
]
|
||||
|
||||
self.violation_list.set_violations(sample_violations)
|
||||
self.update_summary()
|
||||
|
||||
def add_violation(self, violation_data):
|
||||
"""Add a new violation (called from main window)"""
|
||||
self.violation_list.add_violation(violation_data)
|
||||
self.update_summary()
|
||||
|
||||
@Slot(dict)
|
||||
def apply_filter(self, filter_data):
|
||||
"""Apply filter to violation list"""
|
||||
print(f"Applying filter: {filter_data}")
|
||||
# Here you would filter the violations based on criteria
|
||||
# For now, just update summary
|
||||
self.update_summary()
|
||||
|
||||
@Slot(dict)
|
||||
def show_violation_details(self, violation_data):
|
||||
"""Show detailed view of selected violation"""
|
||||
dialog = ViolationDetailDialog(violation_data, self)
|
||||
dialog.exec()
|
||||
|
||||
def update_summary(self):
|
||||
"""Update summary statistics"""
|
||||
violations = self.violation_list.violations
|
||||
|
||||
total = len(violations)
|
||||
pending = len([v for v in violations if v.get('status') == 'Pending'])
|
||||
reviewed = len([v for v in violations if v.get('status') == 'Reviewed'])
|
||||
closed = len([v for v in violations if v.get('status') == 'Closed'])
|
||||
|
||||
self.total_label.setText(f"Total: {total}")
|
||||
self.pending_label.setText(f"Pending: {pending}")
|
||||
self.reviewed_label.setText(f"Reviewed: {reviewed}")
|
||||
self.closed_label.setText(f"Closed: {closed}")
|
||||
|
||||
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};
|
||||
}}
|
||||
""")
|
||||
Reference in New Issue
Block a user