Source code for pyqt_reactive.services.widget_service

"""
Consolidated Widget Service.

Merges:
- WidgetFinderService: Finding widgets in ParameterFormManager
- WidgetStylingService: Read-only styling, dimming, visual state management
- WidgetUpdateService: Low-level widget value update operations

Key features:
1. Centralized widget finding, styling, and update logic
2. Type-safe widget retrieval with fail-loud behavior
3. Signal blocking during value updates
4. Read-only styling that maintains normal appearance
"""

from typing import Any, Optional, List, Type, Callable, Dict
from PyQt6.QtWidgets import (
    QWidget, QCheckBox, QLineEdit, QSpinBox, QDoubleSpinBox, 
    QComboBox, QTextEdit, QAbstractSpinBox
)
from PyQt6.QtCore import Qt
from pyqt_reactive.protocols import ValueGettable
import logging

logger = logging.getLogger(__name__)


[docs] class WidgetService: """ Consolidated service for widget finding, styling, and value updates. Examples: service = WidgetService() # Find widgets: checkbox = WidgetService.find_optional_checkbox(manager, param_name) widget = WidgetService.get_widget_safe(manager, param_name) # Style widgets: WidgetService.make_readonly(widget, color_scheme) WidgetService.apply_dimming(widget, opacity=0.5) # Update widget values: service.update_widget_value(widget, value, param_name, manager=manager) """
[docs] def __init__(self): """Initialize widget service with dependencies.""" from pyqt_reactive.forms.widget_operations import WidgetOperations from pyqt_reactive.forms.widget_strategies import PyQt6WidgetEnhancer self.widget_ops = WidgetOperations self.widget_enhancer = PyQt6WidgetEnhancer
# ========== WIDGET FINDING (from WidgetFinderService) ==========
[docs] @staticmethod def find_optional_checkbox(manager, param_name: str) -> Optional[QCheckBox]: """Find optional checkbox for a parameter.""" if param_name not in manager.widgets: logger.debug(f"No widget for param_name={param_name}") return None container = manager.widgets[param_name] ids = manager.service.generate_field_ids_direct(manager.config.field_id, param_name) checkbox = container.findChild(QCheckBox, ids['optional_checkbox_id']) if checkbox: logger.debug(f"Found optional checkbox for param_name={param_name}") return checkbox
[docs] @staticmethod def find_nested_checkbox(manager, param_name: str) -> Optional[QCheckBox]: """Find checkbox in nested manager's container.""" if param_name not in manager.widgets: return None container = manager.widgets[param_name] ids = manager.service.generate_field_ids_direct(manager.config.field_id, param_name) return container.findChild(QCheckBox, ids['optional_checkbox_id'])
[docs] @staticmethod def find_group_box(container: QWidget, group_box_type: Type = None) -> Optional[QWidget]: """Find group box within container.""" if group_box_type is None: try: from pyqt_reactive.widgets.shared.clickable_help_components import GroupBoxWithHelp group_box_type = GroupBoxWithHelp except ImportError: logger.warning("Could not import GroupBoxWithHelp") return None return container.findChild(group_box_type)
[docs] @staticmethod def get_widget_safe(manager, param_name: str) -> Optional[QWidget]: """Safely get a widget from manager's widgets dict.""" widget = manager.widgets.get(param_name) if widget: logger.debug(f"Found widget for param_name={param_name}, type={type(widget).__name__}") return widget
[docs] @staticmethod def find_all_input_widgets(container: QWidget, widget_ops) -> List[QWidget]: """Find all input widgets within a container.""" widgets = widget_ops.get_all_value_widgets(container) logger.debug(f"Found {len(widgets)} input widgets in container") return widgets
# ========== PARAMETER DISPLAY INFO ==========
[docs] @staticmethod def get_parameter_display_info(param_name: str, param_type: Type, manager=None, description: Optional[str] = None) -> Dict[str, Any]: """ Get parameter display information including description from ObjectState._parameter_descriptions. Args: param_name: Name of the parameter param_type: Type of the parameter manager: Optional ParameterFormManager instance for context description: Optional parameter description to use Returns: Dictionary with display information (field_label, description, etc.) """ # Get manager from context if not provided if manager is None: return {} resolved_description = description param_info = manager.form_structure.get_parameter_info(param_name) if resolved_description is None and param_info is not None: resolved_description = param_info.description if resolved_description is None: dotted = f"{manager.field_id}.{param_name}" if manager.field_id else param_name raw = manager.state.parameter_descriptions.get(dotted) if raw is not None: resolved_description = raw if isinstance(raw, str) else raw.description display_info = manager.service.get_parameter_display_info( param_name, param_type, description=resolved_description, ) logger.debug( "get_parameter_display_info: param_name=%s, description=%s", param_name, display_info.get("description"), ) return display_info
# ========== WIDGET STYLING (from WidgetStylingService) ==========
[docs] @staticmethod def make_readonly(widget: QWidget, color_scheme) -> None: """Make a widget read-only without greying it out.""" if isinstance(widget, (QLineEdit, QTextEdit)): widget.setReadOnly(True) widget.setStyleSheet(f"color: {color_scheme.to_hex(color_scheme.text_primary)};") elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): widget.setReadOnly(True) widget.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.NoButtons) widget.setStyleSheet(f"color: {color_scheme.to_hex(color_scheme.text_primary)};") elif isinstance(widget, QComboBox): widget.setEnabled(False) widget.setStyleSheet(f""" QComboBox:disabled {{ color: {color_scheme.to_hex(color_scheme.text_primary)}; background-color: {color_scheme.to_hex(color_scheme.input_bg)}; }} """) elif isinstance(widget, QCheckBox): widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) widget.setFocusPolicy(Qt.FocusPolicy.NoFocus) else: logger.warning(f"No read-only styling for {type(widget).__name__}")
[docs] @staticmethod def apply_dimming(widget: QWidget, opacity: float = 0.5) -> None: """Apply visual dimming to a widget.""" if not (0.0 <= opacity <= 1.0): raise ValueError(f"Opacity must be 0.0-1.0, got {opacity}") widget.setWindowOpacity(opacity)
[docs] @staticmethod def remove_dimming(widget: QWidget) -> None: """Remove visual dimming from a widget.""" widget.setWindowOpacity(1.0)
[docs] @staticmethod def set_enabled_with_styling(widget: QWidget, enabled: bool, color_scheme=None) -> None: """Set widget enabled state with appropriate styling.""" if enabled: widget.setEnabled(True) if isinstance(widget, (QLineEdit, QTextEdit)): widget.setReadOnly(False) widget.setStyleSheet("") elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): widget.setReadOnly(False) widget.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.UpDownArrows) widget.setStyleSheet("") elif isinstance(widget, QCheckBox): widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) widget.setFocusPolicy(Qt.FocusPolicy.StrongFocus) else: if color_scheme: WidgetService.make_readonly(widget, color_scheme) else: widget.setEnabled(False)
[docs] @staticmethod def clear_stylesheet(widget: QWidget) -> None: """Clear widget's stylesheet.""" widget.setStyleSheet("")
[docs] @staticmethod def apply_error_styling(widget: QWidget, color_scheme) -> None: """Apply error styling to a widget.""" error_color = color_scheme.to_hex(color_scheme.status_error) widget.setStyleSheet(f"border: 2px solid {error_color};")
[docs] @staticmethod def remove_error_styling(widget: QWidget) -> None: """Remove error styling from a widget.""" WidgetService.clear_stylesheet(widget)
# ========== WIDGET VALUE UPDATES (from WidgetUpdateService) ==========
[docs] def update_widget_value( self, widget: QWidget, value: Any, param_name: Optional[str] = None, skip_context_behavior: bool = False, manager=None ) -> None: """Update widget value with signal blocking and optional placeholder application.""" if self._widget_value_needs_update(widget, value): self._execute_with_signal_blocking(widget, lambda: self._dispatch_widget_update(widget, value)) if not skip_context_behavior and manager: self._apply_context_behavior(widget, value, param_name, manager)
@staticmethod def _widget_value_needs_update(widget: QWidget, value: Any) -> bool: """Return whether a widget assignment would change its nominal value.""" if not isinstance(widget, ValueGettable): return True return widget.get_value() != value def _execute_with_signal_blocking(self, widget: QWidget, operation: Callable) -> None: """Execute operation with widget signals blocked.""" widget.blockSignals(True) operation() widget.blockSignals(False) def _dispatch_widget_update(self, widget: QWidget, value: Any) -> None: """Dispatch widget update using ABC-based operations.""" self.widget_ops.set_value(widget, value) def _apply_context_behavior( self, widget: QWidget, value: Any, param_name: str, manager ) -> None: """Apply placeholder behavior based on value.""" logger.debug( "Applying context behavior for %s.%s, value=%r", manager.field_id, param_name, value, ) if not param_name or not manager.object_instance: logger.debug("Skipping context behavior without param_name or object_instance") return if value is None: logger.debug("Computing placeholder for None value") from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager from objectstate import ObjectStateRegistry, build_context_stack # Get ancestor objects for context stack ancestor_objects = ObjectStateRegistry.get_ancestor_objects(manager.scope_id or "") # Build context stack stack = build_context_stack( object_instance=manager.object_instance, ancestor_objects=ancestor_objects, ) with stack: placeholder_text = manager.service.get_placeholder_text(param_name, type(manager.object_instance)) logger.debug("Resolved placeholder text: %r", placeholder_text) if placeholder_text: # Get the resolved value directly for type-safe placeholder application try: resolved_value = getattr(manager.object_instance, param_name) except AttributeError: resolved_value = None self.widget_enhancer.apply_placeholder_with_value(widget, resolved_value, placeholder_text) else: logger.debug("No placeholder text returned") elif value is not None: if self.widget_enhancer.has_placeholder_state(widget): logger.debug("Clearing placeholder state for concrete value") self.widget_enhancer._clear_placeholder_state(widget)
[docs] def clear_widget_to_default_state(self, widget: QWidget) -> None: """Clear widget to its default/empty state for reset operations.""" if isinstance(widget, QLineEdit): widget.clear() elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): widget.setValue(widget.minimum()) elif isinstance(widget, QComboBox): widget.setCurrentIndex(-1) elif isinstance(widget, QCheckBox): widget.setChecked(False) elif isinstance(widget, QTextEdit): widget.clear() else: widget.clear()
[docs] def update_combo_box(self, widget: QComboBox, value: Any) -> None: """Update combo box with value matching.""" widget.setCurrentIndex( -1 if value is None else next((i for i in range(widget.count()) if widget.itemData(i) == value), -1) )
[docs] def update_checkbox_group(self, widget: QWidget, value: Any) -> None: """Update checkbox group using functional operations.""" if isinstance(value, list): [cb.setChecked(False) for cb in widget._checkboxes.values()] [widget._checkboxes[v].setChecked(True) for v in value if v in widget._checkboxes]
[docs] def get_widget_value(self, widget: QWidget) -> Any: """Get widget value using ABC-based polymorphism.""" from pyqt_reactive.protocols.widget_protocols import ( PlaceholderStateTrackable, ValueGettable, WidgetCapability, widget_supports_capability, ) if ( widget_supports_capability(widget, WidgetCapability.PLACEHOLDER_STATE) and isinstance(widget, PlaceholderStateTrackable) and widget.has_placeholder_state() ): return None if isinstance(widget, ValueGettable): return widget.get_value() return None