Window Manager Usage Guide
Overview
WindowManager provides singleton window management with navigation support for inheritance tracking.
Key features:
One window per
scope_id(prevents duplicates).Auto-cleanup on close (no manual unregistration).
Navigation API for focusing and scrolling to fields.
Fail-loud on stale references.
ServiceRegistry Integration
Window handlers use ServiceRegistry for widget access instead of manual traversal:
from pyqt_reactive.services import ServiceRegistry
from my_widgets import PlateManagerWidget
def create_plate_config_window(scope_id: str, object_state=None):
# Get plate manager from ServiceRegistry
plate_manager = ServiceRegistry.get(PlateManagerWidget)
if not plate_manager:
return None
window = ConfigWindow(
config_class=PipelineConfig,
current_config=orchestrator.pipeline_config,
scope_id=scope_id,
)
window.show()
return window
Basic Usage
Show or focus a window via a factory callback:
from openhcs.pyqt_gui.services.window_manager import WindowManager
# Define factory function that creates the window
def create_config_window():
return ConfigWindow(
config_class=PipelineConfig,
initial_config=current_config,
parent=self,
on_save_callback=self._on_config_saved,
scope_id="plate1",
)
# Show window (creates new or focuses existing)
window = WindowManager.show_or_focus("plate1", create_config_window)
Before/after behavior:
config_window = ConfigWindow(...)
config_window.show()
WindowManager.show_or_focus(scope_id, lambda: ConfigWindow(...))
Migration Examples
Future: Inheritance Tracking
Show the source window and scroll to it when the user clicks an inherited field:
class InheritanceTreeWidget(QTreeWidget):
"""Tree widget showing field inheritance."""
def on_field_clicked(self, field_path: str, source_scope: str):
"""User clicked field - show source window and scroll to it.
Args:
field_path: Field that was clicked
source_scope: Scope where field is defined (not inherited)
"""
from openhcs.pyqt_gui.services.window_manager import WindowManager
# Try to focus existing window and navigate
if WindowManager.focus_and_navigate(source_scope, field_path=field_path):
return # Window exists and navigated
# Window not open - create it and navigate
def create_window():
return ConfigWindow(
config_class=self._get_config_class(source_scope),
initial_config=self._get_config(source_scope),
scope_id=source_scope,
)
window = WindowManager.show_or_focus(source_scope, create_window)
# Navigate after window is shown (give Qt time to render)
QTimer.singleShot(100, lambda: window.select_and_scroll_to_field(field_path))
Utility Methods
# Check if window is open
if WindowManager.is_open("plate1"):
print("Config window already open for plate1")
# Get all open window scopes
open_scopes = WindowManager.get_open_scopes()
print(f"Open windows: {open_scopes}")
# Programmatically close window
WindowManager.close_window("plate1")
Architecture Notes
Auto-cleanup
Windows are automatically unregistered when closed (no manual cleanup needed):
# WindowManager hooks into closeEvent
window.closeEvent = lambda event: (
unregister_from_registry(),
call_original_closeEvent(event),
)
Fail-Loud on Stale References
If a window is deleted but still in the registry, a RuntimeError is raised and the stale reference is cleaned up:
try:
window.isVisible() # Test if still valid
except RuntimeError:
# Window deleted - clean up stale reference
del WindowManager._scoped_windows[scope_id]
Benefits
Prevents duplicate windows: only one config window per plate.
Better UX: focusing brings existing window to front.
Auto-cleanup: no memory leaks from forgotten references.
Extensible: navigation API ready for inheritance tracking.
Fail-loud: catches deleted windows early.
Fits OpenHCS patterns: similar to
ObjectStateRegistryfor states.
Declarative Window Specifications
For simple windows that don’t require the full BaseFormDialog machinery, use the WindowSpec pattern with ManagedWindow classes.
Define window specifications declaratively in your main window:
from openhcs.pyqt_gui.services.window_config import WindowSpec
class OpenHCSMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.window_specs = self._get_window_specs()
def _get_window_specs(self) -> dict[str, WindowSpec]:
"""Return declarative window specifications."""
from openhcs.pyqt_gui.windows.managed_windows import (
PlateManagerWindow,
PipelineEditorWindow,
ImageBrowserWindow,
)
return {
"plate_manager": WindowSpec(
window_id="plate_manager",
title="Plate Manager",
window_class=PlateManagerWindow,
initialize_on_startup=True,
),
"pipeline_editor": WindowSpec(
window_id="pipeline_editor",
title="Pipeline Editor",
window_class=PipelineEditorWindow,
),
"image_browser": WindowSpec(
window_id="image_browser",
title="Image Browser",
window_class=ImageBrowserWindow,
),
}
Create a ManagedWindow class for each window type:
# In openhcs/pyqt_gui/windows/managed_windows.py
class PlateManagerWindow(QDialog):
"""Window wrapper for PlateManagerWidget."""
def __init__(self, main_window, service_adapter):
super().__init__(main_window)
self.main_window = main_window
self.service_adapter = service_adapter
self.setWindowTitle("Plate Manager")
self.setModal(False)
self.resize(600, 400)
# Create and add widget
from openhcs.pyqt_gui.widgets.plate_manager import PlateManagerWidget
layout = QVBoxLayout(self)
self.widget = PlateManagerWidget(
self.service_adapter,
self.service_adapter.get_current_color_scheme(),
)
layout.addWidget(self.widget)
# Setup signal connections
self._setup_connections()
def _setup_connections(self):
"""Connect signals to main window and other windows."""
# Connect to main window
self.widget.global_config_changed.connect(
lambda: self.main_window.on_config_changed(
self.service_adapter.get_global_config()
)
)
# Connect progress signals to status bar
if hasattr(self.main_window, "status_bar"):
self._setup_progress_signals()
# Connect to other windows via WindowManager
self._connect_to_pipeline_editor()
Use WindowManager with a factory function:
from pyqt_reactive.services.window_manager import WindowManager
def show_window(self, window_id: str) -> None:
"""Show window using WindowManager."""
factory = self._create_window_factory(window_id)
window = WindowManager.show_or_focus(window_id, factory)
# Optional: Initialize hidden for startup windows
spec = self.window_specs[window_id]
if spec.initialize_on_startup and window_id == "log_viewer":
window.hide()
# Ensure flash overlay is ready
self._ensure_flash_overlay(window)
def _create_window_factory(self, window_id: str) -> Callable[[], QDialog]:
"""Create factory function for a window."""
spec = self.window_specs[window_id]
def factory() -> QDialog:
return spec.window_class(self, self.service_adapter)
return factory
ManagedWindows can communicate via WindowManager:
def _connect_to_pipeline_editor(self):
"""Connect plate manager to pipeline editor."""
from pyqt_reactive.services.window_manager import WindowManager
# Get pipeline window if it exists
pipeline_window = WindowManager._scoped_windows.get("pipeline_editor")
if pipeline_window:
# Access the widget and connect signals
pipeline_widget = pipeline_window.widget
self.widget.plate_selected.connect(pipeline_widget.set_current_plate)
self.widget.orchestrator_config_changed.connect(
pipeline_widget.on_orchestrator_config_changed
)
Use WindowSpec + ManagedWindow for:
Simple container windows (Plate Manager, Image Browser, etc.)
Windows that don’t need ObjectState/form management
Windows that wrap existing widgets
Quick prototyping
Use BaseFormDialog for:
Configuration dialogs (ConfigWindow, DualEditorWindow)
Forms with ParameterFormManager
Windows that need ObjectState integration
Complex multi-tab dialogs
Feature |
WindowSpec + ManagedWindow |
BaseFormDialog |
|---|---|---|
Complexity |
Low |
High |
ObjectState |
Manual |
Automatic |
Form Management |
No |
Built-in |
Singleton Enforcement |
Via WindowManager |
Via WindowManager |
Save/Restore |
Manual |
Automatic |
Best For |
Simple containers |
Complex config dialogs |