Source code for pyqt_reactive.theming.color_scheme

"""
PyQt6 Color Scheme for OpenHCS GUI

Comprehensive color scheme system extending the LogColorScheme pattern to cover
all GUI components. Provides centralized color management with theme support,
JSON configuration, and WCAG accessibility compliance.
"""

import logging
from dataclasses import dataclass
from typing import Tuple, Dict
from pathlib import Path
from PyQt6.QtGui import QColor

logger = logging.getLogger(__name__)


[docs] @dataclass class ColorScheme: """ Comprehensive color scheme for OpenHCS PyQt6 GUI with semantic color names. Extends the LogColorScheme pattern to cover all GUI components including windows, dialogs, widgets, and interactive elements. Supports light/dark theme variants and ensures WCAG accessibility compliance. All colors meet minimum 4.5:1 contrast ratio for normal text readability. """ # ========== BASE UI ARCHITECTURE COLORS ========== # Window and Panel Backgrounds window_bg: Tuple[int, int, int] = (43, 43, 43) # #2b2b2b - Main window/dialog backgrounds panel_bg: Tuple[int, int, int] = (30, 30, 30) # #1e1e1e - Panel/widget backgrounds frame_bg: Tuple[int, int, int] = (43, 43, 43) # #2b2b2b - Frame backgrounds # Borders and Separators border_color: Tuple[int, int, int] = (85, 85, 85) # #555555 - Primary borders border_light: Tuple[int, int, int] = (102, 102, 102) # #666666 - Secondary borders separator_color: Tuple[int, int, int] = (51, 51, 51) # #333333 - Separators/dividers # ========== TEXT HIERARCHY COLORS ========== # Text Colors text_primary: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Primary text text_secondary: Tuple[int, int, int] = (204, 204, 204) # #cccccc - Secondary text/labels text_accent: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Accent text/titles text_disabled: Tuple[int, int, int] = (102, 102, 102) # #666666 - Disabled text # ========== INTERACTIVE ELEMENT COLORS ========== # Button States button_normal_bg: Tuple[int, int, int] = (64, 64, 64) # #404040 - Normal button background button_hover_bg: Tuple[int, int, int] = (80, 80, 80) # #505050 - Button hover state button_pressed_bg: Tuple[int, int, int] = (48, 48, 48) # #303030 - Button pressed state button_disabled_bg: Tuple[int, int, int] = (42, 42, 42) # #2a2a2a - Disabled button background button_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Button text button_disabled_text: Tuple[int, int, int] = (102, 102, 102) # #666666 - Disabled button text # Input Fields input_bg: Tuple[int, int, int] = (64, 64, 64) # #404040 - Input field background input_border: Tuple[int, int, int] = (102, 102, 102) # #666666 - Input field border input_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Input field text input_focus_border: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Focused input border # ========== SELECTION AND HIGHLIGHTING COLORS ========== # Selection States selection_bg: Tuple[int, int, int] = (0, 120, 212) # #0078d4 - Primary selection background selection_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Selected text hover_bg: Tuple[int, int, int] = (51, 51, 51) # #333333 - Hover background focus_outline: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Focus outline # Search and Highlighting search_highlight_bg: Tuple[int, int, int, int] = (255, 255, 0, 100) # Yellow with transparency search_highlight_text: Tuple[int, int, int] = (0, 0, 0) # #000000 - Search highlight text # ========== STATUS COMMUNICATION COLORS ========== # Status Indicators status_success: Tuple[int, int, int] = (0, 255, 0) # #00ff00 - Success/ready states status_warning: Tuple[int, int, int] = (255, 170, 0) # #ffaa00 - Warning messages status_error: Tuple[int, int, int] = (255, 0, 0) # #ff0000 - Error states status_info: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Information/accent # Progress and Activity progress_bg: Tuple[int, int, int] = (30, 30, 30) # #1e1e1e - Progress bar background progress_fill: Tuple[int, int, int] = (0, 120, 212) # #0078d4 - Progress bar fill activity_indicator: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Activity indicators # ========== LOG HIGHLIGHTING COLORS (LogColorScheme compatibility) ========== # Log level colors with semantic meaning (WCAG 4.5:1 compliant) log_critical_fg: Tuple[int, int, int] = (255, 255, 255) # White text log_critical_bg: Tuple[int, int, int] = (139, 0, 0) # Dark red background log_error_color: Tuple[int, int, int] = (255, 85, 85) # Brighter red - WCAG compliant log_warning_color: Tuple[int, int, int] = (255, 140, 0) # Dark orange - attention grabbing log_info_color: Tuple[int, int, int] = (100, 160, 210) # Brighter steel blue - WCAG compliant log_debug_color: Tuple[int, int, int] = (160, 160, 160) # Lighter gray - better contrast # Metadata and structural colors timestamp_color: Tuple[int, int, int] = (105, 105, 105) # Dim gray - unobtrusive logger_name_color: Tuple[int, int, int] = (147, 112, 219) # Medium slate blue - distinctive memory_address_color: Tuple[int, int, int] = (255, 182, 193) # Light pink - technical data file_path_color: Tuple[int, int, int] = (34, 139, 34) # Forest green - file system # Python syntax colors (following VS Code dark theme conventions) python_keyword_color: Tuple[int, int, int] = (86, 156, 214) # Blue - language keywords python_string_color: Tuple[int, int, int] = (206, 145, 120) # Orange - string literals python_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - numeric values python_operator_color: Tuple[int, int, int] = (212, 212, 212) # Light gray - operators/punctuation python_name_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - identifiers python_function_color: Tuple[int, int, int] = (220, 220, 170) # Yellow - function names python_class_color: Tuple[int, int, int] = (78, 201, 176) # Teal - class names python_builtin_color: Tuple[int, int, int] = (86, 156, 214) # Blue - built-in functions python_comment_color: Tuple[int, int, int] = (106, 153, 85) # Green - comments # Special highlighting colors exception_color: Tuple[int, int, int] = (255, 69, 0) # Red orange - error types function_call_color: Tuple[int, int, int] = (255, 215, 0) # Gold - function invocations boolean_color: Tuple[int, int, int] = (86, 156, 214) # Blue - True/False/None # Enhanced syntax colors tuple_parentheses_color: Tuple[int, int, int] = (255, 215, 0) # Gold - tuple delimiters set_braces_color: Tuple[int, int, int] = (255, 140, 0) # Dark orange - set delimiters class_representation_color: Tuple[int, int, int] = (78, 201, 176) # Teal - <class 'name'> function_representation_color: Tuple[int, int, int] = (220, 220, 170) # Yellow - <function name> module_path_color: Tuple[int, int, int] = (147, 112, 219) # Medium slate blue - module.path hex_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0xFF scientific_notation_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 1.23e-4 binary_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0b1010 octal_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0o755 python_special_color: Tuple[int, int, int] = (255, 20, 147) # Deep pink - __name__ single_quoted_string_color: Tuple[int, int, int] = (206, 145, 120) # Orange - 'string' list_comprehension_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - [x for x in y] generator_expression_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - (x for x in y)
[docs] def to_qcolor(self, color_tuple: Tuple[int, int, int]) -> QColor: """ Convert RGB tuple to QColor object. Args: color_tuple: RGB color tuple (r, g, b) Returns: QColor: Qt color object """ return QColor(*color_tuple)
[docs] def to_qcolor_rgba(self, color_tuple: Tuple[int, int, int, int]) -> QColor: """ Convert RGBA tuple to QColor object. Args: color_tuple: RGBA color tuple (r, g, b, a) Returns: QColor: Qt color object with alpha """ return QColor(*color_tuple)
[docs] def to_hex(self, color_tuple: Tuple[int, int, int]) -> str: """ Convert RGB tuple to hex color string. Args: color_tuple: RGB color tuple (r, g, b) Returns: str: Hex color string (e.g., "#ff0000") """ r, g, b = color_tuple return f"#{r:02x}{g:02x}{b:02x}"
[docs] @classmethod def create_dark_theme(cls) -> 'PyQt6ColorScheme': """ Create a dark theme variant with adjusted colors for dark backgrounds. This is the default theme, so most colors remain the same with minor adjustments for better contrast on dark backgrounds. Returns: PyQt6ColorScheme: Dark theme color scheme with enhanced contrast """ return cls( # Enhanced colors for dark backgrounds with better contrast log_error_color=(255, 100, 100), # Brighter red log_info_color=(120, 180, 230), # Brighter steel blue timestamp_color=(160, 160, 160), # Lighter gray python_string_color=(236, 175, 150), # Brighter orange python_number_color=(200, 230, 190), # Brighter green # UI colors optimized for dark theme text_secondary=(220, 220, 220), # Slightly brighter secondary text status_success=(0, 255, 100), # Slightly brighter green # Other colors remain the same as they work well on dark backgrounds )
[docs] @classmethod def create_light_theme(cls) -> 'PyQt6ColorScheme': """ Create a light theme variant with adjusted colors for light backgrounds. All colors are adjusted to maintain WCAG 4.5:1 contrast ratio on light backgrounds while preserving the semantic meaning and visual hierarchy. Returns: PyQt6ColorScheme: Light theme color scheme with appropriate contrast """ return cls( # Base UI colors for light theme window_bg=(245, 245, 245), # Light gray background panel_bg=(255, 255, 255), # White panel background frame_bg=(240, 240, 240), # Light frame background border_color=(180, 180, 180), # Medium gray borders border_light=(160, 160, 160), # Lighter borders separator_color=(200, 200, 200), # Light separators # Text colors for light theme text_primary=(0, 0, 0), # Black primary text text_secondary=(80, 80, 80), # Dark gray secondary text text_accent=(0, 100, 200), # Darker blue accent text_disabled=(160, 160, 160), # Light gray disabled text # Interactive elements for light theme button_normal_bg=(230, 230, 230), # Light button background button_hover_bg=(210, 210, 210), # Button hover state button_pressed_bg=(190, 190, 190), # Button pressed state button_disabled_bg=(250, 250, 250), # Disabled button background button_text=(0, 0, 0), # Black button text button_disabled_text=(160, 160, 160), # Light gray disabled text # Input fields for light theme input_bg=(255, 255, 255), # White input background input_border=(180, 180, 180), # Gray input border input_text=(0, 0, 0), # Black input text input_focus_border=(0, 100, 200), # Blue focus border # Selection and highlighting for light theme selection_bg=(0, 120, 215), # Blue selection background selection_text=(255, 255, 255), # White selected text hover_bg=(240, 240, 240), # Light hover background focus_outline=(0, 100, 200), # Blue focus outline # Search highlighting for light theme search_highlight_bg=(255, 255, 0, 150), # Yellow with transparency search_highlight_text=(0, 0, 0), # Black search text # Status colors for light theme (darker for contrast) status_success=(0, 150, 0), # Darker green status_warning=(200, 100, 0), # Darker orange status_error=(200, 0, 0), # Darker red status_info=(0, 100, 200), # Darker blue # Progress colors for light theme progress_bg=(240, 240, 240), # Light progress background progress_fill=(0, 120, 215), # Blue progress fill activity_indicator=(0, 100, 200), # Blue activity indicator # Log colors for light theme (darker for contrast) log_error_color=(180, 20, 40), # Darker red log_info_color=(30, 80, 130), # Darker steel blue log_warning_color=(200, 100, 0), # Darker orange timestamp_color=(60, 60, 60), # Darker gray logger_name_color=(100, 60, 160), # Darker slate blue python_string_color=(150, 80, 60), # Darker orange python_number_color=(120, 140, 100), # Darker green memory_address_color=(200, 120, 140), # Darker pink file_path_color=(20, 100, 20), # Darker forest green exception_color=(200, 40, 0), # Darker red orange # Adjust other syntax colors for light background contrast python_keyword_color=(0, 0, 150), # Darker blue python_operator_color=(80, 80, 80), # Dark gray python_name_color=(0, 80, 150), # Darker blue python_function_color=(150, 100, 0), # Darker yellow python_class_color=(0, 120, 100), # Darker teal python_builtin_color=(0, 0, 150), # Darker blue python_comment_color=(80, 120, 60), # Darker green boolean_color=(0, 0, 150), # Darker blue )
[docs] @classmethod def load_color_scheme_from_config(cls, config_path: str = None) -> 'PyQt6ColorScheme': """ Load color scheme from external configuration file. Args: config_path: Path to JSON config file (optional) Returns: PyQt6ColorScheme: Loaded color scheme or default if file not found """ if config_path and Path(config_path).exists(): try: import json with open(config_path, 'r') as f: config = json.load(f) # Create color scheme from config scheme_kwargs = {} for key, value in config.items(): if key.endswith('_color') or key.endswith('_fg') or key.endswith('_bg') or key.endswith('_text'): if isinstance(value, list) and len(value) >= 3: # Handle both RGB and RGBA tuples scheme_kwargs[key] = tuple(value) return cls(**scheme_kwargs) except Exception as e: logger.warning(f"Failed to load color scheme from {config_path}: {e}") return cls() # Return default scheme
[docs] def validate_wcag_contrast(self, foreground: Tuple[int, int, int], background: Tuple[int, int, int], min_ratio: float = 4.5) -> bool: """ Validate WCAG contrast ratio between foreground and background colors. Args: foreground: Foreground color RGB tuple background: Background color RGB tuple min_ratio: Minimum contrast ratio (default: 4.5 for normal text) Returns: bool: True if contrast ratio meets minimum requirement """ def relative_luminance(color: Tuple[int, int, int]) -> float: """Calculate relative luminance of a color.""" r, g, b = [c / 255.0 for c in color] # Apply gamma correction def gamma_correct(c): return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = map(gamma_correct, [r, g, b]) return 0.2126 * r + 0.7152 * g + 0.0722 * b # Calculate contrast ratio l1 = relative_luminance(foreground) l2 = relative_luminance(background) # Ensure l1 is the lighter color if l1 < l2: l1, l2 = l2, l1 contrast_ratio = (l1 + 0.05) / (l2 + 0.05) return contrast_ratio >= min_ratio
[docs] def get_color_dict(self) -> Dict[str, Tuple[int, int, int]]: """ Get all colors as a dictionary for serialization or inspection. Returns: Dict[str, Tuple[int, int, int]]: Dictionary of color name to RGB tuple """ color_dict = {} for field_name in self.__dataclass_fields__: color_value = getattr(self, field_name) if isinstance(color_value, tuple) and len(color_value) >= 3: color_dict[field_name] = color_value return color_dict
[docs] def save_to_json(self, config_path: str) -> bool: """ Save color scheme to JSON configuration file. Args: config_path: Path to save JSON config file Returns: bool: True if save successful, False otherwise """ try: import json color_dict = self.get_color_dict() # Convert tuples to lists for JSON serialization json_dict = {k: list(v) for k, v in color_dict.items()} with open(config_path, 'w') as f: json.dump(json_dict, f, indent=2, sort_keys=True) logger.info(f"Color scheme saved to {config_path}") return True except Exception as e: logger.error(f"Failed to save color scheme to {config_path}: {e}") return False