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:

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:

Before WindowManager (duplicates)
config_window = ConfigWindow(...)
config_window.show()
After WindowManager (singleton per scope)
WindowManager.show_or_focus(scope_id, lambda: ConfigWindow(...))

Migration Examples

PlateManager: Edit Config Button

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()
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

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()
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)

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 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

# 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]

Duck Typing for Navigation

Navigation methods are optional—windows implement them if supported:

# 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.