"""Nominal window-navigation capabilities used by pyqt-reactive services."""
from __future__ import annotations
from abc import ABC
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from objectstate import ObjectState
from PyQt6.QtWidgets import QWidget
[docs]
@dataclass(frozen=True, slots=True)
class WindowNavigationRequest:
"""Typed request to focus a scope window and reveal an item or field."""
scope_id: str
object_state: ObjectState | None = None
item_id: str | None = None
field_path: str | None = None
create_if_missing: bool = True
avoid_widgets: tuple[QWidget, ...] = ()
@property
def has_target(self) -> bool:
return self.item_id is not None or self.field_path is not None
[docs]
@dataclass(frozen=True, slots=True)
class WindowNavigationResult:
"""Outcome of one scope-window navigation request."""
request: WindowNavigationRequest
window: QWidget | None
focused: bool
created: bool
window_scope_id: str | None = None
@property
def navigated(self) -> bool:
return self.focused and self.request.has_target
[docs]
class NavigationWaitReason(Enum):
FORM_MANAGER = "form manager"
ROOT_WIDGETS = "root widgets"
NESTED_MANAGER = "nested manager"
FIELD_TARGET = "field target"
LAYOUT = "layout"
LIST_ITEMS = "list items"
[docs]
@dataclass(frozen=True, slots=True)
class RegisteredWindowNavigationRequest:
"""Navigation request after a WindowManager scope resolved to a widget."""
window: QWidget
item_id: str | None = None
field_path: str | None = None
@property
def has_target(self) -> bool:
return self.item_id is not None or self.field_path is not None
[docs]
@dataclass(frozen=True, slots=True)
class RegisteredWindowNavigationReadiness:
"""Readiness result from a registered window navigation driver."""
window_alive: bool = True
wait_reason: NavigationWaitReason | None = None
@property
def needs_wait(self) -> bool:
return self.wait_reason is not None
[docs]
class WindowNavigationDriver(ABC):
"""Registered navigation behavior for one WindowManager scope."""
[docs]
def readiness(
self,
request: RegisteredWindowNavigationRequest,
) -> RegisteredWindowNavigationReadiness:
del request
return RegisteredWindowNavigationReadiness()
[docs]
def build_complete_callbacks(self) -> tuple[list[Callable[[], None]], ...]:
return ()
[docs]
def execute(self, request: RegisteredWindowNavigationRequest) -> None:
del request
[docs]
class NullWindowNavigationDriver(WindowNavigationDriver):
"""No-op navigation driver for windows without navigation behavior."""
[docs]
class CompositeWindowNavigationDriver(WindowNavigationDriver):
"""Combine independent navigation drivers declared by one window."""
[docs]
def __init__(self, drivers: tuple[WindowNavigationDriver, ...]) -> None:
self._drivers = drivers
[docs]
def readiness(
self,
request: RegisteredWindowNavigationRequest,
) -> RegisteredWindowNavigationReadiness:
for driver in self._drivers:
readiness = driver.readiness(request)
if not readiness.window_alive or readiness.needs_wait:
return readiness
return RegisteredWindowNavigationReadiness()
[docs]
def build_complete_callbacks(self) -> tuple[list[Callable[[], None]], ...]:
callback_lists: list[list[Callable[[], None]]] = []
for driver in self._drivers:
callback_lists.extend(driver.build_complete_callbacks())
return tuple(callback_lists)
[docs]
def execute(self, request: RegisteredWindowNavigationRequest) -> None:
for driver in self._drivers:
driver.execute(request)
[docs]
class FieldWindowNavigationDriver(WindowNavigationDriver):
"""Navigate field paths through an explicit field-scrolling callable."""
[docs]
def __init__(self, select_field: Callable[[str], None]) -> None:
self._select_field = select_field
[docs]
def execute(self, request: RegisteredWindowNavigationRequest) -> None:
if request.field_path is None:
return
from pyqt_reactive.animation import WindowFlashOverlay
WindowFlashOverlay.get_for_window(request.window)
self._select_field(request.field_path)
[docs]
class ListItemWindowNavigationDriver(WindowNavigationDriver):
"""Navigate list items through explicit item-selection/readiness callables."""
[docs]
def __init__(
self,
select_item: Callable[[str], None],
has_navigation_items: Callable[[], bool],
) -> None:
self._select_item = select_item
self._has_navigation_items = has_navigation_items
[docs]
def readiness(
self,
request: RegisteredWindowNavigationRequest,
) -> RegisteredWindowNavigationReadiness:
if request.item_id is None:
return RegisteredWindowNavigationReadiness()
if self._has_navigation_items():
return RegisteredWindowNavigationReadiness()
return RegisteredWindowNavigationReadiness(
wait_reason=NavigationWaitReason.LIST_ITEMS,
)
[docs]
def execute(self, request: RegisteredWindowNavigationRequest) -> None:
if request.item_id is None:
return
self._select_item(request.item_id)