869 lines
28 KiB
Python
869 lines
28 KiB
Python
"""
|
|
EU-Utility - Modern UI Design System
|
|
=====================================
|
|
|
|
A beautiful, modern design system for EU-Utility featuring:
|
|
- Dark gaming aesthetic with EU orange accents
|
|
- Glassmorphism effects throughout
|
|
- Smooth 60fps animations
|
|
- Responsive layouts
|
|
- Professional iconography
|
|
|
|
Design Principles:
|
|
1. Visual Hierarchy - Clear distinction between elements
|
|
2. Consistency - Unified design language
|
|
3. Feedback - Clear interactive states
|
|
4. Performance - GPU-accelerated animations
|
|
5. Accessibility - WCAG compliant contrast ratios
|
|
"""
|
|
|
|
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QTimer
|
|
from PyQt6.QtCore import pyqtSignal, QSize, QPoint, QRectF
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QFrame, QPushButton, QLabel, QVBoxLayout, QHBoxLayout,
|
|
QGraphicsDropShadowEffect, QGraphicsOpacityEffect, QLineEdit,
|
|
QScrollArea, QStackedWidget, QProgressBar, QTextEdit, QComboBox
|
|
)
|
|
from PyQt6.QtGui import (
|
|
QColor, QPainter, QLinearGradient, QRadialGradient, QFont,
|
|
QFontDatabase, QIcon, QPixmap, QCursor, QPainterPath
|
|
)
|
|
from typing import Optional, Callable, List, Dict, Any
|
|
import math
|
|
|
|
|
|
# =============================================================================
|
|
# DESIGN TOKENS - Centralized Design System
|
|
# =============================================================================
|
|
|
|
class DesignTokens:
|
|
"""Central design tokens for consistent UI across the application."""
|
|
|
|
# Brand Colors
|
|
BRAND_ORANGE = "#FF6B35"
|
|
BRAND_ORANGE_LIGHT = "#FF8C5A"
|
|
BRAND_ORANGE_DARK = "#E55A2B"
|
|
BRAND_ORANGE_GLOW = "rgba(255, 107, 53, 0.4)"
|
|
|
|
# Extended Color Palette
|
|
COLORS = {
|
|
# Primary
|
|
'primary': '#FF6B35',
|
|
'primary_hover': '#FF8C5A',
|
|
'primary_pressed': '#E55A2B',
|
|
'primary_glow': 'rgba(255, 107, 53, 0.4)',
|
|
|
|
# Backgrounds - Deep space aesthetic
|
|
'bg_darkest': '#0A0C10',
|
|
'bg_dark': '#111318',
|
|
'bg_card': '#161920',
|
|
'bg_elevated': '#1D2129',
|
|
'bg_hover': '#252A33',
|
|
'bg_pressed': '#2D333D',
|
|
|
|
# Surfaces with glassmorphism
|
|
'surface': 'rgba(22, 25, 32, 0.85)',
|
|
'surface_hover': 'rgba(29, 33, 41, 0.9)',
|
|
'surface_active': 'rgba(37, 42, 51, 0.95)',
|
|
|
|
# Accents
|
|
'accent_teal': '#00D4AA',
|
|
'accent_blue': '#4D9CFF',
|
|
'accent_purple': '#A855F7',
|
|
'accent_yellow': '#FBBF24',
|
|
'accent_green': '#22C55E',
|
|
'accent_red': '#EF4444',
|
|
|
|
# Text
|
|
'text_primary': '#F0F4F8',
|
|
'text_secondary': '#9CA3AF',
|
|
'text_muted': '#6B7280',
|
|
'text_disabled': '#4B5563',
|
|
|
|
# Borders
|
|
'border_subtle': 'rgba(255, 255, 255, 0.06)',
|
|
'border_default': 'rgba(255, 255, 255, 0.1)',
|
|
'border_hover': 'rgba(255, 255, 255, 0.15)',
|
|
'border_focus': 'rgba(255, 107, 53, 0.5)',
|
|
'border_active': 'rgba(255, 107, 53, 0.8)',
|
|
|
|
# Status
|
|
'success': '#22C55E',
|
|
'warning': '#FBBF24',
|
|
'error': '#EF4444',
|
|
'info': '#4D9CFF',
|
|
}
|
|
|
|
# Typography
|
|
TYPOGRAPHY = {
|
|
'font_family': '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
|
|
'font_mono': '"JetBrains Mono", "Fira Code", monospace',
|
|
|
|
'size_xs': 11,
|
|
'size_sm': 12,
|
|
'size_base': 13,
|
|
'size_md': 14,
|
|
'size_lg': 16,
|
|
'size_xl': 18,
|
|
'size_2xl': 20,
|
|
'size_3xl': 24,
|
|
'size_4xl': 30,
|
|
'size_5xl': 36,
|
|
}
|
|
|
|
# Spacing (4px grid system)
|
|
SPACING = {
|
|
'0': 0,
|
|
'1': 4,
|
|
'2': 8,
|
|
'3': 12,
|
|
'4': 16,
|
|
'5': 20,
|
|
'6': 24,
|
|
'8': 32,
|
|
'10': 40,
|
|
'12': 48,
|
|
'16': 64,
|
|
}
|
|
|
|
# Border Radius
|
|
RADIUS = {
|
|
'none': 0,
|
|
'sm': 4,
|
|
'md': 8,
|
|
'lg': 12,
|
|
'xl': 16,
|
|
'2xl': 20,
|
|
'3xl': 24,
|
|
'full': 9999,
|
|
}
|
|
|
|
# Shadows
|
|
SHADOWS = {
|
|
'sm': '0 1px 2px rgba(0, 0, 0, 0.3)',
|
|
'md': '0 4px 6px rgba(0, 0, 0, 0.4)',
|
|
'lg': '0 10px 15px rgba(0, 0, 0, 0.5)',
|
|
'xl': '0 20px 25px rgba(0, 0, 0, 0.6)',
|
|
'glow': '0 0 20px rgba(255, 107, 53, 0.3)',
|
|
'glow_strong': '0 0 30px rgba(255, 107, 53, 0.5)',
|
|
}
|
|
|
|
# Animation Durations (ms)
|
|
DURATION = {
|
|
'fast': 150,
|
|
'normal': 250,
|
|
'slow': 350,
|
|
'slower': 500,
|
|
}
|
|
|
|
# Easing Curves
|
|
EASING = {
|
|
'default': QEasingCurve.Type.OutCubic,
|
|
'bounce': QEasingCurve.Type.OutBounce,
|
|
'elastic': QEasingCurve.Type.OutElastic,
|
|
'smooth': QEasingCurve.Type.InOutCubic,
|
|
}
|
|
|
|
@classmethod
|
|
def color(cls, name: str) -> str:
|
|
"""Get color by name."""
|
|
return cls.COLORS.get(name, '#FFFFFF')
|
|
|
|
@classmethod
|
|
def spacing(cls, name: str) -> int:
|
|
"""Get spacing value."""
|
|
return cls.SPACING.get(name, 0)
|
|
|
|
@classmethod
|
|
def radius(cls, name: str) -> int:
|
|
"""Get border radius value."""
|
|
return cls.RADIUS.get(name, 0)
|
|
|
|
|
|
# =============================================================================
|
|
# ANIMATION UTILITIES
|
|
# =============================================================================
|
|
|
|
class AnimationManager:
|
|
"""Manages smooth GPU-accelerated animations."""
|
|
|
|
_active_animations: List[QPropertyAnimation] = []
|
|
|
|
@classmethod
|
|
def fade_in(cls, widget: QWidget, duration: int = 250) -> QPropertyAnimation:
|
|
"""Fade in a widget smoothly."""
|
|
effect = QGraphicsOpacityEffect(widget)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
anim = QPropertyAnimation(effect, b"opacity")
|
|
anim.setDuration(duration)
|
|
anim.setStartValue(0.0)
|
|
anim.setEndValue(1.0)
|
|
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
|
|
cls._active_animations.append(anim)
|
|
anim.finished.connect(lambda: cls._cleanup_animation(anim))
|
|
|
|
return anim
|
|
|
|
@classmethod
|
|
def fade_out(cls, widget: QWidget, duration: int = 200, on_finish: Optional[Callable] = None) -> QPropertyAnimation:
|
|
"""Fade out a widget smoothly."""
|
|
effect = widget.graphicsEffect()
|
|
if not isinstance(effect, QGraphicsOpacityEffect):
|
|
effect = QGraphicsOpacityEffect(widget)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
anim = QPropertyAnimation(effect, b"opacity")
|
|
anim.setDuration(duration)
|
|
anim.setStartValue(1.0)
|
|
anim.setEndValue(0.0)
|
|
anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
|
|
|
if on_finish:
|
|
anim.finished.connect(on_finish)
|
|
|
|
cls._active_animations.append(anim)
|
|
anim.finished.connect(lambda: cls._cleanup_animation(anim))
|
|
|
|
return anim
|
|
|
|
@classmethod
|
|
def slide_in(cls, widget: QWidget, direction: str = "bottom", duration: int = 300) -> QPropertyAnimation:
|
|
"""Slide widget in from specified direction."""
|
|
anim = QPropertyAnimation(widget, b"pos")
|
|
anim.setDuration(duration)
|
|
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
|
|
current_pos = widget.pos()
|
|
|
|
if direction == "left":
|
|
start_pos = current_pos - QPoint(widget.width() + 20, 0)
|
|
elif direction == "right":
|
|
start_pos = current_pos + QPoint(widget.width() + 20, 0)
|
|
elif direction == "top":
|
|
start_pos = current_pos - QPoint(0, widget.height() + 20)
|
|
else: # bottom
|
|
start_pos = current_pos + QPoint(0, widget.height() + 20)
|
|
|
|
anim.setStartValue(start_pos)
|
|
anim.setEndValue(current_pos)
|
|
|
|
cls._active_animations.append(anim)
|
|
anim.finished.connect(lambda: cls._cleanup_animation(anim))
|
|
|
|
return anim
|
|
|
|
@classmethod
|
|
def scale(cls, widget: QWidget, from_scale: float = 0.9, to_scale: float = 1.0, duration: int = 250) -> QPropertyAnimation:
|
|
"""Scale animation for widgets."""
|
|
anim = QPropertyAnimation(widget, b"minimumWidth")
|
|
anim.setDuration(duration)
|
|
anim.setEasingCurve(QEasingCurve.Type.OutBack)
|
|
|
|
base_width = widget.width()
|
|
anim.setStartValue(int(base_width * from_scale))
|
|
anim.setEndValue(int(base_width * to_scale))
|
|
|
|
cls._active_animations.append(anim)
|
|
anim.finished.connect(lambda: cls._cleanup_animation(anim))
|
|
|
|
return anim
|
|
|
|
@classmethod
|
|
def pulse_glow(cls, widget: QWidget, duration: int = 2000) -> QPropertyAnimation:
|
|
"""Create a pulsing glow effect."""
|
|
effect = QGraphicsDropShadowEffect(widget)
|
|
effect.setColor(QColor(255, 107, 53))
|
|
effect.setBlurRadius(20)
|
|
effect.setOffset(0, 0)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
anim = QPropertyAnimation(effect, b"blurRadius")
|
|
anim.setDuration(duration)
|
|
anim.setStartValue(20)
|
|
anim.setEndValue(40)
|
|
anim.setEasingCurve(QEasingCurve.Type.InOutSine)
|
|
anim.setLoopCount(-1)
|
|
|
|
return anim
|
|
|
|
@classmethod
|
|
def _cleanup_animation(cls, anim: QPropertyAnimation):
|
|
"""Remove completed animation from tracking."""
|
|
if anim in cls._active_animations:
|
|
cls._active_animations.remove(anim)
|
|
|
|
|
|
# =============================================================================
|
|
# MODERN COMPONENTS
|
|
# =============================================================================
|
|
|
|
class GlassCard(QFrame):
|
|
"""Glassmorphism card with frosted glass effect."""
|
|
|
|
clicked = pyqtSignal()
|
|
|
|
def __init__(self, parent=None, elevation: int = 1, hover_lift: bool = True):
|
|
super().__init__(parent)
|
|
self.elevation = elevation
|
|
self.hover_lift = hover_lift
|
|
self._hovered = False
|
|
|
|
self._setup_style()
|
|
self._setup_shadow()
|
|
|
|
if hover_lift:
|
|
self.setMouseTracking(True)
|
|
|
|
def _setup_style(self):
|
|
"""Apply glassmorphism styling."""
|
|
c = DesignTokens.COLORS
|
|
opacity = 0.85 + (self.elevation * 0.03)
|
|
|
|
self.setStyleSheet(f"""
|
|
GlassCard {{
|
|
background: rgba(22, 25, 32, {min(opacity, 0.95)});
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: {DesignTokens.radius('xl')}px;
|
|
}}
|
|
""")
|
|
|
|
def _setup_shadow(self):
|
|
"""Apply elevation shadow."""
|
|
self._shadow = QGraphicsDropShadowEffect(self)
|
|
self._shadow.setBlurRadius(self.elevation * 15)
|
|
self._shadow.setColor(QColor(0, 0, 0, int(80 + self.elevation * 20)))
|
|
self._shadow.setOffset(0, self.elevation * 3)
|
|
self.setGraphicsEffect(self._shadow)
|
|
|
|
def enterEvent(self, event):
|
|
"""Hover effect."""
|
|
if self.hover_lift:
|
|
self._hovered = True
|
|
self._shadow.setBlurRadius((self.elevation + 1) * 15)
|
|
self._shadow.setColor(QColor(0, 0, 0, int(100 + (self.elevation + 1) * 20)))
|
|
self.setStyleSheet(f"""
|
|
GlassCard {{
|
|
background: rgba(29, 33, 41, 0.9);
|
|
border: 1px solid rgba(255, 107, 53, 0.2);
|
|
border-radius: {DesignTokens.radius('xl')}px;
|
|
}}
|
|
""")
|
|
super().enterEvent(event)
|
|
|
|
def leaveEvent(self, event):
|
|
"""Reset hover effect."""
|
|
if self.hover_lift:
|
|
self._hovered = False
|
|
self._setup_shadow()
|
|
self._setup_style()
|
|
super().leaveEvent(event)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Handle click."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self.clicked.emit()
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class ModernButton(QPushButton):
|
|
"""Modern button with smooth animations and multiple variants."""
|
|
|
|
VARIANTS = ['primary', 'secondary', 'ghost', 'outline', 'danger', 'glass']
|
|
|
|
def __init__(self, text: str = "", variant: str = 'primary', icon: str = None, parent=None):
|
|
super().__init__(text, parent)
|
|
self.variant = variant
|
|
self.icon_text = icon
|
|
|
|
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
self._apply_style()
|
|
self._setup_animations()
|
|
|
|
def _apply_style(self):
|
|
"""Apply button styling based on variant."""
|
|
c = DesignTokens.COLORS
|
|
r = DesignTokens.radius('full')
|
|
|
|
styles = {
|
|
'primary': f"""
|
|
ModernButton {{
|
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
stop:0 {c['primary']}, stop:1 {c['primary_hover']});
|
|
color: white;
|
|
border: none;
|
|
border-radius: {r}px;
|
|
padding: 12px 24px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 600;
|
|
}}
|
|
ModernButton:hover {{
|
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
stop:0 {c['primary_hover']}, stop:1 {c['primary']});
|
|
}}
|
|
ModernButton:pressed {{
|
|
background: {c['primary_pressed']};
|
|
}}
|
|
""",
|
|
'secondary': f"""
|
|
ModernButton {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {r}px;
|
|
padding: 12px 24px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 600;
|
|
}}
|
|
ModernButton:hover {{
|
|
background: {c['bg_hover']};
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
""",
|
|
'ghost': f"""
|
|
ModernButton {{
|
|
background: transparent;
|
|
color: {c['text_secondary']};
|
|
border: none;
|
|
border-radius: {r}px;
|
|
padding: 12px 24px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 500;
|
|
}}
|
|
ModernButton:hover {{
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: {c['text_primary']};
|
|
}}
|
|
""",
|
|
'outline': f"""
|
|
ModernButton {{
|
|
background: transparent;
|
|
color: {c['primary']};
|
|
border: 2px solid {c['primary']};
|
|
border-radius: {r}px;
|
|
padding: 10px 22px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 600;
|
|
}}
|
|
ModernButton:hover {{
|
|
background: rgba(255, 107, 53, 0.1);
|
|
}}
|
|
""",
|
|
'danger': f"""
|
|
ModernButton {{
|
|
background: {c['error']};
|
|
color: white;
|
|
border: none;
|
|
border-radius: {r}px;
|
|
padding: 12px 24px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 600;
|
|
}}
|
|
ModernButton:hover {{
|
|
background: #DC2626;
|
|
}}
|
|
""",
|
|
'glass': f"""
|
|
ModernButton {{
|
|
background: rgba(255, 255, 255, 0.08);
|
|
color: white;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: {r}px;
|
|
padding: 12px 24px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
font-weight: 600;
|
|
backdrop-filter: blur(10px);
|
|
}}
|
|
ModernButton:hover {{
|
|
background: rgba(255, 255, 255, 0.12);
|
|
border-color: rgba(255, 107, 53, 0.3);
|
|
}}
|
|
"""
|
|
}
|
|
|
|
self.setStyleSheet(styles.get(self.variant, styles['primary']))
|
|
self.setFixedHeight(44)
|
|
|
|
def _setup_animations(self):
|
|
"""Setup press animation."""
|
|
self._press_anim = QPropertyAnimation(self, b"minimumHeight")
|
|
self._press_anim.setDuration(100)
|
|
self._press_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
|
|
|
|
def enterEvent(self, event):
|
|
"""Hover animation."""
|
|
super().enterEvent(event)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Press animation."""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self._press_anim.setStartValue(44)
|
|
self._press_anim.setEndValue(42)
|
|
self._press_anim.start()
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
"""Release animation."""
|
|
self._press_anim.setStartValue(42)
|
|
self._press_anim.setEndValue(44)
|
|
self._press_anim.start()
|
|
super().mouseReleaseEvent(event)
|
|
|
|
|
|
class ModernInput(QLineEdit):
|
|
"""Modern input field with floating label and animations."""
|
|
|
|
def __init__(self, placeholder: str = "", parent=None):
|
|
super().__init__(parent)
|
|
self.setPlaceholderText(placeholder)
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply modern input styling."""
|
|
c = DesignTokens.COLORS
|
|
r = DesignTokens.radius('lg')
|
|
|
|
self.setStyleSheet(f"""
|
|
ModernInput {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 2px solid {c['border_default']};
|
|
border-radius: {r}px;
|
|
padding: 12px 16px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
selection-background-color: {c['primary']};
|
|
}}
|
|
ModernInput:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
ModernInput:focus {{
|
|
border-color: {c['primary']};
|
|
background: {c['bg_card']};
|
|
}}
|
|
ModernInput::placeholder {{
|
|
color: {c['text_muted']};
|
|
}}
|
|
""")
|
|
self.setFixedHeight(48)
|
|
|
|
|
|
class ModernComboBox(QComboBox):
|
|
"""Modern dropdown with custom styling."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply modern combobox styling."""
|
|
c = DesignTokens.COLORS
|
|
r = DesignTokens.radius('lg')
|
|
|
|
self.setStyleSheet(f"""
|
|
ModernComboBox {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 2px solid {c['border_default']};
|
|
border-radius: {r}px;
|
|
padding: 8px 12px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
|
|
min-width: 150px;
|
|
}}
|
|
ModernComboBox:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
ModernComboBox:focus {{
|
|
border-color: {c['primary']};
|
|
}}
|
|
ModernComboBox::drop-down {{
|
|
border: none;
|
|
width: 30px;
|
|
}}
|
|
ModernComboBox::down-arrow {{
|
|
image: none;
|
|
border-left: 5px solid transparent;
|
|
border-right: 5px solid transparent;
|
|
border-top: 5px solid {c['text_secondary']};
|
|
}}
|
|
ModernComboBox QAbstractItemView {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {r}px;
|
|
selection-background-color: {c['bg_hover']};
|
|
selection-color: {c['text_primary']};
|
|
padding: 4px;
|
|
}}
|
|
""")
|
|
self.setFixedHeight(48)
|
|
|
|
|
|
class Badge(QLabel):
|
|
"""Status badge with various styles."""
|
|
|
|
STYLES = ['default', 'success', 'warning', 'error', 'info', 'primary']
|
|
|
|
def __init__(self, text: str = "", style: str = 'default', parent=None):
|
|
super().__init__(text, parent)
|
|
self.badge_style = style
|
|
self._apply_style()
|
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
def _apply_style(self):
|
|
"""Apply badge styling."""
|
|
c = DesignTokens.COLORS
|
|
|
|
colors = {
|
|
'default': ('rgba(255,255,255,0.1)', c['text_secondary']),
|
|
'success': ('rgba(34, 197, 94, 0.2)', c['accent_green']),
|
|
'warning': ('rgba(251, 191, 36, 0.2)', c['accent_yellow']),
|
|
'error': ('rgba(239, 68, 68, 0.2)', c['accent_red']),
|
|
'info': ('rgba(77, 156, 255, 0.2)', c['accent_blue']),
|
|
'primary': ('rgba(255, 107, 53, 0.2)', c['primary']),
|
|
}
|
|
|
|
bg, fg = colors.get(self.badge_style, colors['default'])
|
|
|
|
self.setStyleSheet(f"""
|
|
Badge {{
|
|
background: {bg};
|
|
color: {fg};
|
|
border-radius: {DesignTokens.radius('full')}px;
|
|
padding: 4px 12px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_xs']}px;
|
|
font-weight: 600;
|
|
}}
|
|
""")
|
|
|
|
# Add subtle shadow
|
|
shadow = QGraphicsDropShadowEffect(self)
|
|
shadow.setBlurRadius(4)
|
|
shadow.setColor(QColor(0, 0, 0, 40))
|
|
shadow.setOffset(0, 1)
|
|
self.setGraphicsEffect(shadow)
|
|
|
|
|
|
class ProgressIndicator(QProgressBar):
|
|
"""Modern progress indicator with gradient."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._apply_style()
|
|
self.setTextVisible(False)
|
|
self.setRange(0, 100)
|
|
self.setValue(0)
|
|
|
|
def _apply_style(self):
|
|
"""Apply modern progress styling."""
|
|
c = DesignTokens.COLORS
|
|
|
|
self.setStyleSheet(f"""
|
|
ProgressIndicator {{
|
|
background: {c['bg_elevated']};
|
|
border: none;
|
|
border-radius: {DesignTokens.radius('full')}px;
|
|
height: 6px;
|
|
}}
|
|
ProgressIndicator::chunk {{
|
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
stop:0 {c['primary']}, stop:1 {c['primary_hover']});
|
|
border-radius: {DesignTokens.radius('full')}px;
|
|
}}
|
|
""")
|
|
self.setFixedHeight(6)
|
|
|
|
|
|
class IconButton(QPushButton):
|
|
"""Circular icon button with hover effects."""
|
|
|
|
def __init__(self, icon_text: str = "", size: int = 40, tooltip: str = "", parent=None):
|
|
super().__init__(icon_text, parent)
|
|
self.setFixedSize(size, size)
|
|
self.setToolTip(tooltip)
|
|
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
self._apply_style()
|
|
|
|
def _apply_style(self):
|
|
"""Apply icon button styling."""
|
|
c = DesignTokens.COLORS
|
|
size = self.width()
|
|
|
|
self.setStyleSheet(f"""
|
|
IconButton {{
|
|
background: transparent;
|
|
color: {c['text_secondary']};
|
|
border: none;
|
|
border-radius: {size // 2}px;
|
|
font-size: 16px;
|
|
}}
|
|
IconButton:hover {{
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: {c['text_primary']};
|
|
}}
|
|
IconButton:pressed {{
|
|
background: rgba(255, 255, 255, 0.15);
|
|
}}
|
|
""")
|
|
|
|
|
|
# =============================================================================
|
|
# LAYOUT HELPERS
|
|
# =============================================================================
|
|
|
|
def create_spacer(horizontal: bool = False, size: int = None):
|
|
"""Create a spacer item."""
|
|
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy
|
|
|
|
if horizontal:
|
|
policy = QSizePolicy.Policy.Expanding
|
|
min_policy = QSizePolicy.Policy.Minimum
|
|
return QSpacerItem(size or 0, 0, policy, min_policy)
|
|
else:
|
|
policy = QSizePolicy.Policy.Expanding
|
|
min_policy = QSizePolicy.Policy.Minimum
|
|
return QSpacerItem(0, size or 0, min_policy, policy)
|
|
|
|
|
|
def create_separator(horizontal: bool = True):
|
|
"""Create a styled separator line."""
|
|
separator = QFrame()
|
|
if horizontal:
|
|
separator.setFrameShape(QFrame.Shape.HLine)
|
|
separator.setFixedHeight(1)
|
|
else:
|
|
separator.setFrameShape(QFrame.Shape.VLine)
|
|
separator.setFixedWidth(1)
|
|
|
|
separator.setStyleSheet(f"""
|
|
background: {DesignTokens.color('border_subtle')};
|
|
""")
|
|
return separator
|
|
|
|
|
|
# =============================================================================
|
|
# GLOBAL STYLESHEET
|
|
# =============================================================================
|
|
|
|
def get_global_stylesheet() -> str:
|
|
"""Get complete global stylesheet for the application."""
|
|
c = DesignTokens.COLORS
|
|
|
|
return f"""
|
|
/* Base */
|
|
QWidget {{
|
|
font-family: {DesignTokens.TYPOGRAPHY['font_family']};
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_base']}px;
|
|
color: {c['text_primary']};
|
|
}}
|
|
|
|
/* Main Window */
|
|
QMainWindow {{
|
|
background: {c['bg_darkest']};
|
|
}}
|
|
|
|
/* Selection */
|
|
::selection {{
|
|
background: {c['primary']};
|
|
color: white;
|
|
}}
|
|
|
|
/* Scrollbars */
|
|
QScrollBar:vertical {{
|
|
background: transparent;
|
|
width: 8px;
|
|
margin: 0;
|
|
}}
|
|
QScrollBar::handle:vertical {{
|
|
background: {c['border_default']};
|
|
border-radius: 4px;
|
|
min-height: 40px;
|
|
}}
|
|
QScrollBar::handle:vertical:hover {{
|
|
background: {c['border_hover']};
|
|
}}
|
|
QScrollBar::add-line:vertical,
|
|
QScrollBar::sub-line:vertical {{
|
|
height: 0;
|
|
}}
|
|
|
|
QScrollBar:horizontal {{
|
|
background: transparent;
|
|
height: 8px;
|
|
margin: 0;
|
|
}}
|
|
QScrollBar::handle:horizontal {{
|
|
background: {c['border_default']};
|
|
border-radius: 4px;
|
|
min-width: 40px;
|
|
}}
|
|
|
|
/* Tooltips */
|
|
QToolTip {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: 8px;
|
|
padding: 8px 12px;
|
|
font-size: {DesignTokens.TYPOGRAPHY['size_sm']}px;
|
|
}}
|
|
|
|
/* Menu */
|
|
QMenu {{
|
|
background: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: 12px;
|
|
padding: 8px;
|
|
}}
|
|
QMenu::item {{
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
}}
|
|
QMenu::item:selected {{
|
|
background: {c['bg_hover']};
|
|
}}
|
|
QMenu::separator {{
|
|
height: 1px;
|
|
background: {c['border_subtle']};
|
|
margin: 8px 0;
|
|
}}
|
|
|
|
/* Check Box */
|
|
QCheckBox {{
|
|
spacing: 8px;
|
|
}}
|
|
QCheckBox::indicator {{
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid {c['border_default']};
|
|
border-radius: 6px;
|
|
background: {c['bg_elevated']};
|
|
}}
|
|
QCheckBox::indicator:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
QCheckBox::indicator:checked {{
|
|
background: {c['primary']};
|
|
border-color: {c['primary']};
|
|
}}
|
|
|
|
/* Slider */
|
|
QSlider::groove:horizontal {{
|
|
height: 4px;
|
|
background: {c['bg_elevated']};
|
|
border-radius: 2px;
|
|
}}
|
|
QSlider::handle:horizontal {{
|
|
width: 18px;
|
|
height: 18px;
|
|
background: {c['primary']};
|
|
border-radius: 9px;
|
|
margin: -7px 0;
|
|
}}
|
|
QSlider::sub-page:horizontal {{
|
|
background: {c['primary']};
|
|
border-radius: 2px;
|
|
}}
|
|
"""
|