"""
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)
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,
PlaceholderStateTrackable, RangeConfigurable, ChangeSignalEmitter,
ResolvedValuePreviewSettable,
WidgetCapability, widget_supports_capability
)
if PYQT6_AVAILABLE:
[docs]
class PlaceholderStateMixin(PlaceholderStateTrackable):
"""Nominal placeholder-state storage for Qt widget adapters."""
widget_capabilities = frozenset({WidgetCapability.PLACEHOLDER_STATE})
[docs]
def __init__(self, *args, **kwargs):
self._placeholder_state_active = False
self._cached_placeholder_text: str | None = None
self._cached_placeholder_resolved_value: Any = None
super().__init__(*args, **kwargs)
[docs]
def has_placeholder_state(self) -> bool:
return self._placeholder_state_active or self._cached_placeholder_text is not None
[docs]
def mark_placeholder_state(self) -> None:
self._placeholder_state_active = True
[docs]
def clear_placeholder_state(self) -> None:
self._placeholder_state_active = False
[docs]
def cached_placeholder_text(self) -> str | None:
return self._cached_placeholder_text
[docs]
def set_cached_placeholder_text(self, placeholder_text: str) -> None:
self._cached_placeholder_text = placeholder_text
[docs]
def cached_placeholder_resolved_value(self) -> Any:
return self._cached_placeholder_resolved_value
[docs]
def set_cached_placeholder_resolved_value(self, value: Any) -> None:
self._cached_placeholder_resolved_value = value
[docs]
def clear_placeholder_cache(self) -> None:
self._cached_placeholder_text = None
self._cached_placeholder_resolved_value = None
[docs]
class LineEditAdapter(PlaceholderStateMixin, 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(PlaceholderStateMixin, 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 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(PlaceholderStateMixin, 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 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(PlaceholderStateMixin, 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(PlaceholderStateMixin, 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(PlaceholderStateMixin, QGroupBox, ValueGettable, ValueSettable,
ResolvedValuePreviewSettable, 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 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:
raise TypeError(
"CheckboxGroupAdapter values must be list or None, "
f"got {type(value).__name__}."
)
[docs]
def set_resolved_value_preview(self, value: Any) -> None:
"""Preview a resolved enum list while preserving raw placeholder state."""
if value is None:
inherited_values = frozenset()
elif isinstance(value, list):
inherited_values = frozenset(value)
else:
raise TypeError(
"CheckboxGroupAdapter resolved previews must be list or None, "
f"got {type(value).__name__}."
)
for enum_value, checkbox in self.checkbox_items():
if not isinstance(checkbox, ResolvedValuePreviewSettable):
raise TypeError(
"Checkbox group children must implement "
"ResolvedValuePreviewSettable."
)
checkbox.set_resolved_value_preview(enum_value in inherited_values)
self.mark_placeholder_state()
[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(
self._make_checkbox_group_handler(callback)
)
def _make_checkbox_group_handler(self, callback: Callable[[Any], None]) -> Callable[[Any], None]:
"""Return a handler that promotes the whole enum list to concrete."""
def handler(_state: Any) -> None:
for checkbox in self.checkbox_widgets():
checkbox.convert_placeholder_to_concrete()
self.clear_placeholder_state()
callback(self.get_value())
return handler
[docs]
def has_placeholder_state(self) -> bool:
return super().has_placeholder_state() or any(
widget_supports_capability(checkbox, WidgetCapability.PLACEHOLDER_STATE)
and isinstance(checkbox, PlaceholderStateTrackable)
and checkbox.has_placeholder_state()
for checkbox in self.checkbox_widgets()
)
[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,
PlaceholderStateTrackable, RangeConfigurable, ChangeSignalEmitter,
ResolvedValuePreviewSettable,
)
# 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,
PlaceholderStateTrackable, RangeConfigurable, ChangeSignalEmitter,
ResolvedValuePreviewSettable,
}
for abc_type in abc_types:
if issubclass(adapter_class, abc_type):
capabilities.add(abc_type)