[Qt] Drag and drop between two sorted lists

qt
python

#1

I have a GUI with two lists. Items can be dragged from one list to the other and vice versa. Also, I have “setSortingEnabled(True)” on both lists, so when an item is dragged from one list to the other, it automatically gets inserted in sorted order. The problem is that items can be dragged and rearranged within a single list. This breaks the sorted order. How can I disable internal drag-and-drop on a widget while still allowing drag-and-drop to/from other widgets?

from PySide import QtCore, QtGui

class TestSortedDragDrop(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(TestSortedDragDrop, self).__init__(parent)
        
        self._centralWidget = QtGui.QWidget()
        self.setCentralWidget(self._centralWidget)
        
        layout = QtGui.QHBoxLayout(self._centralWidget)
        self.list1 = QtGui.QListWidget()
        self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list1.setSortingEnabled(True)
        
        self.list2 = QtGui.QListWidget()
        self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list2.setSortingEnabled(True)
        
        layout.addWidget(self.list1)
        layout.addWidget(self.list2)
        
        l1 = ['car', 'bat', 'house', 'elm', 'desk']
        l2 = ['rock', 'spoon', 'cup', 'book']
        self.list1.addItems(sorted(l1))
        self.list2.addItems(sorted(l2))

#2

Not sure about disabling internal drag/drop, but could you hook onto a signal that just resorts the list for you?


#3

I tried that, but it didn’t work 100%. I tried the “rowsInserted”, “rowsRemoved”, and “rowsMoved” signals. I retrieved the list of strings from the widget, sorted it manually, and then set the widget contents to be the newly sorted list. The problem was that it didn’t seem like it had finished updating the model at that point. So when I retrieved the list of strings from the widget, one of them might be an empty string. Then I ended up with empty list items.

I’m currently trying other things but might come back and explore this avenue more if the other things don’t work out.


#4

I think I’ve got something. I created an “event filter” to listen for the drop event. If the source of the drop event is the same as the object listening for the event, then I ignore the drop event. Here is the updated code:

from PySide import QtCore, QtGui

class TestSortedDragDrop(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(TestSortedDragDrop, self).__init__(parent)
        
        self._centralWidget = QtGui.QWidget()
        self.setCentralWidget(self._centralWidget)
        
        self._dragDropFilter = InternalDropFilter(self)
        
        layout = QtGui.QHBoxLayout(self._centralWidget)
        self.list1 = QtGui.QListWidget()
        self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list1.setSortingEnabled(True)
        self.list1.viewport().installEventFilter(self._dragDropFilter)
        
        self.list2 = QtGui.QListWidget()
        self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list2.setSortingEnabled(True)
        self.list2.viewport().installEventFilter(self._dragDropFilter)
        
        layout.addWidget(self.list1)
        layout.addWidget(self.list2)
        
        l1 = ['car', 'bat', 'house', 'elm', 'desk']
        l2 = ['rock', 'spoon', 'cup', 'book']
        self.list1.addItems(sorted(l1))
        self.list2.addItems(sorted(l2))

class InternalDropFilter(QtCore.QObject):
    """ Filter out drop events where the source widget and target widget are the same."""
    def eventFilter(self, viewport, qEvent):
        if qEvent.type() == QtCore.QEvent.Drop:
            if viewport == qEvent.source().viewport():
                qEvent.setDropAction(QtCore.Qt.IgnoreAction)
                qEvent.accept()
        
        return super(InternalDropFilter, self).eventFilter(viewport, qEvent)

Edit: I forgot to mention that I had to install the event filter on the list widget’s viewport, not the list widget itself. I think this is because the viewport processes the drop event, and it never gets propagated up to the list widget.


#5

Hey, future me. If you ever run into this problem again, you should know that another option is to subclass the list widget and override the “dropEvent”.

Here are a couple of pages I found helpful: