Search code examples
pythonpyqt5qtreeviewqfilesystemmodel

Is it possible to create a `QFileSystemModel()` that only shows the file in a certain list?


I'm trying to make an app that shows all the connected files of a certain main file. When you click on a main file, it will show you the list of all files that needs it. As I've started making the app, I'm stuck here one whole day realizing how to create a QFileSystemModel() that only shows the files in a certain list because all the connected files from the mail file are stored in a list.

Here is an example, I want to just show the files in the list which are:

main_file1 = ["[053ALO] - test file.txt", "[053ALO] - test file.txt", "[053ALO] - test file.txt"]

enter image description here

As I've seen on the other related questions, They mentioned the use of QAbstractItemView but I have no Idea how. I'm thinking of iterating the list and making QAbstractItemView in each item and appending it to the treeview but it doesn't show the icon and details of every file which is shown in the picture.

My Question: Is it possible to create a QFileSystemModel() that only shows the files in a certain list?

My Testing Code: (My plan is to use the left side for the main files, and the right one for the connected files)

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, \
    QHBoxLayout

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

        appWidth = 800
        appHeight = 300
        self.setWindowTitle('File System Viewer')
        self.setGeometry(300, 300, appWidth, appHeight)
        
        dir_path = r'<Your directory>'

        self.model = QFileSystemModel()
        self.model.setRootPath(dir_path)
        self.tree =  QTreeView()
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(dir_path))
        self.tree.setColumnWidth(0, 250)
        self.tree.setAlternatingRowColors(True)

        self.model2 = QFileSystemModel()
        self.model2.setRootPath(dir_path)
        self.tree2 =  QTreeView()
        self.tree2.setModel(self.model2)
        self.tree2.setRootIndex(self.model2.index(dir_path))
        self.tree2.setColumnWidth(0, 250)
        self.tree2.setAlternatingRowColors(True)

        layout = QHBoxLayout()
        layout.addWidget(self.tree)
        layout.addWidget(self.tree2)
        self.setLayout(layout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = FileSystemView()
    demo.show()
    sys.exit(app.exec_())

Update:

With the help of @musicamante I'ved realized that I needed to subclass QSortFilterProxyModel in order to customize the filtering that I want to happen. I'm pretty sure that my approach in the code below is close now but I'm still stuck with this problem where when I clicked on a file in the left side, the similar file in the right side disappears. (seen on this video link)

This is the complete oposite of what I want to happen. What I want to happen is when I click a file on the left side, ONLY the file with the same name will aslo appear on the right side.

I tried changing the condition in the if else statement inside the filterAcceptsRow but it just leaves the right side completely empty.

I've provided the Testing code below:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, QHBoxLayout
from PyQt5.QtCore import QSortFilterProxyModel, Qt


class modifiedQSortFilterProxyModel(QSortFilterProxyModel):

    def __init__(self):
        super().__init__()
        self.file = ''

    def filterAcceptsRow(self, source_row, source_parent):

        filename = self.sourceModel().index(source_row, 0, source_parent).data()
        
        if filename == self.file:
            return False
        else:
            return True

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

        appWidth = 800
        appHeight = 300
        self.setWindowTitle('File System Viewer')
        self.setGeometry(300, 300, appWidth, appHeight)
        
        dir_path = r''

        # -- left -- #
        self.model = QFileSystemModel()
        self.model.setRootPath(dir_path)
        self.tree =  QTreeView()
        self.tree.setModel(self.model)
        self.tree.setRootIndex(self.model.index(dir_path))
        self.tree.setColumnWidth(0, 250)
        self.tree.setAlternatingRowColors(True)
        
        self.tree.clicked.connect(self.onClicked)

        # -- right -- #
        self.model2 = QFileSystemModel()
        self.model2.setRootPath(dir_path)

        self.filter_proxy_model = modifiedQSortFilterProxyModel()
        self.filter_proxy_model.setSourceModel(self.model2)
        self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.filter_proxy_model.setDynamicSortFilter(True)
        self.filter_proxy_model.setFilterKeyColumn(0)
        root_index = self.model2.index(dir_path)
        proxy_index = self.filter_proxy_model.mapFromSource(root_index)

        self.tree2 =  QTreeView()
        self.tree2.setModel(self.filter_proxy_model)
        self.tree2.setRootIndex(proxy_index)
        self.tree2.setColumnWidth(0, 250)
        self.tree2.setAlternatingRowColors(True)

        # -- layout -- #
        layout = QHBoxLayout()
        layout.addWidget(self.tree)
        layout.addWidget(self.tree2)
        self.setLayout(layout)

    def onClicked(self, index):
        path = self.sender().model().fileName(index)
        self.filter_proxy_model.file = path
        self.filter_proxy_model.invalidateFilter()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = FileSystemView()
    demo.show()
    sys.exit(app.exec_())

Solution

  • There are two problems.

    First of all, if you want to show only the index that matches the selected file, you should return True, not False (the function says if the row is accepted, meaning that it's shown).

    Then, you cannot just base the filter on the file name: models are hierarchical, and the filter must also match parent indexes (the parent directory); the reason for which you see a blank view is that returning False from a non matching file name, you're filtering out the parent directory.
    Considering this, the filter must always accept an index if the parent is different, and eventually return the result of the comparison. To achieve so, you can use the QFileInfo that refers to the source index, which is returned by the models' fileInfo():

    class ModifiedQSortFilterProxyModel(QSortFilterProxyModel):
        fileInfo = None
        def filterAcceptsRow(self, source_row, source_parent):
            if not self.fileInfo:
                return True
            source_index = self.sourceModel().index(source_row, 0, source_parent)
            info = self.sourceModel().fileInfo(source_index)
            if self.fileInfo.absolutePath() != info.absolutePath():
                return True
            return self.fileInfo.fileName() == info.fileName()
    
        def setFilter(self, info):
            self.fileInfo = info
            self.invalidateFilter()
    
    
    class FileSystemView(QWidget):
        # ...
        def onClicked(self, index):
            self.filter_proxy_model.setFilter(self.model.fileInfo(index))
    

    Note: names of classes and constants should always start with an uppercase letter.