Coverage for openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py: 0.0%
130 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"""
2Synthetic Plate Generator Window for PyQt6
4Provides a user-friendly interface for generating synthetic microscopy plates
5for testing OpenHCS without requiring real microscopy data.
6"""
8import logging
9import tempfile
10from pathlib import Path
11from typing import Optional
13from PyQt6.QtWidgets import (
14 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
15 QScrollArea, QWidget, QMessageBox, QFileDialog
16)
17from PyQt6.QtCore import Qt, pyqtSignal
18from PyQt6.QtGui import QFont
20from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager
21from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator
22from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme
23from openhcs.tests.generators.generate_synthetic_data import SyntheticMicroscopyGenerator
25logger = logging.getLogger(__name__)
28class SyntheticPlateGeneratorWindow(QDialog):
29 """
30 Dialog window for generating synthetic microscopy plates.
32 Provides a parameter form for SyntheticMicroscopyGenerator with sensible
33 defaults from the test suite, allowing users to easily generate test data.
34 """
36 # Signals
37 plate_generated = pyqtSignal(str, str) # output_dir path, pipeline_path
39 def __init__(self, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None):
40 """
41 Initialize the synthetic plate generator window.
43 Args:
44 color_scheme: Color scheme for styling (optional)
45 parent: Parent widget
46 """
47 super().__init__(parent)
49 # Initialize color scheme and style generator
50 self.color_scheme = color_scheme or PyQt6ColorScheme()
51 self.style_generator = StyleSheetGenerator(self.color_scheme)
53 # Output directory (will be set by user or use temp)
54 self.output_dir: Optional[str] = None
56 # Setup UI
57 self.setup_ui()
59 logger.debug("Synthetic plate generator window initialized")
61 def setup_ui(self):
62 """Setup the user interface."""
63 self.setWindowTitle("Generate Synthetic Plate")
64 self.setModal(True)
65 self.setMinimumSize(700, 600)
66 self.resize(800, 700)
68 layout = QVBoxLayout(self)
69 layout.setSpacing(10)
71 # Header with title and description
72 header_widget = QWidget()
73 header_layout = QVBoxLayout(header_widget)
74 header_layout.setContentsMargins(10, 10, 10, 10)
76 header_label = QLabel("Generate Synthetic Microscopy Plate")
77 header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold))
78 header_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};")
79 header_layout.addWidget(header_label)
81 description_label = QLabel(
82 "Generate synthetic microscopy data for testing OpenHCS without real data. "
83 "The defaults match the test suite configuration and will create a small 4-well plate "
84 "with 3x3 grid, 2 channels, and 3 z-levels."
85 )
86 description_label.setWordWrap(True)
87 description_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_secondary)}; padding: 5px;")
88 header_layout.addWidget(description_label)
90 layout.addWidget(header_widget)
92 # Output directory selection
93 output_dir_widget = self._create_output_dir_selector()
94 layout.addWidget(output_dir_widget)
96 # Parameter form with scroll area
97 scroll_area = QScrollArea()
98 scroll_area.setWidgetResizable(True)
99 scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
100 scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
102 # Create form manager from SyntheticMicroscopyGenerator class
103 # This automatically builds the UI from the __init__ signature (same pattern as function_pane.py)
104 # CRITICAL: Pass color_scheme as parameter to ensure consistent theming with other parameter forms
105 self.form_manager = ParameterFormManager(
106 object_instance=SyntheticMicroscopyGenerator, # Pass the class itself, not __init__
107 field_id="synthetic_plate_generator",
108 parent=self,
109 context_obj=None,
110 exclude_params=['output_dir', 'skip_files', 'include_all_components', 'random_seed'], # Exclude advanced params (self is auto-excluded)
111 color_scheme=self.color_scheme # Pass color_scheme as instance parameter, not class attribute
112 )
114 scroll_area.setWidget(self.form_manager)
115 layout.addWidget(scroll_area)
117 # Button row
118 button_row = QHBoxLayout()
119 button_row.setContentsMargins(10, 5, 10, 10)
120 button_row.setSpacing(10)
122 button_row.addStretch()
124 # Get centralized button styles
125 button_styles = self.style_generator.generate_config_button_styles()
127 # Cancel button
128 cancel_button = QPushButton("Cancel")
129 cancel_button.setFixedHeight(28)
130 cancel_button.setMinimumWidth(100)
131 cancel_button.clicked.connect(self.reject)
132 cancel_button.setStyleSheet(button_styles["cancel"])
133 button_row.addWidget(cancel_button)
135 # Generate button
136 generate_button = QPushButton("Generate Plate")
137 generate_button.setFixedHeight(28)
138 generate_button.setMinimumWidth(100)
139 generate_button.clicked.connect(self.generate_plate)
140 generate_button.setStyleSheet(button_styles["save"])
141 button_row.addWidget(generate_button)
143 layout.addLayout(button_row)
145 # Apply window styling
146 self.setStyleSheet(self.style_generator.generate_dialog_style())
148 def _create_output_dir_selector(self) -> QWidget:
149 """Create the output directory selector widget."""
150 widget = QWidget()
151 layout = QHBoxLayout(widget)
152 layout.setContentsMargins(10, 5, 10, 5)
153 layout.setSpacing(10)
155 label = QLabel("Output Directory:")
156 label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)};")
157 layout.addWidget(label)
159 self.output_dir_label = QLabel("<temp directory>")
160 self.output_dir_label.setStyleSheet(
161 f"color: {self.color_scheme.to_hex(self.color_scheme.text_secondary)}; "
162 f"font-style: italic; padding: 5px;"
163 )
164 layout.addWidget(self.output_dir_label, 1)
166 browse_button = QPushButton("Browse...")
167 browse_button.setFixedHeight(28)
168 browse_button.setMinimumWidth(80)
169 browse_button.clicked.connect(self.browse_output_dir)
170 browse_button.setStyleSheet(self.style_generator.generate_button_style())
171 layout.addWidget(browse_button)
173 return widget
175 def browse_output_dir(self):
176 """Open file dialog to select output directory."""
177 dir_path = QFileDialog.getExistingDirectory(
178 self,
179 "Select Output Directory for Synthetic Plate",
180 str(Path.home()),
181 QFileDialog.Option.ShowDirsOnly
182 )
184 if dir_path:
185 self.output_dir = dir_path
186 self.output_dir_label.setText(dir_path)
187 self.output_dir_label.setStyleSheet(
188 f"color: {self.color_scheme.to_hex(self.color_scheme.text_normal)}; padding: 5px;"
189 )
191 def generate_plate(self):
192 """Generate the synthetic plate with current parameters."""
193 try:
194 # Get parameters from form
195 params = self.form_manager.get_current_values()
197 # Determine output directory
198 if self.output_dir is None:
199 # Use temp directory
200 temp_dir = tempfile.mkdtemp(prefix="openhcs_synthetic_plate_")
201 output_dir = temp_dir
202 logger.info(f"Using temporary directory: {output_dir}")
203 else:
204 output_dir = self.output_dir
206 # Add output_dir to params
207 params['output_dir'] = output_dir
209 # Show progress message
210 QMessageBox.information(
211 self,
212 "Generating Plate",
213 f"Generating synthetic plate at:\n{output_dir}\n\n"
214 f"This may take a moment...",
215 QMessageBox.StandardButton.Ok
216 )
218 # Create generator and generate dataset
219 logger.info(f"Generating synthetic plate with params: {params}")
220 generator = SyntheticMicroscopyGenerator(**params)
221 generator.generate_dataset()
223 logger.info(f"Successfully generated synthetic plate at: {output_dir}")
225 # Get path to test_pipeline.py
226 from openhcs.tests import test_pipeline
227 pipeline_path = Path(test_pipeline.__file__)
229 # Emit signal with output directory and pipeline path
230 self.plate_generated.emit(output_dir, str(pipeline_path))
232 # Show success message
233 QMessageBox.information(
234 self,
235 "Success",
236 f"Synthetic plate generated successfully!\n\nLocation: {output_dir}",
237 QMessageBox.StandardButton.Ok
238 )
240 # Close dialog
241 self.accept()
243 except Exception as e:
244 logger.error(f"Failed to generate synthetic plate: {e}", exc_info=True)
245 QMessageBox.critical(
246 self,
247 "Generation Failed",
248 f"Failed to generate synthetic plate:\n\n{str(e)}",
249 QMessageBox.StandardButton.Ok
250 )
252 def reject(self):
253 """Handle dialog rejection (Cancel button)."""
254 # Cleanup before closing
255 self._cleanup()
256 super().reject()
258 def accept(self):
259 """Handle dialog acceptance (Generate button)."""
260 # Cleanup before closing
261 self._cleanup()
262 super().accept()
264 def _cleanup(self):
265 """Cleanup resources before window closes."""
266 # Unregister from cross-window updates
267 if hasattr(self, 'form_manager') and self.form_manager is not None:
268 try:
269 self.form_manager.unregister_from_cross_window_updates()
270 except RuntimeError:
271 # Widget already deleted, ignore
272 pass
274 # Disconnect all signals to prevent accessing deleted Qt objects
275 try:
276 self.plate_generated.disconnect()
277 except (RuntimeError, TypeError):
278 # Already disconnected or no connections, ignore
279 pass