Source code for schrodinger.ui.qt.datamapper
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
MSuper = QtWidgets.QDataWidgetMapper
[docs]class TrackingDataWidgetMapper(MSuper):
    """
    This class augments QtWidgets.QDataWidgetMapper with three additional features:
    1) Emits a widgetChanged signal whenever any mapped widget is modified by
    the user
    2) Provides an isDirty() method to determine whether any of the current
    widget values differ from the model state.
    3) Allows QButtonGroup objects to be mapped
    """
    widgetChanged = QtCore.pyqtSignal()
[docs]    def __init__(self, *args, **kwargs):
        MSuper.__init__(self, *args, **kwargs)
        self.all_widgets = []
[docs]    def isDirty(self):
        """
        Determines whether the values in the widgets match the values in the
        model. This will happen if the user has edited widget values but not
        submitted the changes.
        This method is primarily intended to be used with the submitPolicy
        set to ManualSubmit.
        If the user edits a value back to its original state, isDirty() will
        correctly return False.
        """
        if self.currentIndex() == -1:
            return False
        model = self.model()
        if self.orientation() == Qt.Horizontal:
            count = model.columnCount()
        else:
            count = model.rowCount()
        for i in range(count):
            widget = self.mappedWidgetAt(i)
            if not widget:
                continue
            prop_name = self.mappedPropertyName(widget)
            value = widget.property(prop_name)
            if self.orientation() == Qt.Horizontal:
                row = self.currentIndex()
                index = model.index(row, i)
            else:
                col = self.currentIndex()
                index = model.index(i, col)
            orig_value = model.data(index, Qt.EditRole)
            if value != orig_value:
                return True
        return False
[docs]    def addMapping(self, widget, section):
        """
        See parent class for full documentation for this method. In addition,
        this method connects the appropriate signal on the widget so that all
        widget changes result in the widgetChanged signal being emitted.
        This method also allows QButtonGroup to be mapped, which the parent
        class does not. Only exclusive button groups will work correctly - non-
        exclusive groups will only register the first checked button in the
        group.
        This override of addMapping also eliminates the optional propertyName
        argument.
        """
        if isinstance(widget, QtWidgets.QButtonGroup):
            widget = MappableButtonGroup(widget)
            MSuper.addMapping(self, widget, section, b'currentSelection')
        elif isinstance(widget, QtWidgets.QComboBox):
            # The default, currentText, does not work correctly, because there
            # is no corresponding setter in QComboBox
            MSuper.addMapping(self, widget, section, b'currentIndex')
        else:
            MSuper.addMapping(self, widget, section)
        self.all_widgets.append(widget)
        signal = self._findWidgetSignal(widget)
        signal.connect(self.widgetChanged.emit)
[docs]    def removeMapping(self, widget):
        # See parent class for documentation
        signal = self._findWidgetSignal(widget)
        signal.disconnect(self.widgetChanged.emit)
        MSuper.removeMapping(self, widget)
    def _findWidgetSignal(self, widget):
        """
        Returns the appropriate signal for the widget that indicates that the
        contents of the widget has changed, using the SIGNAL_MAP.
        """
        for widget_class, signal_name in SIGNAL_MAP.items():
            if isinstance(widget, widget_class):
                return getattr(widget, signal_name)
        else:
            raise TypeError('Unhandled widget type: %s' % type(widget))
[docs]class MappableButtonGroup(QtWidgets.QWidget):
    """
    This widget is a container for a QButtonGroup used to enable button groups
    to be mapped by the widget mapper.
    """
    selectionChanged = QtCore.pyqtSignal(int)
[docs]    def __init__(self, button_group, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self._button_group = button_group
        for i, button in enumerate(button_group.buttons()):
            self._button_group.setId(button, i)
            button.toggled.connect(self.selectionChanged.emit)
    currentSelection = QtCore.pyqtProperty(int,
                                           fget=getCurrentSelection,
                                           fset=setCurrentSelection)
# Maps widget types to the name of the signal indicating widget value changed
# Add widget types as needed
SIGNAL_MAP = {
    QtWidgets.QLineEdit: 'textChanged',
    QtWidgets.QComboBox: 'currentIndexChanged',
    QtWidgets.QSpinBox: 'valueChanged',
    QtWidgets.QDoubleSpinBox: 'valueChanged',
    QtWidgets.QCheckBox: 'toggled',
    MappableButtonGroup: 'selectionChanged',
}