'Pyside2 QListView drag and drop reordering based on data from a user role does not work as expected

I am running this on Windows 10 and Python 3.7+ (tested in 3.7 and 3.9 with the same result).

I have a custom QListView and a QStandardItemModel with a QSortFilterProxyModel in between. I am using custom user-role data (UserRoles.ORDER_ROLE) to reorder items in the list view and upon drag-drop re-ordering, I update this custom data so that the reordered item's "twin", which is currently inactive, from the combo_box has the same data as the active item for the QSortFilterProxyModel to sort the way I need it to.

Below is a reproducible example. In the example, if I drag Item_1 to the bottom of the list_view and use the combo_box to switch to its twin, Item_2, Item_2 moves to the top of the list_view when I expect it to be at the bottom where Item_1 was. But if I switch back to Item_1 using the combo_box while selecting Item_2, Item_1 is still at the bottom of the list_view. What am I doing wrong?

I have defined the sortRole in the filter_model as the UserRoles.ORDER_ROLE and have overridden the lessThan method for it to custom sort, as well as update the ORDER_ROLE for the Item_2 to match the ORDER_ROLE of Item_1 in list_view's dropEvent using Item_1's row number. Filtering seems to work as expected(?) where once Item_1 is swapped with Item_2 using the combo_box, Item_1's ACTIVE_ROLE is set to False and Item_2's ACTIVE_ROLE is set to True. However, this seems to happen despite QSortFilterProxyModel.setDynamicSortFilter is by default set to False. Am I misunderstanding how QSortFilterProxyModel.setDynamicSortFilter works?

I have also noticed dropping the item on another item makes the dropped item disappear. Any thoughts on why this may be happening?

from enum import IntEnum
from PySide2 import QtCore, QtGui, QtWidgets


class UserRoles(IntEnum):
    ACTIVE_ROLE = QtCore.Qt.UserRole + 1
    ORDER_ROLE = QtCore.Qt.UserRole + 2
    TWIN_ROLE = QtCore.Qt.UserRole + 3


class MvcModel(QtGui.QStandardItemModel):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        
        items_list = ["Item_1", "Item_2", "Item_3", "Item_4", "Item_5", "Item_6"]

        for i in range(len(items_list)):
            data = items_list[i]
            
            item = QtGui.QStandardItem(data)
            
            if i % 2 == 0:
                item.setData(True, UserRoles.ACTIVE_ROLE)
                item.setData(items_list[i+1], UserRoles.TWIN_ROLE)
            else:
                item.setData(False, UserRoles.ACTIVE_ROLE)
                item.setData(items_list[i-1], UserRoles.TWIN_ROLE)

            item.setData(i + 1, UserRoles.ORDER_ROLE)
            item.setFlags(item.flags() & ~QtCore.Qt.ItemIsDropEnabled)

            self.appendRow(item)


class MvcProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.setSortRole(UserRoles.ORDER_ROLE)
        self.setDynamicSortFilter(True)


    def filterAcceptsRow(self, source_row, source_parent):
        index = self.sourceModel().index(source_row, 0, source_parent)
        return index.data(UserRoles.ACTIVE_ROLE) is True

    def lessThan(self, source_left, source_right):
        return int(source_left.data(UserRoles.ORDER_ROLE)) < int(
            source_right.data(UserRoles.ORDER_ROLE)
        )


class MvcList(QtWidgets.QListView):
    def __init__(self, parent=None):
        
        super().__init__(parent=parent)

        self.set_view_params()

        self.data_model = MvcModel(self)
        self.filter_model = MvcProxyModel(self)

        self.filter_model.setSourceModel(self.data_model)
        self.setModel(self.filter_model)

        self.clicked.connect(self.item_selected)

    def set_view_params(self):
        self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.setDragEnabled(True)
        self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        self.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.setAcceptDrops(True)

    def item_selected(self, index):
        self.parent().update_combo_box.emit(index)


    def dropEvent(self, event):
        super().dropEvent(event)

        index = event.source().currentIndex()
                
        alt_item = index.data(UserRoles.TWIN_ROLE)
        alt_item = self.data_model.findItems(alt_item)
        alt_index = alt_item[0].index()

        alt_index = self.model().mapFromSource(alt_index)

        self.filter_model.setData(index, str(index.row()+1), UserRoles.ORDER_ROLE)
        self.filter_model.setData(alt_index, str(index.row()+1), UserRoles.ORDER_ROLE)


class TestWidget(QtWidgets.QWidget):
    update_combo_box = QtCore.Signal(object)

    def __init__(self):
        super().__init__()

        self.update_combo_box.connect(self.update_combo)

        self.setup_ui()
        self.combo_box.currentIndexChanged.connect(self.switch_to_twin)
        self.show()


    def setup_ui(self):
        self.list_view = MvcList(self)
        self.combo_box = QtWidgets.QComboBox(self)

        self.hbox_layout = QtWidgets.QHBoxLayout(self)
        self.hbox_layout.addStretch(1)
        self.hbox_layout.addWidget(self.list_view)
        self.hbox_layout.addWidget(self.combo_box)
        
        self.setLayout(self.hbox_layout)
        
        self.setGeometry(300, 300, 350, 200)

    def update_combo(self, index):
        self.combo_box.blockSignals(True)
        
        self.combo_box.clear()
        self.combo_box.addItem(index.data())
        self.combo_box.addItem(index.data(UserRoles.TWIN_ROLE))

        self.combo_box.blockSignals(False)


    def get_row_num(self, index):
        increment = 0

        for i in range(self.list_view.model().rowCount()):
            item = self.list_view.data_model.item(i, 0)

            if item.data(QtCore.Qt.DisplayRole) != index.data():
                continue

            if item.data(UserRoles.ACTIVE_ROLE):
               increment += 1 

            return increment+1

        return -1

    def switch_to_twin(self, index):
        current_text = self.combo_box.currentText()

        index_to = self.list_view.data_model.findItems(current_text)
        if not index_to:
            return

        index_to = index_to[0].index()

        index_from = self.list_view.data_model.findItems(index_to.data(UserRoles.TWIN_ROLE))
        if not index_from:
            return

        index_from = index_from[0].index()

        index_from_row = self.get_row_num(index_from)

        print(self.list_view.data_model.setData(index_to, True, UserRoles.ACTIVE_ROLE))
        print(self.list_view.data_model.setData(index_from , False, UserRoles.ACTIVE_ROLE))
        print(index_from.data(), index_to.data(), str(index_from_row+1))
        print(self.list_view.data_model.setData(index_to, str(index_from.row()+1), UserRoles.ORDER_ROLE))



if __name__ == "__main__":
    import sys

    try:
        app = QtWidgets.QApplication(sys.argv)
        
        test_widget = TestWidget()

        sys.exit(app.exec_())
    except:
        import traceback
        print(traceback.format_exc())



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source