0

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

William Eguienta
  • 135
  • 1
  • 13

2 Answers2

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))
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • thank you for your answer " 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." understood, sad news, that's mean that I can't have the desired result as the goal is to hide all directories without .gfr files in it – William Eguienta Aug 31 '20 at 15:48
  • 1
    @WilliamEguienta you must choose: bad performance or ui limitations. Browsing through a directory tree might require a lot of time and system resources. What if a file that satisfies your filter is under two/three levels of directories with 200 folders each? You wouldn't know until you actually reach that file in that directory: if you do it programmatically, it means that your program would try to navigate through the *whole* file system (in some cases this requires at least 10-15 minutes) in order to correctly show only top level folders that at some level contain at least one filtered file. – musicamante Aug 31 '20 at 16:19
  • 1
    Anyway I'm starting to think that you might be facing some sort of an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem), also considering your [other question](https://stackoverflow.com/questions/63657566/pyqt5-have-multiple-roots-qtreeview); if they are related (as I believe they are), you might prefer to explain more carefully what you need to do, the specific context, if those files are normally restricted in some specific locations, and what are the available options for the user (what could he/she do? with what? how? ...). – musicamante Aug 31 '20 at 16:27
  • i don't think it's an XY problem as my goal is clear : show to the user only what he/she needs to start working, in this case, folder with .gfr files. I understand how complex and bad in performance that can be thanks to you, even if my hierarchy won't go over 4 subdirectories per asset (i update the post to be more clear on that) My other question is kind of related yes, but not needed for the same part of the tool so it's still 2 differents problems – William Eguienta Sep 01 '20 at 04:48
  • @musicamante - I tried the subclassed `QSortFilterProxyModel`, but it didn't seem to work the way I expected. You can see in [this picture](https://i.imgur.com/lfvjjXj.png) that instead of removing the folders, it just removed the files that ends with `.ai`. I also added [the code I used](https://gist.github.com/eliazar-sll/a818be3e7ce6114daeb78ad4be0f2e20) to reproduce the issue. – Eliazar May 10 '22 at 11:11
  • @EliazarInso the question is about a tree view that shows the directories and a *separate* listview that shows the files. If you want to implement that in a single model, then you must change the behavior of `filterAcceptsRow()` by checking first if the index [`isDir()`](https://doc.qt.io/qt-5/qfilesystemmodel.html#isDir), then proceed with the existing implementation or use [`QDir.match()`](https://doc.qt.io/qt-5/qdir.html#match-1) if it's not. Besides, you must ***not*** remove `NoDotAndDotDot`, as it has nothing to do with folders having `.` in their names. – musicamante May 10 '22 at 20:55
  • @musicamante Thank you! I tried what you've suggested and posted it as an answer. – Eliazar May 11 '22 at 05:21
0

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

Eliazar
  • 301
  • 3
  • 13