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

129 statements  

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

1""" 

2Plate Viewer Window - Tabbed interface for Image Browser and Metadata Viewer. 

3 

4Combines image browsing and metadata viewing in a single window with tabs. 

5""" 

6 

7import logging 

8from typing import Optional 

9 

10from PyQt6.QtWidgets import ( 

11 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, 

12 QTabWidget, QWidget, QLabel 

13) 

14from PyQt6.QtCore import Qt 

15from PyQt6.QtGui import QFont 

16 

17from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

18from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator 

19 

20logger = logging.getLogger(__name__) 

21 

22 

23class PlateViewerWindow(QDialog): 

24 """ 

25 Tabbed window for viewing plate images and metadata. 

26  

27 Combines: 

28 - Image Browser (tab 1): Browse and view images in Napari 

29 - Metadata Viewer (tab 2): View plate metadata 

30 """ 

31 

32 def __init__(self, orchestrator, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

33 """ 

34 Initialize plate viewer window. 

35  

36 Args: 

37 orchestrator: PipelineOrchestrator instance 

38 color_scheme: Color scheme for styling 

39 parent: Parent widget 

40 """ 

41 super().__init__(parent) 

42 self.orchestrator = orchestrator 

43 self.color_scheme = color_scheme or PyQt6ColorScheme() 

44 self.style_gen = StyleSheetGenerator(self.color_scheme) 

45 

46 plate_name = orchestrator.plate_path.name if orchestrator else "Unknown" 

47 self.setWindowTitle(f"Plate Viewer - {plate_name}") 

48 self.setMinimumSize(1200, 800) 

49 self.resize(1400, 900) 

50 

51 # Make floating window 

52 self.setWindowFlags(Qt.WindowType.Window) 

53 

54 self._setup_ui() 

55 

56 def _setup_ui(self): 

57 """Setup the window UI.""" 

58 layout = QVBoxLayout(self) 

59 layout.setContentsMargins(5, 5, 5, 5) # Reduced margins 

60 layout.setSpacing(5) # Reduced spacing 

61 

62 # Single row: tabs + title + button 

63 tab_row = QHBoxLayout() 

64 tab_row.setContentsMargins(0, 0, 0, 0) # No margins - let tabs breathe 

65 tab_row.setSpacing(10) 

66 

67 # Tab widget (tabs on the left) 

68 self.tab_widget = QTabWidget() 

69 # Get the tab bar and add it to our horizontal layout 

70 self.tab_bar = self.tab_widget.tabBar() 

71 # Prevent tab scrolling by setting expanding to false and using minimum size hint 

72 self.tab_bar.setExpanding(False) 

73 self.tab_bar.setUsesScrollButtons(False) 

74 tab_row.addWidget(self.tab_bar, 0) # 0 stretch - don't expand 

75 

76 # Show plate name with full path in parentheses, with elision (title on right of tabs) 

77 if self.orchestrator: 

78 plate_name = self.orchestrator.plate_path.name 

79 full_path = str(self.orchestrator.plate_path) 

80 title_text = f"Plate: {plate_name} ({full_path})" 

81 else: 

82 title_text = "Plate: Unknown" 

83 

84 title_label = QLabel(title_text) 

85 title_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) 

86 title_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};") 

87 title_label.setWordWrap(False) # Single line 

88 title_label.setTextFormat(Qt.TextFormat.PlainText) 

89 title_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) # Allow copying 

90 # Enable elision (text will be cut with ... when too long) 

91 from PyQt6.QtWidgets import QSizePolicy 

92 title_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) 

93 tab_row.addWidget(title_label, 1) # Stretch to fill available space 

94 

95 tab_row.addStretch() 

96 

97 # Close button 

98 close_btn = QPushButton("Close") 

99 close_btn.clicked.connect(self.accept) 

100 close_btn.setStyleSheet(self.style_gen.generate_button_style()) 

101 tab_row.addWidget(close_btn) 

102 

103 layout.addLayout(tab_row) 

104 

105 # Style the tab bar 

106 self.tab_bar.setStyleSheet(f""" 

107 QTabBar::tab {{ 

108 background-color: {self.color_scheme.to_hex(self.color_scheme.input_bg)}; 

109 color: white; 

110 padding: 8px 16px; 

111 margin-right: 2px; 

112 border-top-left-radius: 4px; 

113 border-top-right-radius: 4px; 

114 border: none; 

115 }} 

116 QTabBar::tab:selected {{ 

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

118 }} 

119 QTabBar::tab:hover {{ 

120 background-color: {self.color_scheme.to_hex(self.color_scheme.button_hover_bg)}; 

121 }} 

122 """) 

123 

124 # Tab 1: Image Browser 

125 self.image_browser_tab = self._create_image_browser_tab() 

126 self.tab_widget.addTab(self.image_browser_tab, "Image Browser") 

127 

128 # Tab 2: Metadata Viewer 

129 self.metadata_viewer_tab = self._create_metadata_viewer_tab() 

130 self.tab_widget.addTab(self.metadata_viewer_tab, "Metadata") 

131 

132 # Add the tab widget's content area (stacked widget) below the tab row 

133 # The tab bar is already in tab_row, so we only add the content pane here 

134 from PyQt6.QtWidgets import QStackedWidget 

135 content_container = QWidget() 

136 content_layout = QVBoxLayout(content_container) 

137 content_layout.setContentsMargins(0, 0, 0, 0) 

138 content_layout.setSpacing(0) 

139 

140 # Get the stacked widget from the tab widget and add it 

141 stacked_widget = self.tab_widget.findChild(QStackedWidget) 

142 if stacked_widget: 

143 content_layout.addWidget(stacked_widget) 

144 

145 layout.addWidget(content_container) 

146 

147 def _create_image_browser_tab(self) -> QWidget: 

148 """Create the image browser tab.""" 

149 from openhcs.pyqt_gui.widgets.image_browser import ImageBrowserWidget 

150 

151 # Create image browser widget 

152 browser = ImageBrowserWidget( 

153 orchestrator=self.orchestrator, 

154 color_scheme=self.color_scheme, 

155 parent=self 

156 ) 

157 

158 # Store reference 

159 self.image_browser = browser 

160 

161 return browser 

162 

163 def _create_metadata_viewer_tab(self) -> QWidget: 

164 """Create the metadata viewer tab.""" 

165 # Create scroll area for metadata content 

166 from PyQt6.QtWidgets import QScrollArea 

167 scroll_area = QScrollArea() 

168 scroll_area.setWidgetResizable(True) 

169 scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) 

170 

171 # Container for metadata forms 

172 container = QWidget() 

173 layout = QVBoxLayout(container) 

174 layout.setContentsMargins(5, 5, 5, 5) 

175 

176 # Load metadata using the same logic as MetadataViewerDialog 

177 try: 

178 metadata_handler = self.orchestrator.microscope_handler.metadata_handler 

179 plate_path = self.orchestrator.plate_path 

180 

181 # Check if this is OpenHCS format 

182 if hasattr(metadata_handler, '_load_metadata_dict'): 

183 # OpenHCS format 

184 from openhcs.microscopes.openhcs import OpenHCSMetadata 

185 metadata_dict = metadata_handler._load_metadata_dict(plate_path) 

186 subdirs_dict = metadata_dict.get("subdirectories", {}) 

187 

188 if not subdirs_dict: 

189 raise ValueError("No subdirectories found in metadata") 

190 

191 # Convert raw dicts to OpenHCSMetadata instances 

192 subdirs_instances = {} 

193 for subdir_name, subdir_data in subdirs_dict.items(): 

194 # Ensure all optional fields have explicit None if missing 

195 # (OpenHCSMetadata requires all fields to be provided, even if Optional) 

196 subdir_data.setdefault('timepoints', None) 

197 subdir_data.setdefault('channels', None) 

198 subdir_data.setdefault('wells', None) 

199 subdir_data.setdefault('sites', None) 

200 subdir_data.setdefault('z_indexes', None) 

201 

202 # Create OpenHCSMetadata from the subdirectory data 

203 subdirs_instances[subdir_name] = OpenHCSMetadata(**subdir_data) 

204 

205 # Create forms for each subdirectory 

206 self._create_multi_subdirectory_forms(layout, subdirs_instances) 

207 else: 

208 # Other microscope formats (ImageXpress, Opera Phenix, etc.) 

209 from openhcs.microscopes.openhcs import OpenHCSMetadata 

210 component_metadata = metadata_handler.parse_metadata(plate_path) 

211 

212 # Get image files list (all handlers have this method) 

213 image_files = metadata_handler.get_image_files(plate_path) 

214 

215 # Get optional metadata with fallback 

216 grid_dims = metadata_handler._get_with_fallback('get_grid_dimensions', plate_path) 

217 pixel_size = metadata_handler._get_with_fallback('get_pixel_size', plate_path) 

218 

219 metadata_instance = OpenHCSMetadata( 

220 microscope_handler_name=self.orchestrator.microscope_handler.microscope_type, 

221 source_filename_parser_name=self.orchestrator.microscope_handler.parser.__class__.__name__, 

222 grid_dimensions=list(grid_dims) if grid_dims else [1, 1], 

223 pixel_size=pixel_size if pixel_size else 1.0, 

224 image_files=image_files, # Now populated! 

225 channels=component_metadata.get('channel'), 

226 wells=component_metadata.get('well'), 

227 sites=component_metadata.get('site'), 

228 z_indexes=component_metadata.get('z_index'), 

229 timepoints=component_metadata.get('timepoint'), 

230 available_backends={'disk': True}, 

231 main=None 

232 ) 

233 

234 self._create_single_metadata_form(layout, metadata_instance) 

235 

236 except Exception as e: 

237 logger.error(f"Failed to load metadata: {e}", exc_info=True) 

238 error_label = QLabel(f"<b>Error loading metadata:</b><br>{str(e)}") 

239 error_label.setWordWrap(True) 

240 error_label.setStyleSheet("color: red; padding: 10px;") 

241 layout.addWidget(error_label) 

242 

243 layout.addStretch() 

244 

245 # Set container as scroll area widget 

246 scroll_area.setWidget(container) 

247 return scroll_area 

248 

249 def _create_single_metadata_form(self, layout, metadata_instance): 

250 """Create a single metadata form.""" 

251 from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager 

252 

253 metadata_form = ParameterFormManager( 

254 object_instance=metadata_instance, 

255 field_id="metadata_viewer", 

256 parent=None, 

257 read_only=True, 

258 color_scheme=self.color_scheme 

259 ) 

260 layout.addWidget(metadata_form) 

261 

262 def _create_multi_subdirectory_forms(self, layout, subdirs_instances): 

263 """Create forms for multiple subdirectories.""" 

264 from PyQt6.QtWidgets import QGroupBox 

265 from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager 

266 

267 for subdir_name, metadata_instance in subdirs_instances.items(): 

268 group_box = QGroupBox(f"Subdirectory: {subdir_name}") 

269 group_layout = QVBoxLayout(group_box) 

270 

271 metadata_form = ParameterFormManager( 

272 object_instance=metadata_instance, 

273 field_id=f"metadata_{subdir_name}", 

274 parent=None, 

275 read_only=True, 

276 color_scheme=self.color_scheme 

277 ) 

278 group_layout.addWidget(metadata_form) 

279 

280 layout.addWidget(group_box) 

281 

282 def cleanup(self): 

283 """Clean up resources.""" 

284 if hasattr(self, 'image_browser'): 

285 self.image_browser.cleanup() 

286