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
« 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.
4Combines image browsing and metadata viewing in a single window with tabs.
5"""
7import logging
8from typing import Optional
10from PyQt6.QtWidgets import (
11 QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
12 QTabWidget, QWidget, QLabel
13)
14from PyQt6.QtCore import Qt
15from PyQt6.QtGui import QFont
17from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme
18from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator
20logger = logging.getLogger(__name__)
23class PlateViewerWindow(QDialog):
24 """
25 Tabbed window for viewing plate images and metadata.
27 Combines:
28 - Image Browser (tab 1): Browse and view images in Napari
29 - Metadata Viewer (tab 2): View plate metadata
30 """
32 def __init__(self, orchestrator, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None):
33 """
34 Initialize plate viewer window.
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)
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)
51 # Make floating window
52 self.setWindowFlags(Qt.WindowType.Window)
54 self._setup_ui()
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
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)
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
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"
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
95 tab_row.addStretch()
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)
103 layout.addLayout(tab_row)
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 """)
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")
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")
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)
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)
145 layout.addWidget(content_container)
147 def _create_image_browser_tab(self) -> QWidget:
148 """Create the image browser tab."""
149 from openhcs.pyqt_gui.widgets.image_browser import ImageBrowserWidget
151 # Create image browser widget
152 browser = ImageBrowserWidget(
153 orchestrator=self.orchestrator,
154 color_scheme=self.color_scheme,
155 parent=self
156 )
158 # Store reference
159 self.image_browser = browser
161 return browser
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)
171 # Container for metadata forms
172 container = QWidget()
173 layout = QVBoxLayout(container)
174 layout.setContentsMargins(5, 5, 5, 5)
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
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", {})
188 if not subdirs_dict:
189 raise ValueError("No subdirectories found in metadata")
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)
202 # Create OpenHCSMetadata from the subdirectory data
203 subdirs_instances[subdir_name] = OpenHCSMetadata(**subdir_data)
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)
212 # Get image files list (all handlers have this method)
213 image_files = metadata_handler.get_image_files(plate_path)
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)
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 )
234 self._create_single_metadata_form(layout, metadata_instance)
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)
243 layout.addStretch()
245 # Set container as scroll area widget
246 scroll_area.setWidget(container)
247 return scroll_area
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
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)
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
267 for subdir_name, metadata_instance in subdirs_instances.items():
268 group_box = QGroupBox(f"Subdirectory: {subdir_name}")
269 group_layout = QVBoxLayout(group_box)
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)
280 layout.addWidget(group_box)
282 def cleanup(self):
283 """Clean up resources."""
284 if hasattr(self, 'image_browser'):
285 self.image_browser.cleanup()