Search code examples
pythondrag-and-droppyside2qtreeview

dragMoveEvent() doesn't work properly when overriding mouseMoveEvent() - Qt Drag&Drop


I'm building a quite complex GUI using PySide2 and I have to implement a Drag and Drop system for a QTreeView widget (both internal and external moves shall be accepted).

GOAL

Be able to copy items from a QTreeView widget (File explorer) to another QTreeView widget (Tests widget) and move items within this Tests widget. Every dragged file shall be checked to make the user understands if he can move that file and where he should put it. Also, the items of the Tests widget shall be highlighted (one by one) when the mouse with the dragged element hovers them. (Actually, I would like to draw a line between the items but I'm not able to do it for now: any suggestion is welcome).

PROBLEM

The external move works perfectly whereas the internal move doesn't work properly: during the drag and drop operation the 'stop' icon is always showing even if the move should be accepted. And it is actually accepted, since the drop operation is succesfull. The highlighting of the hover items doesn't work too. I think that the problem is given by the overriding of the mouseMoveEvent() method of the Tests widget, which I was forced to implement in order to set the QMimeData object.

CODE

Note that some parts of the code have been replaced by '[...]' for privacy reasons. Anyway those parts are not important for the functioning of the system.

class MyStandardItem(QStandardItem):
    def __init__(self, text, icon_path='', value='', num=0, font_size=8, set_bold=False):
        super().__init__()
        self.setDragEnabled(True)
        self.setDropEnabled(True)
        self.num = num
        self.value = value
        self.setText(text)    
        font = QFont('Segoe UI', font_size)
        font.setBold(set_bold)
        self.setFont(font)
        self.setIcon(QIcon(icon_path))

    def setCheckState(self, checkState):
        super().setCheckState(checkState)
        if checkState == Qt.Unchecked:
            self.setForeground(QColor(150, 150, 150))
    
    def get_data(self):
        return self.text(), self.value, self.num


class MyTreeView(QTreeView):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDropIndicatorShown(True)
        self.viewport().setAcceptDrops(True)
        self.hover_item = None
        self.setMouseTracking(True)
        self.start_drag_pos = None

        self.model = QStandardItemModel()
        self.root = self.model.invisibleRootItem()
        self.setModel(self.model)

    def mousePressEvent(self, event: QtGui.QMouseEvent):
        if event.button() == Qt.LeftButton:
            self.start_drag_pos = event.pos()
            super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
        super().mouseMoveEvent(event)
        if not event.buttons() == Qt.LeftButton:
            return
        if (event.pos() - self.start_drag_pos).manhattanLength() < QApplication.startDragDistance():
            return
        index = self.indexAt(self.start_drag_pos)
        item = self.model.itemFromIndex(index)
        if item:
            drag = QDrag(self)
            mime_data = QMimeData()
            mime_data.setText(str(item.get_data()))
            drag.setMimeData(mime_data)
            drag.exec_(Qt.MoveAction)

    def dragEnterEvent(self, event: QDragEnterEvent):
        self.selectionModel().clear()
        if event.mimeData().hasText():
            mime_text = event.mimeData().text()
            if event.source() == self:
                mime_tuple = eval(mime_text)
                if [...]:
                    event.acceptProposedAction()
            else:
                path = Path(mime_text)
                accepted_extensions = ['.txt']
                if path.suffix in accepted_extensions:
                    event.acceptProposedAction()

    def dragMoveEvent(self, event: QDragMoveEvent):
        cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
        index = self.indexAt(cursor_pos)
        item = self.model.itemFromIndex(index)
        if self.hover_item is not item:
            self.hover_item = item
            if self.hover_item is not None:
                self.selectionModel().clear()
                self.selectionModel().select(item.index(), QItemSelectionModel.Rows | QItemSelectionModel.Select)
        super().dragMoveEvent(event)
        if event.source() == self:
            item_data = eval(event.mimeData().text())
            if item is not None:
                if [...]:
                    event.acceptProposedAction()
                else:
                    if [...]:
                        event.acceptProposedAction()
        else:
            path = event.mimeData().text().replace('file:///', '')
            if item is not None:
                if [...]:
                    if [...] in item.text():
                        event.acceptProposedAction()
                else:
                    if [...]:
                        if [...] in item.value:
                            event.acceptProposedAction()
                    else:
                        if [...] in item.value:
                            event.acceptProposedAction()

    def dropEvent(self, event: QDropEvent):
        self.hover_item = None
        cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
        index = self.indexAt(cursor_pos)
        over_dropped_item = self.model.itemFromIndex(index)
        dropped = event.mimeData().text().replace('file:///', '')
        print('Moved item: ', dropped)
        print('Moved over: ', over_dropped_item.get_data())
        [...]
        event.acceptProposedAction()

Solution

  • After thinking about it quite a bit I decided not to override the mouseMoveEvent() method and this solved the problem.

    As I thought there was a conflict between mouseMoveEvent() and dragMoveEvent(). In fact dragMoveEvent() was called only once (it shoud have been called multiple times during the drag movements) and just before dropEvent(). I believe that the problem was given by the fact that the QDrag object was created continuosly during the mouse movement and so dragMoveEvent() was never called until the item was dropped - but it was too late.

    Without the custom QDrop object - previoulsy created in mouseMoveEvent() - I had to find another way to save the dragged QStandardItem and use it afterwards. The solution was quite simple: store the selected item into a class attribute when dragEnterEvent() is called:

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.source() == self:
            self.dragged_index = self.selectionModel().selectedIndexes()[0]
            self.dragged_item = self.model.itemFromIndex(self.dragged_index)
    

    Also, thanks to the native Drag&Drop I don't need anymore to select the items I'm hovering on, since this functionality is built-in and the black line between the rows too. Native Drag&Drop works only with internal moves and only with QStandardItem items, therefore I deleted MyStandardItem class and used QStandardItem instead.

    CODE

    class MyTreeView(QTreeView):
        def __init__(self):
            super().__init__()
            self.setAcceptDrops(True)
            self.setDragEnabled(True)
            self.setDropIndicatorShown(True)
            self.viewport().setAcceptDrops(True)
            self.hover_item = None
            self.setMouseTracking(True)
            self.dragged_item = None
            self.dragged_index = None
    
        def dragEnterEvent(self, event: QDragEnterEvent):
            if event.source() == self:
                self.dragged_index = self.selectionModel().selectedIndexes()[0]
                self.dragged_item = self.model.itemFromIndex(self.dragged_index)
                if [...] not in self.dragged_item.text():
                    super().dragEnterEvent(event)
            else:
                [...]
    
        def dragMoveEvent(self, event: QDragMoveEvent):
            super().dragMoveEvent(event)
            cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
            index = self.indexAt(cursor_pos)
            item = self.model.itemFromIndex(index)
            if event.source() == self:
                if item is not None:
                    if [...] in self.dragged_item.data()[0]:
                        if [...] in item.data()[0]:
                            event.acceptProposedAction()
                    else:
                        if [...] in item.data()[0]:
                            event.acceptProposedAction()
            else:
                [...]
    
        def dropEvent(self, event: QDropEvent):
            super().dropEvent(event)
            [...]