888 lines
27 KiB
Python
888 lines
27 KiB
Python
"""
|
|
EU-Utility - Enhanced EU Styling System
|
|
|
|
Complete design system with:
|
|
- EU game aesthetic matching (dark sci-fi theme)
|
|
- Dark/Light theme support
|
|
- Responsive layout helpers
|
|
- Animation/transition utilities
|
|
- Accessibility features
|
|
"""
|
|
|
|
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QSequentialAnimationGroup
|
|
from PyQt6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
|
|
|
|
|
# ============================================================================
|
|
# COLOR SYSTEM - Dark/Light Theme Support
|
|
# ============================================================================
|
|
|
|
class EUTheme:
|
|
"""Theme manager for dark/light mode support."""
|
|
|
|
_current_theme = "dark"
|
|
|
|
@classmethod
|
|
def set_theme(cls, theme: str):
|
|
"""Set current theme ('dark' or 'light')."""
|
|
cls._current_theme = theme
|
|
|
|
@classmethod
|
|
def get_theme(cls) -> str:
|
|
"""Get current theme name."""
|
|
return cls._current_theme
|
|
|
|
@classmethod
|
|
def is_dark(cls) -> bool:
|
|
"""Check if dark theme is active."""
|
|
return cls._current_theme == "dark"
|
|
|
|
|
|
# Dark Theme - Primary (matches EU game aesthetic)
|
|
EU_DARK_COLORS = {
|
|
# Backgrounds - layered depth
|
|
'bg_primary': '#0d1117',
|
|
'bg_secondary': '#161b22',
|
|
'bg_tertiary': '#21262d',
|
|
'bg_elevated': '#1c2128',
|
|
'bg_overlay': 'rgba(13, 17, 23, 0.95)',
|
|
'bg_hover': 'rgba(48, 54, 61, 0.6)',
|
|
'bg_pressed': 'rgba(33, 38, 45, 0.8)',
|
|
'bg_selected': 'rgba(255, 140, 66, 0.15)',
|
|
|
|
# EU Game Accent Colors
|
|
'accent_orange': '#ff8c42',
|
|
'accent_orange_hover': '#ffa060',
|
|
'accent_orange_pressed': '#e67a35',
|
|
'accent_teal': '#4ecdc4',
|
|
'accent_gold': '#ffc107',
|
|
'accent_blue': '#4a9eff',
|
|
'accent_green': '#4caf50',
|
|
'accent_red': '#f44336',
|
|
'accent_purple': '#9c27b0',
|
|
|
|
# Text colors
|
|
'text_primary': '#f0f6fc',
|
|
'text_secondary': '#8b949e',
|
|
'text_muted': '#6e7681',
|
|
'text_disabled': '#484f58',
|
|
'text_inverse': '#0d1117',
|
|
|
|
# Border colors
|
|
'border_default': '#30363d',
|
|
'border_hover': '#8b949e',
|
|
'border_focus': '#ff8c42',
|
|
'border_active': '#4ecdc4',
|
|
|
|
# Status colors
|
|
'status_success': '#4caf50',
|
|
'status_warning': '#ffc107',
|
|
'status_error': '#f44336',
|
|
'status_info': '#4a9eff',
|
|
|
|
# Progress bars
|
|
'progress_bg': '#21262d',
|
|
'progress_fill': '#4ecdc4',
|
|
'progress_fill_alt': '#ff8c42',
|
|
}
|
|
|
|
# Light Theme
|
|
EU_LIGHT_COLORS = {
|
|
# Backgrounds
|
|
'bg_primary': '#ffffff',
|
|
'bg_secondary': '#f6f8fa',
|
|
'bg_tertiary': '#eaeef2',
|
|
'bg_elevated': '#ffffff',
|
|
'bg_overlay': 'rgba(255, 255, 255, 0.95)',
|
|
'bg_hover': 'rgba(234, 238, 242, 0.8)',
|
|
'bg_pressed': 'rgba(208, 215, 222, 0.8)',
|
|
'bg_selected': 'rgba(255, 140, 66, 0.1)',
|
|
|
|
# EU Game Accent Colors (same for brand consistency)
|
|
'accent_orange': '#ff8c42',
|
|
'accent_orange_hover': '#e67a35',
|
|
'accent_orange_pressed': '#cc6a2f',
|
|
'accent_teal': '#2d9d96',
|
|
'accent_gold': '#d4a017',
|
|
'accent_blue': '#2563eb',
|
|
'accent_green': '#2d8a3e',
|
|
'accent_red': '#dc2626',
|
|
'accent_purple': '#7c3aed',
|
|
|
|
# Text colors
|
|
'text_primary': '#24292f',
|
|
'text_secondary': '#57606a',
|
|
'text_muted': '#6e7781',
|
|
'text_disabled': '#8c959f',
|
|
'text_inverse': '#ffffff',
|
|
|
|
# Border colors
|
|
'border_default': '#d0d7de',
|
|
'border_hover': '#8c959f',
|
|
'border_focus': '#ff8c42',
|
|
'border_active': '#2d9d96',
|
|
|
|
# Status colors
|
|
'status_success': '#2d8a3e',
|
|
'status_warning': '#d4a017',
|
|
'status_error': '#dc2626',
|
|
'status_info': '#2563eb',
|
|
|
|
# Progress bars
|
|
'progress_bg': '#eaeef2',
|
|
'progress_fill': '#2d9d96',
|
|
'progress_fill_alt': '#ff8c42',
|
|
}
|
|
|
|
|
|
def get_color(name: str) -> str:
|
|
"""Get color by name for current theme."""
|
|
colors = EU_DARK_COLORS if EUTheme.is_dark() else EU_LIGHT_COLORS
|
|
return colors.get(name, EU_DARK_COLORS.get(name, '#ffffff'))
|
|
|
|
|
|
def get_all_colors() -> dict:
|
|
"""Get all colors for current theme."""
|
|
return EU_DARK_COLORS if EUTheme.is_dark() else EU_LIGHT_COLORS
|
|
|
|
|
|
# ============================================================================
|
|
# TYPOGRAPHY
|
|
# ============================================================================
|
|
|
|
EU_TYPOGRAPHY = {
|
|
'font_family': '"Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif',
|
|
'font_mono': '"JetBrains Mono", "Fira Code", "Consolas", monospace',
|
|
|
|
'size_xs': '11px',
|
|
'size_sm': '12px',
|
|
'size_base': '13px',
|
|
'size_md': '14px',
|
|
'size_lg': '16px',
|
|
'size_xl': '18px',
|
|
'size_2xl': '20px',
|
|
'size_3xl': '24px',
|
|
|
|
'weight_normal': '400',
|
|
'weight_medium': '500',
|
|
'weight_semibold': '600',
|
|
'weight_bold': '700',
|
|
|
|
'line_tight': '1.25',
|
|
'line_normal': '1.5',
|
|
'line_relaxed': '1.75',
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# SPACING & SIZING
|
|
# ============================================================================
|
|
|
|
EU_SPACING = {
|
|
'xs': '4px',
|
|
'sm': '8px',
|
|
'md': '12px',
|
|
'lg': '16px',
|
|
'xl': '20px',
|
|
'2xl': '24px',
|
|
'3xl': '32px',
|
|
'4xl': '40px',
|
|
}
|
|
|
|
EU_SIZES = {
|
|
'radius_sm': '4px',
|
|
'radius_md': '6px',
|
|
'radius_lg': '8px',
|
|
'radius_xl': '12px',
|
|
'radius_full': '9999px',
|
|
|
|
'shadow_sm': '0 1px 2px rgba(0,0,0,0.1)',
|
|
'shadow_md': '0 4px 6px rgba(0,0,0,0.15)',
|
|
'shadow_lg': '0 10px 15px rgba(0,0,0,0.2)',
|
|
'shadow_glow': '0 0 20px rgba(255, 140, 66, 0.3)',
|
|
'shadow_glow_teal': '0 0 20px rgba(78, 205, 196, 0.3)',
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# COMPONENT STYLES
|
|
# ============================================================================
|
|
|
|
def get_button_style(variant: str = "primary", size: str = "md") -> str:
|
|
"""
|
|
Get button stylesheet.
|
|
|
|
Variants: primary, secondary, ghost, danger, success
|
|
Sizes: sm, md, lg
|
|
"""
|
|
c = get_all_colors()
|
|
|
|
# Size configurations
|
|
sizes = {
|
|
'sm': {'padding': '6px 12px', 'font_size': EU_TYPOGRAPHY['size_xs']},
|
|
'md': {'padding': '8px 16px', 'font_size': EU_TYPOGRAPHY['size_sm']},
|
|
'lg': {'padding': '12px 24px', 'font_size': EU_TYPOGRAPHY['size_base']},
|
|
}
|
|
sz = sizes.get(size, sizes['md'])
|
|
|
|
# Variant configurations
|
|
variants = {
|
|
'primary': {
|
|
'bg': c['accent_orange'],
|
|
'color': '#ffffff',
|
|
'border': c['accent_orange'],
|
|
'hover_bg': c['accent_orange_hover'],
|
|
'hover_border': c['accent_orange_hover'],
|
|
'pressed_bg': c['accent_orange_pressed'],
|
|
},
|
|
'secondary': {
|
|
'bg': c['bg_tertiary'],
|
|
'color': c['text_primary'],
|
|
'border': c['border_default'],
|
|
'hover_bg': c['bg_hover'],
|
|
'hover_border': c['border_hover'],
|
|
'pressed_bg': c['bg_pressed'],
|
|
},
|
|
'ghost': {
|
|
'bg': 'transparent',
|
|
'color': c['text_secondary'],
|
|
'border': 'transparent',
|
|
'hover_bg': c['bg_hover'],
|
|
'hover_border': 'transparent',
|
|
'pressed_bg': c['bg_pressed'],
|
|
},
|
|
'danger': {
|
|
'bg': c['accent_red'],
|
|
'color': '#ffffff',
|
|
'border': c['accent_red'],
|
|
'hover_bg': '#dc2626',
|
|
'hover_border': '#dc2626',
|
|
'pressed_bg': '#b91c1c',
|
|
},
|
|
'success': {
|
|
'bg': c['accent_green'],
|
|
'color': '#ffffff',
|
|
'border': c['accent_green'],
|
|
'hover_bg': '#2d8a3e',
|
|
'hover_border': '#2d8a3e',
|
|
'pressed_bg': '#1f6b2c',
|
|
},
|
|
}
|
|
v = variants.get(variant, variants['primary'])
|
|
|
|
return f"""
|
|
QPushButton {{
|
|
background-color: {v['bg']};
|
|
color: {v['color']};
|
|
border: 1px solid {v['border']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
padding: {sz['padding']};
|
|
font-size: {sz['font_size']};
|
|
font-weight: {EU_TYPOGRAPHY['weight_medium']};
|
|
font-family: {EU_TYPOGRAPHY['font_family']};
|
|
outline: none;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {v['hover_bg']};
|
|
border-color: {v['hover_border']};
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: {v['pressed_bg']};
|
|
}}
|
|
QPushButton:disabled {{
|
|
background-color: {c['bg_tertiary']};
|
|
color: {c['text_disabled']};
|
|
border-color: {c['border_default']};
|
|
}}
|
|
QPushButton:focus {{
|
|
border-color: {c['border_focus']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_input_style() -> str:
|
|
"""Get input field stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QLineEdit, QTextEdit, QPlainTextEdit {{
|
|
background-color: {c['bg_secondary']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
padding: 8px 12px;
|
|
font-size: {EU_TYPOGRAPHY['size_base']};
|
|
font-family: {EU_TYPOGRAPHY['font_family']};
|
|
selection-background-color: {c['accent_orange']};
|
|
selection-color: #ffffff;
|
|
}}
|
|
QLineEdit:hover, QTextEdit:hover, QPlainTextEdit:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{
|
|
border-color: {c['border_focus']};
|
|
background-color: {c['bg_primary']};
|
|
}}
|
|
QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled {{
|
|
background-color: {c['bg_tertiary']};
|
|
color: {c['text_disabled']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_combo_style() -> str:
|
|
"""Get combobox/dropdown stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QComboBox {{
|
|
background-color: {c['bg_secondary']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
padding: 8px 12px;
|
|
min-width: 120px;
|
|
font-size: {EU_TYPOGRAPHY['size_base']};
|
|
}}
|
|
QComboBox:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
QComboBox:focus {{
|
|
border-color: {c['border_focus']};
|
|
}}
|
|
QComboBox::drop-down {{
|
|
border: none;
|
|
width: 24px;
|
|
}}
|
|
QComboBox::down-arrow {{
|
|
image: none;
|
|
border-left: 5px solid transparent;
|
|
border-right: 5px solid transparent;
|
|
border-top: 5px solid {c['text_secondary']};
|
|
}}
|
|
QComboBox QAbstractItemView {{
|
|
background-color: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
selection-background-color: {c['bg_selected']};
|
|
selection-color: {c['text_primary']};
|
|
padding: 4px;
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_table_style() -> str:
|
|
"""Get table widget stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QTableWidget {{
|
|
background-color: {c['bg_secondary']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
gridline-color: {c['border_default']};
|
|
font-size: {EU_TYPOGRAPHY['size_sm']};
|
|
}}
|
|
QTableWidget::item {{
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid {c['border_default']};
|
|
}}
|
|
QTableWidget::item:selected {{
|
|
background-color: {c['bg_selected']};
|
|
color: {c['text_primary']};
|
|
}}
|
|
QTableWidget::item:hover {{
|
|
background-color: {c['bg_hover']};
|
|
}}
|
|
QHeaderView::section {{
|
|
background-color: {c['bg_tertiary']};
|
|
color: {c['text_secondary']};
|
|
padding: 10px 12px;
|
|
border: none;
|
|
border-right: 1px solid {c['border_default']};
|
|
border-bottom: 1px solid {c['border_default']};
|
|
font-weight: {EU_TYPOGRAPHY['weight_semibold']};
|
|
font-size: {EU_TYPOGRAPHY['size_xs']};
|
|
text-transform: uppercase;
|
|
}}
|
|
QHeaderView::section:first {{
|
|
border-top-left-radius: {EU_SIZES['radius_lg']};
|
|
}}
|
|
QHeaderView::section:last {{
|
|
border-top-right-radius: {EU_SIZES['radius_lg']};
|
|
border-right: none;
|
|
}}
|
|
QScrollBar:vertical {{
|
|
background-color: transparent;
|
|
width: 12px;
|
|
border-radius: 6px;
|
|
}}
|
|
QScrollBar::handle:vertical {{
|
|
background-color: {c['border_default']};
|
|
border-radius: 6px;
|
|
min-height: 40px;
|
|
}}
|
|
QScrollBar::handle:vertical:hover {{
|
|
background-color: {c['border_hover']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_card_style() -> str:
|
|
"""Get card/container stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QFrame#card {{
|
|
background-color: {c['bg_secondary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
}}
|
|
QFrame#card:hover {{
|
|
border-color: {c['border_hover']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_panel_style() -> str:
|
|
"""Get panel stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QWidget#panel {{
|
|
background-color: {c['bg_secondary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_scrollbar_style() -> str:
|
|
"""Get scrollbar stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QScrollBar:vertical {{
|
|
background-color: transparent;
|
|
width: 10px;
|
|
border-radius: 5px;
|
|
margin: 2px;
|
|
}}
|
|
QScrollBar::handle:vertical {{
|
|
background-color: {c['border_default']};
|
|
border-radius: 5px;
|
|
min-height: 30px;
|
|
}}
|
|
QScrollBar::handle:vertical:hover {{
|
|
background-color: {c['border_hover']};
|
|
}}
|
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
|
height: 0px;
|
|
}}
|
|
QScrollBar:horizontal {{
|
|
background-color: transparent;
|
|
height: 10px;
|
|
border-radius: 5px;
|
|
margin: 2px;
|
|
}}
|
|
QScrollBar::handle:horizontal {{
|
|
background-color: {c['border_default']};
|
|
border-radius: 5px;
|
|
min-width: 30px;
|
|
}}
|
|
QScrollBar::handle:horizontal:hover {{
|
|
background-color: {c['border_hover']};
|
|
}}
|
|
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
|
|
width: 0px;
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_progress_bar_style() -> str:
|
|
"""Get progress bar stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QProgressBar {{
|
|
background-color: {c['progress_bg']};
|
|
border: none;
|
|
border-radius: 4px;
|
|
height: 8px;
|
|
text-align: center;
|
|
color: transparent;
|
|
}}
|
|
QProgressBar::chunk {{
|
|
background-color: qlineargradient(
|
|
x1: 0, y1: 0, x2: 1, y2: 0,
|
|
stop: 0 {c['accent_teal']},
|
|
stop: 1 {c['accent_blue']}
|
|
);
|
|
border-radius: 4px;
|
|
}}
|
|
QProgressBar::chunk[style_variant="orange"] {{
|
|
background-color: qlineargradient(
|
|
x1: 0, y1: 0, x2: 1, y2: 0,
|
|
stop: 0 {c['accent_orange']},
|
|
stop: 1 {c['accent_gold']}
|
|
);
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_tab_style() -> str:
|
|
"""Get tab widget stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QTabWidget::pane {{
|
|
background-color: {c['bg_secondary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
border-top-left-radius: 0;
|
|
top: -1px;
|
|
}}
|
|
QTabBar::tab {{
|
|
background-color: {c['bg_tertiary']};
|
|
color: {c['text_secondary']};
|
|
padding: 10px 20px;
|
|
border-top-left-radius: {EU_SIZES['radius_md']};
|
|
border-top-right-radius: {EU_SIZES['radius_md']};
|
|
border: 1px solid {c['border_default']};
|
|
border-bottom: none;
|
|
margin-right: 2px;
|
|
font-size: {EU_TYPOGRAPHY['size_sm']};
|
|
font-weight: {EU_TYPOGRAPHY['weight_medium']};
|
|
}}
|
|
QTabBar::tab:selected {{
|
|
background-color: {c['bg_secondary']};
|
|
color: {c['text_primary']};
|
|
border-bottom: 2px solid {c['accent_orange']};
|
|
}}
|
|
QTabBar::tab:hover:!selected {{
|
|
background-color: {c['bg_hover']};
|
|
color: {c['text_primary']};
|
|
}}
|
|
"""
|
|
|
|
|
|
def get_tooltip_style() -> str:
|
|
"""Get tooltip stylesheet."""
|
|
c = get_all_colors()
|
|
return f"""
|
|
QToolTip {{
|
|
background-color: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
padding: 8px 12px;
|
|
font-size: {EU_TYPOGRAPHY['size_sm']};
|
|
}}
|
|
"""
|
|
|
|
|
|
# ============================================================================
|
|
# ANIMATION UTILITIES
|
|
# ============================================================================
|
|
|
|
class AnimationHelper:
|
|
"""Helper class for common widget animations."""
|
|
|
|
@staticmethod
|
|
def fade_in(widget: QWidget, duration: int = 200) -> QPropertyAnimation:
|
|
"""Fade in a widget."""
|
|
effect = QGraphicsOpacityEffect(widget)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
animation = QPropertyAnimation(effect, b"opacity")
|
|
animation.setDuration(duration)
|
|
animation.setStartValue(0.0)
|
|
animation.setEndValue(1.0)
|
|
animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
return animation
|
|
|
|
@staticmethod
|
|
def fade_out(widget: QWidget, duration: int = 200) -> QPropertyAnimation:
|
|
"""Fade out a widget."""
|
|
effect = widget.graphicsEffect()
|
|
if not isinstance(effect, QGraphicsOpacityEffect):
|
|
effect = QGraphicsOpacityEffect(widget)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
animation = QPropertyAnimation(effect, b"opacity")
|
|
animation.setDuration(duration)
|
|
animation.setStartValue(1.0)
|
|
animation.setEndValue(0.0)
|
|
animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
return animation
|
|
|
|
@staticmethod
|
|
def slide_in(widget: QWidget, direction: str = "left", duration: int = 300) -> QPropertyAnimation:
|
|
"""Slide widget in from direction."""
|
|
animation = QPropertyAnimation(widget, b"pos")
|
|
animation.setDuration(duration)
|
|
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
|
|
current_pos = widget.pos()
|
|
|
|
if direction == "left":
|
|
animation.setStartValue(current_pos - widget.width())
|
|
elif direction == "right":
|
|
animation.setStartValue(current_pos + widget.width())
|
|
elif direction == "top":
|
|
animation.setStartValue(current_pos - widget.height())
|
|
elif direction == "bottom":
|
|
animation.setStartValue(current_pos + widget.height())
|
|
|
|
animation.setEndValue(current_pos)
|
|
return animation
|
|
|
|
@staticmethod
|
|
def pulse(widget: QWidget, duration: int = 1000) -> QSequentialAnimationGroup:
|
|
"""Create a pulsing animation."""
|
|
effect = QGraphicsOpacityEffect(widget)
|
|
widget.setGraphicsEffect(effect)
|
|
|
|
group = QSequentialAnimationGroup()
|
|
|
|
fade_out = QPropertyAnimation(effect, b"opacity")
|
|
fade_out.setDuration(duration // 2)
|
|
fade_out.setStartValue(1.0)
|
|
fade_out.setEndValue(0.5)
|
|
fade_out.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
fade_in = QPropertyAnimation(effect, b"opacity")
|
|
fade_in.setDuration(duration // 2)
|
|
fade_in.setStartValue(0.5)
|
|
fade_in.setEndValue(1.0)
|
|
fade_in.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
|
|
group.addAnimation(fade_out)
|
|
group.addAnimation(fade_in)
|
|
group.setLoopCount(-1) # Infinite
|
|
|
|
return group
|
|
|
|
|
|
# ============================================================================
|
|
# ACCESSIBILITY HELPERS
|
|
# ============================================================================
|
|
|
|
class AccessibilityHelper:
|
|
"""Accessibility helpers for better UX."""
|
|
|
|
FOCUS_RING_STYLE = """
|
|
QPushButton:focus, QLineEdit:focus, QComboBox:focus,
|
|
QTableWidget:focus, QTabWidget:focus, QTextEdit:focus {
|
|
outline: 2px solid #ff8c42;
|
|
outline-offset: 2px;
|
|
}
|
|
"""
|
|
|
|
@staticmethod
|
|
def set_accessible_name(widget: QWidget, name: str):
|
|
"""Set accessible name for screen readers."""
|
|
widget.setAccessibleName(name)
|
|
|
|
@staticmethod
|
|
def set_accessible_description(widget: QWidget, description: str):
|
|
"""Set accessible description for screen readers."""
|
|
widget.setAccessibleDescription(description)
|
|
|
|
@staticmethod
|
|
def make_high_contrast():
|
|
"""Enable high contrast mode."""
|
|
# Modify colors for high contrast
|
|
EU_DARK_COLORS['text_primary'] = '#ffffff'
|
|
EU_DARK_COLORS['text_secondary'] = '#cccccc'
|
|
EU_DARK_COLORS['border_default'] = '#666666'
|
|
EU_DARK_COLORS['border_focus'] = '#ffff00'
|
|
|
|
@staticmethod
|
|
def get_keyboard_shortcut_hint(shortcut: str) -> str:
|
|
"""Format keyboard shortcut for display."""
|
|
return f"({shortcut})"
|
|
|
|
|
|
# ============================================================================
|
|
# RESPONSIVE HELPERS
|
|
# ============================================================================
|
|
|
|
class ResponsiveHelper:
|
|
"""Helpers for responsive layouts."""
|
|
|
|
BREAKPOINTS = {
|
|
'sm': 640,
|
|
'md': 768,
|
|
'lg': 1024,
|
|
'xl': 1280,
|
|
'2xl': 1536,
|
|
}
|
|
|
|
@staticmethod
|
|
def get_breakpoint(width: int) -> str:
|
|
"""Get current breakpoint name based on width."""
|
|
for name, size in sorted(ResponsiveHelper.BREAKPOINTS.items(), key=lambda x: x[1]):
|
|
if width < size:
|
|
return name
|
|
return '2xl'
|
|
|
|
@staticmethod
|
|
def should_show_sidebar(width: int) -> bool:
|
|
"""Check if sidebar should be visible at this width."""
|
|
return width >= ResponsiveHelper.BREAKPOINTS['md']
|
|
|
|
@staticmethod
|
|
def get_content_margins(width: int) -> tuple:
|
|
"""Get appropriate content margins for screen width."""
|
|
if width < ResponsiveHelper.BREAKPOINTS['sm']:
|
|
return (8, 8, 8, 8)
|
|
elif width < ResponsiveHelper.BREAKPOINTS['lg']:
|
|
return (16, 16, 16, 16)
|
|
else:
|
|
return (24, 24, 24, 24)
|
|
|
|
|
|
# ============================================================================
|
|
# GLOBAL STYLESHEET
|
|
# ============================================================================
|
|
|
|
def get_global_stylesheet() -> str:
|
|
"""Get complete global stylesheet."""
|
|
c = get_all_colors()
|
|
|
|
return f"""
|
|
/* Base */
|
|
QWidget {{
|
|
font-family: {EU_TYPOGRAPHY['font_family']};
|
|
font-size: {EU_TYPOGRAPHY['size_base']};
|
|
color: {c['text_primary']};
|
|
}}
|
|
|
|
/* Main Window */
|
|
QMainWindow {{
|
|
background-color: {c['bg_primary']};
|
|
}}
|
|
|
|
/* Selection */
|
|
::selection {{
|
|
background-color: {c['accent_orange']};
|
|
color: #ffffff;
|
|
}}
|
|
|
|
/* Scrollbars */
|
|
{get_scrollbar_style()}
|
|
|
|
/* Tooltips */
|
|
{get_tooltip_style()}
|
|
|
|
/* Focus indicators for accessibility */
|
|
{AccessibilityHelper.FOCUS_RING_STYLE}
|
|
|
|
/* Menu */
|
|
QMenu {{
|
|
background-color: {c['bg_elevated']};
|
|
color: {c['text_primary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_md']};
|
|
padding: 8px;
|
|
}}
|
|
QMenu::item {{
|
|
padding: 8px 16px;
|
|
border-radius: {EU_SIZES['radius_sm']};
|
|
}}
|
|
QMenu::item:selected {{
|
|
background-color: {c['bg_hover']};
|
|
}}
|
|
QMenu::separator {{
|
|
height: 1px;
|
|
background-color: {c['border_default']};
|
|
margin: 6px 0;
|
|
}}
|
|
|
|
/* Group Box */
|
|
QGroupBox {{
|
|
background-color: {c['bg_secondary']};
|
|
border: 1px solid {c['border_default']};
|
|
border-radius: {EU_SIZES['radius_lg']};
|
|
margin-top: 12px;
|
|
padding-top: 16px;
|
|
font-weight: {EU_TYPOGRAPHY['weight_semibold']};
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 16px;
|
|
padding: 0 8px;
|
|
color: {c['text_secondary']};
|
|
}}
|
|
|
|
/* Check Box & Radio Button */
|
|
QCheckBox, QRadioButton {{
|
|
spacing: 8px;
|
|
}}
|
|
QCheckBox::indicator, QRadioButton::indicator {{
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 2px solid {c['border_default']};
|
|
border-radius: 4px;
|
|
}}
|
|
QCheckBox::indicator:checked {{
|
|
background-color: {c['accent_orange']};
|
|
border-color: {c['accent_orange']};
|
|
}}
|
|
QRadioButton::indicator {{
|
|
border-radius: 9px;
|
|
}}
|
|
QRadioButton::indicator:checked {{
|
|
background-color: {c['accent_orange']};
|
|
border-color: {c['accent_orange']};
|
|
}}
|
|
|
|
/* Slider */
|
|
QSlider::groove:horizontal {{
|
|
height: 4px;
|
|
background-color: {c['bg_tertiary']};
|
|
border-radius: 2px;
|
|
}}
|
|
QSlider::handle:horizontal {{
|
|
width: 16px;
|
|
height: 16px;
|
|
background-color: {c['accent_orange']};
|
|
border-radius: 8px;
|
|
margin: -6px 0;
|
|
}}
|
|
QSlider::sub-page:horizontal {{
|
|
background-color: {c['accent_orange']};
|
|
border-radius: 2px;
|
|
}}
|
|
"""
|
|
|
|
|
|
# ============================================================================
|
|
# LEGACY COMPATIBILITY
|
|
# ============================================================================
|
|
|
|
# Keep old constants for backward compatibility
|
|
EU_COLORS = EU_DARK_COLORS
|
|
EU_RADIUS = {
|
|
'small': EU_SIZES['radius_sm'],
|
|
'medium': EU_SIZES['radius_md'],
|
|
'large': EU_SIZES['radius_lg'],
|
|
'button': EU_SIZES['radius_sm'],
|
|
}
|
|
EU_FONT = EU_TYPOGRAPHY['font_family']
|
|
|
|
# Legacy style getters
|
|
def get_eu_style(style_name: str) -> str:
|
|
"""Legacy style getter - maps to new system."""
|
|
style_map = {
|
|
'overlay_container': get_panel_style(),
|
|
'panel': get_panel_style(),
|
|
'header': get_card_style(),
|
|
'button_primary': get_button_style('primary'),
|
|
'button_secondary': get_button_style('secondary'),
|
|
'input': get_input_style(),
|
|
'table': get_table_style(),
|
|
'progress_bar': get_progress_bar_style(),
|
|
'tab': get_tab_style(),
|
|
'floating_icon': get_card_style(),
|
|
'floating_icon_hover': get_card_style(),
|
|
}
|
|
return style_map.get(style_name, "")
|