Source code for pyqt_reactive.protocols.widget_adapters

"""
Widget adapters that wrap Qt widgets to implement OpenHCS ABCs.

Normalizes Qt's inconsistent APIs:
- QLineEdit.text() vs QSpinBox.value() vs QComboBox.currentData()
- QLineEdit.setText() vs QSpinBox.setValue() vs QComboBox.setCurrentIndex()
- QLineEdit.setPlaceholderText() vs QSpinBox.setSpecialValueText()

All adapters implement consistent interface via ABCs:
- get_value() / set_value() for all widgets
- set_placeholder() for all widgets
- connect_change_signal() for all widgets

Mirrors MemoryTypeConverter pattern - adapters normalize inconsistent APIs.
"""

from typing import Any, Callable, Optional
from enum import Enum
from abc import ABCMeta

try:
    from PyQt6.QtWidgets import (
        QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox, QWidget, QGroupBox
    )
    from PyQt6.QtCore import Qt, QObject
    PYQT6_AVAILABLE = True
    # PyQt-specific metaclass that combines ABCMeta with Qt's metaclass
    # Order matters: ABCMeta first (it's the "primary" metaclass for ABC functionality)
    _QtMetaclass = type(QObject)
[docs] class PyQtWidgetMeta(_QtMetaclass, ABCMeta): """Metaclass for PyQt widgets that need ABC support.""" pass
except ImportError: PYQT6_AVAILABLE = False # Create dummy base classes for type hints QLineEdit = QSpinBox = QDoubleSpinBox = QComboBox = QCheckBox = QWidget = object PyQtWidgetMeta = ABCMeta from .widget_protocols import ( ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, ChangeSignalEmitter ) if PYQT6_AVAILABLE:
[docs] class LineEditAdapter(QLineEdit, ValueGettable, ValueSettable, PlaceholderCapable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for QLineEdit implementing OpenHCS ABCs. Normalizes Qt API to OpenHCS contracts: - .text() → .get_value() - .setText() → .set_value() - .setPlaceholderText() → .set_placeholder() - .textChanged → .connect_change_signal() """ _widget_id = "line_edit"
[docs] def get_value(self) -> Any: """Implement ValueGettable ABC.""" text = self.text().strip() return None if text == "" else text
[docs] def set_value(self, value: Any) -> None: """Implement ValueSettable ABC.""" self.setText("" if value is None else str(value))
[docs] def set_placeholder(self, text: str) -> None: """Implement PlaceholderCapable ABC.""" self.setPlaceholderText(text)
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" self.textChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" try: self.textChanged.disconnect(callback) except TypeError: # Signal not connected - ignore pass
[docs] class SpinBoxAdapter(QSpinBox, ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for QSpinBox implementing OpenHCS ABCs. Handles None values using special value text mechanism. When value is None, displays placeholder text at minimum value. """ _widget_id = "spin_box"
[docs] def __init__(self, parent=None): super().__init__(parent) # Configure for None-aware behavior self.setSpecialValueText(" ") # Empty special value = None self.setRange(-2147483648, 2147483647) # Default int range
[docs] def get_value(self) -> Any: """Implement ValueGettable ABC.""" if self.value() == self.minimum() and self.specialValueText(): return None return self.value()
[docs] def set_value(self, value: Any) -> None: """Implement ValueSettable ABC.""" if value is None: self.setValue(self.minimum()) else: self.setValue(int(value))
[docs] def set_placeholder(self, text: str) -> None: """Implement PlaceholderCapable ABC.""" # For spinbox, placeholder is shown when at minimum with special value text self.setSpecialValueText(text)
[docs] def configure_range(self, minimum: float, maximum: float) -> None: """Implement RangeConfigurable ABC.""" self.setRange(int(minimum), int(maximum))
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" self.valueChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" try: self.valueChanged.disconnect(callback) except TypeError: pass
[docs] class DoubleSpinBoxAdapter(QDoubleSpinBox, ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for QDoubleSpinBox implementing OpenHCS ABCs. Handles None values and floating-point ranges. """ _widget_id = "double_spin_box"
[docs] def __init__(self, parent=None): from pyqt_reactive.forms.widget_strategies import WidgetConfig super().__init__(parent) self.setSpecialValueText(" ") self.setRange(-1e308, 1e308) # Default float range self.setDecimals(WidgetConfig.FLOAT_PRECISION) # Use configured precision
[docs] def get_value(self) -> Any: """Implement ValueGettable ABC.""" if self.value() == self.minimum() and self.specialValueText(): return None return self.value()
[docs] def set_value(self, value: Any) -> None: """Implement ValueSettable ABC.""" if value is None: self.setValue(self.minimum()) else: self.setValue(float(value))
[docs] def set_placeholder(self, text: str) -> None: """Implement PlaceholderCapable ABC.""" self.setSpecialValueText(text)
[docs] def configure_range(self, minimum: float, maximum: float) -> None: """Implement RangeConfigurable ABC.""" self.setRange(minimum, maximum)
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" self.valueChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" try: self.valueChanged.disconnect(callback) except TypeError: pass
[docs] class ComboBoxAdapter(QComboBox, ValueGettable, ValueSettable, PlaceholderCapable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for QComboBox implementing OpenHCS ABCs. Stores actual values in itemData, not just display text. Supports enum population and selection. """ _widget_id = "combo_box"
[docs] def get_value(self) -> Any: """Implement ValueGettable ABC.""" if self.currentIndex() < 0: return None return self.itemData(self.currentIndex())
[docs] def set_value(self, value: Any) -> None: """Implement ValueSettable ABC.""" # Find index of item with matching data for i in range(self.count()): if self.itemData(i) == value: self.setCurrentIndex(i) return # Value not found - clear selection self.setCurrentIndex(-1)
[docs] def set_placeholder(self, text: str) -> None: """Implement PlaceholderCapable ABC.""" # ComboBox placeholder is shown when no selection self.setPlaceholderText(text)
[docs] def populate_enum(self, enum_type: type) -> None: """ Populate combobox with enum values. Args: enum_type: The Enum class to populate from """ if not isinstance(enum_type, type) or not issubclass(enum_type, Enum): raise TypeError(f"{enum_type} is not an Enum type") self.clear() for enum_value in enum_type: self.addItem(enum_value.name, enum_value)
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" self.currentIndexChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" try: self.currentIndexChanged.disconnect(callback) except TypeError: pass
[docs] class CheckBoxAdapter(QCheckBox, ValueGettable, ValueSettable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for QCheckBox implementing OpenHCS ABCs. Returns bool values, treats None as False. """ _widget_id = "check_box"
[docs] def get_value(self) -> Any: """Implement ValueGettable ABC.""" return self.isChecked()
[docs] def set_value(self, value: Any) -> None: """Implement ValueSettable ABC.""" self.setChecked(bool(value) if value is not None else False)
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" self.stateChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" try: self.stateChanged.disconnect(callback) except TypeError: pass
[docs] class CheckboxGroupAdapter(QGroupBox, ValueGettable, ValueSettable, ChangeSignalEmitter, metaclass=PyQtWidgetMeta): """ Adapter for checkbox group (List[Enum]) implementing OpenHCS ABCs. Manages a group of NoneAwareCheckBox widgets for multi-selection. Returns List[Enum] or None (for placeholder state). This eliminates duck typing - instead of checking for _checkboxes attribute, we use proper ABC inheritance. """ _widget_id = "checkbox_group"
[docs] def __init__(self, parent=None): super().__init__(parent) # Dictionary mapping enum values to checkbox widgets self._checkboxes = {}
[docs] def checkbox_items(self) -> tuple[tuple[Any, Any], ...]: """Return enum-to-checkbox items for group-level placeholder logic.""" return tuple(self._checkboxes.items())
[docs] def checkbox_widgets(self) -> tuple[Any, ...]: """Return checkboxes managed by this group.""" return tuple(self._checkboxes.values())
[docs] def get_value(self) -> Any: """ Implement ValueGettable ABC. Returns: - None if all checkboxes are in placeholder state (inherit from parent) - List[Enum] of checked items if any checkbox has been clicked """ # Check if any checkbox has a concrete value (not placeholder) has_concrete_value = any( checkbox.get_value() is not None for checkbox in self._checkboxes.values() ) if not has_concrete_value: # All checkboxes are in placeholder state - return None to inherit from parent return None # All checkboxes are concrete (signal handler converted them) # Return list of enum values where checkbox is checked return [ enum_val for enum_val, checkbox in self._checkboxes.items() if checkbox.get_value() == True ]
[docs] def set_value(self, value: Any) -> None: """ Implement ValueSettable ABC. Args: value: None (placeholder state) or List[Enum] (concrete values) """ if value is None: # None means inherit from parent - initialize all checkboxes in placeholder state for checkbox in self._checkboxes.values(): checkbox.set_value(None) elif isinstance(value, list): # Explicit list - set concrete values for enum_value, checkbox in self._checkboxes.items(): # Set to True if in list, False if not (both are concrete values) checkbox.set_value(enum_value in value) else: # Fallback: treat as None (placeholder state) for checkbox in self._checkboxes.values(): checkbox.set_value(None)
[docs] def connect_change_signal(self, callback: Callable[[Any], None]) -> None: """ Implement ChangeSignalEmitter ABC. Connects to all checkboxes in the group. """ for checkbox in self._checkboxes.values(): checkbox.stateChanged.connect(lambda: callback(self.get_value()))
[docs] def disconnect_change_signal(self, callback: Callable[[Any], None]) -> None: """Implement ChangeSignalEmitter ABC.""" for checkbox in self._checkboxes.values(): try: checkbox.stateChanged.disconnect(callback) except TypeError: pass
# Manual registration of adapters in WIDGET_IMPLEMENTATIONS # (PyQtWidgetMeta doesn't auto-register like WidgetMeta does) if PYQT6_AVAILABLE: from .widget_protocols import ( ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, ChangeSignalEmitter ) # Register all adapter classes adapters = [ LineEditAdapter, SpinBoxAdapter, DoubleSpinBoxAdapter, ComboBoxAdapter, CheckBoxAdapter, CheckboxGroupAdapter, ] for adapter_class in adapters: widget_id = adapter_class._widget_id # Track capabilities capabilities = set() abc_types = { ValueGettable, ValueSettable, PlaceholderCapable, RangeConfigurable, ChangeSignalEmitter } for abc_type in abc_types: if issubclass(adapter_class, abc_type): capabilities.add(abc_type)