Concatenate Two QFileSystemModels

274 Views Asked by At

In this post, my goal is to concatenate two QFileSystemModels to one and display them together. (Lots of updates has been made)

Context :

In my C drive , I created the folder MyFolder (https://drive.google.com/drive/folders/1M-b2o9CiohXOgvjoZrAnl0iRVQBD1sXY?usp=sharing) , in which there are some folders and some files, for the sake of producing the minimal reproducible example . Their structure is :

enter image description here

The following Python code using PyQt5 library (modified from How to display parent directory in tree view?) runs after importing necessary libraries:

#The purpose of the proxy model is to display the directory.
#This proxy model is copied here from the reference without modification.
class ProxyModel(QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._root_path = ""

    def filterAcceptsRow(self, source_row, source_parent):
        source_model = self.sourceModel()
        if self._root_path and isinstance(source_model, QFileSystemModel):
            root_index = source_model.index(self._root_path).parent()
            if root_index == source_parent:
                index = source_model.index(source_row, 0, source_parent)
                return index.data(QFileSystemModel.FilePathRole) == self._root_path
        return True

    @property
    def root_path(self):
        return self._root_path

    @root_path.setter
    def root_path(self, p):
        self._root_path = p
        self.invalidateFilter()


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.create_treeview()
        self.setCentralWidget(self.treeView_1) #The line I will be talking about.

    def create_treeview(self):
        
        self.treeView_1 = QTreeView()
        self.dirModel_1 = QFileSystemModel()
        self.dirModel_1.setRootPath(QDir.rootPath())
        path_1 = 'C:/MyFolder/SubFolder1' # Changing the path is sufficient to change the displayed directory
        root_index_1 = self.dirModel_1.index(path_1).parent()
        self.proxy_1 = ProxyModel(self.dirModel_1)
        self.proxy_1.setSourceModel(self.dirModel_1)
        self.proxy_1.root_path = path_1
        self.treeView_1.setModel(self.proxy_1)
        proxy_root_index_1 = self.proxy_1.mapFromSource(root_index_1)
        self.treeView_1.setRootIndex(proxy_root_index_1)
        
        self.treeView_2 = QTreeView()
        self.dirModel_2 = QFileSystemModel()
        self.dirModel_2.setRootPath(QDir.rootPath())
        path_2 = 'C:/MyFolder'
        root_index_2 = self.dirModel_2.index(path_2).parent()
        self.proxy_2 = ProxyModel(self.dirModel_2)
        self.proxy_2.setSourceModel(self.dirModel_2)
        self.proxy_2.root_path = path_2
        self.treeView_2.setModel(self.proxy_2)
        proxy_root_index_2 = self.proxy_2.mapFromSource(root_index_2)
        self.treeView_2.setRootIndex(proxy_root_index_2)

if __name__ == "__main__":
    import sys

    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

The line self.setCentralWidget(self.treeView_1) gives:

enter image description here

Changing self.setCentralWidget(self.treeView_1) to self.setCentralWidget(self.treeView_2) gives:

enter image description here

Objective:

My goal is to concatenate the two trees together. That is, when click run, the user should be able to see:

enter image description here

The order which they show up does not matter. All I care is that MyFolder and SubFolder1 show up as if they are completely independent items (even though in reality one is a subfolder of the other). I should remark that everything is static. That is, we are not trying to detect any changes on folders or files. The only time we ever need to peak at the existing folders and files will be when we click on run.

Update:

After several days of studying and trying, a major progress has been made. I thank musicamante for the hint of using QTreeWidget. The idea is, as said in comments, traverse through models and gradually move everything into one new QTreeWidget. To avoid freeze, my solution is to ask the QFileSystemModel to fetchMore whenever the user wants to see more (i.e. when the user wants to extend QTreeWidget).

The following code runs and almost solves my problem:

import os
from PyQt5.QtCore import*
from PyQt5.QtWidgets import*
from PyQt5 import QtTest

class To_Display_Folder(QSortFilterProxyModel):
    def __init__(self, disables=False, parent=None):
        super().__init__(parent)
        #self.setFilterRegularExpression(r'^(.*\.dcm|[^.]+)$')
        self._disables = bool(disables)
        
        self._root_path = ""

    def filterAcceptsRow(self, source_row, source_parent):
        source_model = self.sourceModel()
        #case 1 folder
        if self._root_path and isinstance(source_model, QFileSystemModel):
            root_index = source_model.index(self._root_path).parent()
            if root_index == source_parent:
                index = source_model.index(source_row, 0, source_parent)
                return index.data(QFileSystemModel.FilePathRole) == self._root_path
        
        return True
        
'''
        #case 2 file
        file_index = self.sourceModel().index(source_row, 0, source_parent)
        if not self._disables:
            return self.matchIndex(file_index)
        return file_index.isValid()
'''        
    @property
    def root_path(self):
        return self._root_path

    @root_path.setter
    def root_path(self, p):
        self._root_path = p
        self.invalidateFilter()

    def matchIndex(self, index):
        return (self.sourceModel().isDir(index) or
                super().filterAcceptsRow(index.row(), index.parent()))

    def flags(self, index):
        flags = super().flags(index)
        if (self._disables and
            not self.matchIndex(self.mapToSource(index))):
            flags &= ~Qt.ItemIsEnabled
        return flags

class Widget_Item_from_Proxy(QTreeWidgetItem):
    def __init__(self, index_in_dirModel, parent = None):
        super().__init__(parent)
        self.setText(0, index_in_dirModel.data(QFileSystemModel.FileNameRole))
        self.setText(1, index_in_dirModel.data(QFileSystemModel.FilePathRole))
        if os.path.isfile(index_in_dirModel.data(QFileSystemModel.FilePathRole)):
            self.setIcon(0,QApplication.style().standardIcon(QStyle.SP_FileIcon))
        else:
            self.setIcon(0,QApplication.style().standardIcon(QStyle.SP_DirIcon))

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        global treeWidget 
        treeWidget = QTreeWidget()
        self.treeWidget = treeWidget
        self.treeWidget.itemExpanded.connect(self.upon_expansion)
        self.treeWidget.itemClicked.connect(self.tree_click)
        
        #The following directories will be displayed on the tree.
        self.add_path_to_tree_widget('C:/MyFolder')
        self.add_path_to_tree_widget('C:/Users/r2d2w/OneDrive/Desktop')
        self.add_path_to_tree_widget('C:/')
        
        self.setCentralWidget(self.treeWidget)
    
    def add_path_to_tree_widget(self,path):
        dirModel = QFileSystemModel()
        dirModel.setRootPath(QDir.rootPath())
        dirModel.directoryLoaded.connect(lambda: self.once_loaded(path, dirModel))
    
    def once_loaded(self, path, dirModel):
        if dirModel.canFetchMore(dirModel.index(path)):
            dirModel.fetchMore(dirModel.index(path))
            return
        root_index = dirModel.index(path).parent()
        proxy = To_Display_Folder(disables = False, parent = dirModel)
        proxy.setSourceModel(dirModel)
        proxy.root_path = path
        proxy_root_index = proxy.mapFromSource(root_index)
        origin_in_proxy = proxy.index(0,0,parent = proxy_root_index)
        root_item = Widget_Item_from_Proxy(
            proxy.mapToSource(origin_in_proxy))
        self.treeWidget.addTopLevelItem(root_item)
        
        for row in range(0, proxy.rowCount(origin_in_proxy)):
            proxy_index = proxy.index(row,0,parent = origin_in_proxy)
            child = Widget_Item_from_Proxy(
                proxy.mapToSource(proxy_index), 
                parent = self.treeWidget.topLevelItem(self.treeWidget.topLevelItemCount()-1))
        dirModel.directoryLoaded.disconnect()
        
    @pyqtSlot(QTreeWidgetItem)
    def upon_expansion(self, treeitem):
        for i in range(0, treeitem.childCount()):
            if os.path.isdir(treeitem.child(i).text(1)):
                self.add_child_path_to_tree_widget(treeitem.child(i))

    def add_child_path_to_tree_widget(self,subfolder_item):
        subfolder_path = subfolder_item.text(1)
        dirModel = QFileSystemModel()
        dirModel.setRootPath(QDir.rootPath())
        dirModel.directoryLoaded.connect(lambda: self.child_once_loaded(subfolder_item, subfolder_path,dirModel))

    def child_once_loaded(self, subfolder_item, subfolder_path, dirModel):
        if dirModel.canFetchMore(dirModel.index(subfolder_path)):
            dirModel.fetchMore(dirModel.index(subfolder_path))
            return
        
        root_index = dirModel.index(subfolder_path).parent()
        proxy = To_Display_Folder(disables = False, parent = dirModel)
        proxy.setSourceModel(dirModel)
        proxy.root_path = subfolder_path
        proxy_root_index = proxy.mapFromSource(root_index)
        origin_in_proxy = proxy.index(0,0,parent = proxy_root_index)
        
        root_item = Widget_Item_from_Proxy(
            proxy.mapToSource(origin_in_proxy))

        folder_item = subfolder_item.parent()
        itemIndex = folder_item.indexOfChild(subfolder_item)
        folder_item.removeChild(subfolder_item)
        folder_item.insertChild(itemIndex, root_item)

        for row in range(0, proxy.rowCount(origin_in_proxy)):
            proxy_index = proxy.index(row,0,parent = origin_in_proxy)
            child = Widget_Item_from_Proxy(
                proxy.mapToSource(proxy_index), 
                parent = root_item)

        dirModel.directoryLoaded.disconnect()

    @pyqtSlot(QTreeWidgetItem)
    def tree_click(self, item):
        print(item.text(0))
        print(item.text(1))

if __name__ == "__main__":
    import sys

    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

Since the bounty period is still not over, I will use the time to post two new questions:

  1. Sometimes, especially when the line self.add_path_to_tree_widget('C:/') is present, the code does not give all directories when we click run. This problem is easily fixed by closing the window and clicking on run again. This problem occurs because the QFileSystemModel does not yet have enough time to traverse through the designated folder. If it has just a little bit more time, it will be able to. I wonder if there is a way to fix this programatically.

  2. The function add_path_to_tree_widget is similar to add_child_path_to_tree_widget. The function once_loaded is similar to child_once_loaded. I wonder if there is a way to write these functions more succinctly.

1

There are 1 best solutions below

1
musicamante On

While not impossible, it's quite difficult to create a unique and dynamic model that is able to access different QFileSystemModel structures.

An easier and simpler implementation, which would be more practical for static purposes, is to use a QTreeWidget and create items recursively.

class MultiBrowser(QTreeWidget):
    def __init__(self, *pathList):
        super().__init__()
        self.iconProvider = QFileIconProvider()
        self.setHeaderLabels(['Name'])

        for path in pathList:
            item = self.createFSItem(QFileInfo(path), self.invisibleRootItem())
            self.expand(self.indexFromItem(item))

    def createFSItem(self, info, parent):
        item = QTreeWidgetItem(parent, [info.fileName()])
        item.setIcon(0, self.iconProvider.icon(info))
        if info.isDir():
            infoList = QDir(info.absoluteFilePath()).entryInfoList(
                filters=QDir.AllEntries | QDir.NoDotAndDotDot, 
                sort=QDir.DirsFirst
            )
            for childInfo in infoList:
                self.createFSItem(childInfo, item)
        return item

# ...
multiBrowser = MultiBrowser('path1', 'path2')

For obvious reasons, the depth of each path and their contents will freeze the UI from interaction until the whole structure has been crawled.

If you need a more dynamic approach, consider using the QFileSystemModel as a source for path crawling, along with its directoryLoaded signal, which will obviously require a more complex implementation.