0

I am trying to create a file browser which will group sequential files in the same director together in a tree view. For example if you had the following files in a directory: img.001.png img.002.png img.003.png, images 2 & 3 would appear as children of image 1 in a QTreeView. They would simply be made children of the first sequential item in the directory.

I have tried a number of ways to accomplish this but so far performance has been a huge issue. Doing it all manually with a QStandardItemModel and python os module was very slow and cumbersome. The QFileSystemModel is surprisingly fast and includes a ton of convenient functions for renaming files, etc, so I am trying to work with that as my base. I tried creating a QStandardItemModel as a sort of "proxy model" which iterated over the QFileSystemModel when the root dir changed. This allowed me to create all of the functionality I need, but was shockingly slow (I guess there is a lot of signaling under the hood or something?). Setting a root dir in the FileSystemModel takes virtually no time, but with a custom model iterating over rows and re-interpreting data it can take 5-10 seconds for one folder with 1000+ files.

So I am thinking that the best solution would be to subclass the QFileSystemModel directly because it has methods built in I want and is lightning fast. However I do not know very much about models and I'm not quite sure where to start. I see that there are quite a few signals that can notify me when rows are going to be inserted, have been inserted, are being moved, etc, etc. But at what point would I be able to check if a row was a sequential file, and if so "re-direct" it to be a child of another index?

Hopefully that is all clear, here is a snippet to get up and running. I do not usually post such general questions on SO but I have put a lot of time into this problem and need some direction as to where to go. I don't need you to write my code for me, but rather pointing me in the right direction would be very helpful!

import sys, os, re
from PyQt5 import QtWidgets

class TestApp(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()

        self.tree = QtWidgets.QTreeView(self)

        self.model = QtWidgets.QFileSystemModel()
        self.proxyModel = ProxyModel(self.model)
        self.tree.setModel(self.proxyModel)
        self.model.setRootPath("test/dir/with/lots/of/files")
        self.model.directoryLoaded.connect(self.update)

    def update(self):
        self.proxyModel.update_model_from_source()

class ProxyModel(QtGui.QStandardItemModel):
    def __init__(self, sourceModel):
        super().__init__()
        self.sourceModel = sourceModel

    def update_model_from_source(self):
        self.removeRows(0, self.rowCount())

        self.sourceModel.sort(0, QtCore.Qt.AscendingOrder)

        candidates = {}

        parent = self.sourceModel.index(self.sourceModel.rootPath())
        for row in range(self.sourceModel.rowCount(parent)):
            index = self.sourceModel.index(row, 0, parent)
            filename = self.sourceModel.fileName(index)

            file_item = QtGui.QStandardItem(filename)
            file_item.setIcon(self.sourceModel.fileIcon(index))
            filetype = QtGui.QStandardItem(self.sourceModel.type(index))
            date = QtGui.QStandardItem(self.sourceModel.lastModified(index).toString("yyyy-MM-dd h:mm.ss")))
            size = QtGui.QStandardItem(str(self.sourceModel.size(indes)))

            # check for sequences
            sequence = False
            if not self.sourceModel.isDir(index):
                search_str = os.path.splitext(filename)[0]
                matches = re.search(r'([0-9]+)$', search_str)
                if matches:
                    candidate = filename[0:matches.start(0)]

                    if candidate in candidates:
                        parent_item = candidates[candidate]
                        sequence = True
                    else:
                        candidates[candidate] = file_item

            row = [file_item, filetype, date, size]
            if sequence:
                parent_item.appendRow(row)
            else:
                self.appendRow(row)


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    ex = TestApp()
    ex.show()
    sys.exit(app.exec_())
Spencer
  • 1,931
  • 1
  • 21
  • 44
  • 1) When speaking of sequential, it is understood that there is an order, so what is the order? Are the names ordered alphabetically? 2) In a QFileSystemModel the parent is always in a directory that has other directories and files as children, when you modify it, what happens to the child directories? Will the file used as root of the other files be the child of the folder or will the root file replace the folder? – eyllanesc Mar 11 '20 at 19:18
  • @eyllanesc 1) Once the directory is loaded I was calling a sort on column 0 (filename) so that it is forced to be alphabetical. So yes I imagine my "sequencing" logic will need the directory fully loaded and sorted before this can occur. 2) I do not quite understand your question, but I believe I would simply refresh the cache (or whatever) when a modification takes place. I think it is simple enough for me to rebuild the tree whenever a change occurs. – Spencer Mar 11 '20 at 19:39
  • For point (2) I will propose an example: Let's say that the folder is called "A" and has folders "B" and "C" inside it and files "d" and "e" Who must be root "A "or" d "? Do folders need to be displayed? – eyllanesc Mar 11 '20 at 19:42
  • @eyllanesc Folders do need to be displayed. The root is 'dynamic' in that the user can set the root at any time with a text box by typing in a path, or by double clicking a folder which would now make that the root index and root directory. This is a file browser which allows the user to surf around the drive. I was planning on only showing the current directory at one time, the reason I use a tree view is so that I can take advantage of the parent/child relationship for sequences. – Spencer Mar 11 '20 at 19:49
  • I think the simplicity is that once a directory is loaded I want to iterate over the rows in that directory and parent the sequential items to their first index. – Spencer Mar 11 '20 at 20:03
  • mmm, that is to say if folder "A" is selected and inside it there are other subfolders (which can also have subfolders and files) and files "a", "b", "c" and "d" then just open a single root "a" and its children will be "b", "c" and "d"? I am right? Also as you point out that you have already implemented the code with QStandardItemModel but you point out that it is slow then you could show me that code to understand you better. – eyllanesc Mar 11 '20 at 20:04
  • @eyllanesc Sorry that took a minute, my internet computer and workstation are separate so I needed to retype everything. As such please excuse any typos... Anyway I modified my question to have the working code. Interestingly this is now running very quickly once i extracted it from my main app, so perhaps my question is not relevent and my problem lies elsewhere... *sigh*. Anyway it still would be much nicer/cleaner to build this into the FileSystemModel as described – Spencer Mar 11 '20 at 20:36
  • @eyllanesc FYI I found the bottleneck. Funny enough calling `self.sourceModel.filePath(index)` for each row slowed my code down by 8x! But `self.sourceModel.fileName(index)` is no issue, so I did `os.path.join` on the rootPath and it is much faster. – Spencer Mar 11 '20 at 21:50

0 Answers0