Coverage for openhcs/pyqt_gui/widgets/shared/clickable_help_components.py: 0.0%

157 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +0000

1"""PyQt6 clickable help components - clean architecture without circular imports.""" 

2 

3import logging 

4from typing import Union, Callable, Optional 

5from PyQt6.QtWidgets import QLabel, QPushButton, QWidget, QHBoxLayout, QGroupBox, QVBoxLayout 

6from PyQt6.QtCore import Qt, pyqtSignal 

7from PyQt6.QtGui import QFont, QCursor 

8 

9from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

10 

11logger = logging.getLogger(__name__) 

12 

13 

14class ClickableHelpLabel(QLabel): 

15 """PyQt6 clickable label that shows help information - reuses Textual TUI help logic.""" 

16 

17 help_requested = pyqtSignal() 

18 

19 def __init__(self, text: str, help_target: Union[Callable, type] = None, 

20 param_name: str = None, param_description: str = None, 

21 param_type: type = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

22 """Initialize clickable help label. 

23 

24 Args: 

25 text: Display text for the label 

26 help_target: Function or class to show help for (for function help) 

27 param_name: Parameter name (for parameter help) 

28 param_description: Parameter description (for parameter help) 

29 param_type: Parameter type (for parameter help) 

30 color_scheme: Color scheme for styling (optional, uses default if None) 

31 """ 

32 # Add help indicator to text 

33 display_text = f"{text} (?)" 

34 super().__init__(display_text, parent) 

35 

36 # Initialize color scheme 

37 self.color_scheme = color_scheme or PyQt6ColorScheme() 

38 

39 self.help_target = help_target 

40 self.param_name = param_name 

41 self.param_description = param_description 

42 self.param_type = param_type 

43 

44 # Style as clickable 

45 self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) 

46 self.setStyleSheet(f""" 

47 QLabel {{ 

48 color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

49 text-decoration: underline; 

50 }} 

51 QLabel:hover {{ 

52 color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

53 }} 

54 """) 

55 

56 def mousePressEvent(self, event): 

57 """Handle mouse press to show help - reuses Textual TUI help manager pattern.""" 

58 if event.button() == Qt.MouseButton.LeftButton: 

59 try: 

60 # Import inside method to avoid circular imports (same pattern as Textual TUI) 

61 from openhcs.pyqt_gui.windows.help_windows import HelpWindowManager 

62 

63 if self.help_target: 

64 # Show function/class help using unified manager 

65 HelpWindowManager.show_docstring_help(self.help_target, parent=self) 

66 elif self.param_name: 

67 # Show parameter help using the description passed from parameter analysis 

68 HelpWindowManager.show_parameter_help( 

69 self.param_name, self.param_description or "No description available", self.param_type, parent=self 

70 ) 

71 

72 self.help_requested.emit() 

73 

74 except Exception as e: 

75 logger.error(f"Failed to show help: {e}") 

76 raise 

77 

78 super().mousePressEvent(event) 

79 

80 

81 

82 

83class ClickableFunctionTitle(ClickableHelpLabel): 

84 """PyQt6 clickable function title that shows function documentation - mirrors Textual TUI.""" 

85 

86 def __init__(self, func: Callable, index: int = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

87 func_name = getattr(func, '__name__', 'Unknown Function') 

88 module_name = getattr(func, '__module__', '').split('.')[-1] if func else '' 

89 

90 # Build title text 

91 title = f"{index + 1}: {func_name}" if index is not None else func_name 

92 if module_name: 

93 title += f" ({module_name})" 

94 

95 super().__init__( 

96 text=title, 

97 help_target=func, 

98 color_scheme=color_scheme, 

99 parent=parent 

100 ) 

101 

102 # Make title bold 

103 font = QFont() 

104 font.setBold(True) 

105 self.setFont(font) 

106 

107 

108class ClickableParameterLabel(ClickableHelpLabel): 

109 """PyQt6 clickable parameter label that shows parameter documentation - mirrors Textual TUI.""" 

110 

111 def __init__(self, param_name: str, param_description: str = None, 

112 param_type: type = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

113 # Format parameter name nicely 

114 display_name = param_name.replace('_', ' ').title() 

115 

116 super().__init__( 

117 text=display_name, 

118 param_name=param_name, 

119 param_description=param_description or "No description available", 

120 param_type=param_type, 

121 color_scheme=color_scheme, 

122 parent=parent 

123 ) 

124 

125 

126class HelpIndicator(QLabel): 

127 """PyQt6 simple help indicator that can be added next to any widget - mirrors Textual TUI.""" 

128 

129 help_requested = pyqtSignal() 

130 

131 def __init__(self, help_target: Union[Callable, type] = None, 

132 param_name: str = None, param_description: str = None, 

133 param_type: type = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

134 super().__init__("(?)", parent) 

135 

136 # Initialize color scheme 

137 self.color_scheme = color_scheme or PyQt6ColorScheme() 

138 

139 self.help_target = help_target 

140 self.param_name = param_name 

141 self.param_description = param_description 

142 self.param_type = param_type 

143 

144 # Style as clickable help indicator 

145 self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) 

146 self.setStyleSheet(f""" 

147 QLabel {{ 

148 color: {self.color_scheme.to_hex(self.color_scheme.border_light)}; 

149 font-size: 10px; 

150 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_light)}; 

151 border-radius: 8px; 

152 padding: 2px 4px; 

153 background-color: {self.color_scheme.to_hex(self.color_scheme.window_bg)}; 

154 }} 

155 QLabel:hover {{ 

156 color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

157 border-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

158 background-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

159 }} 

160 """) 

161 

162 # Set fixed size for consistent appearance 

163 self.setFixedSize(20, 16) 

164 self.setAlignment(Qt.AlignmentFlag.AlignCenter) 

165 

166 def mousePressEvent(self, event): 

167 """Handle mouse press to show help - reuses Textual TUI help manager pattern.""" 

168 if event.button() == Qt.MouseButton.LeftButton: 

169 try: 

170 # Import inside method to avoid circular imports (same pattern as Textual TUI) 

171 from openhcs.pyqt_gui.windows.help_windows import HelpWindowManager 

172 

173 if self.help_target: 

174 # Show function/class help using unified manager 

175 HelpWindowManager.show_docstring_help(self.help_target, parent=self) 

176 elif self.param_name: 

177 # Show parameter help using the description passed from parameter analysis 

178 HelpWindowManager.show_parameter_help( 

179 self.param_name, self.param_description or "No description available", self.param_type, parent=self 

180 ) 

181 

182 self.help_requested.emit() 

183 

184 except Exception as e: 

185 logger.error(f"Failed to show help: {e}") 

186 raise 

187 

188 super().mousePressEvent(event) 

189 

190 

191 

192 

193class HelpButton(QPushButton): 

194 """PyQt6 help button for adding help functionality to any widget - mirrors Textual TUI.""" 

195 

196 def __init__(self, help_target: Union[Callable, type] = None, 

197 param_name: str = None, param_description: str = None, 

198 param_type: type = None, text: str = "Help", 

199 color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

200 super().__init__(text, parent) 

201 

202 # Initialize color scheme 

203 self.color_scheme = color_scheme or PyQt6ColorScheme() 

204 

205 self.help_target = help_target 

206 self.param_name = param_name 

207 self.param_description = param_description 

208 self.param_type = param_type 

209 

210 # Connect click to help display 

211 self.clicked.connect(self.show_help) 

212 

213 # Style as help button 

214 self.setMaximumWidth(60) 

215 self.setStyleSheet(f""" 

216 QPushButton {{ 

217 background-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

218 color: white; 

219 border: none; 

220 padding: 4px 8px; 

221 border-radius: 3px; 

222 }} 

223 QPushButton:hover {{ 

224 background-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

225 }} 

226 QPushButton:pressed {{ 

227 background-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

228 }} 

229 """) 

230 

231 def show_help(self): 

232 """Show help using the unified help manager - reuses Textual TUI logic.""" 

233 try: 

234 # Import inside method to avoid circular imports (same pattern as Textual TUI) 

235 from openhcs.pyqt_gui.windows.help_windows import HelpWindowManager 

236 

237 if self.help_target: 

238 # Show function/class help using unified manager 

239 HelpWindowManager.show_docstring_help(self.help_target, parent=self) 

240 elif self.param_name: 

241 # Show parameter help using the description passed from parameter analysis 

242 HelpWindowManager.show_parameter_help( 

243 self.param_name, self.param_description or "No description available", self.param_type, parent=self 

244 ) 

245 

246 except Exception as e: 

247 logger.error(f"Failed to show help: {e}") 

248 raise 

249 

250 

251 

252 

253class LabelWithHelp(QWidget): 

254 """PyQt6 widget that combines a label with a help indicator - mirrors Textual TUI pattern.""" 

255 

256 def __init__(self, text: str, help_target: Union[Callable, type] = None, 

257 param_name: str = None, param_description: str = None, 

258 param_type: type = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

259 super().__init__(parent) 

260 

261 # Initialize color scheme 

262 self.color_scheme = color_scheme or PyQt6ColorScheme() 

263 

264 layout = QHBoxLayout(self) 

265 layout.setContentsMargins(0, 0, 0, 0) 

266 layout.setSpacing(5) 

267 

268 # Main label 

269 label = QLabel(text) 

270 layout.addWidget(label) 

271 

272 # Help indicator 

273 help_indicator = HelpIndicator( 

274 help_target=help_target, 

275 param_name=param_name, 

276 param_description=param_description, 

277 param_type=param_type, 

278 color_scheme=self.color_scheme 

279 ) 

280 layout.addWidget(help_indicator) 

281 

282 layout.addStretch() 

283 

284 

285class FunctionTitleWithHelp(QWidget): 

286 """PyQt6 function title with integrated help - mirrors Textual TUI ClickableFunctionTitle.""" 

287 

288 def __init__(self, func: Callable, index: int = None, 

289 color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

290 super().__init__(parent) 

291 

292 # Initialize color scheme 

293 self.color_scheme = color_scheme or PyQt6ColorScheme() 

294 

295 layout = QHBoxLayout(self) 

296 layout.setContentsMargins(0, 0, 0, 0) 

297 layout.setSpacing(10) 

298 

299 # Function title 

300 func_name = getattr(func, '__name__', 'Unknown Function') 

301 module_name = getattr(func, '__module__', '').split('.')[-1] if func else '' 

302 

303 title = f"{index + 1}: {func_name}" if index is not None else func_name 

304 if module_name: 

305 title += f" ({module_name})" 

306 

307 title_label = QLabel(title) 

308 title_font = QFont() 

309 title_font.setBold(True) 

310 title_label.setFont(title_font) 

311 layout.addWidget(title_label) 

312 

313 # Help button 

314 help_btn = HelpButton(help_target=func, text="?", color_scheme=self.color_scheme) 

315 help_btn.setMaximumWidth(25) 

316 help_btn.setMaximumHeight(20) 

317 layout.addWidget(help_btn) 

318 

319 layout.addStretch() 

320 

321 

322class GroupBoxWithHelp(QGroupBox): 

323 """PyQt6 group box with integrated help for dataclass titles - mirrors Textual TUI pattern.""" 

324 

325 def __init__(self, title: str, help_target: Union[Callable, type] = None, 

326 color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

327 super().__init__(parent) 

328 

329 # Initialize color scheme 

330 self.color_scheme = color_scheme or PyQt6ColorScheme() 

331 self.help_target = help_target 

332 

333 # Create custom title widget with help 

334 title_widget = QWidget() 

335 title_layout = QHBoxLayout(title_widget) 

336 title_layout.setContentsMargins(0, 0, 0, 0) 

337 title_layout.setSpacing(5) 

338 

339 # Title label 

340 title_label = QLabel(title) 

341 title_font = QFont() 

342 title_font.setBold(True) 

343 title_label.setFont(title_font) 

344 title_layout.addWidget(title_label) 

345 

346 # Help button for dataclass 

347 if help_target: 

348 help_btn = HelpButton(help_target=help_target, text="?", color_scheme=self.color_scheme) 

349 help_btn.setMaximumWidth(25) 

350 help_btn.setMaximumHeight(20) 

351 title_layout.addWidget(help_btn) 

352 

353 title_layout.addStretch() 

354 

355 # Set the custom title widget 

356 self.setTitle("") # Clear default title 

357 

358 # Create main layout and add title widget at top 

359 main_layout = QVBoxLayout(self) 

360 main_layout.addWidget(title_widget) 

361 

362 # Content area for child widgets 

363 self.content_layout = QVBoxLayout() 

364 main_layout.addLayout(self.content_layout) 

365 

366 def addWidget(self, widget): 

367 """Add widget to the content area.""" 

368 self.content_layout.addWidget(widget) 

369 

370 def addLayout(self, layout): 

371 """Add layout to the content area.""" 

372 self.content_layout.addLayout(layout)