]> cloud.milkyroute.net Git - dolphin.git/blobdiff - src/kitemviews/kfileitemmodel.cpp
Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups'
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
index 564ac42ea15a4228e42f6c108bf00cb76dff2c7a..603c16e0d37ff816488aa69b9122308335a36d45 100644 (file)
@@ -20,6 +20,9 @@
 #include <KLocalizedString>
 #include <KUrlMimeData>
 
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
 #include <QElapsedTimer>
 #include <QIcon>
 #include <QMimeData>
@@ -34,11 +37,12 @@ Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
 // #define KFILEITEMMODEL_DEBUG
 
 KFileItemModel::KFileItemModel(QObject *parent)
-    : KItemModelBase("text", "text", parent)
+    : KItemModelBase("text", "none", parent)
     , m_dirLister(nullptr)
     , m_sortDirsFirst(true)
     , m_sortHiddenLast(false)
     , m_sortRole(NameRole)
+    , m_groupRole(NoRole)
     , m_sortingProgressPercent(-1)
     , m_roles()
     , m_itemData()
@@ -202,13 +206,20 @@ bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &value
         return false;
     }
 
-    m_itemData[index]->values = currentValues;
     if (changedRoles.contains("text")) {
         QUrl url = m_itemData[index]->item.url();
+        m_items.remove(url);
         url = url.adjusted(QUrl::RemoveFilename);
         url.setPath(url.path() + currentValues["text"].toString());
         m_itemData[index]->item.setUrl(url);
+        m_items.insert(url, index);
+
+        if (!changedRoles.contains("url")) {
+            changedRoles.insert("url");
+            currentValues["url"] = url;
+        }
     }
+    m_itemData[index]->values = currentValues;
 
     emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
 
@@ -325,16 +336,32 @@ QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const
     return data;
 }
 
+namespace
+{
+QString removeMarks(const QString &original)
+{
+    const auto normalized = original.normalized(QString::NormalizationForm_D);
+    QString res;
+    for (auto ch : normalized) {
+        if (!ch.isMark()) {
+            res.append(ch);
+        }
+    }
+    return res;
+}
+}
+
 int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const
 {
+    const auto noMarkText = removeMarks(text);
     startFromIndex = qMax(0, startFromIndex);
     for (int i = startFromIndex; i < count(); ++i) {
-        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
+        if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
             return i;
         }
     }
     for (int i = 0; i < startFromIndex; ++i) {
-        if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
+        if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
             return i;
         }
     }
@@ -387,7 +414,14 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
         QElapsedTimer timer;
         timer.start();
 #endif
-        switch (typeForRole(groupRole())) {
+        QByteArray role = groupRole();
+        if (typeForRole(role) == NoRole) {
+            // Handle extra grouping information
+            if (m_groupExtraInfo == "followSort") {
+                role = sortRole();
+            }
+        }
+        switch (typeForRole(role)) {
         case NoRole:
             m_groups.clear();
             break;
@@ -424,7 +458,7 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
             m_groups = ratingRoleGroups();
             break;
         default:
-            m_groups = genericStringRoleGroups(groupRole());
+            m_groups = genericStringRoleGroups(role);
             break;
         }
 
@@ -939,6 +973,15 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
     return rolesInfo;
 }
 
+QList<KFileItemModel::RoleInfo> KFileItemModel::extraGroupingInformation()
+{
+    static QList<RoleInfo> rolesInfo{
+        {QByteArray("none"),         kli18nc("@label", "No grouping").toString(),       nullptr,     nullptr,     false,      false},
+        {QByteArray("followSort"),   kli18nc("@label", "Follow sorting").toString(),    nullptr,    nullptr,     false,      false}
+    };
+    return rolesInfo;
+}
+
 void KFileItemModel::onGroupedSortingChanged(bool current)
 {
     Q_UNUSED(current)
@@ -949,6 +992,13 @@ void KFileItemModel::onSortRoleChanged(const QByteArray &current, const QByteArr
 {
     Q_UNUSED(previous)
     m_sortRole = typeForRole(current);
+    if (m_sortRole == NoRole) {
+        // Requested role not in list of roles. This could
+        // be used for indicating non-trivial sorting behavior
+        m_sortExtraInfo = current;
+    } else {
+        m_sortExtraInfo.clear();
+    }
 
     if (!m_requestRole[m_sortRole]) {
         QSet<QByteArray> newRoles = m_roles;
@@ -961,22 +1011,26 @@ void KFileItemModel::onSortRoleChanged(const QByteArray &current, const QByteArr
     }
 }
 
-void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
 {
     Q_UNUSED(current)
     Q_UNUSED(previous)
-
-    if (resortItems) {
-        resortAllItems();
-    }
+    resortAllItems();
 }
 
 void KFileItemModel::onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems)
 {
     Q_UNUSED(previous)
     m_groupRole = typeForRole(current);
+    if (m_groupRole == NoRole) {
+        // Requested role not in list of roles. This could
+        // be used for indicating non-trivial grouping behavior
+        m_groupExtraInfo = current;
+    } else {
+        m_groupExtraInfo.clear();
+    }
 
-    if (!m_requestRole[m_sortRole]) {
+    if (!m_requestRole[m_groupRole]) {
         QSet<QByteArray> newRoles = m_roles;
         newRoles << current;
         setRoles(newRoles);
@@ -987,14 +1041,11 @@ void KFileItemModel::onGroupRoleChanged(const QByteArray &current, const QByteAr
     }
 }
 
-void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
 {
     Q_UNUSED(current)
     Q_UNUSED(previous)
-
-    if (resortItems) {
-        resortAllItems();
-    }
+    resortAllItems();
 }
 
 void KFileItemModel::loadSortingSettings()
@@ -1019,6 +1070,7 @@ void KFileItemModel::loadSortingSettings()
     // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361
     // Force the clean state of QCollator in single thread to avoid thread safety problems in sort
     m_collator.compare(QString(), QString());
+    ContentDisplaySettings::self();
 }
 
 void KFileItemModel::resortAllItems()
@@ -1084,7 +1136,8 @@ void KFileItemModel::resortAllItems()
         }
 
         Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
-    } else if (groupedSorting()) {
+    }
+    if (groupedSorting()) {
         // The groups might have changed even if the order of the items has not.
         const QList<QPair<int, QVariant>> oldGroups = m_groups;
         m_groups.clear();
@@ -1669,7 +1722,7 @@ void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBe
 
 QList<KFileItemModel::ItemData *> KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const
 {
-    if (m_sortRole == TypeRole) {
+    if (m_sortRole == TypeRole || m_groupRole == TypeRole) {
         // Try to resolve the MIME-types synchronously to prevent a reordering of
         // the items when sorting by type (per default MIME-types are resolved
         // asynchronously by KFileItemModelRolesUpdater).
@@ -1693,9 +1746,9 @@ QList<KFileItemModel::ItemData *> KFileItemModel::createItemDataList(const QUrl
     return itemDataList;
 }
 
-void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+void KFileItemModel::prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType)
 {
-    switch (m_sortRole) {
+    switch (roleType) {
     case ExtensionRole:
     case PermissionsRole:
     case OwnerRole:
@@ -1734,6 +1787,12 @@ void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
     }
 }
 
+void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+{
+    prepareItemsWithRole(itemDataList, m_sortRole);
+    prepareItemsWithRole(itemDataList, m_groupRole);
+}
+
 int KFileItemModel::expandedParentsCount(const ItemData *data)
 {
     // The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
@@ -2079,8 +2138,7 @@ bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QColla
                 return true;
             }
         }
-        if (m_sortDirsFirst
-            || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
+        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) {
@@ -2288,135 +2346,102 @@ int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const
 int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
 {
     // Unlike sortRoleCompare, this function can and often will return 0.
-    const KFileItem &itemA = a->item;
-    const KFileItem &itemB = b->item;
-
     int result = 0;
 
+    ItemGroupInfo groupA, groupB;
     switch (m_groupRole) {
     case NoRole:
+        // Non-trivial grouping behavior might be handled there in the future.
+        return 0;
+    case NameRole:
+        groupA = nameRoleGroup(a, false);
+        groupB = nameRoleGroup(b, false);
         break;
-    case NameRole: {
-        QChar groupA = getNameRoleGroup(a, false).toChar();
-        QChar groupB = getNameRoleGroup(b, false).toChar();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case SizeRole:
+        groupA = sizeRoleGroup(a, false);
+        groupB = sizeRoleGroup(b, false);
         break;
-    }
-    case SizeRole: {
-        int groupA = getSizeRoleGroup(a, false).toInt();
-        int groupB = getSizeRoleGroup(b, false).toInt();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case ModificationTimeRole:
+        groupA = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::ModificationTime);
+            },
+            a,
+            false);
+        groupB = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::ModificationTime);
+            },
+            b,
+            false);
         break;
-    }
-    case ModificationTimeRole: {
-        int groupA = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::ModificationTime);
-                         },
-                         a,
-                         false)
-                         .toInt();
-        int groupB = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::ModificationTime);
-                         },
-                         b,
-                         false)
-                         .toInt();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case CreationTimeRole:
+        groupA = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::CreationTime);
+            },
+            a,
+            false);
+        groupB = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::CreationTime);
+            },
+            b,
+            false);
         break;
-    }
-    case CreationTimeRole: {
-        int groupA = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::CreationTime);
-                         },
-                         a,
-                         false)
-                         .toInt();
-        int groupB = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::CreationTime);
-                         },
-                         b,
-                         false)
-                         .toInt();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case AccessTimeRole:
+        groupA = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::AccessTime);
+            },
+            a,
+            false);
+        groupB = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->item.time(KFileItem::AccessTime);
+            },
+            b,
+            false);
         break;
-    }
-    case AccessTimeRole: {
-        int groupA = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::AccessTime);
-                         },
-                         a,
-                         false)
-                         .toInt();
-        int groupB = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->item.time(KFileItem::AccessTime);
-                         },
-                         b,
-                         false)
-                         .toInt();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case DeletionTimeRole:
+        groupA = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->values.value("deletiontime").toDateTime();
+            },
+            a,
+            false);
+        groupB = timeRoleGroup(
+            [](const ItemData *item) {
+                return item->values.value("deletiontime").toDateTime();
+            },
+            b,
+            false);
         break;
-    }
-    case DeletionTimeRole: {
-        int groupA = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->values.value("deletiontime").toDateTime();
-                         },
-                         a,
-                         false)
-                         .toInt();
-        int groupB = getTimeRoleGroup(
-                         [](const ItemData *item) {
-                             return item->values.value("deletiontime").toDateTime();
-                         },
-                         b,
-                         false)
-                         .toInt();
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+    case PermissionsRole:
+        groupA = permissionRoleGroup(a, false);
+        groupB = permissionRoleGroup(b, false);
+        break;
+    case RatingRole:
+        groupA = ratingRoleGroup(a, false);
+        groupB = ratingRoleGroup(b, false);
+        break;
+    case TypeRole:
+        groupA = typeRoleGroup(a);
+        groupB = typeRoleGroup(b);
         break;
-    }
-    // case PermissionsRole:
-    //  case RatingRole:
     default: {
-        QString groupA = getGenericStringRoleGroup(groupRole(), a);
-        QString groupB = getGenericStringRoleGroup(groupRole(), b);
-        if (groupA < groupB) {
-            result = -1;
-        } else if (groupA > groupB) {
-            result = 1;
-        }
+        groupA = genericStringRoleGroup(groupRole(), a);
+        groupB = genericStringRoleGroup(groupRole(), b);
         break;
     }
     }
+    if (groupA.comparable < groupB.comparable) {
+        result = -1;
+    } else if (groupA.comparable > groupB.comparable) {
+        result = 1;
+    } else {
+        result = stringCompare(groupA.text, groupB.text, collator);
+    }
     return result;
 }
 
@@ -2425,7 +2450,24 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol
     QMutexLocker collatorLock(s_collatorMutex());
 
     if (m_naturalSorting) {
-        return collator.compare(a, b);
+        // Split extension, taking into account it can be empty
+        constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
+
+        // Sort by baseName first
+        const QString aBaseName = a.section('.', 0, 0, flags);
+        const QString bBaseName = b.section('.', 0, 0, flags);
+
+        const int res = collator.compare(aBaseName, bBaseName);
+        if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) {
+            return res;
+        }
+
+        // sliced() has undefined behavior when pos < 0 or pos > size().
+        Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0);
+        Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0);
+
+        // baseNames were equal, sort by extension
+        return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length()));
     }
 
     const int result = QString::compare(a, b, collator.caseSensitivity());
@@ -2439,19 +2481,29 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol
     return QString::compare(a, b, Qt::CaseSensitive);
 }
 
-QVariant KFileItemModel::getNameRoleGroup(const ItemData *itemData, bool asString) const
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
 {
-    const KFileItem item = itemData->item;
-    const QString name = item.text();
-    QVariant newGroupValue;
+    static bool oldWithString;
+    static ItemGroupInfo oldGroupInfo;
+    static QChar oldFirstChar;
+    ItemGroupInfo groupInfo;
+    QChar firstChar;
+
+    const QString name = itemData->item.text();
+
+    QMutexLocker collatorLock(s_collatorMutex());
+
     // 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();
+    firstChar = name.at(0).toUpper();
+    
+    if (firstChar == oldFirstChar && withString == oldWithString) {
+        return oldGroupInfo;
     }
-
-    if (newFirstChar.isLetter()) {
-        if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) {
+    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.
 
             // Try to find a matching group in the range 'A' to 'Z'.
@@ -2467,146 +2519,173 @@ QVariant KFileItemModel::getNameRoleGroup(const ItemData *itemData, bool asStrin
                 return m_collator.compare(c1, c2) < 0;
             };
 
-            std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
+            std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
             if (it != lettersAtoZ.end()) {
-                if (localeAwareLessThan(newFirstChar, *it)) {
+                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;
                 }
-                newGroupValue = *it;
+                if (withString) {
+                    groupInfo.text = *it;
+                }
+                groupInfo.comparable = (*it).unicode();
             }
 
         } else {
             // Symbols from non Latin-based scripts
-            newGroupValue = newFirstChar;
+            if (withString) {
+                groupInfo.text = firstChar;
+            }
+            groupInfo.comparable = firstChar.unicode();
         }
-    } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
+    } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
         // Apply group '0 - 9' for any name that starts with a digit
-        if (asString) {
-            newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
-        } else {
-            newGroupValue = QChar('0');
+        if (withString) {
+            groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
         }
+        groupInfo.comparable = (int)'0';
     } else {
-        if (asString) {
-            newGroupValue = i18nc("@title:group", "Others");
-        } else {
-            newGroupValue = QChar('.');
+        if (withString) {
+            groupInfo.text = i18nc("@title:group", "Others");
         }
+        groupInfo.comparable = (int)'.';
     }
-    return newGroupValue;
+    oldWithString = withString;
+    oldFirstChar = firstChar;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
 }
 
-QVariant KFileItemModel::getSizeRoleGroup(const ItemData *itemData, bool asString) const
+KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
 {
+    ItemGroupInfo groupInfo;
+    KIO::filesize_t fileSize;
+
     const KFileItem item = itemData->item;
+    fileSize = !item.isNull() ? item.size() : ~0U;
 
-    KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
-    int newGroupValue = -1; // None
+    groupInfo.comparable = -1; // None
     if (!item.isNull() && item.isDir()) {
-        if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) {
-            newGroupValue = 0; // Folders
+        if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) {
+            groupInfo.comparable = 0; // Folders
         } else {
             fileSize = itemData->values.value("size").toULongLong();
         }
     }
-
-    if (newGroupValue < 0) {
+    if (groupInfo.comparable < 0) {
         if (fileSize < 5 * 1024 * 1024) { // < 5 MB
-            newGroupValue = 1; // Small
+            groupInfo.comparable = 1; // Small
         } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
-            newGroupValue = 2; // Medium
+            groupInfo.comparable = 2; // Medium
         } else {
-            newGroupValue = 3; // Big
+            groupInfo.comparable = 3; // Big
         }
     }
 
-    if (asString) {
+    if (withString) {
         char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
-        return i18nc("@title:group Size", groupNames[newGroupValue]);
-    } else {
-        return newGroupValue;
+        groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
     }
+    return groupInfo;
 }
 
-QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool asString) const
+KFileItemModel::ItemGroupInfo
+KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
 {
+    static bool oldWithString;
+    static ItemGroupInfo oldGroupInfo;
+    static QDate oldFileDate;
+    ItemGroupInfo groupInfo;
+
     const QDate currentDate = QDate::currentDate();
-    const QDateTime fileTime = fileTimeCb(itemData);
-    const QDate fileDate = fileTime.date();
-    const int daysDistance = fileDate.daysTo(currentDate);
 
-    int intGroupValue;
-    QString strGroupValue;
+    QDate previousFileDate;
+    QString groupValue;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
 
-    if (!asString) {
-        // Simplified grouping algorithm, preserving dates
-        // but not taking "pretty printing" into account
-        if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+        const QLocale locale;
+        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;
+        }
+        previousFileDate = fileDate;
+
+        const int daysDistance = fileDate.daysTo(currentDate);
+
+    if (fileDate == oldFileDate && withString == oldWithString) {
+        return oldGroupInfo;
+    }
+    // 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
+        }
+    } else {
+        const QDate lastMonthDate = currentDate.addMonths(-1);
+        if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
             if (daysDistance < 7) {
-                intGroupValue = daysDistance; // Today, Yesterday and week days
+                groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
             } else if (daysDistance < 14) {
-                intGroupValue = 10; // One Week Ago
+                groupInfo.comparable = 11; // One Week Ago (Month, Year)
             } else if (daysDistance < 21) {
-                intGroupValue = 20; // Two Weeks Ago
+                groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
             } else if (daysDistance < 28) {
-                intGroupValue = 30; // Three Weeks Ago
+                groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
             } else {
-                intGroupValue = 40; // Earlier This Month
+                groupInfo.comparable = 41; // Earlier on Month, Year
             }
         } else {
-            const QDate lastMonthDate = currentDate.addMonths(-1);
-            if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
-                if (daysDistance < 7) {
-                    intGroupValue = daysDistance; // Today, Yesterday and week days (Month, Year)
-                } else if (daysDistance < 14) {
-                    intGroupValue = 9; // One Week Ago (Month, Year)
-                } else if (daysDistance < 21) {
-                    intGroupValue = 19; // Two Weeks Ago (Month, Year)
-                } else if (daysDistance < 28) {
-                    intGroupValue = 29; // Three Weeks Ago (Month, Year)
-                } else {
-                    intGroupValue = 39; // Earlier on Month, Year
-                }
-            } else {
-                // The trick will fail for dates past April, 178956967 or before 1 AD.
-                intGroupValue = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
-            }
+            // 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
         }
-        return QVariant(intGroupValue);
-    } else {
+    }
+    if (withString) {
         if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
             switch (daysDistance / 7) {
             case 0:
                 switch (daysDistance) {
                 case 0:
-                    strGroupValue = i18nc("@title:group Date", "Today");
+                    groupInfo.text = i18nc("@title:group Date", "Today");
                     break;
                 case 1:
-                    strGroupValue = i18nc("@title:group Date", "Yesterday");
+                    groupInfo.text = i18nc("@title:group Date", "Yesterday");
                     break;
                 default:
-                    strGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
-                    strGroupValue = i18nc(
+                    newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
+                    newGroupValue = i18nc(
                         "Can be used to script translation of \"dddd\""
                         "with context @title:group Date",
                         "%1",
-                        strGroupValue);
+                        groupInfo.text);
                 }
                 break;
             case 1:
-                strGroupValue = i18nc("@title:group Date", "One Week Ago");
+                groupInfo.text = i18nc("@title:group Date", "One Week Ago");
                 break;
             case 2:
-                strGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
+                groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
                 break;
             case 3:
-                strGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
+                groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
                 break;
             case 4:
             case 5:
-                strGroupValue = i18nc("@title:group Date", "Earlier this Month");
+                groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
                 break;
             default:
                 Q_ASSERT(false);
@@ -2622,31 +2701,31 @@ QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const It
                         "part of the text that should not be formatted as a date",
                         "'Yesterday' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
-                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        strGroupValue = fileTime.toString(translatedFormat);
-                        strGroupValue = i18nc(
+                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+                        newGroupValue = locale.toString(fileTime, translatedFormat);
+                        newGroupValue = i18nc(
                             "Can be used to script translation of "
                             "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            strGroupValue);
+                            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")});
-                        strGroupValue = fileTime.toString(untranslatedFormat);
+                        newGroupValue = locale.toString(fileTime, untranslatedFormat);
                     }
                 } else if (daysDistance <= 7) {
-                    strGroupValue =
-                        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)"));
-                    strGroupValue = i18nc(
+                    newGroupValue = locale.toString(fileTime,
+                                                    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(
                         "Can be used to script translation of "
                         "\"dddd (MMMM, yyyy)\" with context @title:group Date",
                         "%1",
-                        strGroupValue);
-                } 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 "
@@ -2654,20 +2733,20 @@ QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const It
                         "part of the text that should not be formatted as a date",
                         "'One Week Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
-                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        strGroupValue = fileTime.toString(translatedFormat);
-                        strGroupValue = i18nc(
+                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+                        newGroupValue = locale.toString(fileTime, translatedFormat);
+                        newGroupValue = i18nc(
                             "Can be used to script translation of "
                             "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            strGroupValue);
+                            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")});
-                        strGroupValue = fileTime.toString(untranslatedFormat);
+                        newGroupValue = locale.toString(fileTime, 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 "
@@ -2675,20 +2754,20 @@ QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const It
                         "part of the text that should not be formatted as a date",
                         "'Two Weeks Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
-                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        strGroupValue = fileTime.toString(translatedFormat);
-                        strGroupValue = i18nc(
+                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+                        newGroupValue = locale.toString(fileTime, translatedFormat);
+                        newGroupValue = i18nc(
                             "Can be used to script translation of "
                             "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            strGroupValue);
+                            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")});
-                        strGroupValue = fileTime.toString(untranslatedFormat);
+                        newGroupValue = locale.toString(fileTime, 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 "
@@ -2696,18 +2775,18 @@ QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const It
                         "part of the text that should not be formatted as a date",
                         "'Three Weeks Ago' (MMMM, yyyy)");
                     const QString translatedFormat = format.toString();
-                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        strGroupValue = fileTime.toString(translatedFormat);
-                        strGroupValue = i18nc(
+                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+                        newGroupValue = locale.toString(fileTime, translatedFormat);
+                        newGroupValue = i18nc(
                             "Can be used to script translation of "
                             "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
                             "%1",
-                            strGroupValue);
+                            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")});
-                        strGroupValue = fileTime.toString(untranslatedFormat);
+                        newGroupValue = locale.toString(fileTime, untranslatedFormat);
                     }
                 } else {
                     const KLocalizedString format = ki18nc(
@@ -2717,40 +2796,116 @@ QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const It
                         "part of the text that should not be formatted as a date",
                         "'Earlier on' MMMM, yyyy");
                     const QString translatedFormat = format.toString();
-                    if (translatedFormat.count(QLatin1Char('\'')) == 2) {
-                        strGroupValue = fileTime.toString(translatedFormat);
-                        strGroupValue = i18nc(
+                    if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+                        newGroupValue = locale.toString(fileTime, translatedFormat);
+                        newGroupValue = i18nc(
                             "Can be used to script translation of "
                             "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
                             "%1",
-                            strGroupValue);
+                            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")});
-                        strGroupValue = fileTime.toString(untranslatedFormat);
+                        newGroupValue = locale.toString(fileTime, untranslatedFormat);
                     }
                 }
             } else {
-                strGroupValue =
-                    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"));
-                strGroupValue = i18nc(
+                newGroupValue = locale.toString(fileTime,
+                                                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(
                     "Can be used to script translation of "
                     "\"MMMM, yyyy\" with context @title:group Date",
                     "%1",
-                    strGroupValue);
+                    groupInfo.text);
             }
         }
-        return QVariant(strGroupValue);
     }
+    oldWithString = withString;
+    oldFileDate = fileDate;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
+{
+    static bool oldWithString;
+    static ItemGroupInfo oldGroupInfo;
+    static QFileDevice::Permissions oldPermissions;
+    ItemGroupInfo groupInfo;
+
+    const QFileInfo info(itemData->item.url().toLocalFile());
+    const QFileDevice::Permissions permissions = info.permissions();
+    if (permissions == oldPermissions && withString == oldWithString) {
+        return oldGroupInfo;
+    }
+    groupInfo.comparable = (int)permissions;
+
+    if (withString) {
+        // Set user string
+        QString user;
+        if (permissions & QFile::ReadUser) {
+            user = i18nc("@item:intext Access permission, concatenated", "Read, ");
+        }
+        if (permissions & QFile::WriteUser) {
+            user += i18nc("@item:intext Access permission, concatenated", "Write, ");
+        }
+        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 (permissions & QFile::ReadGroup) {
+            group = i18nc("@item:intext Access permission, concatenated", "Read, ");
+        }
+        if (permissions & QFile::WriteGroup) {
+            group += i18nc("@item:intext Access permission, concatenated", "Write, ");
+        }
+        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 (permissions & QFile::ReadOther) {
+            others = i18nc("@item:intext Access permission, concatenated", "Read, ");
+        }
+        if (permissions & QFile::WriteOther) {
+            others += i18nc("@item:intext Access permission, concatenated", "Write, ");
+        }
+        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);
+    }
+    oldWithString = withString;
+    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 %i", QString::number(groupInfo.comparable));
+    }
+    return groupInfo;
 }
 
-QString KFileItemModel::getGenericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
 {
-    return itemData->values.value(role).toString();
+    return {0, itemData->values.value(role).toString()};
 }
 
 QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
@@ -2760,21 +2915,18 @@ QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    QString groupValue;
-    QChar firstChar;
+    ItemGroupInfo groupInfo;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
 
-        QString newGroupValue = getNameRoleGroup(m_itemData.at(i)).toString();
+        ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
 
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
-
-        // firstChar = newFirstChar;
     }
     return groups;
 }
@@ -2786,23 +2938,36 @@ QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    QString groupValue;
+    ItemGroupInfo groupInfo;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
 
-        QString newGroupValue = getSizeRoleGroup(m_itemData.at(i)).toString();
+        ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
 
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
-
     return groups;
 }
 
+KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const
+{
+    int priority = 0;
+    if (itemData->item.isDir() && m_sortDirsFirst) {
+        // Ensure folders stay first regardless of grouping order
+        if (groupOrder() == Qt::AscendingOrder) {
+            priority = -1;
+        } else {
+            priority = 1;
+        }
+    }
+    return {priority, itemData->values.value("type").toString()};
+}
+
 QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
 {
     Q_ASSERT(!m_itemData.isEmpty());
@@ -2810,31 +2975,19 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    const QDate currentDate = QDate::currentDate();
-
-    QDate previousFileDate;
-    QString groupValue;
+    ItemGroupInfo groupInfo;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
 
-        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;
-        }
-        previousFileDate = fileDate;
+        ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
 
-        QString newGroupValue = getTimeRoleGroup(fileTimeCb, m_itemData.at(i)).toString();
-
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
-
     return groups;
 }
 
@@ -2845,68 +2998,19 @@ QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    QString permissionsString;
-    QString groupValue;
+    ItemGroupInfo groupInfo;
     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;
-
-        const QFileInfo info(itemData->item.url().toLocalFile());
-
-        // Set user string
-        QString user;
-        if (info.permission(QFile::ReadUser)) {
-            user = i18nc("@item:intext Access permission, concatenated", "Read, ");
-        }
-        if (info.permission(QFile::WriteUser)) {
-            user += i18nc("@item:intext Access permission, concatenated", "Write, ");
-        }
-        if (info.permission(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)) {
-            group = i18nc("@item:intext Access permission, concatenated", "Read, ");
-        }
-        if (info.permission(QFile::WriteGroup)) {
-            group += i18nc("@item:intext Access permission, concatenated", "Write, ");
-        }
-        if (info.permission(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);
+        ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
 
-        // Set others string
-        QString others;
-        if (info.permission(QFile::ReadOther)) {
-            others = i18nc("@item:intext Access permission, concatenated", "Read, ");
-        }
-        if (info.permission(QFile::WriteOther)) {
-            others += i18nc("@item:intext Access permission, concatenated", "Write, ");
-        }
-        if (info.permission(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);
-
-        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));
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
-
     return groups;
 }
 
@@ -2917,19 +3021,21 @@ QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
     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 = 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;
 }
 
@@ -2940,21 +3046,19 @@ QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteA
     const int maxIndex = count() - 1;
     QList<QPair<int, QVariant>> groups;
 
-    bool isFirstGroupValue = true;
-    QString groupValue;
+    ItemGroupInfo groupInfo;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
         }
 
-        const QString newGroupValue = getGenericStringRoleGroup(role, m_itemData.at(i));
-        if (newGroupValue != groupValue || isFirstGroupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
-            isFirstGroupValue = false;
+        ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
         }
     }
-
     return groups;
 }
 
@@ -3021,6 +3125,7 @@ const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
         { "releaseYear",         ReleaseYearRole,         kli18nc("@label", "Release Year"),         kli18nc("@label", "Audio"),    KLazyLocalizedString(),                    true,            true  },
         { "aspectRatio",         AspectRatioRole,         kli18nc("@label", "Aspect Ratio"),         kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
         { "frameRate",           FrameRateRole,           kli18nc("@label", "Frame Rate"),           kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
+        { "duration",            DurationRole,            kli18nc("@label", "Duration"),             kli18nc("@label", "Video"),    KLazyLocalizedString(),                    true,            true  },
         { "path",                PathRole,                kli18nc("@label", "Path"),                 kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
         { "extension",           ExtensionRole,           kli18nc("@label", "File Extension"),       kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
         { "deletiontime",        DeletionTimeRole,        kli18nc("@label", "Deletion Time"),        kli18nc("@label", "Other"),    KLazyLocalizedString(),                    false,           false },
@@ -3122,13 +3227,14 @@ bool KFileItemModel::isConsistent() const
 
 void KFileItemModel::slotListerError(KIO::Job *job)
 {
-    if (job->error() == KIO::ERR_IS_FILE) {
+    const int jobError = job->error();
+    if (jobError == KIO::ERR_IS_FILE) {
         if (auto *listJob = qobject_cast<KIO::ListJob *>(job)) {
             Q_EMIT urlIsFileError(listJob->url());
         }
     } else {
         const QString errorString = job->errorString();
-        Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."));
+        Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError);
     }
 }