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. Basic Usage ----------- Show or focus a window via a factory callback: .. code-block:: python from pyqt_reactive.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: .. code-block:: python :caption: Before WindowManager (duplicates) config_window = ConfigWindow(...) config_window.show() .. code-block:: python :caption: After WindowManager (singleton per scope) WindowManager.show_or_focus(scope_id, lambda: ConfigWindow(...)) Migration Examples ------------------ PlateManager: Edit Config Button ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python :caption: Before def action_edit_config(self): """Edit configuration for selected plate.""" config_window = ConfigWindow( config_class=PipelineConfig, initial_config=self._get_current_config(), parent=self, on_save_callback=self._on_config_saved, scope_id=str(self.selected_plate_path), ) config_window.show() .. code-block:: python :caption: After def action_edit_config(self): """Edit configuration for selected plate.""" from pyqt_reactive.services.window_manager import WindowManager scope_id = str(self.selected_plate_path) def create_window(): return ConfigWindow( config_class=PipelineConfig, initial_config=self._get_current_config(), parent=self, on_save_callback=self._on_config_saved, scope_id=scope_id, ) WindowManager.show_or_focus(scope_id, create_window) PipelineEditor: Edit Step Button ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python :caption: Before def action_edit(self): """Edit selected step.""" step_window = DualEditorWindow( editing_step=selected_step, parent=self, on_save_callback=self._on_step_saved, scope_id=f"{self.scope_id}::step_{index}", ) step_window.show() .. code-block:: python :caption: After def action_edit(self): """Edit selected step.""" from pyqt_reactive.services.window_manager import WindowManager scope_id = f"{self.scope_id}::step_{index}" def create_window(): return DualEditorWindow( editing_step=selected_step, parent=self, on_save_callback=self._on_step_saved, scope_id=scope_id, ) WindowManager.show_or_focus(scope_id, create_window) Navigation API (Future: Inheritance Tracking) --------------------------------------------- Basic navigation calls: .. code-block:: python # Focus window without navigation WindowManager.focus_and_navigate(scope_id="plate1") # Focus window and navigate to item WindowManager.focus_and_navigate( scope_id="plate1", item_id="step_3", # Select step 3 in list ) # Focus window and navigate to field WindowManager.focus_and_navigate( scope_id="plate1", field_path="well_filter_config.well_filter", # Scroll to this field ) Implementing navigation in windows (optional methods): .. code-block:: python class ConfigWindow(BaseFormDialog): """Config window with navigation support.""" def select_and_scroll_to_field(self, field_path: str): """Navigate to field in tree/form (called by WindowManager). Args: field_path: Dotted path like "well_filter_config.well_filter" """ # 1. Find tree item matching field_path tree_item = self.tree_helper.find_item_by_path(self.tree_widget, field_path) if not tree_item: return # 2. Expand parent nodes parent = tree_item.parent() while parent: parent.setExpanded(True) parent = parent.parent() # 3. Select and scroll to item self.tree_widget.setCurrentItem(tree_item) self.tree_widget.scrollToItem(tree_item) # 4. Flash to highlight self.queue_flash(field_path) class DualEditorWindow(BaseFormDialog): """Step editor with navigation support.""" def select_and_scroll_to_item(self, item_id: str): """Navigate to tab/section for item (called by WindowManager). Args: item_id: Item identifier (e.g., tab index, field name) """ # Switch to tab containing the item tab_index = self._get_tab_for_item(item_id) if tab_index >= 0: self.tab_widget.setCurrentIndex(tab_index) # Scroll to widget widget = self._get_widget_for_item(item_id) if widget: widget.setFocus() Future: Inheritance Tracking ---------------------------- Show the source window and scroll to it when the user clicks an inherited field: .. code-block:: python 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 pyqt_reactive.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 --------------- .. code-block:: python # 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): .. code-block:: python # 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: .. code-block:: python try: window.isVisible() # Test if still valid except RuntimeError: # Window deleted - clean up stale reference del WindowManager._scoped_windows[scope_id] Duck Typing for Navigation ^^^^^^^^^^^^^^^^^^^^^^^^^^ Navigation methods are optional—windows implement them if supported: .. code-block:: python # WindowManager checks if method exists before calling if hasattr(window, "select_and_scroll_to_field"): window.select_and_scroll_to_field(field_path) Benefits -------- 1. Prevents duplicate windows: only one config window per plate. 2. Better UX: focusing brings existing window to front. 3. Auto-cleanup: no memory leaks from forgotten references. 4. Extensible: navigation API ready for inheritance tracking. 5. Fail-loud: catches deleted windows early. 6. Fits pyqt-reactive patterns: similar to ``ObjectStateRegistry`` for states.