Source code for pyqt_reactive.services.signal_service

"""
Consolidated Signal Service.

Merges:
- SignalBlockingService: Context managers for widget signal blocking
- SignalConnectionService: Signal wiring for ParameterFormManager
- CrossWindowRegistration: Context manager for cross-window registration

Key features:
1. Context manager guarantees signal unblocking
2. Supports single or multiple widgets
3. Consolidates all signal wiring logic
4. Cross-window registration with RAII cleanup
"""

from contextlib import contextmanager
from typing import Any, Callable, Optional, TYPE_CHECKING
from PyQt6.QtWidgets import QWidget, QCheckBox, QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox
import logging

if TYPE_CHECKING:
    from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager

logger = logging.getLogger(__name__)


[docs] class SignalService: """ Consolidated service for signal blocking, connection, and cross-window registration. Examples: # Block signals (context manager): with SignalService.block_signals(checkbox): checkbox.setChecked(True) # Multiple widgets: with SignalService.block_signals(widget1, widget2): widget1.setValue(1) widget2.setValue(2) # Connect all signals for a manager: SignalService.connect_all_signals(manager) # Cross-window registration: with SignalService.cross_window_registration(manager): dialog.exec() """ # ========== SIGNAL BLOCKING (from SignalBlockingService) ==========
[docs] @staticmethod @contextmanager def block_signals(*widgets: QWidget): """Context manager for blocking widget signals.""" for widget in widgets: if widget is not None: widget.blockSignals(True) logger.debug(f"Blocked signals on {type(widget).__name__}") try: yield finally: for widget in widgets: if widget is not None: widget.blockSignals(False) logger.debug(f"Unblocked signals on {type(widget).__name__}")
[docs] @staticmethod def with_signals_blocked(widget: QWidget, operation: Callable) -> None: """Execute operation with widget signals blocked (lambda-based).""" with SignalService.block_signals(widget): operation()
[docs] @staticmethod @contextmanager def block_signals_if(condition: bool, *widgets: QWidget): """Conditionally block signals based on a condition.""" if condition: with SignalService.block_signals(*widgets): yield else: yield
[docs] @staticmethod def update_widget_value(widget: QWidget, value, setter: Optional[Callable] = None) -> None: """Update widget value with signals blocked.""" with SignalService.block_signals(widget): if setter: setter(widget, value) else: if isinstance(widget, QCheckBox): widget.setChecked(value) elif isinstance(widget, QLineEdit): widget.setText(str(value) if value is not None else "") elif isinstance(widget, QComboBox): if isinstance(value, int): widget.setCurrentIndex(value) else: widget.setCurrentText(str(value)) elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): widget.setValue(value) else: if hasattr(widget, 'setValue'): widget.setValue(value) else: raise ValueError(f"Cannot auto-detect setter for {type(widget).__name__}")
# ========== SIGNAL CONNECTION (from SignalConnectionService) ==========
[docs] @staticmethod def connect_all_signals(manager: Any) -> None: """Wire all signals for the manager. DISPATCHER ARCHITECTURE: Most signal handling moved to FieldChangeDispatcher. This method now only handles: - Cleanup on destroy """ manager.destroyed.connect(manager.unregister_from_cross_window_updates)
[docs] @staticmethod def register_cross_window_signals(manager: Any) -> None: """Register manager for cross-window updates (only root managers). SIMPLIFIED: No N×N signal wiring. LiveContextService.increment_token() notifies all listeners via simple callbacks. """ if manager._parent_manager is not None: return # Snapshot initial values for change detection (non-None only) manager._initial_values_on_open = {k: v for k, v in manager.state.parameters.items() if v is not None} from objectstate import ObjectStateRegistry logger.debug(f"🔍 REGISTER: {manager.field_id} (total: {len(ObjectStateRegistry.get_all())})")
# ========== CROSS-WINDOW REGISTRATION (from CrossWindowRegistration) ==========
[docs] @staticmethod @contextmanager def cross_window_registration(manager: 'ParameterFormManager'): """ Context manager for cross-window registration. Ensures proper registration and cleanup of form managers for cross-window updates. """ if manager._parent_manager is not None: yield manager return try: SignalService.register_cross_window_signals(manager) yield manager finally: manager.unregister_from_cross_window_updates()