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