Source code for pymodaq_gui.utils.widget_sync.factories

"""
Factory methods for common widget types.

This module can be extended by users to add their own factory methods.
"""
from __future__ import annotations

from typing import TYPE_CHECKING, Any
from weakref import ref

if TYPE_CHECKING:
    from qtpy.QtWidgets import QWidget
    from .core import WidgetSync, SyncMode


[docs] class WidgetSyncFactories: """ Mixin class providing factory methods for common Qt widgets. Users can create their own factory methods by: 1. Inheriting from WidgetSync 2. Adding their own @classmethod factories Example: class MyWidgetSync(WidgetSync): @classmethod def for_my_custom_widget(cls, widget, initial=None): return cls.for_property(widget, 'customProperty', 'customSignal', initial) """
[docs] @classmethod def for_property(cls, widget: QWidget, property_name: str, signal_name: str | None = None, initial: Any = None, mode: SyncMode | None = None, data_type: type | None = None, validator: Any = None) -> WidgetSync: """ Create a sync for any Qt property with auto-detection. This is the most flexible factory - all others build on this. Parameters ---------- widget : QWidget The first widget to sync property_name : str Name of the Qt property (e.g., 'checked', 'value', 'text') signal_name : str, optional Name of the change signal. If None, auto-detects from property initial : Any, optional Initial value (if None, uses widget's current value) mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) data_type : type, optional Explicit data type for type checking (default: inferred from initial) validator : callable, optional Optional validator function to correct/constrain values Returns ------- WidgetSync A new sync instance with the widget connected Example ------- >>> sync = WidgetSync.for_property(my_spinbox, 'value', initial=50) >>> sync.add(other_spinbox) # Add more spinboxes """ if mode is None: from .core import SyncMode mode = SyncMode.BIDIRECTIONAL # Get property metadata for auto-detection meta = widget.metaObject() prop_index = meta.indexOfProperty(property_name) if prop_index == -1: raise ValueError( f"Property '{property_name}' not found on {type(widget).__name__}", ) prop = meta.property(prop_index) # Auto-detect signal if not provided if signal_name is None: notify_signal = prop.notifySignal() if notify_signal.isValid(): signal_name = notify_signal.name().data().decode() else: raise ValueError( f"Property '{property_name}' has no notify signal. " f"Please provide signal_name explicitly.", ) # Get signal object try: signal = getattr(widget, signal_name) except AttributeError: raise ValueError( f"Signal '{signal_name}' not found on {type(widget).__name__}", ) # Store property name in local variable to avoid capturing self in lambda prop_name = property_name # Create weak reference to widget to avoid circular references widget_ref = ref(widget) # Create getter/setter using weak reference def getter(): w = widget_ref() return w.property(prop_name) if w is not None else None def setter(value): w = widget_ref() if w is not None: w.setProperty(prop_name, value) # Determine initial value if initial is None: initial = widget.property(prop_name) # Use widget directly before connecting # Create sync and bind sync = cls(initial_value=initial, data_type=data_type, validator=validator) sync.bind(widget, signal, getter, setter, mode) # Store pattern info in the connection for add() method to use widget_id = id(widget) connection_key = (widget_id, None) # Use composite key (widget_id, property_key) if connection_key in sync._connections: sync._connections[connection_key]['property_name'] = property_name sync._connections[connection_key]['signal_name'] = signal_name return sync
[docs] @classmethod def for_checkbox(cls, checkbox, initial: bool = False, mode=None, validator=None) -> WidgetSync: """ Create a sync for checkbox widgets. Parameters ---------- checkbox : QCheckBox The first checkbox to sync initial : bool, optional Initial checked state (default: False) mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) validator : callable, optional Optional validator function Returns ------- WidgetSync A new sync instance Example ------- >>> sync = WidgetSync.for_checkbox(my_checkbox, initial=True) >>> sync.add(another_checkbox) """ from .core import SyncMode if mode is None: mode = SyncMode.BIDIRECTIONAL return cls.for_property(checkbox, 'checked', 'toggled', initial, mode, validator=validator)
[docs] @classmethod def for_spinbox(cls, spinbox, initial: int | float = 0, mode=None, validator=None) -> WidgetSync: """ Create a sync for QSpinBox or QDoubleSpinBox widgets. Parameters ---------- spinbox : QSpinBox | QDoubleSpinBox The first spinbox to sync initial : int or float, optional Initial value (default: 0) mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) validator : callable, optional Optional validator function Returns ------- WidgetSync A new sync instance Example ------- >>> sync = WidgetSync.for_spinbox(spinbox1, initial=50) >>> sync.add(spinbox2) >>> sync.add(spinbox3) """ from .core import SyncMode if mode is None: mode = SyncMode.BIDIRECTIONAL return cls.for_property(spinbox, 'value', 'valueChanged', initial, mode, validator=validator)
[docs] @classmethod def for_slider(cls, slider, initial: int = 0, mode=None, validator=None) -> WidgetSync: """ Create a sync for QSlider widgets. Parameters ---------- slider : QSlider The first slider to sync initial : int, optional Initial value (default: 0) mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) validator : callable, optional Optional validator function Returns ------- WidgetSync A new sync instance Example ------- >>> sync = WidgetSync.for_slider(slider1, initial=75) >>> sync.add(slider2) """ from .core import SyncMode if mode is None: mode = SyncMode.BIDIRECTIONAL return cls.for_property(slider, 'value', 'valueChanged', initial, mode, validator=validator)
[docs] @classmethod def for_combobox(cls, combobox, initial: int | str = 0, use_text: bool = False, mode=None, validator=None) -> WidgetSync: """ Create a sync for QComboBox widgets. Parameters ---------- combobox : QComboBox The first combobox to sync initial : int or str, optional Initial value - index if use_text=False, text if use_text=True (default: 0) use_text : bool, optional If True, sync currentText; if False, sync currentIndex (default: False) mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) validator : callable, optional Optional validator function Returns ------- WidgetSync A new sync instance Examples -------- >>> # Sync by index (default) >>> sync = WidgetSync.for_combobox(combo1, initial=0) >>> sync.add(combo2) >>> # Sync by text >>> sync = WidgetSync.for_combobox(combo1, initial="Option A", use_text=True) >>> sync.add(combo2) """ from .core import SyncMode if mode is None: mode = SyncMode.BIDIRECTIONAL if use_text: return cls.for_property(combobox, 'currentText', 'currentTextChanged', initial, mode, validator=validator) else: return cls.for_property(combobox, 'currentIndex', 'currentIndexChanged', initial, mode, validator=validator)
[docs] @classmethod def for_lineedit(cls, lineedit, initial: str = "", mode=None, validator=None) -> WidgetSync: """ Create a sync for QLineEdit widgets. Parameters ---------- lineedit : QLineEdit The first line edit to sync initial : str, optional Initial text (default: "") mode : SyncMode, optional Sync mode (default: BIDIRECTIONAL) validator : callable, optional Optional validator function Returns ------- WidgetSync A new sync instance Example ------- >>> sync = WidgetSync.for_lineedit(edit1, initial="Hello") >>> sync.add(edit2) """ from .core import SyncMode if mode is None: mode = SyncMode.BIDIRECTIONAL return cls.for_property(lineedit, 'text', 'textChanged', initial, mode, validator=validator)