Search code examples
pyqt5drag-and-dropdrag

Implementing copy and move when dropping an Item in `QTreeView`


I revised the whole question because the behavior I want is hard to implement and actually use.

I'm trying to imitate the behavior in the File Explorer where when I press Shift while dragging, the file will be moved instead of copied.

This is the behavior I'm trying to imitate: enter image description here

The behavior: is I'm using my LeftClick for selecting, and dragging. video of the behavior


About The behavior itself:

I overridden the mousePressEvent and mouseMoveEvent to start the drag. When the drag is created, it uses QTimer to detect if I pressed the Control and Shift modifier. Once a modifier is detected it sets the default drop action using setDefaultDropAction. (I think I should use setDropAction but It's only available in the dragMoveEvent and I'm doing it inside the QDrag Class)


The Issues: Part of the behavior is working now but there is still some issues.

  1. Even I press Shift, the DropIndicator is not changing from + to ->
  2. Related to the issue above, The dropAction is only copyAction instead of moveAction even I'm pressing the Shift key. video of the issues

My Question: What causes these issues? My gut tells me that I should've used setDropAction instead of setDefaultDropAction but again it's only available in the dragMoveEvent


My Testing Code:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class ModifiedQDrag(QDrag):
    def __init__(self, source):
        super().__init__(source)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.process_event)
        self.timer.setInterval(100)
        self.timer.start()

    def process_event(self):
        if qApp.keyboardModifiers() & Qt.ControlModifier:
            self.source().setDefaultDropAction(Qt.CopyAction)

        elif qApp.keyboardModifiers() & Qt.ShiftModifier:
            print("shift pressed")
            self.source().setDefaultDropAction(Qt.MoveAction)

class Tree(QTreeView):
    def __init__(self):
        super().__init__()
        self.setDragDropMode(QAbstractItemView.DragDrop)
        self.setDropIndicatorShown(True)
        self.viewport().setAcceptDrops(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

    # -- mouse dragging -- #
    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            self.dragStartPosition = event.pos()

        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() != Qt.RightButton:
            return
        if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
            return
        
        drag = ModifiedQDrag(self)
        mimeData = QMimeData()
        mimeData = self.model().mimeData([self.indexAt(event.pos())])
        drag.setMimeData(mimeData)

        dragAction = drag.exec(Qt.MoveAction | Qt.CopyAction, Qt.CopyAction)
        return super().mouseMoveEvent(event)

    def dragMoveEvent(self, event):
        m = event.mimeData()
        if m.hasUrls():
            event.accept()
            return
    
        event.ignore()
        
    def dropEvent(self, event):
        print("[drop event] - dropped")

class FileSystemView(QWidget):
    def __init__(self):
        super().__init__()

        # -- left side -- #
        left_side_dir = r"<Dir>"

        self.model = QFileSystemModel()
        self.model.setRootPath(left_side_dir)

        self.tree = Tree()
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(left_side_dir))

        # -- right side -- #
        right_side_dir = r"<Dir>"

        self.model2 = QFileSystemModel()
        self.model2.setRootPath(right_side_dir)

        self.tree2 = Tree()
        self.tree2.setModel(self.model2)
        self.tree2.setRootIndex(self.model2.index(right_side_dir))
        
        # -- layout -- #
        self.tree_layout = QHBoxLayout()
        self.tree_layout.addWidget(self.tree)
        self.tree_layout.addWidget(self.tree2)

        self.setLayout(self.tree_layout)

app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())

Solution

  • Qt can only react to mouse movements in order to trigger changes in the drop action: as the name suggests, dragMoveEvent() can only be called by a mouse move.

    Considering that, a possible solution is to manually force the mouse movement whenever the keyboard modifiers change. In this way you don't even need to create a QDrag subclass and you can keep the default behavior.

    Be aware that to properly get modifiers, you should not use keyboardModifiers(), but queryKeyboardModifiers(), as the first is only reliable when keyboard events are directly handled and might not be updated with the actual current state of the keyboard.

    class Tree(QTreeView):
        # ...
        def checkDrag(self):
            modifiers = qApp.queryKeyboardModifiers()
            if self.modifiers != modifiers:
                self.modifiers = modifiers
                pos = QCursor.pos()
                # slightly move the mouse to trigger dragMoveEvent
                QCursor.setPos(pos + QPoint(1, 1))
                # restore the previous position
                QCursor.setPos(pos)
    
        def mouseMoveEvent(self, event):
            if event.buttons() != Qt.RightButton:
                return
            if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()):
                return
    
            self.modifiers = qApp.queryKeyboardModifiers()
            # a local timer, it will be deleted when the function returns
            dragTimer = QTimer(interval=100, timeout=self.checkDrag)
            dragTimer.start()
            self.startDrag(Qt.MoveAction|Qt.CopyAction)
    
        def dragMoveEvent(self, event):
            if not event.mimeData().hasUrls():
                event.ignore()
                return
            if qApp.queryKeyboardModifiers() & Qt.ShiftModifier:
                event.setDropAction(Qt.MoveAction)
            else:
                event.setDropAction(Qt.CopyAction)
            event.accept()