PySide Widget not updated by QDataWidgetMapper when shown

356 Views Asked by At

I'm trying to use a QDataWidgetMapper to drive a custom QComboBox using Enum.

I'm on Windows 10, using Python 2.7.13, PySide 1.2.4 (Qt 4.8.7), here's the code:

from PySide.QtCore import Qt, Property
from PySide.QtGui import (
    QApplication,
    QComboBox,
    QDataWidgetMapper,
    QFormLayout,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QStandardItem,
    QStandardItemModel,
    QWidget,
)
from enum import Enum


class State(Enum):

    foo = 1
    bar = 2
    baz = 3


class StateEdit(QComboBox):

    def __init__(self, parent=None):
        super(StateEdit, self).__init__(parent)
        self.addItems(State._member_names_)

    def state(self):
        text = self.currentText()
        return State[text] if text else None

    def setState(self, value):
        if value is None:
            index = -1
        else:
            index = self.findText(value.name, Qt.MatchExactly)
        self.setCurrentIndex(index)

    state = Property(State, state, setState, user=True)


class Window(QMainWindow):

    def __init__(self, parent=None):
        super(Window, self).__init__(parent)

        self._name_edit = QLineEdit()
        self._state_edit = StateEdit()
        self._state_button = QPushButton('Change State')

        self._content = QWidget()
        self.setCentralWidget(self._content)

        self._form = QFormLayout()
        self._content.setLayout(self._form)

        self._form.addRow('Name', self._name_edit)
        self._form.addRow('State', self._state_edit)
        self._form.addRow('Action', self._state_button)

        self._state_button.released.connect(self._on_state_button_clicked)

        self._model = QStandardItemModel()
        name_item = QStandardItem()
        state_item = QStandardItem()
        name_item.setText('My Name')
        state_item.setData(State.bar)
        self._model.appendRow([name_item, state_item])

        self._mapper = QDataWidgetMapper()
        self._mapper.setModel(self._model)
        self._mapper.addMapping(self._name_edit, 0)
        self._mapper.addMapping(self._state_edit, 1, 'state')
        self._mapper.toFirst()

    def _on_state_button_clicked(self):
        self._state_edit.state = State.baz

    def data(self):
        return {
            'name': self._name_edit.text(),
            'state': self._state_edit.state,
        }



if __name__ == "__main__":
    import sys
    from pprint import pprint

    app = QApplication(sys.argv)
    win = Window()
    win.show()
    app.exec_()
    pprint(win.data())

The problem is, the enum widget always pops up displaying its first index.

There seems to be no issue with the property itself, as setting it using the button works.

The state is also updated when choosing a different index using the combo box, as demonstrated by the data printed when the window closes.

I've looked into properties and dynamic properties, the user flag on properties, even overriding setProperty and property on the widget, to no avail.

I've also looked into this guide, but it seems regular issues with QComboBox and QDataWidgetMapper don't really apply to my case.

The only solution I see is to use the regular workflow with QComboBox and just use plain old indices instead of enum values, but this would be a shame, I just need the initial mapping to be properly triggered and everything would work perfectly.

I don't really know where to look anymore, maybe it's a PySide specific issue, so any pointers will help !

2

There are 2 best solutions below

0
On

So I ended up rolling my own data mapper for simplicity.

It does not have all the features QDataWidgetMapper exposes, mainly updates when model data changes, but I can specify whichever property I want to use, or a pair of getter and setter.

Here's the implementation if that may help someone:

from Qt import QtCore, QtWidgets  # pylint: disable=no-name-in-module


class ModelWidgetMapper(QtCore.QObject):
    """
    :type model: Qt.QtCore.QAbstractItemModel
    :type parent: Qt.QtWidget.QWidget
    """

    class MappingEntry(object):
        """
        :type widget: Qt.QtWidget.QWidget
        :type section: int
        :type property_name: str
        :type getter: callable
        :type setter: callable
        """

        def __init__(self, widget, section, property_name, getter, setter):
            self.widget = widget
            self.section = section
            self.property_name = property_name
            self.getter = getter
            self.setter = setter

    def __init__(self, model, parent=None):
        super(ModelWidgetMapper, self).__init__(parent)
        self._model = model
        self._mapping = []
        self._current_index = None

    def model(self):
        """
        :rtype: Qt.QtCore.QAbstractItemModel
        """
        return self._model

    def add_mapping(self, widget, section, property_name=None, getter=None, setter=None):
        """
        :type widget: Qt.QtWidget.QWidget
        :type section: int
        :type proerty_name: str
        :type getter: callable
        :type setter: callable
        :rtype: ModelWidgetMapper.MappingEntry
        """
        entry = ModelWidgetMapper.MappingEntry(
            widget,
            section,
            property_name,
            getter,
            setter
        )
        self._mapping.append(entry)
        if self._current_index and self._current_index.isValid():
            self._update_widget_from_model(entry)
        return entry

    def submit(self):
        """
        :rtype: bool
        """
        res = True
        for entry in self._mapping:
            res = res & self._update_model_from_widget(entry)
        res & self._model.submit()
        return res

    def revert(self):
        self._model.revert()
        for entry in self._mapping:
            self._update_widget_from_model(entry)

    def set_current_index(self, index):
        """
        :type index: Qt.QtCore.QModelIndex
        """
        self._current_index = index
        for entry in self._mapping:
            self._update_widget_from_model(entry)

    def current_index(self):
        """
        :rtype: Qt.QtCore.QModelIndex
        """
        return self._current_index

    def edits(self):
        """
        :rtype: Dict[int, Any]
        """
        res = {}
        for entry in self._mapping:
            model_value = self._model_value(entry)
            widget_value = self._get_widget_property(entry)
            try:
                widget_value = type(model_value)(widget_value)
            except TypeError:
                pass
            if widget_value != model_value:
                res[entry.section] = widget_value
        return res

    def _model_value(self, entry):
        row = self._current_index.row()
        index = self._model.index(row, entry.section)
        value = self._model.data(index)
        return value

    def _update_widget_from_model(self, entry):
        value = self._model_value(entry)
        self._set_widget_property(entry, value)

    def _update_model_from_widget(self, entry):
        value = self._get_widget_property(entry)
        row = self._current_index.row()
        index = self._model.index(row, entry.section)
        return self._model.setData(index, value)

    def _get_widget_property(self, entry):
        if entry.getter:
            return entry.getter()
        elif not entry.property_name:
            return self._get_widget_user_property(entry.widget)
        return entry.widget.property(entry.property_name)

    def _get_widget_user_property(self, widget):
        return widget.metaObject().userProperty().read(widget)

    def _set_widget_property(self, entry, value):
        if entry.setter:
            return entry.setter(value)
        elif not entry.property_name:
            return self._set_widget_user_property(entry.widget, value)
        return entry.widget.setProperty(entry.property_name, value)

    def _set_widget_user_property(self, widget, value):
        return widget.metaObject().userProperty().write(widget, value)

It uses the Qt.py wrapper for compatibility.

0
On

I test your code. And I found the simple soulution.

# At line 68
name_item.setText('My Name')
state_item.setText('')
state_item.setData(State.bar)

Implement the setText method, then everything works correctly.