Search code examples
pythonpyside2qlistviewqfilesystemmodel

How to drag and drop files items inside a folder item using QFileSystemModel and QListView?


I'm creating a widget to explore and manage files inside my qt application. To construct that, I'm using a QFileSystemModel and a QListView with the IconMode view mode.

It should allow moving files (items) into folders (other items) with the QListView.

My question is how do I implement this?

First, I'm trying to override the supportedDragActions and supportedDropActions functions from ContentFileSystemModel to allow Move and Copy actions. Also, I override the flags function to enable drag and drop. Finally, I override the canDropMimeData and dropMimeData to check if they are running, but it looks like they are not.

The first problem is that the model does not allow to drop an item file into the QListView area once it displays a prohibited icon in the cursor (shown in the image below).

First, I have to set the model to allow the drops of items in folders. After that, I can implement the code to transfer the dragged items into the folder.

enter image description here

Code ready to reproduce the problem:

import sys
import os

from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *


class ContentFileSystemModel(QFileSystemModel):

    def __init__(self):
        super(ContentFileSystemModel, self).__init__()

    def supportedDragActions(self) -> Qt.DropActions:
        print("supportedDragActions")
        return Qt.MoveAction | super(ContentFileSystemModel, self).supportedDragActions() | Qt.CopyAction

    def supportedDropActions(self) -> Qt.DropActions:
        print("supportedDropActions")
        return Qt.MoveAction | super(ContentFileSystemModel, self).supportedDropActions() | Qt.CopyAction

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        defaultFlags = super(ContentFileSystemModel, self).flags(index)
        if not index.isValid():
            return defaultFlags
        fileInfo = self.fileInfo(index)
        # The target
        if fileInfo.isDir():
            # Allowed drop
            return Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | defaultFlags
        # The source: should be directory( in that case)
        elif fileInfo.isFile():
            # Allowed drag
            return Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | defaultFlags
        return defaultFlags

    def canDropMimeData(self, data: QMimeData, action: Qt.DropAction,
                        row: int, column: int, parent: QModelIndex) -> bool:
        print("canDropMimeData")
        return True

    def dropMimeData(self, data: QMimeData, action: Qt.DropAction,
                     row: int, column: int, parent: QModelIndex) -> bool:
        print("dropMimeData")
        return True


def main(argv):
    app = QApplication(sys.argv)

    path = "C:\\Users\\Me\\Desktop"
    file_system_model = ContentFileSystemModel()
    file_system_model.setRootPath(path)
    file_system_model.setReadOnly(False)

    lv_file_manager = QListView()
    lv_file_manager.setModel(file_system_model)
    lv_file_manager.setViewMode(QListView.IconMode)
    lv_file_manager.setRootIndex(file_system_model.index(path))
    lv_file_manager.setResizeMode(QListView.Adjust)

    lv_file_manager.setMovement(QListView.Static)
    lv_file_manager.setSelectionMode(QAbstractItemView.ExtendedSelection)
    lv_file_manager.setWrapping(True)
    lv_file_manager.setAcceptDrops(True)
    lv_file_manager.setDragEnabled(True)
    lv_file_manager.setDropIndicatorShown(True)
    lv_file_manager.setUniformItemSizes(True)
    lv_file_manager.setDragDropMode(QAbstractItemView.InternalMove)
    lv_file_manager.setFlow(QListView.LeftToRight)

    lv_file_manager.show()
    app.exec_()


if __name__ == "__main__":
    main(sys.argv)

Solution

  • You are setting the wrong movement property, since you're using Static:

    The items cannot be moved by the user.

    When using the IconMode, that property is automatically set to Free, so you can just remove the following line:

    lv_file_manager.setMovement(QListView.Static)
    

    The other important implementations are in the model's canDropMimeData() (which must return True if the target is a writable directory) and dropMimeData() (that will actually move the files).

    The final step is to override the dragMoveEvent() to prevent moving icons around the current view.

    Note that the following changes have also been made:

    • flags() should not return ItemIsDragEnabled if the target is a file;
    • setAcceptDrops(True) and setDragEnabled(True) are not required as they are automatically set when the movement is not Static (which is the case when using the IconMode as explained above);
    • setDragDropMode() is also not required;
    class ContentFileSystemModel(QFileSystemModel):
        # ...
        def flags(self, index: QModelIndex) -> Qt.ItemFlags:
            defaultFlags = super(ContentFileSystemModel, self).flags(index)
            if not index.isValid():
                return defaultFlags
            fileInfo = self.fileInfo(index)
            if fileInfo.isDir():
                return Qt.ItemIsDropEnabled | Qt.ItemIsDragEnabled | defaultFlags
            elif fileInfo.isFile():
                # files should *not* be drop enabled
                return Qt.ItemIsDragEnabled | defaultFlags
            return defaultFlags
    
        def canDropMimeData(self, data: QMimeData, action: Qt.DropAction,
                            row: int, column: int, parent: QModelIndex) -> bool:
            if row < 0 and column < 0:
                target = self.fileInfo(parent)
            else:
                target = self.fileInfo(self.index(row, column, parent))
            return target.isDir() and target.isWritable()
    
        def dropMimeData(self, data: QMimeData, action: Qt.DropAction,
                         row: int, column: int, parent: QModelIndex) -> bool:
            if row < 0 and column < 0:
                targetDir = QDir(self.fileInfo(parent).absoluteFilePath())
            else:
                targetDir = QDir(self.fileInfo(self.index(row, column, parent)).absoluteFilePath())
            dataList = []
            # first check if the source is writable (so that we can move it) 
            # and that it doesn't already exist on the target path
            for url in data.text().splitlines():
                path = QUrl(url).toLocalFile()
                fileObject = QFile(path)
                if not fileObject.permissions() & QFile.WriteUser:
                    return False
                targetPath = targetDir.absoluteFilePath(QFileInfo(path).fileName())
                if targetDir.exists(targetPath):
                    return False
                dataList.append((fileObject, targetPath))
            # actually move the objects, you might want to add some feedback
            # if movement failed (eg, no space left) and eventually undo the
            # whole operation
            for fileObject, targetPath in dataList:
                if not fileObject.rename(targetPath):
                    return False
            return True
    
    class FileView(QListView):
        def dragMoveEvent(self, event):
            # accept drag movements only if the target supports drops
            if self.model().flags(self.indexAt(event.pos())) & Qt.ItemIsDropEnabled:
                super().dragMoveEvent(event)
            else:
                event.ignore()
    
    
    def main(argv):
        # ...
        lv_file_manager = FileView()