Source code for pyqt_reactive.services.window_navigation

"""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] @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 FormFieldWindowNavigationDriver(FieldWindowNavigationDriver): """Field navigation driver that can report async form-build readiness."""
[docs] def __init__( self, select_field: Callable[[str], None], form_manager: Callable[[], FormNavigationManager | None], ) -> None: super().__init__(select_field) self._form_manager = form_manager
[docs] def readiness( self, request: RegisteredWindowNavigationRequest, ) -> RegisteredWindowNavigationReadiness: if request.field_path is None: return RegisteredWindowNavigationReadiness() form_manager = self._form_manager() if form_manager is None: return RegisteredWindowNavigationReadiness( wait_reason=NavigationWaitReason.FORM_MANAGER, ) if len(form_manager.widgets) == 0: return RegisteredWindowNavigationReadiness( wait_reason=NavigationWaitReason.ROOT_WIDGETS, ) if "." in request.field_path and not self._nested_manager_exists( form_manager, request.field_path, ): return RegisteredWindowNavigationReadiness( wait_reason=NavigationWaitReason.NESTED_MANAGER, ) return RegisteredWindowNavigationReadiness()
[docs] def build_complete_callbacks(self) -> tuple[list[Callable[[], None]], ...]: form_manager = self._form_manager() if form_manager is None: return () return (form_manager._on_build_complete_callbacks,)
@staticmethod def _nested_manager_exists( form_manager: FormNavigationManager, field_path: str, ) -> bool: current_manager = form_manager path_parts = field_path.split(".") for part in path_parts[:-1]: if part not in current_manager.nested_managers: return FormFieldWindowNavigationDriver._is_inline_dataclass_field( current_manager, part, ) current_manager = current_manager.nested_managers[part] return True @staticmethod def _is_inline_dataclass_field( form_manager: FormNavigationManager, field_name: str, ) -> bool: from pyqt_reactive.widgets.shared.clickable_help_components import ( InlineDataclassGroupBox, ) return isinstance( form_manager.widgets.get(field_name), InlineDataclassGroupBox, )
[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)
[docs] class FormNavigationManager(ABC): """Form manager surface needed for deferred field navigation.""" widgets: dict nested_managers: dict _on_build_complete_callbacks: list