678 lines
18 KiB
Python
678 lines
18 KiB
Python
"""
|
|
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;
|
|
}
|
|
"""
|