Coverage for openhcs/pyqt_gui/windows/help_windows.py: 0.0%

160 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +0000

1"""PyQt6 help system - reuses Textual TUI help logic and components.""" 

2 

3import logging 

4from typing import Union, Callable, Optional 

5from PyQt6.QtWidgets import ( 

6 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

7 QTextEdit, QScrollArea, QWidget, QMessageBox 

8) 

9from PyQt6.QtCore import Qt 

10 

11# REUSE the actual working Textual TUI help components 

12from openhcs.introspection.signature_analyzer import DocstringExtractor 

13from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

14from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19class BaseHelpWindow(QDialog): 

20 """Base class for all PyQt6 help windows - reuses Textual TUI help logic.""" 

21 

22 def __init__(self, title: str = "Help", color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

23 super().__init__(parent) 

24 

25 # Initialize color scheme and style generator 

26 self.color_scheme = color_scheme or PyQt6ColorScheme() 

27 self.style_generator = StyleSheetGenerator(self.color_scheme) 

28 

29 self.setWindowTitle(title) 

30 self.setModal(False) # Allow interaction with main window 

31 

32 # Setup UI 

33 self.setup_ui() 

34 

35 # Apply centralized styling 

36 self.setStyleSheet(self.style_generator.generate_dialog_style()) 

37 

38 def setup_ui(self): 

39 """Setup the base help window UI.""" 

40 layout = QVBoxLayout(self) 

41 

42 # Content area (to be filled by subclasses) 

43 self.content_area = QScrollArea() 

44 self.content_area.setWidgetResizable(True) 

45 self.content_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

46 self.content_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

47 layout.addWidget(self.content_area) 

48 

49 # Close button 

50 button_layout = QHBoxLayout() 

51 button_layout.addStretch() 

52 

53 close_btn = QPushButton("Close") 

54 close_btn.clicked.connect(self.close) 

55 button_layout.addWidget(close_btn) 

56 

57 layout.addLayout(button_layout) 

58 

59 

60class DocstringHelpWindow(BaseHelpWindow): 

61 """Help window for functions and classes - reuses Textual TUI DocstringExtractor.""" 

62 

63 def __init__(self, target: Union[Callable, type], title: Optional[str] = None, 

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

65 self.target = target 

66 

67 # REUSE Textual TUI docstring extraction logic 

68 self.docstring_info = DocstringExtractor.extract(target) 

69 

70 # Generate title from target if not provided 

71 if title is None: 

72 if hasattr(target, '__name__'): 

73 title = f"Help: {target.__name__}" 

74 else: 

75 title = "Help" 

76 

77 super().__init__(title, color_scheme, parent) 

78 self.populate_content() 

79 

80 def populate_content(self): 

81 """Populate the help content with minimal styling.""" 

82 content_widget = QWidget() 

83 layout = QVBoxLayout(content_widget) 

84 layout.setContentsMargins(10, 10, 10, 10) 

85 layout.setSpacing(5) 

86 

87 # Function/class summary 

88 if self.docstring_info.summary: 

89 summary_label = QLabel(self.docstring_info.summary) 

90 summary_label.setWordWrap(True) 

91 summary_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

92 summary_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px;") 

93 layout.addWidget(summary_label) 

94 

95 # Full description 

96 if self.docstring_info.description: 

97 desc_label = QLabel(self.docstring_info.description) 

98 desc_label.setWordWrap(True) 

99 desc_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

100 desc_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px;") 

101 layout.addWidget(desc_label) 

102 

103 # Parameters section 

104 if self.docstring_info.parameters: 

105 params_label = QLabel("Parameters:") 

106 params_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

107 params_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;") 

108 layout.addWidget(params_label) 

109 

110 for param_name, param_desc in self.docstring_info.parameters.items(): 

111 # Parameter name 

112 name_label = QLabel(f"{param_name}") 

113 name_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

114 name_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 5px; margin-top: 3px;") 

115 layout.addWidget(name_label) 

116 

117 # Parameter description 

118 if param_desc: 

119 desc_label = QLabel(param_desc) 

120 desc_label.setWordWrap(True) 

121 desc_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

122 desc_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 20px;") 

123 layout.addWidget(desc_label) 

124 

125 # Returns section 

126 if self.docstring_info.returns: 

127 returns_label = QLabel("Returns:") 

128 returns_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

129 returns_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;") 

130 layout.addWidget(returns_label) 

131 

132 returns_desc = QLabel(self.docstring_info.returns) 

133 returns_desc.setWordWrap(True) 

134 returns_desc.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

135 returns_desc.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 5px;") 

136 layout.addWidget(returns_desc) 

137 

138 # Examples section 

139 if self.docstring_info.examples: 

140 examples_label = QLabel("Examples:") 

141 examples_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

142 examples_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;") 

143 layout.addWidget(examples_label) 

144 

145 examples_text = QTextEdit() 

146 examples_text.setPlainText(self.docstring_info.examples) 

147 examples_text.setReadOnly(True) 

148 examples_text.setMaximumHeight(150) 

149 examples_text.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) 

150 examples_text.setStyleSheet(f""" 

151 QTextEdit {{ 

152 background-color: transparent; 

153 color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; 

154 border: none; 

155 font-family: monospace; 

156 font-size: 11px; 

157 }} 

158 QTextEdit:hover {{ 

159 background-color: transparent; 

160 }} 

161 """) 

162 layout.addWidget(examples_text) 

163 

164 layout.addStretch() 

165 self.content_area.setWidget(content_widget) 

166 

167 # Auto-size to content 

168 self.adjustSize() 

169 # Set reasonable min/max sizes 

170 self.setMinimumSize(400, 200) 

171 self.setMaximumSize(800, 600) 

172 

173 

174class HelpWindowManager: 

175 """PyQt6 help window manager - unified window for all help content.""" 

176 

177 # Class-level window reference for singleton behavior 

178 _help_window = None 

179 

180 @classmethod 

181 def show_docstring_help(cls, target: Union[Callable, type], title: Optional[str] = None, parent=None): 

182 """Show help for a function or class - reuses Textual TUI extraction logic.""" 

183 try: 

184 # Check if existing window is still valid 

185 if cls._help_window and hasattr(cls._help_window, 'isVisible'): 

186 try: 

187 if not cls._help_window.isHidden(): 

188 cls._help_window.target = target 

189 cls._help_window.docstring_info = DocstringExtractor.extract(target) 

190 cls._help_window.setWindowTitle(title or f"Help: {getattr(target, '__name__', 'Unknown')}") 

191 cls._help_window.populate_content() 

192 cls._help_window.raise_() 

193 cls._help_window.activateWindow() 

194 return 

195 except RuntimeError: 

196 # Window was deleted, clear reference 

197 cls._help_window = None 

198 

199 # Create new window 

200 cls._help_window = DocstringHelpWindow(target, title=title, parent=parent) 

201 cls._help_window.show() 

202 

203 except Exception as e: 

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

205 QMessageBox.warning(parent, "Help Error", f"Failed to show help: {e}") 

206 

207 @classmethod 

208 def show_parameter_help(cls, param_name: str, param_description: str, param_type: type = None, parent=None): 

209 """Show help for a parameter - creates a fake docstring object and uses DocstringHelpWindow.""" 

210 try: 

211 # Create a fake docstring info object for the parameter 

212 from dataclasses import dataclass 

213 

214 @dataclass 

215 class FakeDocstringInfo: 

216 summary: str = "" 

217 description: str = "" 

218 parameters: dict = None 

219 returns: str = "" 

220 examples: str = "" 

221 

222 # Build parameter display 

223 type_str = f" ({getattr(param_type, '__name__', str(param_type))})" if param_type else "" 

224 fake_info = FakeDocstringInfo( 

225 summary=f"{param_name}{type_str}", 

226 description=param_description or "No description available", 

227 parameters={}, 

228 returns="", 

229 examples="" 

230 ) 

231 

232 # Check if existing window is still valid 

233 if cls._help_window and hasattr(cls._help_window, 'isVisible'): 

234 try: 

235 if not cls._help_window.isHidden(): 

236 cls._help_window.docstring_info = fake_info 

237 cls._help_window.setWindowTitle(f"Parameter: {param_name}") 

238 cls._help_window.populate_content() 

239 cls._help_window.raise_() 

240 cls._help_window.activateWindow() 

241 return 

242 except RuntimeError: 

243 # Window was deleted, clear reference 

244 cls._help_window = None 

245 

246 # Create new window with fake target 

247 class FakeTarget: 

248 __name__ = param_name 

249 

250 cls._help_window = DocstringHelpWindow(FakeTarget, title=f"Parameter: {param_name}", parent=parent) 

251 cls._help_window.docstring_info = fake_info 

252 cls._help_window.populate_content() 

253 cls._help_window.show() 

254 

255 except Exception as e: 

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

257 QMessageBox.warning(parent, "Help Error", f"Failed to show help: {e}") 

258 

259 

260class HelpableWidget: 

261 """Mixin class to add help functionality to PyQt6 widgets - mirrors Textual TUI.""" 

262 

263 def show_function_help(self, target: Union[Callable, type]) -> None: 

264 """Show help window for a function or class.""" 

265 HelpWindowManager.show_docstring_help(target, parent=self) 

266 

267 def show_parameter_help(self, param_name: str, param_description: str, param_type: type = None) -> None: 

268 """Show help window for a parameter.""" 

269 HelpWindowManager.show_parameter_help(param_name, param_description, param_type, parent=self)