pyqt5drag-and-dropqtreeviewqfilesystemmodel

Drag and Drop not working in `QFileModelSystem`


I'm trying to make a drag and drop behavior in QFileSystemModel but because I have no experience in making a drag and drop before, I tried it first on QTreeView. (I attached the video of the behavior)

enter image description here

Now that I'm fine with the behavior I want, I then just changed the model to QFileSystemModel but sadly It's not working. So I tried to read the QFileSystemModel, QTreeView, and Drag and Drop from Qt and I ended up with the code below:

The code I ended up with:

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

class MQTreeView(QTreeView):
    def __init__(self, model):
        super().__init__()
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        # self.setDragDropMode(QAbstractItemView.InternalMove)
        self.setModel(model)
        self.setDragDropMode(QAbstractItemView.DragDrop)
        self.setRootIndex(model.index(os.path.dirname(os.path.abspath("__file__"))))
        self.setDefaultDropAction(Qt.MoveAction)
        self.viewport().setAcceptDrops(True)

    def dragEnterEvent(self, event):
        m = event.mimeData()
        if m.hasUrls():
            event.accept()
            return
        event.ignore()
        # return super().dragEnterEvent(event)

    def dropEvent(self, event):
        print("[drop event] - dropped")
        if event.source():
            QTreeView.dropEvent(self, event)
        else:
            ix = self.indexAt(event.pos())
            model = self.model()

            if ix.isValid():
                if not model.isDir(ix):
                    ix = ix.parent()      # In case of folder/Dir
                pathDir = model.filePath(ix)
            else:
                # for empty drag and drop
                pathDir = model.rootPath()

            m = event.mimeData()
            if m.hasUrls():
                urlLocals = [url for url in m.urls() if url.isLocalFile()]
                accepted = False
                for urlLocal in urlLocals:
                    path = urlLocal.toLocalFile()
                    info = QFileInfo(path)
                    n_path = QDir(pathDir).filePath(info.fileName())
                    o_path = info.absoluteFilePath()
                    if n_path == o_path:
                        continue
                    if info.isDir():
                        QDir().rename(o_path, n_path)
                    else:
                        qfile = QFile(o_path)
                        if QFile(n_path).exists():
                            n_path += "(copy)" 
                        qfile.rename(n_path)
                        print(f"added -> {info.fileName()}")

                    accepted = True
                if accepted:
                    event.acceptProposedAction()

        # return super().dropEvent(event)

class AppDemo(QWidget):
    def __init__(self):
        super().__init__()
        # -- right -- #
        self.model1 = QFileSystemModel()
        self.model1.setRootPath(os.path.dirname(os.path.abspath("__file__")))

        self.view1 = MQTreeView(self.model1)

        # -- left -- #
        self.model2 = QFileSystemModel()
        self.model2.setRootPath(os.path.dirname(os.path.abspath("__file__")))

        self.view2 = MQTreeView(self.model2)

        # -- layout -- #
        layout = QHBoxLayout(self)
        layout.addWidget(self.view1)
        layout.addWidget(self.view2)

app = QApplication(sys.argv)
main = AppDemo()
main.show()
app.exec_()

The code above is still not doing the behavior I want but I'm pretty sure that something else is wrong and it is not with the overridden function (dragEnterEvent and dropEvent). My best guess is that I didn't set properly the correct way QTreeView accepts the drops although I'm not really sure.

My Question: What is wrong with my Implementation? Is it the way I accept drops or it is something else?


Solution

  • Found what's wrong! I didn't override the dragMoveEvent method. I need to override the dragMoveEvent to make sure that the drag will not be forbidden.

    I need to accept all drag event in the dragEnterEvent:

    def dragEnterEvent(self, event):
        event.accept()
    

    Then I need to filter the events in the dragMoveEvent:

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

    I attached the video and code of the working behavior below.

    enter image description here

    The final implementation:

    import os
    import sys
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    from PyQt5.QtGui import *
    
    class MQTreeView(QTreeView):
        def __init__(self, model, path):
            super().__init__()
    
            self.setSelectionMode(QAbstractItemView.ExtendedSelection)
            self.setModel(model)
            self.setDragDropMode(QAbstractItemView.DragDrop)
            self.setRootIndex(model.index(path))
            self.setDefaultDropAction(Qt.MoveAction)
            self.viewport().setAcceptDrops(True)
    
        def dragEnterEvent(self, event):
            event.accept()
    
        def dragMoveEvent(self, event):
            m = event.mimeData()
            if m.hasUrls():
                event.accept()
                print("[dropEnterEvent] - event accepted")
                return
            event.ignore()
    
        def dropEvent(self, event):
            print("[drop event] - dropped")
            if event.source():
                ix = self.indexAt(event.pos())
                model = self.model()
    
                if ix.isValid():
                    if not model.isDir(ix):
                        ix = ix.parent()
                    pathDir = model.filePath(ix)
                else:
                    # for empty drag and drop
                    pathDir = model.rootPath()
    
                m = event.mimeData()
                if m.hasUrls():
                    urlLocals = [url for url in m.urls() if url.isLocalFile()]
                    accepted = False
                    for urlLocal in urlLocals:
                        path = urlLocal.toLocalFile()
                        info = QFileInfo(path)
                        destination = QDir(pathDir).filePath(info.fileName())
                        source = info.absoluteFilePath()
                        if destination == source:
                            continue  # means they are in the same folder
                        if info.isDir():
                            QDir().rename(source, destination)
                        else:
                            qfile = QFile(source)
                            if QFile(destination).exists():
                                n_info = QFileInfo(destination)
                                
                                destination = n_info.canonicalPath() + QDir.separator() + n_info.completeBaseName() + " (copy)"
                                if n_info.completeSuffix():   # for moving files without suffix
                                    destination += "." + n_info.completeSuffix()
    
                            qfile.rename(destination)
                            print(f"added -> {info.fileName()}")  # for debugging
    
                        accepted = True
                    if accepted:
                        event.acceptProposedAction()
    
    class AppDemo(QWidget):
        def __init__(self):
            super().__init__()
            self.setAcceptDrops(True)
    
            cwd = "test/"
            nw = "test copy/"
    
            # -- right -- #
            self.model1 = QFileSystemModel()
            self.model1.setRootPath(os.path.dirname(cwd))
    
            self.view1 = MQTreeView(self.model1, cwd)
    
            # -- left -- #
            self.model2 = QFileSystemModel()
            self.model2.setRootPath(os.path.dirname(nw))
    
            self.view2 = MQTreeView(self.model2, nw)
    
            # -- layout -- #
            layout = QHBoxLayout(self)
            layout.addWidget(self.view1)
            layout.addWidget(self.view2)
    
    app = QApplication(sys.argv)
    main = AppDemo()
    main.show()
    app.exec_()