1

I have two QListWidgets. The user can select multiple items from one list and drag them to the other list. But within each list, some items are draggable and some are not. If the selection contains both draggable and non-draggable items, a problem happens. Only the draggable items appear in the second list, which is correct. But all the items disappear from the first list. animation of items being dragged from one list to another

In the animated image above, items 00, 01, and 02 are selected. Only items 00 and 02 are drag enabled. After the drag-and-drop, all three items are gone from the first list. How can I fix this?

Here is some code to reproduce the problem:

import random
import sys
from PySide import QtCore, QtGui

class TestMultiDragDrop(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(TestMultiDragDrop, self).__init__(parent)

        centralWidget = QtGui.QWidget()
        self.setCentralWidget(centralWidget)

        layout = QtGui.QHBoxLayout(centralWidget)
        self.list1 = QtGui.QListWidget()
        self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list1.setSelectionMode(QtGui.QListWidget.ExtendedSelection)

        self.list2 = QtGui.QListWidget()
        self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
        self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.list2.setSelectionMode(QtGui.QListWidget.ExtendedSelection)

        layout.addWidget(self.list1)
        layout.addWidget(self.list2)

        self.fillListWidget(self.list1, 8, 'someItem')
        self.fillListWidget(self.list2, 4, 'anotherItem')

    def fillListWidget(self, listWidget, numItems, txt):
        for i in range(numItems):
            item = QtGui.QListWidgetItem()
            newTxt = '{0}{1:02d}'.format(txt, i)
            if random.randint(0, 1):
                item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
            else:
                # If the item is draggable, indicate it with a *
                newTxt += ' *'
                item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
            item.setText(newTxt)
            listWidget.addItem(item)

def openMultiDragDrop():
    global multiDragDropUI
    try:
        multiDragDropUI.close()
    except:
        pass
    multiDragDropUI = TestMultiDragDrop()
    multiDragDropUI.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    multiDragDropUI.show()
    return multiDragDropUI

if __name__ == '__main__':
    app = QtGui.QApplication([])
    openMultiDragDrop()
    sys.exit(app.exec_())
Becca codes
  • 542
  • 1
  • 4
  • 14

2 Answers2

0

Here I have some suspicion on setDefaultDropAction(QtCore.Qt.MoveAction)

Read below para from documentation: Specially the bold line

In the simplest case, the target of a drag and drop action receives a copy of the data being dragged, and the source decides whether to delete the original. This is described by the CopyAction action. The target may also choose to handle other actions, specifically the MoveAction and LinkAction actions. If the source calls QDrag::exec(), and it returns MoveAction, the source is responsible for deleting any original data if it chooses to do so. The QMimeData and QDrag objects created by the source widget should not be deleted - they will be destroyed by Qt.

(http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions)

First give a try with QtCore.Qt.CopyAction

Second, if MoveAction is mandatory, try creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent.

Here in below link, you can find some help for creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent. (code is in C++, My intention is to get conceptual idea).

http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions

Pavan Chandaka
  • 11,671
  • 5
  • 26
  • 34
  • I need to use MoveAction, because the items should be removed from the list. But I only want the drag-enabled items to be removed. I am looking into your other suggestions and will post an update later. Thanks. – Becca codes Apr 07 '17 at 19:08
0

I think Kuba Ober is right that this is a Qt bug. In the C++ source code, there is a function void QAbstractItemViewPrivate::clearOrRemove(). It deletes all selected rows, but it does not look at whether each item is drag-enabled or not.

That being the case, I came up with a few workarounds:


Method 1: Make all non-draggable items non-selectable as well

This is the easiest method. Just remove the QtCore.Qt.ItemIsEnabled flag from all non-draggable items. Of course if you want all of your items to be selectable, this won't work.


Method 2: Recreate the "startDrag" function

Since the clearOrRemove function belongs to a private class, I cannot override it. But that function is called by the startDrag function, which can be overridden. So I essentially duplicated the function in Python and replaced the call to clearOrRemove with my own function removeSelectedDraggableItems.

The problem with this method is that startDrag contains calls to a few other functions belonging to a private class. And those functions call other private class functions. Specifically, these functions are responsible for controlling how the items are drawn during the drag event. Since I didn't want to recreate all the functions, I just ignored those. The result is that this method results in the correct functionality, but it loses the graphical indication of which items are being dragged.

class DragListWidget(QtGui.QListWidget):
    def __init__(self):
        super(DragListWidget, self).__init__()

    def startDrag(self, supportedDragActions):
        indexes = self.getSelectedDraggableIndexes()
        if not indexes:
            return

        mimeData = self.model().mimeData(indexes)
        if not mimeData:
            return

        drag = QtGui.QDrag(self)
        rect = QtCore.QRect()
        # "renderToPixmap" is from a private class in the C++ code, so I can't use it.
        #pixmap = renderToPixmap(indexes, rect)
        #drag.setPixmap(pixmap)
        drag.setMimeData(mimeData)
        # "pressedPosition" is from a private class in the C++ code, so I can't use it.
        #drag.setHotSpot(pressedPostion() - rect.topLeft())
        defaultDropAction = self.defaultDropAction()
        dropAction = QtCore.Qt.IgnoreAction
        if ((defaultDropAction != QtCore.Qt.IgnoreAction) and 
            (supportedDragActions & defaultDropAction)):
            dropAction = defaultDropAction
        elif ((supportedDragActions & QtCore.Qt.CopyAction) and
              (self.dragDropMode() != self.InternalMove)):
            dropAction = QtCore.Qt.CopyAction

        dragResult = drag.exec_(supportedDragActions, dropAction)
        if dragResult == QtCore.Qt.MoveAction:
            self.removeSelectedDraggableItems()

    def getSelectedDraggableIndexes(self):
        """ Get a list of indexes for selected items that are drag-enabled. """
        indexes = []
        for index in self.selectedIndexes():
            item = self.itemFromIndex(index)
            if item.flags() & QtCore.Qt.ItemIsDragEnabled:
                indexes.append(index)
        return indexes

    def removeSelectedDraggableItems(self):
        selectedDraggableIndexes = self.getSelectedDraggableIndexes()
        # Use persistent indices so we don't lose track of the correct rows as
        # we are deleting things.
        root = self.rootIndex()
        model = self.model()
        persistentIndices = [QtCore.QPersistentModelIndex(i) for i in selectedDraggableIndexes]
        for pIndex in persistentIndices:
            model.removeRows(pIndex.row(), 1, root)

Method 3: Hack "startDrag"

This method changes the drop action from "MoveAction" to "CopyAction" before calling the built-in "startDrag" method. Then it calls a custom function for deleting the selected drag-enabled items. This solves the problem of losing the graphical dragging animation.

This is a pretty easy hack, but it comes with its own problem. Say the user installs an event filter that changes the drop action from "MoveAction" to "IgnoreAction" in certain cases. This hack code doesn't get the updated value. It will still delete the items as though the action is "MoveAction". (Method 2 does not have this problem.) There are workarounds for this problem, but I won't go into them here.

class DragListWidget2(QtGui.QListWidget):
    def startDrag(self, supportedDragActions):
        dropAction = self.defaultDropAction()
        if dropAction == QtCore.Qt.MoveAction:
            self.setDefaultDropAction(QtCore.Qt.CopyAction)
        super(DragListWidget2, self).startDrag(supportedDragActions)
        if dropAction == QtCore.Qt.MoveAction:
            self.setDefaultDropAction(dropAction)
            self.removeSelectedDraggableItems()

    def removeSelectedDraggableItems(self):
        # Same code from Method 2.  Removed here for brevity.
        pass
Becca codes
  • 542
  • 1
  • 4
  • 14