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

1""" 

2PyQt6 Color Scheme for OpenHCS GUI 

3 

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

8 

9import logging 

10from dataclasses import dataclass 

11from typing import Tuple, Dict, Optional 

12from pathlib import Path 

13from PyQt6.QtGui import QColor 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18@dataclass 

19class PyQt6ColorScheme: 

20 """ 

21 Comprehensive color scheme for OpenHCS PyQt6 GUI with semantic color names. 

22  

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. 

26  

27 All colors meet minimum 4.5:1 contrast ratio for normal text readability. 

28 """ 

29 

30 # ========== BASE UI ARCHITECTURE COLORS ========== 

31 

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 

36 

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 

41 

42 # ========== TEXT HIERARCHY COLORS ========== 

43 

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 

49 

50 # ========== INTERACTIVE ELEMENT COLORS ========== 

51 

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 

59 

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 

65 

66 # ========== SELECTION AND HIGHLIGHTING COLORS ========== 

67 

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 

73 

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 

77 

78 # ========== STATUS COMMUNICATION COLORS ========== 

79 

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 

85 

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 

90 

91 # ========== LOG HIGHLIGHTING COLORS (LogColorScheme compatibility) ========== 

92 

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 

100 

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 

106 

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 

117 

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 

122 

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) 

137 

138 def to_qcolor(self, color_tuple: Tuple[int, int, int]) -> QColor: 

139 """ 

140 Convert RGB tuple to QColor object. 

141  

142 Args: 

143 color_tuple: RGB color tuple (r, g, b) 

144  

145 Returns: 

146 QColor: Qt color object 

147 """ 

148 return QColor(*color_tuple) 

149 

150 def to_qcolor_rgba(self, color_tuple: Tuple[int, int, int, int]) -> QColor: 

151 """ 

152 Convert RGBA tuple to QColor object. 

153  

154 Args: 

155 color_tuple: RGBA color tuple (r, g, b, a) 

156  

157 Returns: 

158 QColor: Qt color object with alpha 

159 """ 

160 return QColor(*color_tuple) 

161 

162 def to_hex(self, color_tuple: Tuple[int, int, int]) -> str: 

163 """ 

164 Convert RGB tuple to hex color string. 

165 

166 Args: 

167 color_tuple: RGB color tuple (r, g, b) 

168 

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

174 

175 @classmethod 

176 def create_dark_theme(cls) -> 'PyQt6ColorScheme': 

177 """ 

178 Create a dark theme variant with adjusted colors for dark backgrounds. 

179 

180 This is the default theme, so most colors remain the same with minor 

181 adjustments for better contrast on dark backgrounds. 

182 

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 ) 

198 

199 @classmethod 

200 def create_light_theme(cls) -> 'PyQt6ColorScheme': 

201 """ 

202 Create a light theme variant with adjusted colors for light backgrounds. 

203 

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. 

206 

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 

218 

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 

224 

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 

232 

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 

238 

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 

244 

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 

248 

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 

254 

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 

259 

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 ) 

281 

282 @classmethod 

283 def load_color_scheme_from_config(cls, config_path: str = None) -> 'PyQt6ColorScheme': 

284 """ 

285 Load color scheme from external configuration file. 

286 

287 Args: 

288 config_path: Path to JSON config file (optional) 

289 

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) 

298 

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) 

306 

307 return cls(**scheme_kwargs) 

308 

309 except Exception as e: 

310 logger.warning(f"Failed to load color scheme from {config_path}: {e}") 

311 

312 return cls() # Return default scheme 

313 

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. 

319 

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) 

324 

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] 

331 

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 

335 

336 r, g, b = map(gamma_correct, [r, g, b]) 

337 return 0.2126 * r + 0.7152 * g + 0.0722 * b 

338 

339 # Calculate contrast ratio 

340 l1 = relative_luminance(foreground) 

341 l2 = relative_luminance(background) 

342 

343 # Ensure l1 is the lighter color 

344 if l1 < l2: 

345 l1, l2 = l2, l1 

346 

347 contrast_ratio = (l1 + 0.05) / (l2 + 0.05) 

348 return contrast_ratio >= min_ratio 

349 

350 def get_color_dict(self) -> Dict[str, Tuple[int, int, int]]: 

351 """ 

352 Get all colors as a dictionary for serialization or inspection. 

353 

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 

363 

364 def save_to_json(self, config_path: str) -> bool: 

365 """ 

366 Save color scheme to JSON configuration file. 

367 

368 Args: 

369 config_path: Path to save JSON config file 

370 

371 Returns: 

372 bool: True if save successful, False otherwise 

373 """ 

374 try: 

375 import json 

376 color_dict = self.get_color_dict() 

377 

378 # Convert tuples to lists for JSON serialization 

379 json_dict = {k: list(v) for k, v in color_dict.items()} 

380 

381 with open(config_path, 'w') as f: 

382 json.dump(json_dict, f, indent=2, sort_keys=True) 

383 

384 logger.info(f"Color scheme saved to {config_path}") 

385 return True 

386 

387 except Exception as e: 

388 logger.error(f"Failed to save color scheme to {config_path}: {e}") 

389 return False