'pyqt5 : don't show empty folders after filtering files with a setNameFilters

Using a QTreeView and a QListView, I want to show only usable files for a certain software.

the QTreeview shows folders only and QListView show the files in folders :

self.treeview = QtWidgets.QTreeView()
self.listview = QtWidgets.QListView()

self.dirModel = QtWidgets.QFileSystemModel()
self.dirModel.setRootPath(QtCore.QDir.rootPath())
self.dirModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)

self.dirModel.setFilter()
self.fileModel = QtWidgets.QFileSystemModel()
self.fileModel.setFilter(QtCore.QDir.NoDotAndDotDot |  QtCore.QDir.Files)

the QListView is filtered to show only .gfr files with

self.fileModel.setNameFilters(['*.gfr'])

it works as expected, but the treeview now shows many folders without content.

my question is : how can I hide automatically folders shows as empty due to filtering ?

EDIT : The goal of it is to offer the user only folders where he/she can find usable files and avoid random search in different folders. when the user found the needed file, it double click it to open with the software

folders structures are the following, capitals name are fixed :

root

  |__assetType1 (changing name, 5 possible names )

        |__asset1 (changing name, from 1 to around 50 possible names)

             |__WORK(fixed name for all assets )

                 |__SHD(fixed name - contain the wanted gfr files when they exist)

                 |__TEX (fixed name, 5 possible names, all needs to be hidden except 'SHD' )

             |__PUBLISH (fixed name - needs to be hidden )

the goal is to hide 'asset' folder and all subfolders from TreeView if no '.gfr' files are found in it's WORK/SHD subdirectory



Solution 1:[1]

The only option is to use a QSortFilterProxyModel subclass.

In the following example, I've reimplemented both filterAcceptsRow and hasChildren; note that for performance reasons QFileSystemModel obviously doesn't load the whole directory tree instantly, but only whenever required; this requires that all directories that have child directory will be always visible and some directories might look clickable (because they have a subfolder), even if their contents don't match the filter.

class DirProxy(QtCore.QSortFilterProxyModel):
    nameFilters = ''
    def __init__(self):
        super().__init__()
        self.dirModel = QtWidgets.QFileSystemModel()
        self.dirModel.setRootPath(QtCore.QDir.rootPath())
        self.dirModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)
        self.setSourceModel(self.dirModel)

    def setNameFilters(self, filters):
        if not isinstance(filters, (tuple, list)):
            filters = [filters]
        self.nameFilters = filters
        self.invalidateFilter()

    def fileInfo(self, index):
        return self.dirModel.fileInfo(self.mapToSource(index))

    def hasChildren(self, parent):
        sourceParent = self.mapToSource(parent)
        if not self.dirModel.hasChildren(sourceParent):
            return False
        qdir = QtCore.QDir(self.dirModel.filePath(sourceParent))
        return bool(qdir.entryInfoList(qdir.NoDotAndDotDot|qdir.Dirs))

    def filterAcceptsRow(self, row, parent):
        source = self.dirModel.index(row, 0, parent)
        if source.isValid():
            qdir = QtCore.QDir(self.dirModel.filePath(source))
            if self.nameFilters:
                qdir.setNameFilters(self.nameFilters)
            return bool(qdir.entryInfoList(
                qdir.NoDotAndDotDot|qdir.AllEntries|qdir.AllDirs))
        return True


class Test(QtWidgets.QWidget):
    def __init__(self):
        # ...
        self.dirProxy = DirProxy()
        self.treeView.setModel(self.dirProxy)
        self.dirProxy.setNameFilters(['*.py'])
        self.treeView.clicked.connect(self.treeClicked)

        self.fileModel = QtWidgets.QFileSystemModel()
        self.listView.setModel(self.fileModel)
        self.fileModel.setNameFilters(['*.py'])
        self.fileModel.setFilter(QtCore.QDir.NoDotAndDotDot |  QtCore.QDir.Files)

    def treeClicked(self, index):
        path = self.dirProxy.fileInfo(index).absoluteFilePath()
        self.listView.setRootIndex(self.fileModel.setRootPath(path))

Solution 2:[2]

Thank you @musicamante, for helping me solve my issue in the comments.

According to @musicamante, if you need to implement this in a single model, then you must change the behavior of filterAcceptsRow() by checking first if the index isDir(), then proceed with the existing implementation or use QDir.match() if it's not.

This is the code I ended up basing on the implementation above:

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

class DirProxy(QSortFilterProxyModel):
    nameFilters = ''
    def __init__(self):
        super().__init__()
        self.dirModel = QFileSystemModel()
        self.dirModel.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Files) # <- added QDir.Files to view all files
        self.setSourceModel(self.dirModel)

    def setNameFilters(self, filters):
        if not isinstance(filters, (tuple, list)):
            filters = [filters]
        self.nameFilters = filters
        self.invalidateFilter()

    def hasChildren(self, parent):
        sourceParent = self.mapToSource(parent)
        if not self.dirModel.hasChildren(sourceParent):
            return False
        qdir = QDir(self.dirModel.filePath(sourceParent))
        return bool(qdir.entryInfoList(qdir.NoDotAndDotDot|qdir.AllEntries|qdir.AllDirs))
    
    def filterAcceptsRow(self, row, parent):
        source = self.dirModel.index(row, 0, parent)
        if source.isValid():
            if self.dirModel.isDir(source):
                qdir = QDir(self.dirModel.filePath(source))
                if self.nameFilters:
                    qdir.setNameFilters(self.nameFilters)
                return bool(qdir.entryInfoList(qdir.NoDotAndDotDot|qdir.AllEntries|qdir.AllDirs))

            elif self.nameFilters:  # <- index refers to a file
                qdir = QDir(self.dirModel.filePath(source))
                return qdir.match(self.nameFilters, self.dirModel.fileName(source)) # <- returns true if the file matches the nameFilters
        return True

class Test(QWidget):
    def __init__(self):
        super().__init__()
        
        self.dirProxy = DirProxy()
        self.dirProxy.dirModel.directoryLoaded.connect(lambda : self.treeView.expandAll())
        self.dirProxy.setNameFilters(['*.ai'])  # <- filtering all files and folders with "*.ai"
        self.dirProxy.dirModel.setRootPath(r"<Dir>")

        self.treeView = QTreeView()
        self.treeView.setModel(self.dirProxy)

        root_index = self.dirProxy.dirModel.index(r"<Dir>")
        proxy_index = self.dirProxy.mapFromSource(root_index)
        self.treeView.setRootIndex(proxy_index)

        self.treeView.show()

app = QApplication(sys.argv)
ex = Test()
sys.exit(app.exec_())

This is the testing I did and the result looks just fine to me:

Trial 1: enter image description here

Trial 2: enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 musicamante
Solution 2