""" 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, "")