Search code examples
pythonpyqtpyqt5qfiledialog

QFileDialog with proxy model sort by date actually uses alphabetical order of date


In PyQt 5.11.2 (on Windows 10), QFileDialog with the DontUseNativeDialog option set has a bug when sorting by the 'Date Modified' column: it does not sort by the actual date - it sorts alphabetically by the non-zero-padded MM-DD-YYYY string, meaning that 9-12-2018 shows up as being more recent than 12-12-2018. enter image description here Is there a workaround or a fix?

The native dialog does sort by date correctly, but, the motivation for using the non-native dialog is that the native dialog does not respect fileDialog.setProxyModel (used to filter out certain files by more complex regex rules), described here and here, which I think is spelled out / alluded to by this line in the docs:

By default, a platform-native file dialog will be used if the platform has one. In that case, the widgets which would otherwise be used to construct the dialog will not be instantiated, so related accessors such as layout() and itemDelegate() will return null. You can set the DontUseNativeDialog option to ensure that the widget-based implementation will be used instead of the native dialog.

The code: the dialog is spawned by calling self.load() (no arguments)

def load(self,fileName=None):
    if not fileName:
        fileDialog=QFileDialog()
        fileDialog.setOption(QFileDialog.DontUseNativeDialog)
        fileDialog.setProxyModel(CSVFileSortFilterProxyModel(self))
        fileDialog.setNameFilter("CSV Radio Log Data Files (*.csv)")
        fileDialog.setDirectory(self.firstWorkingDir)
        if fileDialog.exec_():
            fileName=fileDialog.selectedFiles()[0]

... and the entire CSVFileSortFilterProxyModel class which only has one function:

# code for CSVFileSortFilterProxyModel partially taken from
#  https://github.com/ZhuangLab/storm-control/blob/master/steve/qtRegexFileDialog.py
class CSVFileSortFilterProxyModel(QSortFilterProxyModel):
    def __init__(self,parent=None):
#       print("initializing CSVFileSortFilterProxyModel")
        super(CSVFileSortFilterProxyModel,self).__init__(parent)

    # filterAcceptsRow - return True if row should be included in the model, False otherwise
    #
    # do not list files named *_fleetsync.csv or *_clueLog.csv
    #  do a case-insensitive comparison just in case
    def filterAcceptsRow(self,source_row,source_parent):
#       print("CSV filterAcceptsRow called")
        source_model=self.sourceModel()
        index0=source_model.index(source_row,0,source_parent)
        # Always show directories
        if source_model.isDir(index0):
            return True
        # filter files
        filename=source_model.fileName(index0).lower()
#       filename=self.sourceModel().index(row,0,parent).data().lower()
#       print("testing lowercased filename:"+filename)
        # never show non- .csv files
        if filename.count(".csv")<1:
            return False
        if filename.count("_fleetsync.csv")+filename.count("_cluelog.csv")==0:
            return True
        else:
            return False

Solution

  • The problem is caused by the custom QSortFilterProxyModel since in the lessThan method the values are strings, the solution is to convert to the appropriate type for the comparison:

    class CSVFileSortFilterProxyModel(QtCore.QSortFilterProxyModel):
        def filterAcceptsRow(self,source_row,source_parent):
            source_model = self.sourceModel()
            index0=source_model.index(source_row,0,source_parent)
            if source_model.isDir(index0):
                return True
            filename = source_model.fileName(index0).lower()
            if filename.count(".csv")<1:
                return False
            return filename.count("_fleetsync.csv")+filename.count("_cluelog.csv") == 0
    
        def lessThan(self, left, right):
            source_model = self.sourceModel()
    
            if left.column() == right.column() == 1: 
                if source_model.isDir(left) and not source_model.isDir(right):
                    return True
                return source_model.size(left) < source_model.size(right)
    
            if left.column() == right.column() == 2:
                return source_model.type(left) < source_model.type(right)
    
            if left.column() == right.column() == 3:
                return source_model.lastModified(left) < source_model.lastModified(right)
            return super(CSVFileSortFilterProxyModel, self).lessThan(left, right)
    

    As indicated by @ekhumoro a simpler option is to overwrite the proxy sort:

    class CSVFileSortFilterProxyModel(QtCore.QSortFilterProxyModel):
        def filterAcceptsRow(self,source_row,source_parent):
            source_model = self.sourceModel()
            index0=source_model.index(source_row,0,source_parent)
            if source_model.isDir(index0):
                return True
            filename = source_model.fileName(index0).lower()
            if filename.count(".csv")<1:
                return False
            return filename.count("_fleetsync.csv")+filename.count("_cluelog.csv") == 0
    
        def sort(self, column, order):
            self.sourceModel().sort(column, order)