Coverage for openhcs/pyqt_gui/shared/color_scheme.py: 0.0%
137 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2PyQt6 Color Scheme for OpenHCS GUI
4Comprehensive color scheme system extending the LogColorScheme pattern to cover
5all GUI components. Provides centralized color management with theme support,
6JSON configuration, and WCAG accessibility compliance.
7"""
9import logging
10from dataclasses import dataclass
11from typing import Tuple, Dict, Optional
12from pathlib import Path
13from PyQt6.QtGui import QColor
15logger = logging.getLogger(__name__)
18@dataclass
19class PyQt6ColorScheme:
20 """
21 Comprehensive color scheme for OpenHCS PyQt6 GUI with semantic color names.
23 Extends the LogColorScheme pattern to cover all GUI components including
24 windows, dialogs, widgets, and interactive elements. Supports light/dark
25 theme variants and ensures WCAG accessibility compliance.
27 All colors meet minimum 4.5:1 contrast ratio for normal text readability.
28 """
30 # ========== BASE UI ARCHITECTURE COLORS ==========
32 # Window and Panel Backgrounds
33 window_bg: Tuple[int, int, int] = (43, 43, 43) # #2b2b2b - Main window/dialog backgrounds
34 panel_bg: Tuple[int, int, int] = (30, 30, 30) # #1e1e1e - Panel/widget backgrounds
35 frame_bg: Tuple[int, int, int] = (43, 43, 43) # #2b2b2b - Frame backgrounds
37 # Borders and Separators
38 border_color: Tuple[int, int, int] = (85, 85, 85) # #555555 - Primary borders
39 border_light: Tuple[int, int, int] = (102, 102, 102) # #666666 - Secondary borders
40 separator_color: Tuple[int, int, int] = (51, 51, 51) # #333333 - Separators/dividers
42 # ========== TEXT HIERARCHY COLORS ==========
44 # Text Colors
45 text_primary: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Primary text
46 text_secondary: Tuple[int, int, int] = (204, 204, 204) # #cccccc - Secondary text/labels
47 text_accent: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Accent text/titles
48 text_disabled: Tuple[int, int, int] = (102, 102, 102) # #666666 - Disabled text
50 # ========== INTERACTIVE ELEMENT COLORS ==========
52 # Button States
53 button_normal_bg: Tuple[int, int, int] = (64, 64, 64) # #404040 - Normal button background
54 button_hover_bg: Tuple[int, int, int] = (80, 80, 80) # #505050 - Button hover state
55 button_pressed_bg: Tuple[int, int, int] = (48, 48, 48) # #303030 - Button pressed state
56 button_disabled_bg: Tuple[int, int, int] = (42, 42, 42) # #2a2a2a - Disabled button background
57 button_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Button text
58 button_disabled_text: Tuple[int, int, int] = (102, 102, 102) # #666666 - Disabled button text
60 # Input Fields
61 input_bg: Tuple[int, int, int] = (64, 64, 64) # #404040 - Input field background
62 input_border: Tuple[int, int, int] = (102, 102, 102) # #666666 - Input field border
63 input_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Input field text
64 input_focus_border: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Focused input border
66 # ========== SELECTION AND HIGHLIGHTING COLORS ==========
68 # Selection States
69 selection_bg: Tuple[int, int, int] = (0, 120, 212) # #0078d4 - Primary selection background
70 selection_text: Tuple[int, int, int] = (255, 255, 255) # #ffffff - Selected text
71 hover_bg: Tuple[int, int, int] = (51, 51, 51) # #333333 - Hover background
72 focus_outline: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Focus outline
74 # Search and Highlighting
75 search_highlight_bg: Tuple[int, int, int, int] = (255, 255, 0, 100) # Yellow with transparency
76 search_highlight_text: Tuple[int, int, int] = (0, 0, 0) # #000000 - Search highlight text
78 # ========== STATUS COMMUNICATION COLORS ==========
80 # Status Indicators
81 status_success: Tuple[int, int, int] = (0, 255, 0) # #00ff00 - Success/ready states
82 status_warning: Tuple[int, int, int] = (255, 170, 0) # #ffaa00 - Warning messages
83 status_error: Tuple[int, int, int] = (255, 0, 0) # #ff0000 - Error states
84 status_info: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Information/accent
86 # Progress and Activity
87 progress_bg: Tuple[int, int, int] = (30, 30, 30) # #1e1e1e - Progress bar background
88 progress_fill: Tuple[int, int, int] = (0, 120, 212) # #0078d4 - Progress bar fill
89 activity_indicator: Tuple[int, int, int] = (0, 170, 255) # #00aaff - Activity indicators
91 # ========== LOG HIGHLIGHTING COLORS (LogColorScheme compatibility) ==========
93 # Log level colors with semantic meaning (WCAG 4.5:1 compliant)
94 log_critical_fg: Tuple[int, int, int] = (255, 255, 255) # White text
95 log_critical_bg: Tuple[int, int, int] = (139, 0, 0) # Dark red background
96 log_error_color: Tuple[int, int, int] = (255, 85, 85) # Brighter red - WCAG compliant
97 log_warning_color: Tuple[int, int, int] = (255, 140, 0) # Dark orange - attention grabbing
98 log_info_color: Tuple[int, int, int] = (100, 160, 210) # Brighter steel blue - WCAG compliant
99 log_debug_color: Tuple[int, int, int] = (160, 160, 160) # Lighter gray - better contrast
101 # Metadata and structural colors
102 timestamp_color: Tuple[int, int, int] = (105, 105, 105) # Dim gray - unobtrusive
103 logger_name_color: Tuple[int, int, int] = (147, 112, 219) # Medium slate blue - distinctive
104 memory_address_color: Tuple[int, int, int] = (255, 182, 193) # Light pink - technical data
105 file_path_color: Tuple[int, int, int] = (34, 139, 34) # Forest green - file system
107 # Python syntax colors (following VS Code dark theme conventions)
108 python_keyword_color: Tuple[int, int, int] = (86, 156, 214) # Blue - language keywords
109 python_string_color: Tuple[int, int, int] = (206, 145, 120) # Orange - string literals
110 python_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - numeric values
111 python_operator_color: Tuple[int, int, int] = (212, 212, 212) # Light gray - operators/punctuation
112 python_name_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - identifiers
113 python_function_color: Tuple[int, int, int] = (220, 220, 170) # Yellow - function names
114 python_class_color: Tuple[int, int, int] = (78, 201, 176) # Teal - class names
115 python_builtin_color: Tuple[int, int, int] = (86, 156, 214) # Blue - built-in functions
116 python_comment_color: Tuple[int, int, int] = (106, 153, 85) # Green - comments
118 # Special highlighting colors
119 exception_color: Tuple[int, int, int] = (255, 69, 0) # Red orange - error types
120 function_call_color: Tuple[int, int, int] = (255, 215, 0) # Gold - function invocations
121 boolean_color: Tuple[int, int, int] = (86, 156, 214) # Blue - True/False/None
123 # Enhanced syntax colors
124 tuple_parentheses_color: Tuple[int, int, int] = (255, 215, 0) # Gold - tuple delimiters
125 set_braces_color: Tuple[int, int, int] = (255, 140, 0) # Dark orange - set delimiters
126 class_representation_color: Tuple[int, int, int] = (78, 201, 176) # Teal - <class 'name'>
127 function_representation_color: Tuple[int, int, int] = (220, 220, 170) # Yellow - <function name>
128 module_path_color: Tuple[int, int, int] = (147, 112, 219) # Medium slate blue - module.path
129 hex_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0xFF
130 scientific_notation_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 1.23e-4
131 binary_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0b1010
132 octal_number_color: Tuple[int, int, int] = (181, 206, 168) # Light green - 0o755
133 python_special_color: Tuple[int, int, int] = (255, 20, 147) # Deep pink - __name__
134 single_quoted_string_color: Tuple[int, int, int] = (206, 145, 120) # Orange - 'string'
135 list_comprehension_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - [x for x in y]
136 generator_expression_color: Tuple[int, int, int] = (156, 220, 254) # Light blue - (x for x in y)
138 def to_qcolor(self, color_tuple: Tuple[int, int, int]) -> QColor:
139 """
140 Convert RGB tuple to QColor object.
142 Args:
143 color_tuple: RGB color tuple (r, g, b)
145 Returns:
146 QColor: Qt color object
147 """
148 return QColor(*color_tuple)
150 def to_qcolor_rgba(self, color_tuple: Tuple[int, int, int, int]) -> QColor:
151 """
152 Convert RGBA tuple to QColor object.
154 Args:
155 color_tuple: RGBA color tuple (r, g, b, a)
157 Returns:
158 QColor: Qt color object with alpha
159 """
160 return QColor(*color_tuple)
162 def to_hex(self, color_tuple: Tuple[int, int, int]) -> str:
163 """
164 Convert RGB tuple to hex color string.
166 Args:
167 color_tuple: RGB color tuple (r, g, b)
169 Returns:
170 str: Hex color string (e.g., "#ff0000")
171 """
172 r, g, b = color_tuple
173 return f"#{r:02x}{g:02x}{b:02x}"
175 @classmethod
176 def create_dark_theme(cls) -> 'PyQt6ColorScheme':
177 """
178 Create a dark theme variant with adjusted colors for dark backgrounds.
180 This is the default theme, so most colors remain the same with minor
181 adjustments for better contrast on dark backgrounds.
183 Returns:
184 PyQt6ColorScheme: Dark theme color scheme with enhanced contrast
185 """
186 return cls(
187 # Enhanced colors for dark backgrounds with better contrast
188 log_error_color=(255, 100, 100), # Brighter red
189 log_info_color=(120, 180, 230), # Brighter steel blue
190 timestamp_color=(160, 160, 160), # Lighter gray
191 python_string_color=(236, 175, 150), # Brighter orange
192 python_number_color=(200, 230, 190), # Brighter green
193 # UI colors optimized for dark theme
194 text_secondary=(220, 220, 220), # Slightly brighter secondary text
195 status_success=(0, 255, 100), # Slightly brighter green
196 # Other colors remain the same as they work well on dark backgrounds
197 )
199 @classmethod
200 def create_light_theme(cls) -> 'PyQt6ColorScheme':
201 """
202 Create a light theme variant with adjusted colors for light backgrounds.
204 All colors are adjusted to maintain WCAG 4.5:1 contrast ratio on light
205 backgrounds while preserving the semantic meaning and visual hierarchy.
207 Returns:
208 PyQt6ColorScheme: Light theme color scheme with appropriate contrast
209 """
210 return cls(
211 # Base UI colors for light theme
212 window_bg=(245, 245, 245), # Light gray background
213 panel_bg=(255, 255, 255), # White panel background
214 frame_bg=(240, 240, 240), # Light frame background
215 border_color=(180, 180, 180), # Medium gray borders
216 border_light=(160, 160, 160), # Lighter borders
217 separator_color=(200, 200, 200), # Light separators
219 # Text colors for light theme
220 text_primary=(0, 0, 0), # Black primary text
221 text_secondary=(80, 80, 80), # Dark gray secondary text
222 text_accent=(0, 100, 200), # Darker blue accent
223 text_disabled=(160, 160, 160), # Light gray disabled text
225 # Interactive elements for light theme
226 button_normal_bg=(230, 230, 230), # Light button background
227 button_hover_bg=(210, 210, 210), # Button hover state
228 button_pressed_bg=(190, 190, 190), # Button pressed state
229 button_disabled_bg=(250, 250, 250), # Disabled button background
230 button_text=(0, 0, 0), # Black button text
231 button_disabled_text=(160, 160, 160), # Light gray disabled text
233 # Input fields for light theme
234 input_bg=(255, 255, 255), # White input background
235 input_border=(180, 180, 180), # Gray input border
236 input_text=(0, 0, 0), # Black input text
237 input_focus_border=(0, 100, 200), # Blue focus border
239 # Selection and highlighting for light theme
240 selection_bg=(0, 120, 215), # Blue selection background
241 selection_text=(255, 255, 255), # White selected text
242 hover_bg=(240, 240, 240), # Light hover background
243 focus_outline=(0, 100, 200), # Blue focus outline
245 # Search highlighting for light theme
246 search_highlight_bg=(255, 255, 0, 150), # Yellow with transparency
247 search_highlight_text=(0, 0, 0), # Black search text
249 # Status colors for light theme (darker for contrast)
250 status_success=(0, 150, 0), # Darker green
251 status_warning=(200, 100, 0), # Darker orange
252 status_error=(200, 0, 0), # Darker red
253 status_info=(0, 100, 200), # Darker blue
255 # Progress colors for light theme
256 progress_bg=(240, 240, 240), # Light progress background
257 progress_fill=(0, 120, 215), # Blue progress fill
258 activity_indicator=(0, 100, 200), # Blue activity indicator
260 # Log colors for light theme (darker for contrast)
261 log_error_color=(180, 20, 40), # Darker red
262 log_info_color=(30, 80, 130), # Darker steel blue
263 log_warning_color=(200, 100, 0), # Darker orange
264 timestamp_color=(60, 60, 60), # Darker gray
265 logger_name_color=(100, 60, 160), # Darker slate blue
266 python_string_color=(150, 80, 60), # Darker orange
267 python_number_color=(120, 140, 100), # Darker green
268 memory_address_color=(200, 120, 140), # Darker pink
269 file_path_color=(20, 100, 20), # Darker forest green
270 exception_color=(200, 40, 0), # Darker red orange
271 # Adjust other syntax colors for light background contrast
272 python_keyword_color=(0, 0, 150), # Darker blue
273 python_operator_color=(80, 80, 80), # Dark gray
274 python_name_color=(0, 80, 150), # Darker blue
275 python_function_color=(150, 100, 0), # Darker yellow
276 python_class_color=(0, 120, 100), # Darker teal
277 python_builtin_color=(0, 0, 150), # Darker blue
278 python_comment_color=(80, 120, 60), # Darker green
279 boolean_color=(0, 0, 150), # Darker blue
280 )
282 @classmethod
283 def load_color_scheme_from_config(cls, config_path: str = None) -> 'PyQt6ColorScheme':
284 """
285 Load color scheme from external configuration file.
287 Args:
288 config_path: Path to JSON config file (optional)
290 Returns:
291 PyQt6ColorScheme: Loaded color scheme or default if file not found
292 """
293 if config_path and Path(config_path).exists():
294 try:
295 import json
296 with open(config_path, 'r') as f:
297 config = json.load(f)
299 # Create color scheme from config
300 scheme_kwargs = {}
301 for key, value in config.items():
302 if key.endswith('_color') or key.endswith('_fg') or key.endswith('_bg') or key.endswith('_text'):
303 if isinstance(value, list) and len(value) >= 3:
304 # Handle both RGB and RGBA tuples
305 scheme_kwargs[key] = tuple(value)
307 return cls(**scheme_kwargs)
309 except Exception as e:
310 logger.warning(f"Failed to load color scheme from {config_path}: {e}")
312 return cls() # Return default scheme
314 def validate_wcag_contrast(self, foreground: Tuple[int, int, int],
315 background: Tuple[int, int, int],
316 min_ratio: float = 4.5) -> bool:
317 """
318 Validate WCAG contrast ratio between foreground and background colors.
320 Args:
321 foreground: Foreground color RGB tuple
322 background: Background color RGB tuple
323 min_ratio: Minimum contrast ratio (default: 4.5 for normal text)
325 Returns:
326 bool: True if contrast ratio meets minimum requirement
327 """
328 def relative_luminance(color: Tuple[int, int, int]) -> float:
329 """Calculate relative luminance of a color."""
330 r, g, b = [c / 255.0 for c in color]
332 # Apply gamma correction
333 def gamma_correct(c):
334 return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
336 r, g, b = map(gamma_correct, [r, g, b])
337 return 0.2126 * r + 0.7152 * g + 0.0722 * b
339 # Calculate contrast ratio
340 l1 = relative_luminance(foreground)
341 l2 = relative_luminance(background)
343 # Ensure l1 is the lighter color
344 if l1 < l2:
345 l1, l2 = l2, l1
347 contrast_ratio = (l1 + 0.05) / (l2 + 0.05)
348 return contrast_ratio >= min_ratio
350 def get_color_dict(self) -> Dict[str, Tuple[int, int, int]]:
351 """
352 Get all colors as a dictionary for serialization or inspection.
354 Returns:
355 Dict[str, Tuple[int, int, int]]: Dictionary of color name to RGB tuple
356 """
357 color_dict = {}
358 for field_name in self.__dataclass_fields__:
359 color_value = getattr(self, field_name)
360 if isinstance(color_value, tuple) and len(color_value) >= 3:
361 color_dict[field_name] = color_value
362 return color_dict
364 def save_to_json(self, config_path: str) -> bool:
365 """
366 Save color scheme to JSON configuration file.
368 Args:
369 config_path: Path to save JSON config file
371 Returns:
372 bool: True if save successful, False otherwise
373 """
374 try:
375 import json
376 color_dict = self.get_color_dict()
378 # Convert tuples to lists for JSON serialization
379 json_dict = {k: list(v) for k, v in color_dict.items()}
381 with open(config_path, 'w') as f:
382 json.dump(json_dict, f, indent=2, sort_keys=True)
384 logger.info(f"Color scheme saved to {config_path}")
385 return True
387 except Exception as e:
388 logger.error(f"Failed to save color scheme to {config_path}: {e}")
389 return False