]> cloud.milkyroute.net Git - dolphin.git/blobdiff - src/kitemviews/kfileitemmodel.cpp
Fixed grouping again, implemented permission and rating grouping.
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
index 4463b37ae3f747fc54673c6c6ae9608a6b7e38fc..5620460591206d5e43fb5323da6721dffb67f322 100644 (file)
@@ -12,6 +12,7 @@
 #include "dolphin_generalsettings.h"
 #include "dolphindebug.h"
 #include "private/kfileitemmodelsortalgorithm.h"
+#include "views/draganddrophelper.h"
 
 #include <KDirLister>
 #include <KIO/Job>
@@ -33,7 +34,7 @@ Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
 // #define KFILEITEMMODEL_DEBUG
 
 KFileItemModel::KFileItemModel(QObject *parent)
-    : KItemModelBase("text", parent)
+    : KItemModelBase("text", "text", parent)
     , m_dirLister(nullptr)
     , m_sortDirsFirst(true)
     , m_sortHiddenLast(false)
@@ -99,13 +100,14 @@ KFileItemModel::KFileItemModel(QObject *parent)
     // for a lot of items within a quite small timeslot. To prevent expensive resortings the
     // resorting is postponed until the timer has been exceeded.
     m_resortAllItemsTimer = new QTimer(this);
-    m_resortAllItemsTimer->setInterval(50);
+    m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable
+                                             // and sorting too infrequently which leads to users seeing an outdated sort order.
     m_resortAllItemsTimer->setSingleShot(true);
     connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
 
     connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
 
-    setShowTrashMime(m_dirLister->showHiddenFiles());
+    setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile());
 }
 
 KFileItemModel::~KFileItemModel()
@@ -239,25 +241,18 @@ bool KFileItemModel::sortHiddenLast() const
     return m_sortHiddenLast;
 }
 
-void KFileItemModel::setShowTrashMime(bool show)
+void KFileItemModel::setShowTrashMime(bool showTrashMime)
 {
     const auto trashMime = QStringLiteral("application/x-trash");
     QStringList excludeFilter = m_filter.excludeMimeTypes();
-    bool wasShown = !excludeFilter.contains(trashMime);
 
-    if (show) {
-        if (!wasShown) {
-            excludeFilter.removeAll(trashMime);
-        }
-    } else {
-        if (wasShown) {
-            excludeFilter.append(trashMime);
-        }
+    if (showTrashMime) {
+        excludeFilter.removeAll(trashMime);
+    } else if (!excludeFilter.contains(trashMime)) {
+        excludeFilter.append(trashMime);
     }
 
-    if (wasShown != show) {
-        setExcludeMimeTypeFilter(excludeFilter);
-    }
+    setExcludeMimeTypeFilter(excludeFilter);
 }
 
 void KFileItemModel::scheduleResortAllItems()
@@ -270,7 +265,7 @@ void KFileItemModel::scheduleResortAllItems()
 void KFileItemModel::setShowHiddenFiles(bool show)
 {
     m_dirLister->setShowHiddenFiles(show);
-    setShowTrashMime(show);
+    setShowTrashMime(show || !GeneralSettings::hideXTrashFile());
     m_dirLister->emitChanges();
     if (show) {
         dispatchPendingItemsToInsert();
@@ -354,7 +349,18 @@ bool KFileItemModel::supportsDropping(int index) const
     } else {
         item = fileItem(index);
     }
-    return !item.isNull() && ((item.isDir() && item.isWritable()) || item.isDesktopFile());
+    return !item.isNull() && DragAndDropHelper::supportsDropping(item);
+}
+
+bool KFileItemModel::canEnterOnHover(int index) const
+{
+    KFileItem item;
+    if (index == -1) {
+        item = rootItem();
+    } else {
+        item = fileItem(index);
+    }
+    return !item.isNull() && (item.isDir() || item.isDesktopFile());
 }
 
 QString KFileItemModel::roleDescription(const QByteArray &role) const
@@ -381,7 +387,10 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
         QElapsedTimer timer;
         timer.start();
 #endif
-        switch (typeForRole(sortRole())) {
+        switch (typeForRole(groupRole())) {
+        case NoRole:
+            m_groups.clear();
+            break;
         case NameRole:
             m_groups = nameRoleGroups();
             break;
@@ -415,7 +424,7 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
             m_groups = ratingRoleGroups();
             break;
         default:
-            m_groups = genericStringRoleGroups(sortRole());
+            m_groups = genericStringRoleGroups(groupRole());
             break;
         }
 
@@ -880,6 +889,39 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges)
     }
 }
 
+KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role)
+{
+    static QHash<QByteArray, RoleInfo> information;
+    if (information.isEmpty()) {
+        int count = 0;
+        const RoleInfoMap *map = rolesInfoMap(count);
+        for (int i = 0; i < count; ++i) {
+            RoleInfo info;
+            info.role = map[i].role;
+            info.translation = map[i].roleTranslation.toString();
+            if (!map[i].groupTranslation.isEmpty()) {
+                info.group = map[i].groupTranslation.toString();
+            } else {
+                // For top level roles, groupTranslation is 0. We must make sure that
+                // info.group is an empty string then because the code that generates
+                // menus tries to put the actions into sub menus otherwise.
+                info.group = QString();
+            }
+            info.requiresBaloo = map[i].requiresBaloo;
+            info.requiresIndexer = map[i].requiresIndexer;
+            if (!map[i].tooltipTranslation.isEmpty()) {
+                info.tooltip = map[i].tooltipTranslation.toString();
+            } else {
+                info.tooltip = QString();
+            }
+
+            information.insert(map[i].role, info);
+        }
+    }
+
+    return information.value(role);
+}
+
 QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
 {
     static QList<RoleInfo> rolesInfo;
@@ -888,24 +930,7 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
         const RoleInfoMap *map = rolesInfoMap(count);
         for (int i = 0; i < count; ++i) {
             if (map[i].roleType != NoRole) {
-                RoleInfo info;
-                info.role = map[i].role;
-                info.translation = map[i].roleTranslation.toString();
-                if (!map[i].groupTranslation.isEmpty()) {
-                    info.group = map[i].groupTranslation.toString();
-                } else {
-                    // For top level roles, groupTranslation is 0. We must make sure that
-                    // info.group is an empty string then because the code that generates
-                    // menus tries to put the actions into sub menus otherwise.
-                    info.group = QString();
-                }
-                info.requiresBaloo = map[i].requiresBaloo;
-                info.requiresIndexer = map[i].requiresIndexer;
-                if (!map[i].tooltipTranslation.isEmpty()) {
-                    info.tooltip = map[i].tooltipTranslation.toString();
-                } else {
-                    info.tooltip = QString();
-                }
+                RoleInfo info = roleInformation(map[i].role);
                 rolesInfo.append(info);
             }
         }
@@ -936,11 +961,40 @@ void KFileItemModel::onSortRoleChanged(const QByteArray &current, const QByteArr
     }
 }
 
-void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
 {
     Q_UNUSED(current)
     Q_UNUSED(previous)
-    resortAllItems();
+
+    if (resortItems) {
+        resortAllItems();
+    }
+}
+
+void KFileItemModel::onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems)
+{
+    Q_UNUSED(previous)
+    m_groupRole = typeForRole(current);
+
+    if (!m_requestRole[m_sortRole]) {
+        QSet<QByteArray> newRoles = m_roles;
+        newRoles << current;
+        setRoles(newRoles);
+    }
+
+    if (resortItems) {
+        resortAllItems();
+    }
+}
+
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+
+    if (resortItems) {
+        resortAllItems();
+    }
 }
 
 void KFileItemModel::loadSortingSettings()
@@ -1203,12 +1257,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList &items)
 
     for (const KFileItem &item : items) {
         if (item.url() == currentDir) {
-            // #473377: Delay emitting currentDirectoryRemoved() to avoid modifying KCoreDirLister
-            // before KCoreDirListerCache::deleteDir() returns.
-            QTimer::singleShot(0, this, [this] {
-                Q_EMIT currentDirectoryRemoved();
-            });
-
+            Q_EMIT currentDirectoryRemoved();
             return;
         }
 
@@ -1737,7 +1786,8 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i
 
     // Trigger a resorting if necessary. Note that this can happen even if the sort
     // role has not changed at all because the file name can be used as a fallback.
-    if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
+    if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))
+        || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting.
         for (const KItemRange &range : itemRanges) {
             bool needsResorting = false;
 
@@ -1762,7 +1812,7 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i
             }
 
             if (needsResorting) {
-                m_resortAllItemsTimer->start();
+                scheduleResortAllItems();
                 return;
             }
         }
@@ -2017,30 +2067,34 @@ bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QColla
         }
     }
 
-    // Show hidden files and folders last
-    if (m_sortHiddenLast) {
-        const bool isHiddenA = a->item.isHidden();
-        const bool isHiddenB = b->item.isHidden();
-        if (isHiddenA && !isHiddenB) {
-            return false;
-        } else if (!isHiddenA && isHiddenB) {
-            return true;
+    result = groupRoleCompare(a, b, collator);
+    if (result == 0) {
+        // Show hidden files and folders last
+        if (m_sortHiddenLast) {
+            const bool isHiddenA = a->item.isHidden();
+            const bool isHiddenB = b->item.isHidden();
+            if (isHiddenA && !isHiddenB) {
+                return false;
+            } else if (!isHiddenA && isHiddenB) {
+                return true;
+            }
         }
-    }
-
-    if (m_sortDirsFirst || (ContentDisplaySettings::directorySizeCount() && m_sortRole == SizeRole)) {
-        const bool isDirA = a->item.isDir();
-        const bool isDirB = b->item.isDir();
-        if (isDirA && !isDirB) {
-            return true;
-        } else if (!isDirA && isDirB) {
-            return false;
+        if (m_sortDirsFirst
+            || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
+            const bool isDirA = a->item.isDir();
+            const bool isDirB = b->item.isDir();
+            if (isDirA && !isDirB) {
+                return true;
+            } else if (!isDirA && isDirB) {
+                return false;
+            }
         }
+        result = sortRoleCompare(a, b, collator);
+        result = (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+    } else {
+        result = (groupOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
     }
-
-    result = sortRoleCompare(a, b, collator);
-
-    return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+    return result;
 }
 
 void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
@@ -2080,7 +2134,7 @@ int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const
         break;
 
     case SizeRole: {
-        if (ContentDisplaySettings::directorySizeCount() && itemA.isDir()) {
+        if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && itemA.isDir()) {
             // folders first then
             // items A and B are folders thanks to lessThan checks
             auto valueA = a->values.value("count");
@@ -2231,6 +2285,112 @@ int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const
     return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
 }
 
+int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
+{
+    // Unlike sortRoleCompare, this function can and often will return 0.
+    int result = 0;
+
+    int groupA, groupB;
+    switch (m_groupRole) {
+    case NoRole:
+        break;
+    case NameRole:
+        groupA = nameRoleGroup(a, false).comparable;
+        groupB = nameRoleGroup(b, false).comparable;
+        break;
+    case SizeRole:
+        groupA = sizeRoleGroup(a, false).comparable;
+        groupB = sizeRoleGroup(b, false).comparable;
+        break;
+    case ModificationTimeRole:
+        groupA = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::ModificationTime);
+                     },
+                     a,
+                     false)
+                     .comparable;
+        groupB = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::ModificationTime);
+                     },
+                     b,
+                     false)
+                     .comparable;
+        break;
+    case CreationTimeRole:
+        groupA = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::CreationTime);
+                     },
+                     a,
+                     false)
+                     .comparable;
+        groupB = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::CreationTime);
+                     },
+                     b,
+                     false)
+                     .comparable;
+        break;
+    case AccessTimeRole:
+        groupA = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::AccessTime);
+                     },
+                     a,
+                     false)
+                     .comparable;
+        groupB = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->item.time(KFileItem::AccessTime);
+                     },
+                     b,
+                     false)
+                     .comparable;
+        break;
+    case DeletionTimeRole:
+        groupA = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->values.value("deletiontime").toDateTime();
+                     },
+                     a,
+                     false)
+                     .comparable;
+        groupB = timeRoleGroup(
+                     [](const ItemData *item) {
+                         return item->values.value("deletiontime").toDateTime();
+                     },
+                     b,
+                     false)
+                     .comparable;
+        break;
+    case PermissionsRole:
+        groupA = permissionRoleGroup(a, false).comparable;
+        groupB = permissionRoleGroup(b, false).comparable;
+        break;
+    case RatingRole:
+        groupA = ratingRoleGroup(a, false).comparable;
+        groupB = ratingRoleGroup(b, false).comparable;
+        break;
+    default: {
+        QString strGroupA = genericStringRoleGroup(groupRole(), a);
+        QString strGroupB = genericStringRoleGroup(groupRole(), b);
+        result = stringCompare(strGroupA, strGroupB, collator);
+        break;
+    }
+    }
+    if (result == 0) {
+        if (groupA < groupB) {
+            result = -1;
+        } else if (groupA > groupB) {
+            result = 1;
+        }
+    }
+    return result;
+}
+
 int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
 {
     QMutexLocker collatorLock(s_collatorMutex());
@@ -2250,180 +2410,195 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol
     return QString::compare(a, b, Qt::CaseSensitive);
 }
 
-QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
 {
-    Q_ASSERT(!m_itemData.isEmpty());
-
-    const int maxIndex = count() - 1;
-    QList<QPair<int, QVariant>> groups;
-
-    QString groupValue;
+    static ItemGroupInfo oldGroupInfo;
+    static QChar oldFirstChar;
+    ItemGroupInfo groupInfo;
     QChar firstChar;
-    for (int i = 0; i <= maxIndex; ++i) {
-        if (isChildItem(i)) {
-            continue;
-        }
 
-        const QString name = m_itemData.at(i)->item.text();
+    const QString name = itemData->item.text();
 
-        // Use the first character of the name as group indication
-        QChar newFirstChar = name.at(0).toUpper();
-        if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
-            newFirstChar = name.at(1).toUpper();
-        }
-
-        if (firstChar != newFirstChar) {
-            QString newGroupValue;
-            if (newFirstChar.isLetter()) {
-                if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) {
-                    // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
+    // Use the first character of the name as group indication
+    firstChar = name.at(0).toUpper();
 
-                    // Try to find a matching group in the range 'A' to 'Z'.
-                    static std::vector<QChar> lettersAtoZ;
-                    lettersAtoZ.reserve('Z' - 'A' + 1);
-                    if (lettersAtoZ.empty()) {
-                        for (char c = 'A'; c <= 'Z'; ++c) {
-                            lettersAtoZ.push_back(QLatin1Char(c));
-                        }
-                    }
+    if (firstChar == oldFirstChar) {
+        return oldGroupInfo;
+    }
+    if (firstChar == QLatin1Char('~') && name.length() > 1) {
+        firstChar = name.at(1).toUpper();
+    }
+    if (firstChar.isLetter()) {
+        if (m_collator.compare(firstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(firstChar, QChar(QLatin1Char('Z'))) <= 0) {
+            // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
 
-                    auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
-                        return m_collator.compare(c1, c2) < 0;
-                    };
+            // Try to find a matching group in the range 'A' to 'Z'.
+            static std::vector<QChar> lettersAtoZ;
+            lettersAtoZ.reserve('Z' - 'A' + 1);
+            if (lettersAtoZ.empty()) {
+                for (char c = 'A'; c <= 'Z'; ++c) {
+                    lettersAtoZ.push_back(QLatin1Char(c));
+                }
+            }
 
-                    std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
-                    if (it != lettersAtoZ.end()) {
-                        if (localeAwareLessThan(newFirstChar, *it)) {
-                            // newFirstChar belongs to the group preceding *it.
-                            // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
-                            --it;
-                        }
-                        newGroupValue = *it;
-                    }
+            auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
+                return m_collator.compare(c1, c2) < 0;
+            };
 
-                } else {
-                    // Symbols from non Latin-based scripts
-                    newGroupValue = newFirstChar;
+            std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
+            if (it != lettersAtoZ.end()) {
+                if (localeAwareLessThan(firstChar, *it)) {
+                    // newFirstChar belongs to the group preceding *it.
+                    // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
+                    --it;
                 }
-            } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
-                // Apply group '0 - 9' for any name that starts with a digit
-                newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
-            } else {
-                newGroupValue = i18nc("@title:group", "Others");
+                if (withString) {
+                    groupInfo.text = *it;
+                }
+                groupInfo.comparable = (*it).unicode();
             }
 
-            if (newGroupValue != groupValue) {
-                groupValue = newGroupValue;
-                groups.append(QPair<int, QVariant>(i, newGroupValue));
+        } else {
+            // Symbols from non Latin-based scripts
+            if (withString) {
+                groupInfo.text = firstChar;
             }
-
-            firstChar = newFirstChar;
+            groupInfo.comparable = firstChar.unicode();
+        }
+    } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
+        // Apply group '0 - 9' for any name that starts with a digit
+        if (withString) {
+            groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
         }
+        groupInfo.comparable = (int)'0';
+    } else {
+        if (withString) {
+            groupInfo.text = i18nc("@title:group", "Others");
+        }
+        groupInfo.comparable = (int)'.';
     }
-    return groups;
+    oldFirstChar = firstChar;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
 }
 
-QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
 {
-    Q_ASSERT(!m_itemData.isEmpty());
+    static ItemGroupInfo oldGroupInfo;
+    static KIO::filesize_t oldFileSize;
+    ItemGroupInfo groupInfo;
+    KIO::filesize_t fileSize;
 
-    const int maxIndex = count() - 1;
-    QList<QPair<int, QVariant>> groups;
+    const KFileItem item = itemData->item;
+    fileSize = !item.isNull() ? item.size() : ~0U;
 
-    QString groupValue;
-    for (int i = 0; i <= maxIndex; ++i) {
-        if (isChildItem(i)) {
-            continue;
-        }
-
-        const KFileItem &item = m_itemData.at(i)->item;
-        KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
-        QString newGroupValue;
-        if (!item.isNull() && item.isDir()) {
-            if (ContentDisplaySettings::directorySizeCount() || m_sortDirsFirst) {
-                newGroupValue = i18nc("@title:group Size", "Folders");
-            } else {
-                fileSize = m_itemData.at(i)->values.value("size").toULongLong();
-            }
-        }
-
-        if (newGroupValue.isEmpty()) {
-            if (fileSize < 5 * 1024 * 1024) { // < 5 MB
-                newGroupValue = i18nc("@title:group Size", "Small");
-            } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
-                newGroupValue = i18nc("@title:group Size", "Medium");
-            } else {
-                newGroupValue = i18nc("@title:group Size", "Big");
-            }
+    groupInfo.comparable = -1; // None
+    if (!item.isNull() && item.isDir()) {
+        if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) {
+            groupInfo.comparable = 0; // Folders
+        } else {
+            fileSize = itemData->values.value("size").toULongLong();
         }
-
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+    }
+    if (fileSize == oldFileSize) {
+        return oldGroupInfo;
+    }
+    if (groupInfo.comparable < 0) {
+        if (fileSize < 5 * 1024 * 1024) { // < 5 MB
+            groupInfo.comparable = 1; // Small
+        } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
+            groupInfo.comparable = 2; // Medium
+        } else {
+            groupInfo.comparable = 3; // Big
         }
     }
 
-    return groups;
+    if (withString) {
+        char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
+        groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
+    }
+    oldFileSize = fileSize;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
 }
 
-QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+KFileItemModel::ItemGroupInfo
+KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
 {
-    Q_ASSERT(!m_itemData.isEmpty());
-
-    const int maxIndex = count() - 1;
-    QList<QPair<int, QVariant>> groups;
+    static ItemGroupInfo oldGroupInfo;
+    static QDate oldFileDate;
+    ItemGroupInfo groupInfo;
 
     const QDate currentDate = QDate::currentDate();
-
-    QDate previousFileDate;
-    QString groupValue;
-    for (int i = 0; i <= maxIndex; ++i) {
-        if (isChildItem(i)) {
-            continue;
+    const QDateTime fileTime = fileTimeCb(itemData);
+    const QDate fileDate = fileTime.date();
+    const int daysDistance = fileDate.daysTo(currentDate);
+
+    // Simplified grouping algorithm, preserving dates
+    // but not taking "pretty printing" into account
+    if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+        if (daysDistance < 7) {
+            groupInfo.comparable = daysDistance; // Today, Yesterday and week days
+        } else if (daysDistance < 14) {
+            groupInfo.comparable = 10; // One Week Ago
+        } else if (daysDistance < 21) {
+            groupInfo.comparable = 20; // Two Weeks Ago
+        } else if (daysDistance < 28) {
+            groupInfo.comparable = 30; // Three Weeks Ago
+        } else {
+            groupInfo.comparable = 40; // Earlier This Month
         }
-
-        const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
-        const QDate fileDate = fileTime.date();
-        if (fileDate == previousFileDate) {
-            // The current item is in the same group as the previous item
-            continue;
+    } else {
+        const QDate lastMonthDate = currentDate.addMonths(-1);
+        if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
+            if (daysDistance < 7) {
+                groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
+            } else if (daysDistance < 14) {
+                groupInfo.comparable = 11; // One Week Ago (Month, Year)
+            } else if (daysDistance < 21) {
+                groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
+            } else if (daysDistance < 28) {
+                groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
+            } else {
+                groupInfo.comparable = 41; // Earlier on Month, Year
+            }
+        } else {
+            // The trick will fail for dates past April, 178956967 or before 1 AD.
+            groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
         }
-        previousFileDate = fileDate;
-
-        const int daysDistance = fileDate.daysTo(currentDate);
-
-        QString newGroupValue;
+    }
+    if (withString) {
         if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
             switch (daysDistance / 7) {
             case 0:
                 switch (daysDistance) {
                 case 0:
-                    newGroupValue = i18nc("@title:group Date", "Today");
+                    groupInfo.text = i18nc("@title:group Date", "Today");
                     break;
                 case 1:
-                    newGroupValue = i18nc("@title:group Date", "Yesterday");
+                    groupInfo.text = i18nc("@title:group Date", "Yesterday");
                     break;
                 default:
-                    newGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
-                    newGroupValue = i18nc(
+                    groupInfo.text = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
+                    groupInfo.text = i18nc(
                         "Can be used to script translation of \"dddd\""
                         "with context @title:group Date",
                         "%1",
-                        newGroupValue);
+                        groupInfo.text);
                 }
                 break;
             case 1:
-                newGroupValue = i18nc("@title:group Date", "One Week Ago");
+                groupInfo.text = i18nc("@title:group Date", "One Week Ago");
                 break;
             case 2:
-                newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
+                groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
                 break;
             case 3:
-                newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
+                groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
                 break;
             case 4:
             case 5:
-                newGroupValue = i18nc("@title:group Date", "Earlier this Month");
+                groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
                 break;
             default:
                 Q_ASSERT(false);
@@ -2440,30 +2615,30 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "'Yesterday' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
                     if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        newGroupValue = fileTime.toString(translatedFormat);
-                        newGroupValue = i18nc(
+                        groupInfo.text = fileTime.toString(translatedFormat);
+                        groupInfo.text = i18nc(
                             "Can be used to script translation of "
                             "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            newGroupValue);
+                            groupInfo.text);
                     } else {
                         qCWarning(DolphinDebug).nospace()
                             << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                         const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
-                        newGroupValue = fileTime.toString(untranslatedFormat);
+                        groupInfo.text = fileTime.toString(untranslatedFormat);
                     }
-                } else if (daysDistance <= 7) {
-                    newGroupValue =
+                } else if (daysDistance < 7) {
+                    groupInfo.text =
                         fileTime.toString(i18nc("@title:group Date: "
                                                 "The week day name: dddd, MMMM is full month name "
                                                 "in current locale, and yyyy is full year number.",
                                                 "dddd (MMMM, yyyy)"));
-                    newGroupValue = i18nc(
+                    groupInfo.text = i18nc(
                         "Can be used to script translation of "
                         "\"dddd (MMMM, yyyy)\" with context @title:group Date",
                         "%1",
-                        newGroupValue);
-                } else if (daysDistance <= 7 * 2) {
+                        groupInfo.text);
+                } else if (daysDistance < 7 * 2) {
                     const KLocalizedString format = ki18nc(
                         "@title:group Date: "
                         "MMMM is full month name in current locale, and yyyy is "
@@ -2472,19 +2647,19 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "'One Week Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
                     if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        newGroupValue = fileTime.toString(translatedFormat);
-                        newGroupValue = i18nc(
+                        groupInfo.text = fileTime.toString(translatedFormat);
+                        groupInfo.text = i18nc(
                             "Can be used to script translation of "
                             "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            newGroupValue);
+                            groupInfo.text);
                     } else {
                         qCWarning(DolphinDebug).nospace()
                             << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                         const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
-                        newGroupValue = fileTime.toString(untranslatedFormat);
+                        groupInfo.text = fileTime.toString(untranslatedFormat);
                     }
-                } else if (daysDistance <= 7 * 3) {
+                } else if (daysDistance < 7 * 3) {
                     const KLocalizedString format = ki18nc(
                         "@title:group Date: "
                         "MMMM is full month name in current locale, and yyyy is "
@@ -2493,19 +2668,19 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "'Two Weeks Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
                     if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        newGroupValue = fileTime.toString(translatedFormat);
-                        newGroupValue = i18nc(
+                        groupInfo.text = fileTime.toString(translatedFormat);
+                        groupInfo.text = i18nc(
                             "Can be used to script translation of "
                             "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            newGroupValue);
+                            groupInfo.text);
                     } else {
                         qCWarning(DolphinDebug).nospace()
                             << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                         const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
-                        newGroupValue = fileTime.toString(untranslatedFormat);
+                        groupInfo.text = fileTime.toString(untranslatedFormat);
                     }
-                } else if (daysDistance <= 7 * 4) {
+                } else if (daysDistance < 7 * 4) {
                     const KLocalizedString format = ki18nc(
                         "@title:group Date: "
                         "MMMM is full month name in current locale, and yyyy is "
@@ -2514,17 +2689,17 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "'Three Weeks Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
                     if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        newGroupValue = fileTime.toString(translatedFormat);
-                        newGroupValue = i18nc(
+                        groupInfo.text = fileTime.toString(translatedFormat);
+                        groupInfo.text = i18nc(
                             "Can be used to script translation of "
                             "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            newGroupValue);
+                            groupInfo.text);
                     } else {
                         qCWarning(DolphinDebug).nospace()
                             << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                         const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
-                        newGroupValue = fileTime.toString(untranslatedFormat);
+                        groupInfo.text = fileTime.toString(untranslatedFormat);
                     }
                 } else {
                     const KLocalizedString format = ki18nc(
@@ -2535,133 +2710,228 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "'Earlier on' MMMM, yyyy");
                     const QString translatedFormat = format.toString();
                     if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        newGroupValue = fileTime.toString(translatedFormat);
-                        newGroupValue = i18nc(
+                        groupInfo.text = fileTime.toString(translatedFormat);
+                        groupInfo.text = i18nc(
                             "Can be used to script translation of "
                             "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
                             "%1",
-                            newGroupValue);
+                            groupInfo.text);
                     } else {
                         qCWarning(DolphinDebug).nospace()
                             << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
                         const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
-                        newGroupValue = fileTime.toString(untranslatedFormat);
+                        groupInfo.text = fileTime.toString(untranslatedFormat);
                     }
                 }
             } else {
-                newGroupValue =
+                groupInfo.text =
                     fileTime.toString(i18nc("@title:group "
                                             "The month and year: MMMM is full month name in current locale, "
                                             "and yyyy is full year number",
                                             "MMMM, yyyy"));
-                newGroupValue = i18nc(
+                groupInfo.text = i18nc(
                     "Can be used to script translation of "
                     "\"MMMM, yyyy\" with context @title:group Date",
                     "%1",
-                    newGroupValue);
+                    groupInfo.text);
             }
         }
-
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
-        }
     }
-
-    return groups;
+    oldFileDate = fileDate;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
 }
 
-QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
 {
-    Q_ASSERT(!m_itemData.isEmpty());
-
-    const int maxIndex = count() - 1;
-    QList<QPair<int, QVariant>> groups;
-
-    QString permissionsString;
-    QString groupValue;
-    for (int i = 0; i <= maxIndex; ++i) {
-        if (isChildItem(i)) {
-            continue;
-        }
-
-        const ItemData *itemData = m_itemData.at(i);
-        const QString newPermissionsString = itemData->values.value("permissions").toString();
-        if (newPermissionsString == permissionsString) {
-            continue;
-        }
-        permissionsString = newPermissionsString;
+    static ItemGroupInfo oldGroupInfo;
+    static QFileDevice::Permissions oldPermissions;
+    ItemGroupInfo groupInfo;
 
-        const QFileInfo info(itemData->item.url().toLocalFile());
+    const QFileInfo info(itemData->item.url().toLocalFile());
+    const QFileDevice::Permissions permissions = info.permissions();
+    if (permissions == oldPermissions) {
+        return oldGroupInfo;
+    }
+    groupInfo.comparable = (int)permissions;
 
+    if (withString) {
         // Set user string
         QString user;
-        if (info.permission(QFile::ReadUser)) {
+        if (permissions & QFile::ReadUser) {
             user = i18nc("@item:intext Access permission, concatenated", "Read, ");
         }
-        if (info.permission(QFile::WriteUser)) {
+        if (permissions & QFile::WriteUser) {
             user += i18nc("@item:intext Access permission, concatenated", "Write, ");
         }
-        if (info.permission(QFile::ExeUser)) {
+        if (permissions & QFile::ExeUser) {
             user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
         }
         user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2);
 
         // Set group string
         QString group;
-        if (info.permission(QFile::ReadGroup)) {
+        if (permissions & QFile::ReadGroup) {
             group = i18nc("@item:intext Access permission, concatenated", "Read, ");
         }
-        if (info.permission(QFile::WriteGroup)) {
+        if (permissions & QFile::WriteGroup) {
             group += i18nc("@item:intext Access permission, concatenated", "Write, ");
         }
-        if (info.permission(QFile::ExeGroup)) {
+        if (permissions & QFile::ExeGroup) {
             group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
         }
         group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2);
 
         // Set others string
         QString others;
-        if (info.permission(QFile::ReadOther)) {
+        if (permissions & QFile::ReadOther) {
             others = i18nc("@item:intext Access permission, concatenated", "Read, ");
         }
-        if (info.permission(QFile::WriteOther)) {
+        if (permissions & QFile::WriteOther) {
             others += i18nc("@item:intext Access permission, concatenated", "Write, ");
         }
-        if (info.permission(QFile::ExeOther)) {
+        if (permissions & QFile::ExeOther) {
             others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
         }
         others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2);
+        groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
+    }
+    oldPermissions = permissions;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const
+{
+    ItemGroupInfo groupInfo;
+    groupInfo.comparable = itemData->values.value("rating", 0).toInt();
+    if (withString) {
+        // Dolphin does not currently use string representation of star rating
+        // as stars are rendered as graphics in group headers.
+        groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated ") + QString::number(groupInfo.comparable);
+    }
+    return groupInfo;
+}
+
+QString KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+    return itemData->values.value(role).toString();
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+{
+    Q_ASSERT(!m_itemData.isEmpty());
 
-        const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+    const int maxIndex = count() - 1;
+    QList<QPair<int, QVariant>> groups;
+
+    ItemGroupInfo groupInfo;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
+
+        ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
+    return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+{
+    Q_ASSERT(!m_itemData.isEmpty());
+
+    const int maxIndex = count() - 1;
+    QList<QPair<int, QVariant>> groups;
 
+    ItemGroupInfo groupInfo;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
+
+        ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+        }
+    }
     return groups;
 }
 
-QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
+QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+{
+    Q_ASSERT(!m_itemData.isEmpty());
+
+    const int maxIndex = count() - 1;
+    QList<QPair<int, QVariant>> groups;
+
+    ItemGroupInfo groupInfo;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
+
+        ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+        }
+    }
+    return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
 {
     Q_ASSERT(!m_itemData.isEmpty());
 
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    int groupValue = -1;
+    ItemGroupInfo groupInfo;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
-        const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+
+        ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
+    return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
+{
+    Q_ASSERT(!m_itemData.isEmpty());
 
+    const int maxIndex = count() - 1;
+    QList<QPair<int, QVariant>> groups;
+
+    ItemGroupInfo groupInfo;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
+
+        ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            // Using the numeric representation because Dolphin has a special
+            // case for drawing stars.
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.comparable));
+        }
+    }
     return groups;
 }
 
@@ -2672,20 +2942,19 @@ QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteA
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    bool isFirstGroupValue = true;
-    QString groupValue;
+    QString groupText;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
-        const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
-        if (newGroupValue != groupValue || isFirstGroupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
-            isFirstGroupValue = false;
+
+        QString newGroupText = genericStringRoleGroup(role, m_itemData.at(i));
+
+        if (newGroupText != groupText) {
+            groupText = newGroupText;
+            groups.append(QPair<int, QVariant>(i, newGroupText));
         }
     }
-
     return groups;
 }
 
@@ -2722,7 +2991,7 @@ const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
     static const RoleInfoMap rolesInfoMap[] = {
         // clang-format off
     //  |         role           |        roleType        |                role translation          |         group translation                                        | requires Baloo | requires indexer
-        { nullptr,               NoRole,                  KLazyLocalizedString(),                    KLazyLocalizedString(),        KLazyLocalizedString(),                    false,           false },
+        { nullptr,               NoRole,                  kli18nc("@label", "None"),                 KLazyLocalizedString(),        KLazyLocalizedString(),                    false,           false },
         { "text",                NameRole,                kli18nc("@label", "Name"),                 KLazyLocalizedString(),        KLazyLocalizedString(),                    false,           false },
         { "size",                SizeRole,                kli18nc("@label", "Size"),                 KLazyLocalizedString(),        KLazyLocalizedString(),                    false,           false },
         { "modificationtime",    ModificationTimeRole,    kli18nc("@label", "Modified"),             KLazyLocalizedString(),        kli18nc("@tooltip", "The date format can be selected in settings."),                    false,           false },