# The functions in this module are used to connect callback properties to Qt
# widgets.
import math
from datetime import datetime
import numpy as np
from qtpy import QtGui, QtWidgets
from qtpy.QtCore import QDateTime, Qt
from ..core import add_callback, remove_callback
from ..selection import ChoiceSeparator, SelectionCallbackProperty
__all__ = [
"connect_checkable_button",
"connect_text",
"connect_combo_data",
"connect_combo_text",
"connect_float_text",
"connect_value",
"connect_combo_selection",
"connect_list_selection",
"connect_datetime",
"BaseConnection",
]
class UserDataWrapper:
def __init__(self, data):
self.data = data
[docs]
class BaseConnection:
def __init__(self, instance, prop, widget):
self._instance = instance
self._prop = prop
self._widget = widget
[docs]
class connect_text(BaseConnection):
"""
Connect a string callback property with a Qt widget containing text.
Parameters
----------
instance : object
The class instance that the callback property is attached to
prop : str
The name of the callback property
widget : QtWidget
The Qt widget to connect. This should implement the ``setText`` and
``text`` methods as well optionally the ``editingFinished`` signal.
"""
def __init__(self, instance, prop, widget):
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_prop(self):
value = self._widget.text()
setattr(self._instance, self._prop, value)
[docs]
def update_widget(self, value):
if hasattr(self._widget, "editingFinished"):
self._widget.blockSignals(True)
self._widget.setText(value)
self._widget.blockSignals(False)
self._widget.editingFinished.emit()
else:
self._widget.setText(value)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
try:
self._widget.editingFinished.connect(self.update_prop)
except AttributeError:
pass
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
try:
self._widget.editingFinished.disconnect(self.update_prop)
except AttributeError:
pass
[docs]
class connect_combo_data(BaseConnection):
"""
Connect a callback property with a QComboBox widget based on the userData.
Parameters
----------
instance : object
The class instance that the callback property is attached to
prop : str
The name of the callback property
widget : QComboBox
The combo box to connect.
See Also
--------
connect_combo_text: connect a callback property with a QComboBox widget based on the text.
"""
def __init__(self, instance, prop, widget):
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_prop(self, idx):
if idx == -1:
setattr(self._instance, self._prop, None)
else:
data_wrapper = self._widget.itemData(idx)
if data_wrapper is None:
setattr(self._instance, self._prop, None)
else:
setattr(self._instance, self._prop, data_wrapper.data)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.disconnect(self.update_prop)
[docs]
class connect_combo_text(BaseConnection):
"""
Connect a callback property with a QComboBox widget based on the text.
Parameters
----------
instance : object
The class instance that the callback property is attached to
prop : str
The name of the callback property
widget : QComboBox
The combo box to connect.
See Also
--------
connect_combo_data: connect a callback property with a QComboBox widget based on the userData.
"""
def __init__(self, instance, prop, widget):
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_widget(self, value):
try:
idx = _find_combo_text(self._widget, value)
except ValueError:
if value is None:
idx = -1
else:
raise
self._widget.setCurrentIndex(idx)
[docs]
def update_prop(self, idx):
if idx == -1:
setattr(self._instance, self._prop, None)
else:
setattr(self._instance, self._prop, self._widget.itemText(idx))
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.disconnect(self.update_prop)
[docs]
class connect_float_text(BaseConnection):
"""
Connect a numerical callback property with a Qt widget containing text.
Parameters
----------
instance : object
The class instance that the callback property is attached to
prop : str
The name of the callback property
widget : QtWidget
The Qt widget to connect. This should implement the ``setText`` and
``text`` methods as well optionally the ``editingFinished`` signal.
fmt : str or func
This should be either a format string (in the ``{}`` notation), or a
function that takes a number and returns a string.
"""
def __init__(self, instance, prop, widget, fmt="{:g}"):
super().__init__(instance, prop, widget)
if callable(fmt):
format_func = fmt
else:
def format_func(x):
try:
return fmt.format(x)
except ValueError:
return str(x)
self._format_func = format_func
self.connect()
[docs]
def update_prop(self):
value = self._widget.text()
try:
value = float(value)
except ValueError:
try:
value = np.datetime64(value)
except Exception:
value = 0
setattr(self._instance, self._prop, value)
[docs]
def update_widget(self, value):
if value is None:
value = 0.0
self._widget.setText(self._format_func(value))
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
try:
self._widget.editingFinished.connect(self.update_prop)
except AttributeError:
pass
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
try:
self._widget.editingFinished.disconnect(self.update_prop)
except AttributeError:
pass
[docs]
class connect_value(BaseConnection):
"""
Connect a numerical callback property with a Qt widget representing a value.
Parameters
----------
instance : object
The class instance that the callback property is attached to
prop : str
The name of the callback property
widget : QtWidget
The Qt widget to connect. This should implement the ``setText`` and
``text`` methods as well optionally the ``editingFinished`` signal.
value_range : iterable, optional
A pair of two values representing the true range of values (since
Qt widgets such as sliders can only have values in certain ranges).
log : bool, optional
Whether the Qt widget value should be mapped to the log of the callback
property.
"""
def __init__(self, instance, prop, widget, value_range=None, log=False):
super().__init__(instance, prop, widget)
if log:
if value_range is None:
raise ValueError("log option can only be set if value_range is given")
else:
self._value_range = math.log10(value_range[0]), math.log10(value_range[1])
else:
self._value_range = value_range
self._log = log
self.connect()
[docs]
def update_prop(self):
value = self._widget.value()
if self._value_range is not None:
imin, imax = self._widget.minimum(), self._widget.maximum()
value = (value - imin) / (imax - imin) * (self._value_range[1] - self._value_range[0]) + self._value_range[
0
]
if self._log:
value = 10**value
setattr(self._instance, self._prop, value)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.valueChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.valueChanged.disconnect(self.update_prop)
class connect_button(BaseConnection):
"""
Connect a button with a callback method
Parameters
----------
instance : object
The class instance that the callback method is attached to
prop : str
The name of the callback method
widget : QtWidget
The Qt widget to connect. This should implement the ``clicked`` method
"""
def __init__(self, instance, prop, widget):
super().__init__(instance, prop, widget)
self.connect()
def connect(self):
self._widget.clicked.connect(getattr(self._instance, self._prop))
def disconnect(self):
self._widget.clicked.disconnect(self.update_prop)
def _find_combo_data(widget, value):
"""
Returns the index in a combo box where itemData == value
Raises a ValueError if data is not found
"""
# Here we check that the result is True, because some classes may overload
# == and return other kinds of objects whether true or false.
for idx in range(widget.count()):
if widget.itemData(idx) is not None:
if isinstance(widget.itemData(idx), UserDataWrapper):
if widget.itemData(idx).data is value or (widget.itemData(idx).data == value) is True:
return idx
else:
if widget.itemData(idx) is value or (widget.itemData(idx) == value) is True:
return idx
else:
raise ValueError(f"{value} not found in combo box")
def _find_combo_text(widget, value):
"""
Returns the index in a combo box where text == value
Raises a ValueError if data is not found
"""
i = widget.findText(value)
if i == -1:
raise ValueError(f"{value} not found in combo box")
else:
return i
[docs]
class connect_combo_selection(BaseConnection):
def __init__(self, instance, prop, widget):
prop_obj = getattr(type(instance), prop)
# Handle aliases - resolve to target property for type checking
if hasattr(prop_obj, "_target_property") and prop_obj._target_property is not None:
prop_obj = prop_obj._target_property
if not isinstance(prop_obj, SelectionCallbackProperty):
raise TypeError("connect_combo_selection requires a SelectionCallbackProperty")
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_prop(self, idx):
if idx == -1:
setattr(self._instance, self._prop, None)
else:
data_wrapper = self._widget.itemData(idx)
if data_wrapper is None:
setattr(self._instance, self._prop, None)
else:
setattr(self._instance, self._prop, data_wrapper.data)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.currentIndexChanged.disconnect(self.update_prop)
[docs]
class connect_list_selection(BaseConnection):
def __init__(self, instance, prop, widget):
"""
Connect a SelectionCallbackProperty with a QListWidget that supports
single-item selection.
"""
prop_obj = getattr(type(instance), prop)
# Handle aliases - resolve to target property for type checking
if hasattr(prop_obj, "_target_property") and prop_obj._target_property is not None:
prop_obj = prop_obj._target_property
if not isinstance(prop_obj, SelectionCallbackProperty):
raise TypeError("connect_list_selection requires a SelectionCallbackProperty")
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_prop(self):
if len(self._widget.selectedItems()) == 0:
setattr(self._instance, self._prop, None)
else:
data_wrapper = self._widget.selectedItems()[0].data(Qt.UserRole)
if data_wrapper is None:
setattr(self._instance, self._prop, None)
else:
setattr(self._instance, self._prop, data_wrapper.data)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.itemSelectionChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.itemSelectionChanged.disconnect(self.update_prop)
[docs]
class connect_datetime(BaseConnection):
"""
Connect a CallbackProperty to a QDateTimeEdit.
Since QDateEdit and QTimeEdit are subclasses of QDateTimeEdit, this connection
will work for those more specific widgets as well.
"""
def __init__(self, instance, prop, widget):
super().__init__(instance, prop, widget)
self.connect()
[docs]
def update_prop(self):
qdatetime = self._widget.dateTime().toUTC()
value = np.datetime64(qdatetime.toPython())
setattr(self._instance, self._prop, value)
[docs]
def connect(self):
add_callback(self._instance, self._prop, self.update_widget)
self._widget.dateTimeChanged.connect(self.update_prop)
self._widget.dateChanged.connect(self.update_prop)
self._widget.timeChanged.connect(self.update_prop)
self.update_widget(getattr(self._instance, self._prop))
[docs]
def disconnect(self):
remove_callback(self._instance, self._prop, self.update_widget)
self._widget.dateTimeChanged.disconnect(self.update_prop)
self._widget.dateChanged.disconnect(self.update_prop)
self._widget.timeChanged.disconnect(self.update_prop)