Search code examples
pythonqtreeviewpyside6

PySide6 Exclude Folders from QTreeView


I have an application where i need to display 2 different TreeViews. One for showing the folders (folderView) and the other will display the Files (fileView) inside the selected folder from the folderView. The following Code works fine but i am having a strange issue: in the screen shot below, if i click on the bin folder for example, then switch back to VBoxGuestAdd.., the fileView will display the bin folder in the fileView. p.s.: using an ubuntu 22.04 machine

Please notice the bin folder in the fileView

and here my code:

import sys
from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QWidget, QHBoxLayout, QTreeView, QFileSystemModel


def folderView_selectionchanged():
    current_index = folderView.currentIndex()
    selected_folder_path = folderModel.fileInfo(current_index).absoluteFilePath()
    fileView.setRootIndex(fileModel.setRootPath(selected_folder_path))


app = QApplication(sys.argv)

window = QWidget()
layout = QHBoxLayout()

folderView = QTreeView()
folderModel = QFileSystemModel()
folderModel.setRootPath("/")
folderModel.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs)
folderView.setModel(folderModel)
folderView.selectionModel().selectionChanged.connect(folderView_selectionchanged)

fileView = QTreeView()
fileModel = QFileSystemModel()
fileModel.setRootPath("/")
fileModel.setFilter(QDir.NoDotAndDotDot | QDir.Files)
fileView.setModel(fileModel)

layout.addWidget(folderView)
layout.addWidget(fileView)
window.setLayout(layout)

window.show()
app.exec()

Solution

  • This is a "bug" probably caused by the asynchronous nature of QFileSystemModel, which uses threading to fill the model and delays calls for the model structure updates.

    It seems that it's also been already reported as QTBUG-93634, but it has got no attention yet.

    A possible workaround is to "reset" the filter and set it again:

    def folderView_selectionchanged():
        current_index = folderView.currentIndex()
        selected_folder_path = folderModel.fileInfo(current_index).absoluteFilePath()
        fileView.setRootIndex(fileModel.setRootPath(selected_folder_path))
        fileModel.setFilter(QDir.AllDirs)
        fileModel.setFilter(QDir.NoDotAndDotDot | QDir.Files)
    

    But the above might not work for big/slow file systems. The only possible solution I can think of is to use a QSortFilterProxyModel and override the filterAcceptsRow() function. It will not be as fast as the basic model, but it will work as expected.

    Note that:

    • filterAcceptsRow() is always checked against the model hierarchy, so always allowing the filter to pass anything outside the filtered directory is mandatory, otherwise it will be filtered out (if the parent directory is filtered out, there is no child to show);
    • since setRootPath() invalidates the layout and checks the filters again, we must clear the validation until the new root path is set, and then restore it and reset the filters again; this is done by temporarily replacing the actual filtering function with a dummy one that always returns True;
    class FileProxy(QSortFilterProxyModel):
        validParent = None
        def __init__(self):
            super().__init__()
            self.fsModel = QFileSystemModel()
            self.setSourceModel(self.fsModel)
    
        def filterAcceptsRow(self, row, parent):
            return (
                self.validParent != parent
                or not self.fsModel.isDir(
                    self.fsModel.index(row, 0, parent))
            )
    
        def setRootPath(self, path):
            func = self.filterAcceptsRow
            self.filterAcceptsRow = lambda *args: True
            self.validParent = self.fsModel.setRootPath(path)
            self.filterAcceptsRow = func
            self.invalidateFilter()
            return self.mapFromSource(self.validParent)