Coverage for openhcs/pyqt_gui/app.py: 0.0%
100 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"""
2OpenHCS PyQt6 Application
4Main application class that initializes the PyQt6 application and
5manages global configuration and services.
6"""
8import sys
9import logging
10from typing import Optional
11from pathlib import Path
13from PyQt6.QtWidgets import QApplication, QMessageBox
14from PyQt6.QtGui import QIcon
16from openhcs.core.config import GlobalPipelineConfig
17from openhcs.io.base import storage_registry
18from openhcs.io.filemanager import FileManager
20from openhcs.pyqt_gui.main import OpenHCSMainWindow
22logger = logging.getLogger(__name__)
25class OpenHCSPyQtApp(QApplication):
26 """
27 OpenHCS PyQt6 Application.
29 Main application class that manages global state, configuration,
30 and the main window lifecycle.
31 """
33 def __init__(self, argv: list, global_config: Optional[GlobalPipelineConfig] = None):
34 """
35 Initialize the OpenHCS PyQt6 application.
37 Args:
38 argv: Command line arguments
39 global_config: Global configuration (uses default if None)
40 """
41 super().__init__(argv)
43 # Application metadata
44 self.setApplicationName("OpenHCS")
45 self.setApplicationVersion("1.0.0")
46 self.setOrganizationName("OpenHCS Development Team")
47 self.setOrganizationDomain("openhcs.org")
49 # Global configuration
50 self.global_config = global_config or GlobalPipelineConfig()
52 # Shared components
53 self.storage_registry = storage_registry
54 self.file_manager = FileManager(self.storage_registry)
56 # Main window
57 self.main_window: Optional[OpenHCSMainWindow] = None
59 # Setup application
60 self.setup_application()
62 logger.info("OpenHCS PyQt6 application initialized")
64 def setup_application(self):
65 """Setup application-wide configuration."""
66 # Start async storage registry initialization in background thread
67 import threading
68 def init_storage_registry_background():
69 from openhcs.io.base import ensure_storage_registry
70 ensure_storage_registry()
71 logger.info("Storage registry initialized in background")
73 thread = threading.Thread(target=init_storage_registry_background, daemon=True, name="storage-registry-init")
74 thread.start()
75 logger.info("Storage registry initialization started in background")
77 # Start async function registry initialization in background thread
78 # This creates virtual modules (openhcs.cucim, openhcs.pyclesperanto, etc.)
79 def init_function_registry_background():
80 from openhcs.processing.func_registry import initialize_registry
81 initialize_registry()
82 logger.info("Function registry initialized in background - virtual modules created")
84 func_thread = threading.Thread(target=init_function_registry_background, daemon=True, name="function-registry-init")
85 func_thread.start()
86 logger.info("Function registry initialization started in background")
88 # CRITICAL FIX: Establish global config context for lazy dataclass resolution
89 # This was missing and caused placeholder resolution to fall back to static defaults
90 from openhcs.config_framework.global_config import set_global_config_for_editing
91 from openhcs.config_framework.lazy_factory import ensure_global_config_context
92 from openhcs.core.config import GlobalPipelineConfig
94 # Set for editing (UI placeholders)
95 set_global_config_for_editing(GlobalPipelineConfig, self.global_config)
97 # ALSO ensure context for orchestrator creation (required by orchestrator.__init__)
98 ensure_global_config_context(GlobalPipelineConfig, self.global_config)
100 logger.info("Global configuration context established for lazy dataclass resolution")
102 # Set application icon (if available)
103 icon_path = Path(__file__).parent / "resources" / "openhcs_icon.png"
104 if icon_path.exists():
105 self.setWindowIcon(QIcon(str(icon_path)))
107 # Setup exception handling
108 sys.excepthook = self.handle_exception
110 def create_main_window(self) -> OpenHCSMainWindow:
111 """
112 Create and show the main window.
114 Returns:
115 Created main window
116 """
117 if self.main_window is None:
118 self.main_window = OpenHCSMainWindow(self.global_config)
120 # Connect application-level signals
121 self.main_window.config_changed.connect(self.on_config_changed)
123 return self.main_window
125 def show_main_window(self):
126 """Show the main window."""
127 if self.main_window is None:
128 self.create_main_window()
130 self.main_window.show()
131 self.main_window.raise_()
132 self.main_window.activateWindow()
134 # Trigger deferred initialization AFTER window is visible
135 # This includes log viewer and default windows (pipeline editor)
136 from PyQt6.QtCore import QTimer
137 QTimer.singleShot(100, self.main_window._deferred_initialization)
139 def on_config_changed(self, new_config: GlobalPipelineConfig):
140 """
141 Handle global configuration changes.
143 Args:
144 new_config: New global configuration
145 """
146 self.global_config = new_config
147 logger.info("Global configuration updated")
149 def handle_exception(self, exc_type, exc_value, exc_traceback):
150 """
151 Handle uncaught exceptions.
153 Args:
154 exc_type: Exception type
155 exc_value: Exception value
156 exc_traceback: Exception traceback
157 """
158 if issubclass(exc_type, KeyboardInterrupt):
159 # Handle Ctrl+C gracefully
160 sys.__excepthook__(exc_type, exc_value, exc_traceback)
161 return
163 # Log the exception
164 logger.critical(
165 "Uncaught exception",
166 exc_info=(exc_type, exc_value, exc_traceback)
167 )
169 # Show error dialog
170 error_msg = f"An unexpected error occurred:\n\n{exc_type.__name__}: {exc_value}"
172 if self.main_window:
173 QMessageBox.critical(
174 self.main_window,
175 "Unexpected Error",
176 error_msg
177 )
178 else:
179 # No main window - application is in invalid state
180 raise RuntimeError("Uncaught exception occurred but no main window available for error dialog")
182 def run(self) -> int:
183 """
184 Run the application.
186 Returns:
187 Application exit code
188 """
189 try:
190 # Show main window
191 self.show_main_window()
193 # Start event loop
194 exit_code = self.exec()
196 # Ensure clean shutdown
197 self.cleanup()
199 return exit_code
201 except Exception as e:
202 logger.error(f"Error during application run: {e}", exc_info=True)
203 self.cleanup()
204 return 1
206 def cleanup(self):
207 """Clean up application resources."""
208 try:
209 logger.info("Starting application cleanup...")
211 # Process any remaining events
212 self.processEvents()
214 # Clean up main window
215 if hasattr(self, 'main_window') and self.main_window:
216 # Force close if not already closed
217 if not self.main_window.isHidden():
218 self.main_window.close()
219 self.main_window.deleteLater()
220 self.main_window = None
222 # Process events again to handle deleteLater
223 self.processEvents()
225 # Force garbage collection
226 import gc
227 gc.collect()
229 logger.info("Application cleanup completed")
231 except Exception as e:
232 logger.warning(f"Error during application cleanup: {e}")
235if __name__ == "__main__":
236 # Don't run directly - use launch.py instead
237 print("Use 'python -m openhcs.pyqt_gui' or 'python -m openhcs.pyqt_gui.launch' to start the GUI")
238 sys.exit(1)