"""Generic scope-based window factory for pyqt-reactive.
Provides a registry-based system where applications register handlers
for different scope patterns. The factory dispatches to the appropriate
handler based on scope_id patterns.
Example:
# Register a handler for a scope pattern
ScopeWindowRegistry.register_handler(
pattern=r"^$", # Empty scope (global config)
handler=create_global_config_window
)
# Create window via factory
window = WindowFactory.create_window_for_scope(scope_id)
"""
from __future__ import annotations
import logging
import re
from collections.abc import Callable
from dataclasses import dataclass
from objectstate import ObjectState
from PyQt6.QtWidgets import QWidget
logger = logging.getLogger(__name__)
[docs]
@dataclass(frozen=True, slots=True)
class ScopeWindowCreationRequest:
"""Typed request to materialize one ObjectState-backed UI scope."""
scope_id: str
object_state: ObjectState | None = None
ScopeWindowCreationHandler = Callable[[ScopeWindowCreationRequest], QWidget | None]
ScopeWindowScopeResolver = Callable[[str], str]
ScopeWindowFieldPathResolver = Callable[[str, str | None], str | None]
ScopeWindowItemIdResolver = Callable[[str, str | None], str | None]
def _identity_scope(scope_id: str) -> str:
return scope_id
def _identity_field_path(scope_id: str, field_path: str | None) -> str | None:
del scope_id
return field_path
def _identity_item_id(scope_id: str, item_id: str | None) -> str | None:
del scope_id
return item_id
[docs]
@dataclass(frozen=True, slots=True)
class ScopeWindowNavigationTarget:
"""Resolved WindowManager target for one requested ObjectState scope."""
requested_scope_id: str
window_scope_id: str
item_id: str | None
field_path: str | None
[docs]
@dataclass(frozen=True, slots=True)
class ScopeWindowRoute:
"""One registered scope matcher and its window creation behavior."""
pattern: str
handler: ScopeWindowCreationHandler | None = None
window_scope_resolver: ScopeWindowScopeResolver = _identity_scope
field_path_resolver: ScopeWindowFieldPathResolver = _identity_field_path
item_id_resolver: ScopeWindowItemIdResolver = _identity_item_id
[docs]
def matches(self, scope_id: str) -> bool:
return re.match(self.pattern, scope_id) is not None
[docs]
def create_window(self, request: ScopeWindowCreationRequest) -> QWidget | None:
if self.handler is None:
return None
return self.handler(request)
[docs]
def navigation_target(
self,
scope_id: str,
*,
item_id: str | None = None,
field_path: str | None = None,
) -> ScopeWindowNavigationTarget:
return ScopeWindowNavigationTarget(
requested_scope_id=scope_id,
window_scope_id=self.window_scope_resolver(scope_id),
item_id=self.item_id_resolver(scope_id, item_id),
field_path=self.field_path_resolver(scope_id, field_path),
)
[docs]
class ScopeWindowRegistry:
"""Registry mapping scope patterns to window creation handlers.
Handlers are matched in registration order (first match wins).
"""
_routes: list[ScopeWindowRoute] = []
[docs]
@classmethod
def register_handler(
cls,
pattern: str,
handler: ScopeWindowCreationHandler | None = None,
*,
window_scope_resolver: ScopeWindowScopeResolver = _identity_scope,
field_path_resolver: ScopeWindowFieldPathResolver = _identity_field_path,
item_id_resolver: ScopeWindowItemIdResolver = _identity_item_id,
) -> None:
"""Register a handler for scopes matching the given regex pattern.
Args:
pattern: Regex pattern to match against scope_id
handler: Callable(ScopeWindowCreationRequest) -> QWidget | None
"""
cls.register_route(
ScopeWindowRoute(
pattern=pattern,
handler=handler,
window_scope_resolver=window_scope_resolver,
field_path_resolver=field_path_resolver,
item_id_resolver=item_id_resolver,
)
)
[docs]
@classmethod
def register_route(cls, route: ScopeWindowRoute) -> None:
"""Register one nominal scope-window route."""
cls._routes.append(route)
logger.debug(f"[SCOPE_REGISTRY] Registered handler for pattern: {route.pattern}")
[docs]
@classmethod
def unregister_handler(cls, pattern: str) -> None:
"""Remove a handler by pattern."""
cls._routes = [route for route in cls._routes if route.pattern != pattern]
[docs]
@classmethod
def clear(cls) -> None:
"""Clear all registered handlers."""
cls._routes.clear()
[docs]
@classmethod
def find_handler(
cls,
scope_id: str,
) -> ScopeWindowRoute | None:
"""Find the first route matching the scope_id."""
for route in cls._routes:
if route.matches(scope_id):
return route
return None
[docs]
class WindowFactory:
"""Generic window factory that dispatches to registered handlers.
Applications register handlers for their specific scope patterns,
then use this factory to create windows without hardcoding domain logic.
"""
[docs]
@classmethod
def create_window_for_scope(
cls,
scope_id: str,
object_state: ObjectState | None = None,
) -> QWidget | None:
"""Create a window for the given scope_id.
Dispatches to the first registered handler that matches the scope_id.
Args:
scope_id: Unique identifier for the scope/object
object_state: Optional ObjectState instance (for time-travel scenarios)
Returns:
The created window, or None if no handler matched
"""
route = ScopeWindowRegistry.find_handler(scope_id)
if route is not None:
return route.create_window(
ScopeWindowCreationRequest(
scope_id=scope_id,
object_state=object_state,
)
)
logger.warning(f"[WINDOW_FACTORY] No handler found for scope_id: {scope_id}")
return None