]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups' master
authormilkyroute <milkyroute@milkyroute.net>
Sun, 10 Aug 2025 11:06:17 +0000 (13:06 +0200)
committermilkyroute <milkyroute@milkyroute.net>
Sun, 10 Aug 2025 11:06:17 +0000 (13:06 +0200)
27 files changed:
src/dolphincontextmenu.cpp
src/dolphinmainwindow.cpp
src/dolphinpart.rc
src/dolphinui.rc
src/kitemviews/kfileitemlistview.cpp
src/kitemviews/kfileitemmodel.cpp
src/kitemviews/kfileitemmodel.h
src/kitemviews/kfileitemmodelrolesupdater.cpp
src/kitemviews/kfileitemmodelrolesupdater.h
src/kitemviews/kitemlistview.cpp
src/kitemviews/kitemlistview.h
src/kitemviews/kitemmodelbase.cpp
src/kitemviews/kitemmodelbase.h
src/settings/applyviewpropsjob.cpp
src/settings/contextmenu/contextmenusettingspage.cpp
src/settings/dolphin_contextmenusettings.kcfg
src/settings/dolphin_directoryviewpropertysettings.kcfg
src/settings/dolphinsettingsdialog.cpp
src/settings/viewpropertiesdialog.cpp
src/settings/viewpropertiesdialog.h
src/tests/kfileitemmodeltest.cpp
src/views/dolphinview.cpp
src/views/dolphinview.h
src/views/dolphinviewactionhandler.cpp
src/views/dolphinviewactionhandler.h
src/views/viewproperties.cpp
src/views/viewproperties.h

index e1c67aad1fc53821f1d5b667fbd48e34940a0f11..b6c70b48ab24fd60427690324c5f5b36d762d2b8 100644 (file)
@@ -340,10 +340,13 @@ void DolphinContextMenu::addViewportContextMenu()
     }
     addSeparator();
 
-    // Insert 'Sort By' and 'View Mode'
+    // Insert 'Sort By', 'Group By' and 'View Mode'
     if (ContextMenuSettings::showSortBy()) {
         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort")));
     }
+    if (ContextMenuSettings::showGroupBy()) {
+        addAction(m_mainWindow->actionCollection()->action(QStringLiteral("group")));
+    }
     if (ContextMenuSettings::showViewMode()) {
         addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode")));
     }
index 51772eac28b1abb7897abe3d58387f32f9298472..d08d6f6b396fe75bc9dda6cb48d67eabe354e41d 100644 (file)
@@ -1568,12 +1568,13 @@ void DolphinMainWindow::updateHamburgerMenu()
         menu->addAction(ac->action(QStringLiteral("view_mode")));
     }
     if (!toolBar()->isVisible() || !toolbarActions.contains(ac->action(QStringLiteral("view_settings")))) {
-        menu->addAction(ac->action(QStringLiteral("show_hidden_files")));
-        menu->addAction(ac->action(QStringLiteral("sort")));
-        menu->addAction(ac->action(QStringLiteral("additional_info")));
-        if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) {
-            menu->addAction(ac->action(QStringLiteral("zoom")));
-        }
+           menu->addAction(ac->action(QStringLiteral("show_hidden_files")));
+           menu->addAction(ac->action(QStringLiteral("sort")));
+           menu->addAction(ac->action(QStringLiteral("group")));
+           menu->addAction(ac->action(QStringLiteral("additional_info")));
+           if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) {
+                       menu->addAction(ac->action(QStringLiteral("zoom")));
+               }
     }
     menu->addAction(ac->action(QStringLiteral("panels")));
 
@@ -2413,7 +2414,7 @@ void DolphinMainWindow::setupDockWidgets()
     placesDock->setLocked(lock);
     placesDock->setObjectName(QStringLiteral("placesDock"));
     placesDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
-
+    
     m_placesPanel = new PlacesPanel(placesDock);
     m_placesPanel->setCustomContextMenuActions({lockLayoutAction});
     placesDock->setWidget(m_placesPanel);
index 5f57f586ab568a79d89bfcc5873331fe99aa1975..5dacc2b81382656cf88480354571582834a2b1d9 100644 (file)
@@ -23,6 +23,7 @@
   </Menu>
   <Menu name="view"><text>&amp;View</text>
    <Action name="sort" />
+   <Action name="group" />
    <Action name="additional_info" />
    <Action name="show_preview" />
    <Action name="show_in_groups" />
index 3e6e4c463034e4ec100188a0f623eedc7da619f3..b611b91703d885f96b30ac851feda68732335d4c 100644 (file)
@@ -43,6 +43,7 @@
             <Action name="view_zoom_out"/>
             <Separator/>
             <Action name="sort" />
+            <Action name="group" />
             <Action name="view_mode" />
             <Action name="additional_info" />
             <Action name="show_preview" />
index d763fe287924325b3a92e62c1e74c109cdd06cd7..d58a1a8baf3ac982d82135f1ed1ba55a2ae41404 100644 (file)
@@ -452,8 +452,9 @@ void KFileItemListView::applyRolesToModel()
         roles.insert("expandedParentsCount");
     }
 
-    // Assure that the role that is used for sorting will be determined
+    // Assure that the roles used for sorting and grouping will be determined
     roles.insert(fileItemModel->sortRole());
+    roles.insert(fileItemModel->groupRole());
 
     fileItemModel->setRoles(roles);
     m_modelRolesUpdater->setRoles(roles);
index 4386bca16b6ada60a808a9764ff83ed8d74826ed..603c16e0d37ff816488aa69b9122308335a36d45 100644 (file)
@@ -37,11 +37,12 @@ Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
 // #define KFILEITEMMODEL_DEBUG
 
 KFileItemModel::KFileItemModel(QObject *parent)
-    : KItemModelBase("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()
@@ -413,7 +414,17 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
         QElapsedTimer timer;
         timer.start();
 #endif
-        switch (typeForRole(sortRole())) {
+        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;
         case NameRole:
             m_groups = nameRoleGroups();
             break;
@@ -447,7 +458,7 @@ QList<QPair<int, QVariant>> KFileItemModel::groups() const
             m_groups = ratingRoleGroups();
             break;
         default:
-            m_groups = genericStringRoleGroups(sortRole());
+            m_groups = genericStringRoleGroups(role);
             break;
         }
 
@@ -912,6 +923,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;
@@ -920,24 +964,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);
             }
         }
@@ -946,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)
@@ -956,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;
@@ -975,6 +1018,36 @@ void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre
     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_groupRole]) {
+        QSet<QByteArray> newRoles = m_roles;
+        newRoles << current;
+        setRoles(newRoles);
+    }
+
+    if (resortItems) {
+        resortAllItems();
+    }
+}
+
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+    resortAllItems();
+}
+
 void KFileItemModel::loadSortingSettings()
 {
     using Choice = GeneralSettings::EnumSortingChoice;
@@ -997,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()
@@ -1062,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();
@@ -1647,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).
@@ -1671,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:
@@ -1712,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"
@@ -2045,31 +2126,33 @@ 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::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;
+        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
@@ -2260,6 +2343,108 @@ 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;
+
+    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 SizeRole:
+        groupA = sizeRoleGroup(a, false);
+        groupB = sizeRoleGroup(b, false);
+        break;
+    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 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 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 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 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;
+    default: {
+        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;
+}
+
 int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
 {
     QMutexLocker collatorLock(s_collatorMutex());
@@ -2296,128 +2481,122 @@ 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 bool oldWithString;
+    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();
-
-        // 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.
+    const QString name = itemData->item.text();
 
-                    // 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));
-                        }
-                    }
+    QMutexLocker collatorLock(s_collatorMutex());
 
-                    auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
-                        return m_collator.compare(c1, c2) < 0;
-                    };
+    // Use the first character of the name as group indication
+    firstChar = name.at(0).toUpper();
+    
+    if (firstChar == oldFirstChar && withString == oldWithString) {
+        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.
+
+            // 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;
+    oldWithString = withString;
+    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());
+    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::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || 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::ContentSize) {
+            groupInfo.comparable = 0; // Folders
+        } else {
+            fileSize = itemData->values.value("size").toULongLong();
         }
-
-        if (newGroupValue != groupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
+    }
+    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]);
+    }
+    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 bool oldWithString;
+    static ItemGroupInfo oldGroupInfo;
+    static QDate oldFileDate;
+    ItemGroupInfo groupInfo;
 
     const QDate currentDate = QDate::currentDate();
 
@@ -2439,16 +2618,52 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
 
         const int daysDistance = fileDate.daysTo(currentDate);
 
-        QString newGroupValue;
+    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) {
+                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
+        }
+    }
+    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 = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
@@ -2456,21 +2671,21 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "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);
@@ -2492,7 +2707,7 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                             "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";
@@ -2509,8 +2724,8 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                         "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 "
@@ -2524,14 +2739,14 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                             "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 = 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 "
@@ -2545,14 +2760,14 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                             "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 = 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 "
@@ -2566,7 +2781,7 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                             "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";
@@ -2587,7 +2802,7 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                             "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";
@@ -2605,134 +2820,245 @@ QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<Q
                     "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;
+    oldWithString = withString;
+    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;
-        }
+    static bool oldWithString;
+    static ItemGroupInfo oldGroupInfo;
+    static QFileDevice::Permissions oldPermissions;
+    ItemGroupInfo groupInfo;
 
-        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());
+    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 (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);
+    }
+    oldWithString = withString;
+    oldPermissions = permissions;
+    oldGroupInfo = groupInfo;
+    return groupInfo;
+}
 
-        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));
+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;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+    return {0, itemData->values.value(role).toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() 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 = 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
+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());
 
     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 = 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;
+
+    ItemGroupInfo groupInfo;
+    for (int i = 0; i <= maxIndex; ++i) {
+        if (isChildItem(i)) {
+            continue;
+        }
+
+        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::genericStringRoleGroups(const QByteArray &role) const
+QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
 {
     Q_ASSERT(!m_itemData.isEmpty());
 
     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 = m_itemData.at(i)->values.value(role).toString();
-        if (newGroupValue != groupValue || isFirstGroupValue) {
-            groupValue = newGroupValue;
-            groups.append(QPair<int, QVariant>(i, newGroupValue));
-            isFirstGroupValue = false;
+
+        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;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) 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 = genericStringRoleGroup(role, m_itemData.at(i));
+
+        if (newGroupInfo != groupInfo) {
+            groupInfo = newGroupInfo;
+            groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+        }
+    }
     return groups;
 }
 
@@ -2769,7 +3095,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 },
index 10be27128832d3bb8e02ff87bdf55a23ec2d0de4..13554d8c7fa1999dc62e8fd355c71967096ca9e5 100644 (file)
@@ -196,6 +196,13 @@ public:
         bool requiresIndexer;
     };
 
+    /**
+     * @return Provides static information for a role that is supported
+     *         by KFileItemModel. Some roles can only be determined if
+     *         Baloo is enabled and/or the Baloo indexing is enabled.
+     */
+    static RoleInfo roleInformation(const QByteArray &role);
+
     /**
      * @return Provides static information for all available roles that
      *         are supported by KFileItemModel. Some roles can only be
@@ -204,6 +211,13 @@ public:
      */
     static QList<RoleInfo> rolesInformation();
 
+    /**
+     * @return Provides static information for all available grouping
+     *         behaviors supported by KFileItemModel but not directly
+     *         mapped to roles of KFileItemModel.
+     */
+    static QList<RoleInfo> extraGroupingInformation();
+
     /** set to true to hide application/x-trash files */
     void setShowTrashMime(bool show);
 
@@ -287,11 +301,13 @@ protected:
     void onGroupedSortingChanged(bool current) override;
     void onSortRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems = true) override;
     void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override;
+    void onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems = true) override;
+    void onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override;
 
 private Q_SLOTS:
     /**
-     * Resorts all items dependent on the set sortRole(), sortOrder()
-     * and foldersFirst() settings.
+     * Resorts all items dependent on the set sortRole(), sortOrder(),
+     * groupRole(), groupOrder() and foldersFirst() settings.
      */
     void resortAllItems();
 
@@ -364,13 +380,18 @@ private:
         QHash<QByteArray, QVariant> values;
         ItemData *parent;
     };
-
-    enum RemoveItemsBehavior {
-        KeepItemData,
-        DeleteItemData,
-        DeleteItemDataIfUnfiltered
+    
+    struct ItemGroupInfo {
+        int comparable;
+        QString text;
+
+        bool operator==(const ItemGroupInfo &other) const;
+        bool operator!=(const ItemGroupInfo &other) const;
+        bool operator<(const ItemGroupInfo &other) const;
     };
 
+    enum RemoveItemsBehavior { KeepItemData, DeleteItemData, DeleteItemDataIfUnfiltered };
+
     void insertItems(QList<ItemData *> &items);
     void removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior);
 
@@ -382,6 +403,12 @@ private:
      */
     QList<ItemData *> createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const;
 
+    /**
+     * Helper method for prepareItemsForSorting(). 
+     * For a set role, fills 'values' of ItemData non-lazily.
+     */
+    void prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType);
+
     /**
      * Prepares the items for sorting. Normally, the hash 'values' in ItemData is filled
      * lazily to save time and memory, but for some sort roles, it is expected that the
@@ -449,13 +476,29 @@ private:
      */
     int sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const;
 
+    /**
+     * Helper method for lessThan() and expandedParentsCountCompare(): Compares
+     * the passed item-data using m_groupRole as criteria. Both items must
+     * have the same parent item, otherwise the comparison will be wrong.
+     */
+    int groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const;
+
     int stringCompare(const QString &a, const QString &b, const QCollator &collator) const;
 
+    ItemGroupInfo nameRoleGroup(const ItemData *itemData, bool withString = true) const;
+    ItemGroupInfo sizeRoleGroup(const ItemData *itemData, bool withString = true) const;
+    ItemGroupInfo timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString = true) const;
+    ItemGroupInfo permissionRoleGroup(const ItemData *itemData, bool withString = true) const;
+    ItemGroupInfo ratingRoleGroup(const ItemData *itemData, bool withString = true) const;
+    ItemGroupInfo typeRoleGroup(const ItemData *itemData) const;
+    ItemGroupInfo genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const;
+
     QList<QPair<int, QVariant>> nameRoleGroups() const;
     QList<QPair<int, QVariant>> sizeRoleGroups() const;
     QList<QPair<int, QVariant>> timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const;
     QList<QPair<int, QVariant>> permissionRoleGroups() const;
     QList<QPair<int, QVariant>> ratingRoleGroups() const;
+    QList<QPair<int, QVariant>> typeRoleGroups() const;
     QList<QPair<int, QVariant>> genericStringRoleGroups(const QByteArray &typeForRole) const;
 
     /**
@@ -546,6 +589,10 @@ private:
     bool m_sortHiddenLast;
 
     RoleType m_sortRole;
+    RoleType m_groupRole;
+    QByteArray m_sortExtraInfo;
+    QByteArray m_groupExtraInfo;
+
     int m_sortingProgressPercent; // Value of directorySortingProgress() signal
     QSet<QByteArray> m_roles;
 
@@ -606,4 +653,14 @@ inline bool KFileItemModel::isChildItem(int index) const
     }
 }
 
+inline bool KFileItemModel::ItemGroupInfo::operator==(const ItemGroupInfo &other) const
+{
+    return comparable == other.comparable && text == other.text;
+}
+
+inline bool KFileItemModel::ItemGroupInfo::operator!=(const ItemGroupInfo &other) const
+{
+    return comparable != other.comparable || text != other.text;
+}
+
 #endif
index 497c13b525c6da3708d597057ea792585fdcec37..44165dedcb0ed3a903cd09d688a10acab412b4d5 100644 (file)
@@ -369,9 +369,8 @@ void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRan
     timer.start();
 
     // Determine the sort role synchronously for as many items as possible.
-    if (m_resolvableRoles.contains(m_model->sortRole())) {
-        QList<QUrl> dirsWithAddedItems;
-
+    if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) {
+       QList<QUrl> dirsWithAddedItems;
         int insertedCount = 0;
         for (const KItemRange &range : itemRanges) {
             const int lastIndex = insertedCount + range.index + range.count - 1;
@@ -1218,13 +1217,13 @@ void KFileItemModelRolesUpdater::applySortRole(int index)
     QHash<QByteArray, QVariant> data;
     const KFileItem item = m_model->fileItem(index);
 
-    if (m_model->sortRole() == "type") {
+    if (m_model->sortRole() == "type" || m_model->groupRole() == "type") {
         if (!item.isMimeTypeKnown()) {
             item.determineMimeType();
         }
 
         data.insert("type", item.mimeComment());
-    } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
+    } else if ((m_model->sortRole() == "size" || m_model->groupRole() == "size") && item.isLocalFile() && item.isDir()) {
         startDirectorySizeCounting(item, index);
         return;
     } else {
index 56e28ce72587dbdae53096c1b7cbc3da57df9079..73e42cf17f2e76eaba9173c0bd3ad5199e2ec8e6 100644 (file)
@@ -319,6 +319,8 @@ private:
 
     /**
      * Resolves the sort role of the item and applies it to the model.
+     * Despite the name, this handles both sorting and grouping, as 
+     * regrouping never happens without resorting at the same time.
      */
     void applySortRole(int index);
 
index 75d85be358953e7897936800d303142b183ef9f5..82325cb1978813f38747d2203f84189461932b66 100644 (file)
@@ -1521,6 +1521,26 @@ void KItemListView::slotSortRoleChanged(const QByteArray &current, const QByteAr
     }
 }
 
+void KItemListView::slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+    if (m_grouped) {
+        updateVisibleGroupHeaders();
+        doLayout(NoAnimation);
+    }
+}
+
+void KItemListView::slotGroupRoleChanged(const QByteArray &current, const QByteArray &previous)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+    if (m_grouped) {
+        updateVisibleGroupHeaders();
+        doLayout(NoAnimation);
+    }
+}
+
 void KItemListView::slotCurrentChanged(int current, int previous)
 {
     // In SingleSelection mode (e.g., in the Places Panel), the current item is
@@ -1797,6 +1817,8 @@ void KItemListView::setModel(KItemModelBase *model)
         disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged);
         disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged);
         disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged);
+        disconnect(m_model, &KItemModelBase::groupOrderChanged, this, &KItemListView::slotGroupOrderChanged);
+        disconnect(m_model, &KItemModelBase::groupRoleChanged, this, &KItemListView::slotGroupRoleChanged);
 
         m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count()));
     }
@@ -2242,7 +2264,7 @@ void KItemListView::updateGroupHeaderForWidget(KItemListWidget *widget)
     const int groupIndex = groupIndexForItem(index);
     Q_ASSERT(groupIndex >= 0);
     groupHeader->setData(groups.at(groupIndex).second);
-    groupHeader->setRole(model()->sortRole());
+    groupHeader->setRole(model()->groupRole());
     groupHeader->setStyleOption(m_styleOption);
     groupHeader->setScrollOrientation(scrollOrientation());
     groupHeader->setItemIndex(index);
index 1a4ff0df1c7bf9b280574f35810545c6d762121e..1382405d058b89e87fe218cd6a6bd12cc20e8e89 100644 (file)
@@ -441,6 +441,8 @@ protected Q_SLOTS:
     virtual void slotGroupedSortingChanged(bool current);
     virtual void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
     virtual void slotSortRoleChanged(const QByteArray &current, const QByteArray &previous);
+    virtual void slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+    virtual void slotGroupRoleChanged(const QByteArray &current, const QByteArray &previous);
     virtual void slotCurrentChanged(int current, int previous);
     virtual void slotSelectionChanged(const KItemSet &current, const KItemSet &previous);
 
@@ -563,8 +565,9 @@ private:
     void recycleGroupHeaderForWidget(KItemListWidget *widget);
 
     /**
-     * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged()
-     * and slotSortRoleChanged(): Iterates through all visible items and updates
+     * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged(),
+     * slotSortRoleChanged(), slotGroupOrderChanged() and slotGroupRoleChanged():
+     * Iterates through all visible items and updates
      * the group-header widgets.
      */
     void updateVisibleGroupHeaders();
index 9fdecafb864425cc887ca47b1e164b1a04cdb769..17716f57f5d52155a5bafaa5e3fe592953a8023d 100644 (file)
 
 KItemModelBase::KItemModelBase(QObject *parent)
     : QObject(parent)
-    , m_groupedSorting(false)
+    , m_groupedSorting(true)
     , m_sortRole()
     , m_sortOrder(Qt::AscendingOrder)
+    , m_groupRole()
+    , m_groupOrder(Qt::AscendingOrder)
 {
 }
 
-KItemModelBase::KItemModelBase(const QByteArray &sortRole, QObject *parent)
+KItemModelBase::KItemModelBase(const QByteArray &sortRole, const QByteArray &groupRole, QObject *parent)
     : QObject(parent)
-    , m_groupedSorting(false)
+    , m_groupedSorting(true)
     , m_sortRole(sortRole)
     , m_sortOrder(Qt::AscendingOrder)
+    , m_groupRole(groupRole)
+    , m_groupOrder(Qt::AscendingOrder)
 {
 }
 
@@ -74,6 +78,31 @@ void KItemModelBase::setSortOrder(Qt::SortOrder order)
     }
 }
 
+void KItemModelBase::setGroupRole(const QByteArray &role, bool regroupItems)
+{
+    if (role != m_groupRole) {
+        const QByteArray previous = m_groupRole;
+        m_groupRole = role;
+        onGroupRoleChanged(role, previous, regroupItems);
+        Q_EMIT groupRoleChanged(role, previous);
+    }
+}
+
+QByteArray KItemModelBase::groupRole() const
+{
+    return m_groupRole;
+}
+
+void KItemModelBase::setGroupOrder(Qt::SortOrder order)
+{
+    if (order != m_groupOrder) {
+        const Qt::SortOrder previous = m_groupOrder;
+        m_groupOrder = order;
+        onGroupOrderChanged(order, previous);
+        Q_EMIT groupOrderChanged(order, previous);
+    }
+}
+
 QString KItemModelBase::roleDescription(const QByteArray &role) const
 {
     return QString::fromLatin1(role);
@@ -157,6 +186,19 @@ void KItemModelBase::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre
     Q_UNUSED(previous)
 }
 
+void KItemModelBase::onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+    Q_UNUSED(resortItems)
+}
+
+void KItemModelBase::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+{
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
+}
+
 QUrl KItemModelBase::url(int index) const
 {
     return data(index).value("url").toUrl();
index 42a9c54c94ca915474a305bffee18c309cd59914..bc8ab64427d40577b300bcf82935b1f632fc33b6 100644 (file)
@@ -41,7 +41,7 @@ class DOLPHIN_EXPORT KItemModelBase : public QObject
 
 public:
     explicit KItemModelBase(QObject *parent = nullptr);
-    explicit KItemModelBase(const QByteArray &sortRole, QObject *parent = nullptr);
+    explicit KItemModelBase(const QByteArray &sortRole, const QByteArray &groupRole, QObject *parent = nullptr);
     ~KItemModelBase() override;
 
     /** @return The number of items. */
@@ -84,6 +84,23 @@ public:
     void setSortOrder(Qt::SortOrder order);
     Qt::SortOrder sortOrder() const;
 
+    /**
+     * Sets the group-role to \a role. The method KItemModelBase::onGroupRoleChanged() will be
+     * called so that model-implementations can react on the group-role change. Afterwards the
+     * signal groupRoleChanged() will be emitted.
+     * The implementation should regroup only if \a regroupItems is true.
+     */
+    void setGroupRole(const QByteArray &role, bool regroupItems = true);
+    QByteArray groupRole() const;
+
+    /**
+     * Sets the group order to \a order. The method KItemModelBase::onGroupOrderChanged() will be
+     * called so that model-implementations can react on the group order change. Afterwards the
+     * signal groupOrderChanged() will be emitted.
+     */
+    void setGroupOrder(Qt::SortOrder order);
+    Qt::SortOrder groupOrder() const;
+
     /**
      * @return Translated description for the \p role. The description is e.g. used
      *         for the header in KItemListView.
@@ -247,6 +264,8 @@ Q_SIGNALS:
     void groupedSortingChanged(bool current);
     void sortRoleChanged(const QByteArray &current, const QByteArray &previous);
     void sortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+    void groupRoleChanged(const QByteArray &current, const QByteArray &previous);
+    void groupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
 
 protected:
     /**
@@ -276,10 +295,33 @@ protected:
      */
     virtual void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
 
+    /**
+     * Is invoked if the sort role has been changed by KItemModelBase::setSortRole(). Allows
+     * to react on the changed sort role before the signal sortRoleChanged() will be emitted.
+     * The implementation must assure that the items are sorted by the role given by \a current.
+     * Usually the most efficient way is to emit a
+     * itemsRemoved() signal for all items, reorder the items internally and to emit a
+     * itemsInserted() signal afterwards.
+     * The implementation should resort only if \a regroupItems is true.
+     */
+    virtual void onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool regroupItems = true);
+
+    /**
+     * Is invoked if the sort order has been changed by KItemModelBase::setSortOrder(). Allows
+     * to react on the changed sort order before the signal sortOrderChanged() will be emitted.
+     * The implementation must assure that the items are sorted by the order given by \a current.
+     * Usually the most efficient way is to emit a
+     * itemsRemoved() signal for all items, reorder the items internally and to emit a
+     * itemsInserted() signal afterwards.
+     */
+    virtual void onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous);
+
 private:
     bool m_groupedSorting;
     QByteArray m_sortRole;
     Qt::SortOrder m_sortOrder;
+    QByteArray m_groupRole;
+    Qt::SortOrder m_groupOrder;
 };
 
 inline Qt::SortOrder KItemModelBase::sortOrder() const
@@ -287,4 +329,9 @@ inline Qt::SortOrder KItemModelBase::sortOrder() const
     return m_sortOrder;
 }
 
+inline Qt::SortOrder KItemModelBase::groupOrder() const
+{
+    return m_groupOrder;
+}
+
 #endif
index 2a2b4bfe4f5ef0c30a7fe6e4a3f374b1af65c027..21c30af022b0b3803e0ad395689ed9be988f0f3f 100644 (file)
@@ -24,6 +24,8 @@ ApplyViewPropsJob::ApplyViewPropsJob(const QUrl &dir, const ViewProperties &view
     m_viewProps->setHiddenFilesShown(viewProps.hiddenFilesShown());
     m_viewProps->setSortRole(viewProps.sortRole());
     m_viewProps->setSortOrder(viewProps.sortOrder());
+    m_viewProps->setGroupRole(viewProps.groupRole());
+    m_viewProps->setGroupOrder(viewProps.groupOrder());
 
     KIO::ListJob *listJob = KIO::listRecursive(dir, KIO::HideProgressInfo);
     connect(listJob, &KIO::ListJob::entries, this, &ApplyViewPropsJob::slotEntries);
index b3585e3fa7faa1072e8fd6b54e85297567b561c2..6046de4c687683e8cc4af4f6812d2d12c923584e 100644 (file)
@@ -110,6 +110,8 @@ bool ContextMenuSettingsPage::entryVisible(const QString &id)
         return ContextMenuSettings::showAddToPlaces();
     } else if (id == "sort") {
         return ContextMenuSettings::showSortBy();
+    } else if (id == "group") {
+        return ContextMenuSettings::showGroupBy();
     } else if (id == "view_mode") {
         return ContextMenuSettings::showViewMode();
     } else if (id == "open_in_new_tab") {
@@ -138,6 +140,8 @@ void ContextMenuSettingsPage::setEntryVisible(const QString &id, bool visible)
         ContextMenuSettings::setShowAddToPlaces(visible);
     } else if (id == "sort") {
         ContextMenuSettings::setShowSortBy(visible);
+    } else if (id == "group") {
+        ContextMenuSettings::setShowGroupBy(visible);
     } else if (id == "view_mode") {
         ContextMenuSettings::setShowViewMode(visible);
     } else if (id == "open_in_new_tab") {
index 6e45d9bcdf80f4c9321992803f0ac5ebc5b1ae0a..ac7e8f9ae3a5e2a2ad8052a0bc544cea8605169d 100644 (file)
             <label>Show 'Sort By' in context menu.</label>
             <default>true</default>
         </entry>
+        <entry name="ShowGroupBy" type="Bool">
+            <label>Show 'Group By' in context menu.</label>
+            <default>true</default>
+        </entry>
         <entry name="ShowViewMode" type="Bool">
             <label>Show 'View Mode' in context menu.</label>
             <default>true</default>
index bae1f409fff62970168a5080a375f3364cbe557d..0044170a7e78f2f61cd01b6e4e23e9f8a32de6c1 100644 (file)
@@ -35,8 +35,8 @@
 
         <entry name="GroupedSorting" type="Bool" >
             <label context="@label">Grouped Sorting</label>
-            <whatsthis context="@info:whatsthis">When this option is enabled, the sorted items are categorized into groups.</whatsthis>
-            <default>false</default>
+            <whatsthis context="@info:whatsthis">When this option is enabled, the items are categorized into groups.</whatsthis>
+            <default>true</default>
         </entry>
 
         <entry name="SortRole" type="String" >
             <max code="true">Qt::DescendingOrder</max>
         </entry>
 
+        <entry name="GroupRole" type="String" >
+            <label context="@label">Group files by</label>
+            <whatsthis context="@info:whatsthis">This option defines which attribute (text, size, date, etc.) grouping is performed on.</whatsthis>
+            <default>none</default>
+        </entry>
+
+        <entry name="GroupOrder" type="Int" >
+            <label context="@label">Order in which to group files</label>
+            <default code="true">Qt::AscendingOrder</default>
+            <min code="true">Qt::AscendingOrder</min>
+            <max code="true">Qt::DescendingOrder</max>
+        </entry>
+
         <entry name="SortFoldersFirst" type="Bool" >
             <label context="@label">Show folders first when sorting files and folders</label>
             <default>true</default>
         </entry>
     </group>
 </kcfg>
-
-
index 6410d9655fe6b24360dd4ebc172378e83c703464..1c8178651abdbdee5ff7505e7cc8e38d6c6d3cd2 100644 (file)
@@ -58,6 +58,7 @@ DolphinSettingsDialog::DolphinSettingsDialog(const QUrl &url, QWidget *parent, K
                                                                actions,
                                                                {QStringLiteral("add_to_places"),
                                                                 QStringLiteral("sort"),
+                                                                QStringLiteral("group"),
                                                                 QStringLiteral("view_mode"),
                                                                 QStringLiteral("open_in_new_tab"),
                                                                 QStringLiteral("open_in_new_window"),
index 37c3d539b3bd305d6d980f76730a5f68ba6a4f22..a13b0b117bbee5b8f43f0d37e958b5f1ecc7d802 100644 (file)
@@ -44,6 +44,8 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView *dolphinView)
     , m_viewMode(nullptr)
     , m_sortOrder(nullptr)
     , m_sorting(nullptr)
+    , m_groupOrder(nullptr)
+    , m_grouping(nullptr)
     , m_sortFoldersFirst(nullptr)
     , m_sortHiddenLast(nullptr)
     , m_previewsShown(nullptr)
@@ -67,7 +69,7 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView *dolphinView)
     // Otherwise the dialog won't resize when we collapse the KCollapsibleGroupBox.
     layout->setSizeConstraint(QLayout::SetFixedSize);
 
-    // create 'Properties' group containing view mode, sorting, sort order and show hidden files
+    // create 'Properties' group containing view mode, sorting/grouping, sort/group order and show hidden files
     m_viewMode = new QComboBox();
     m_viewMode->addItem(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18nc("@item:inlistbox", "Icons"), DolphinView::IconsView);
     m_viewMode->addItem(QIcon::fromTheme(QStringLiteral("view-list-details")), i18nc("@item:inlistbox", "Compact"), DolphinView::CompactView);
@@ -83,6 +85,16 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView *dolphinView)
         m_sorting->addItem(info.translation, info.role);
     }
 
+    m_groupOrder = new QComboBox();
+    m_groupOrder->addItem(i18nc("@item:inlistbox Group", "Ascending"));
+    m_groupOrder->addItem(i18nc("@item:inlistbox Group", "Descending"));
+
+    m_grouping = new QComboBox();
+    const QList<KFileItemModel::RoleInfo> combinedGroupingInfo = rolesInfo + KFileItemModel::extraGroupingInformation();
+    for (const KFileItemModel::RoleInfo &info : combinedGroupingInfo) {
+        m_grouping->addItem(info.translation, info.role);
+    }
+
     m_sortFoldersFirst = new QCheckBox(i18nc("@option:check", "Show folders first"));
     m_sortHiddenLast = new QCheckBox(i18nc("@option:check", "Show hidden files last"));
     m_previewsShown = new QCheckBox(i18nc("@option:check", "Show preview"));
@@ -139,8 +151,14 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView *dolphinView)
     sortingLayout->addWidget(m_sortOrder);
     sortingLayout->addWidget(m_sorting);
 
+    QHBoxLayout *groupingLayout = new QHBoxLayout();
+    groupingLayout->setContentsMargins(0, 0, 0, 0);
+    groupingLayout->addWidget(m_groupOrder);
+    groupingLayout->addWidget(m_grouping);
+
     layout->addRow(i18nc("@label:listbox", "View mode:"), m_viewMode);
     layout->addRow(i18nc("@label:listbox", "Sorting:"), sortingLayout);
+    layout->addRow(i18nc("@label:listbox", "Grouping:"), groupingLayout);
 
     layout->addItem(new QSpacerItem(0, Dolphin::VERTICAL_SPACER_HEIGHT, QSizePolicy::Fixed, QSizePolicy::Fixed));
 
@@ -153,6 +171,8 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView *dolphinView)
     connect(m_viewMode, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotViewModeChanged);
     connect(m_sorting, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortingChanged);
     connect(m_sortOrder, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortOrderChanged);
+    connect(m_grouping, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotGroupingChanged);
+    connect(m_groupOrder, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotGroupOrderChanged);
     connect(m_sortFoldersFirst, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotSortFoldersFirstChanged);
     connect(m_sortHiddenLast, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotSortHiddenLastChanged);
     connect(m_previewsShown, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotShowPreviewChanged);
@@ -259,6 +279,20 @@ void ViewPropertiesDialog::slotSortOrderChanged(int index)
     markAsDirty(true);
 }
 
+void ViewPropertiesDialog::slotGroupingChanged(int index)
+{
+    const QByteArray role = m_grouping->itemData(index).toByteArray();
+    m_viewProps->setGroupRole(role);
+    markAsDirty(true);
+}
+
+void ViewPropertiesDialog::slotGroupOrderChanged(int index)
+{
+    const Qt::SortOrder groupOrder = (index == 0) ? Qt::AscendingOrder : Qt::DescendingOrder;
+    m_viewProps->setGroupOrder(groupOrder);
+    markAsDirty(true);
+}
+
 void ViewPropertiesDialog::slotGroupedSortingChanged()
 {
     m_viewProps->setGroupedSorting(m_showInGroups->isChecked());
@@ -377,6 +411,8 @@ void ViewPropertiesDialog::applyViewProperties()
     m_dolphinView->setViewMode(m_viewProps->viewMode());
     m_dolphinView->setSortRole(m_viewProps->sortRole());
     m_dolphinView->setSortOrder(m_viewProps->sortOrder());
+    m_dolphinView->setGroupRole(m_viewProps->groupRole());
+    m_dolphinView->setGroupOrder(m_viewProps->groupOrder());
     m_dolphinView->setSortFoldersFirst(m_viewProps->sortFoldersFirst());
     m_dolphinView->setSortHiddenLast(m_viewProps->sortHiddenLast());
     m_dolphinView->setGroupedSorting(m_viewProps->groupedSorting());
@@ -423,6 +459,20 @@ void ViewPropertiesDialog::loadSettings()
     m_sortFoldersFirst->setChecked(m_viewProps->sortFoldersFirst());
     m_sortHiddenLast->setChecked(m_viewProps->sortHiddenLast());
 
+    // Load group order and sorting
+    const int groupOrderIndex = (m_viewProps->groupOrder() == Qt::AscendingOrder) ? 0 : 1;
+    m_groupOrder->setCurrentIndex(groupOrderIndex);
+
+    const QList<KFileItemModel::RoleInfo> combinedGroupingInfo = rolesInfo + KFileItemModel::extraGroupingInformation();
+    int groupRoleIndex = 0;
+    for (int i = 0; i < combinedGroupingInfo.count(); ++i) {
+        if (combinedGroupingInfo[i].role == m_viewProps->groupRole()) {
+            groupRoleIndex = i;
+            break;
+        }
+    }
+    m_grouping->setCurrentIndex(groupRoleIndex);
+
     // Load show preview, show in groups and show hidden files settings
     m_previewsShown->setChecked(m_viewProps->previewsShown());
     m_showInGroups->setChecked(m_viewProps->groupedSorting());
index d1f056fbbfe562e05675b2e2a6bc2a1d421b3ebb..49536dcc7b48cd85d20b2bd4a7430ab36147007c 100644 (file)
@@ -44,6 +44,8 @@ private Q_SLOTS:
     void slotViewModeChanged(int index);
     void slotSortingChanged(int index);
     void slotSortOrderChanged(int index);
+    void slotGroupingChanged(int index);
+    void slotGroupOrderChanged(int index);
     void slotGroupedSortingChanged();
     void slotSortFoldersFirstChanged();
     void slotSortHiddenLastChanged();
@@ -67,6 +69,8 @@ private:
     QComboBox *m_viewMode;
     QComboBox *m_sortOrder;
     QComboBox *m_sorting;
+    QComboBox *m_groupOrder;
+    QComboBox *m_grouping;
     QCheckBox *m_sortFoldersFirst;
     QCheckBox *m_sortHiddenLast;
     QCheckBox *m_previewsShown;
index 4c6db590d8c78bfb40b1d42a6f8d7b9a1a24823c..34ae2ba8721c02479005e5e7011053a4dfbc4b63 100644 (file)
@@ -165,7 +165,7 @@ void KFileItemModelTest::testDefaultSortRole()
 
 void KFileItemModelTest::testDefaultGroupedSorting()
 {
-    QCOMPARE(m_model->groupedSorting(), false);
+    QCOMPARE(m_model->groupedSorting(), true);
 }
 
 void KFileItemModelTest::testNewItems()
@@ -2094,6 +2094,7 @@ void KFileItemModelTest::testNameRoleGroups()
     m_testDir->createFiles({"b.txt", "c.txt", "d.txt", "e.txt"});
 
     m_model->setGroupedSorting(true);
+    m_model->setGroupRole("text");
     m_model->loadDirectory(m_testDir->url());
     QVERIFY(itemsInsertedSpy.wait());
     QCOMPARE(itemsInModel(),
@@ -2180,6 +2181,8 @@ void KFileItemModelTest::testNameRoleGroupsWithExpandedItems()
     m_testDir->createFiles({"a/b.txt", "a/c.txt", "d/e.txt", "d/f.txt"});
 
     m_model->setGroupedSorting(true);
+    m_model->setGroupRole("text");
+
     m_model->loadDirectory(m_testDir->url());
     QVERIFY(itemsInsertedSpy.wait());
     QCOMPARE(itemsInModel(),
@@ -2189,6 +2192,7 @@ void KFileItemModelTest::testNameRoleGroupsWithExpandedItems()
     QList<QPair<int, QVariant>> expectedGroups;
     expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
     expectedGroups << QPair<int, QVariant>(1, QLatin1String("D"));
+
     QCOMPARE(m_model->groups(), expectedGroups);
 
     // Verify that expanding "a" and "d" will not change the groups (except for the index of "D").
index b702f5b13bf49d72c9789310fd48e77183bcd396..5c961b47b594c18d0710a3f408dd78668b5a7a64 100644 (file)
@@ -515,6 +515,42 @@ Qt::SortOrder DolphinView::sortOrder() const
     return m_model->sortOrder();
 }
 
+void DolphinView::setGroupRole(const QByteArray &role)
+{
+    if (role != groupRole()) {
+        ViewProperties props(viewPropertiesUrl());
+        props.setGroupRole(role);
+
+        KItemModelBase *model = m_container->controller()->model();
+        model->setGroupRole(role);
+
+        Q_EMIT groupRoleChanged(role);
+    }
+}
+
+QByteArray DolphinView::groupRole() const
+{
+    const KItemModelBase *model = m_container->controller()->model();
+    return model->groupRole();
+}
+
+void DolphinView::setGroupOrder(Qt::SortOrder order)
+{
+    if (groupOrder() != order) {
+        ViewProperties props(viewPropertiesUrl());
+        props.setGroupOrder(order);
+
+        m_model->setGroupOrder(order);
+
+        Q_EMIT groupOrderChanged(order);
+    }
+}
+
+Qt::SortOrder DolphinView::groupOrder() const
+{
+    return m_model->groupOrder();
+}
+
 void DolphinView::setSortFoldersFirst(bool foldersFirst)
 {
     if (sortFoldersFirst() != foldersFirst) {
@@ -2154,6 +2190,18 @@ void DolphinView::applyViewProperties(const ViewProperties &props)
         Q_EMIT sortOrderChanged(sortOrder);
     }
 
+    const QByteArray groupRole = props.groupRole();
+    if (groupRole != m_model->groupRole()) {
+        m_model->setGroupRole(groupRole);
+        Q_EMIT groupRoleChanged(groupRole);
+    }
+
+    const Qt::SortOrder groupOrder = props.groupOrder();
+    if (groupOrder != m_model->groupOrder()) {
+        m_model->setGroupOrder(groupOrder);
+        Q_EMIT groupOrderChanged(groupOrder);
+    }
+
     const bool sortFoldersFirst = props.sortFoldersFirst();
     if (sortFoldersFirst != m_model->sortDirectoriesFirst()) {
         m_model->setSortDirectoriesFirst(sortFoldersFirst);
index f491b6dd563ad9f79b07d27051d1eccdf6bb5d1f..d50c932df409b67cd7460732864879dfe4d0aa7d 100644 (file)
@@ -51,6 +51,8 @@ class QRegularExpression;
  * - show hidden files
  * - show previews
  * - enable grouping
+ * - grouping order
+ * - grouping type
  */
 class DOLPHIN_EXPORT DolphinView : public QWidget
 {
@@ -219,6 +221,20 @@ public:
     void setSortOrder(Qt::SortOrder order);
     Qt::SortOrder sortOrder() const;
 
+    /**
+     * Updates the view properties of the current URL to the
+     * grouping given by \a role.
+     */
+    void setGroupRole(const QByteArray &role);
+    QByteArray groupRole() const;
+
+    /**
+     * Updates the view properties of the current URL to the
+     * sort order given by \a order.
+     */
+    void setGroupOrder(Qt::SortOrder order);
+    Qt::SortOrder groupOrder() const;
+
     /** Sets a separate sorting with folders first (true) or a mixed sorting of files and folders (false). */
     void setSortFoldersFirst(bool foldersFirst);
     bool sortFoldersFirst() const;
@@ -534,6 +550,12 @@ Q_SIGNALS:
     /** Is emitted if the sort order (ascending or descending) has been changed. */
     void sortOrderChanged(Qt::SortOrder order);
 
+    /** Is emitted if the grouping by name, size or date has been changed. */
+    void groupRoleChanged(const QByteArray &role);
+
+    /** Is emitted if the group order (ascending or descending) has been changed. */
+    void groupOrderChanged(Qt::SortOrder order);
+
     /**
      * Is emitted if the sorting of files and folders (separate with folders
      * first or mixed) has been changed.
index e504fd831ab5effbf81c392be6fad2b825213f48..14600c133af2187647146d5022b7f7a122ff8281 100644 (file)
@@ -34,6 +34,7 @@ DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection *collection
     , m_actionCollection(collection)
     , m_currentView(nullptr)
     , m_sortByActions()
+    , m_groupByActions()
     , m_visibleRoles()
 {
     Q_ASSERT(m_actionCollection);
@@ -59,6 +60,8 @@ void DolphinViewActionHandler::setCurrentView(DolphinView *view)
     connect(view, &DolphinView::groupedSortingChanged, this, &DolphinViewActionHandler::slotGroupedSortingChanged);
     connect(view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewActionHandler::slotHiddenFilesShownChanged);
     connect(view, &DolphinView::sortRoleChanged, this, &DolphinViewActionHandler::slotSortRoleChanged);
+    connect(view, &DolphinView::groupRoleChanged, this, &DolphinViewActionHandler::slotGroupRoleChanged);
+    connect(view, &DolphinView::groupOrderChanged, this, &DolphinViewActionHandler::slotGroupOrderChanged);
     connect(view, &DolphinView::zoomLevelChanged, this, &DolphinViewActionHandler::slotZoomLevelChanged);
     connect(view, &DolphinView::writeStateChanged, this, &DolphinViewActionHandler::slotWriteStateChanged);
     slotWriteStateChanged(view->isFolderWritable());
@@ -290,27 +293,60 @@ void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *ac
 
     sortByActionMenu->addSeparator();
 
-    QActionGroup *group = new QActionGroup(sortByActionMenu);
-    group->setExclusive(true);
+    QActionGroup *groupForSort = new QActionGroup(sortByActionMenu);
+    groupForSort->setExclusive(true);
 
-    KToggleAction *ascendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("ascending"));
-    ascendingAction->setActionGroup(group);
-    connect(ascendingAction, &QAction::triggered, this, [this] {
+    KToggleAction *sortAscendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("sort_ascending"));
+    sortAscendingAction->setActionGroup(groupForSort);
+    connect(sortAscendingAction, &QAction::triggered, this, [this] {
         m_currentView->setSortOrder(Qt::AscendingOrder);
     });
 
-    KToggleAction *descendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("descending"));
-    descendingAction->setActionGroup(group);
-    connect(descendingAction, &QAction::triggered, this, [this] {
+    KToggleAction *sortDescendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("sort_descending"));
+    sortDescendingAction->setActionGroup(groupForSort);
+    connect(sortDescendingAction, &QAction::triggered, this, [this] {
         m_currentView->setSortOrder(Qt::DescendingOrder);
     });
 
-    sortByActionMenu->addAction(ascendingAction);
-    sortByActionMenu->addAction(descendingAction);
+    sortByActionMenu->addAction(sortAscendingAction);
+    sortByActionMenu->addAction(sortDescendingAction);
     sortByActionMenu->addSeparator();
     sortByActionMenu->addAction(sortFoldersFirst);
     sortByActionMenu->addAction(sortHiddenLast);
 
+    // View -> Group By
+    QActionGroup *groupByActionGroup = createFileItemRolesActionGroup(QStringLiteral("group_by_"));
+
+    KActionMenu *groupByActionMenu = m_actionCollection->add<KActionMenu>(QStringLiteral("group"));
+    groupByActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-group")));
+    groupByActionMenu->setText(i18nc("@action:inmenu View", "Group By"));
+    groupByActionMenu->setPopupMode(QToolButton::InstantPopup);
+
+    const auto groupByActionGroupActions = groupByActionGroup->actions();
+    for (QAction *action : groupByActionGroupActions) {
+        groupByActionMenu->addAction(action);
+    }
+
+    groupByActionMenu->addSeparator();
+
+    QActionGroup *groupForGroup = new QActionGroup(groupByActionMenu);
+    groupForGroup->setExclusive(true);
+
+    KToggleAction *groupAscendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("group_ascending"));
+    groupAscendingAction->setActionGroup(groupForGroup);
+    connect(groupAscendingAction, &QAction::triggered, this, [this] {
+        m_currentView->setGroupOrder(Qt::AscendingOrder);
+    });
+
+    KToggleAction *groupDescendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("group_descending"));
+    groupDescendingAction->setActionGroup(groupForGroup);
+    connect(groupDescendingAction, &QAction::triggered, this, [this] {
+        m_currentView->setGroupOrder(Qt::DescendingOrder);
+    });
+
+    groupByActionMenu->addAction(groupAscendingAction);
+    groupByActionMenu->addAction(groupDescendingAction);
+
     // View -> Additional Information
     QActionGroup *visibleRolesGroup = createFileItemRolesActionGroup(QStringLiteral("show_"));
 
@@ -379,12 +415,15 @@ void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *ac
 QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QString &groupPrefix)
 {
     const bool isSortGroup = (groupPrefix == QLatin1String("sort_by_"));
-    Q_ASSERT(isSortGroup || groupPrefix == QLatin1String("show_"));
+    const bool isGroupGroup = (groupPrefix == QLatin1String("group_by_"));
+    Q_ASSERT(isSortGroup || isGroupGroup || groupPrefix == QLatin1String("show_"));
 
     QActionGroup *rolesActionGroup = new QActionGroup(m_actionCollection);
-    rolesActionGroup->setExclusive(isSortGroup);
+    rolesActionGroup->setExclusive(isSortGroup || isGroupGroup);
     if (isSortGroup) {
         connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered);
+    } else if (isGroupGroup) {
+        connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered);
     } else {
         connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole);
     }
@@ -399,9 +438,13 @@ QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt
     indexingEnabled = config.fileIndexingEnabled();
 #endif
 
-    const QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
+    QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
+    if (isGroupGroup) {
+        rolesInfo += KFileItemModel::extraGroupingInformation();
+    }
+
     for (const KFileItemModel::RoleInfo &info : rolesInfo) {
-        if (!isSortGroup && info.role == "text") {
+        if (!isSortGroup && !isGroupGroup && info.role == "text") {
             // It should not be possible to hide the "text" role
             continue;
         }
@@ -419,9 +462,11 @@ QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt
                 groupMenu->setActionGroup(rolesActionGroup);
 
                 groupMenuGroup = new QActionGroup(groupMenu);
-                groupMenuGroup->setExclusive(isSortGroup);
+                groupMenuGroup->setExclusive(isSortGroup || isGroupGroup);
                 if (isSortGroup) {
                     connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered);
+                } else if (isGroupGroup) {
+                    connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered);
                 } else {
                     connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole);
                 }
@@ -439,6 +484,8 @@ QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt
 
         if (isSortGroup) {
             m_sortByActions.insert(info.role, action);
+        } else if (isGroupGroup) {
+            m_groupByActions.insert(info.role, action);
         } else {
             m_visibleRoles.insert(info.role, action);
         }
@@ -549,6 +596,8 @@ void DolphinViewActionHandler::updateViewActions()
     slotVisibleRolesChanged(m_currentView->visibleRoles(), QList<QByteArray>());
     slotGroupedSortingChanged(m_currentView->groupedSorting());
     slotSortRoleChanged(m_currentView->sortRole());
+    slotGroupRoleChanged(m_currentView->groupRole());
+    slotGroupOrderChanged(m_currentView->groupOrder());
     slotZoomLevelChanged(m_currentView->zoomLevel(), -1);
 
     // Updates the "show_hidden_files" action state and icon
@@ -589,13 +638,22 @@ void DolphinViewActionHandler::toggleSortHiddenLast()
 
 void DolphinViewActionHandler::slotSortOrderChanged(Qt::SortOrder order)
 {
-    QAction *descending = m_actionCollection->action(QStringLiteral("descending"));
-    QAction *ascending = m_actionCollection->action(QStringLiteral("ascending"));
+    QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending"));
+    QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending"));
     const bool sortDescending = (order == Qt::DescendingOrder);
     descending->setChecked(sortDescending);
     ascending->setChecked(!sortDescending);
 }
 
+void DolphinViewActionHandler::slotGroupOrderChanged(Qt::SortOrder order)
+{
+    QAction *descending = m_actionCollection->action(QStringLiteral("group_descending"));
+    QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending"));
+    const bool groupDescending = (order == Qt::DescendingOrder);
+    descending->setChecked(groupDescending);
+    ascending->setChecked(!groupDescending);
+}
+
 void DolphinViewActionHandler::slotSortFoldersFirstChanged(bool foldersFirst)
 {
     m_actionCollection->action(QStringLiteral("folders_first"))->setChecked(foldersFirst);
@@ -717,8 +775,8 @@ void DolphinViewActionHandler::slotSortRoleChanged(const QByteArray &role)
         }
     }
 
-    QAction *descending = m_actionCollection->action(QStringLiteral("descending"));
-    QAction *ascending = m_actionCollection->action(QStringLiteral("ascending"));
+    QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending"));
+    QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending"));
 
     if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") {
         descending->setText(i18nc("Sort descending", "Z-A"));
@@ -740,6 +798,50 @@ void DolphinViewActionHandler::slotSortRoleChanged(const QByteArray &role)
     slotSortOrderChanged(m_currentView->sortOrder());
 }
 
+void DolphinViewActionHandler::slotGroupRoleChanged(const QByteArray &role)
+{
+    KToggleAction *action = m_groupByActions.value(role);
+    if (action) {
+        action->setChecked(true);
+
+        if (!action->icon().isNull()) {
+            QAction *groupByMenu = m_actionCollection->action(QStringLiteral("group"));
+            groupByMenu->setIcon(action->icon());
+        }
+    }
+
+    QAction *descending = m_actionCollection->action(QStringLiteral("group_descending"));
+    QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending"));
+
+    if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") {
+        descending->setText(i18nc("Group descending", "Z-A"));
+        ascending->setText(i18nc("Group ascending", "A-Z"));
+    } else if (role == "size") {
+        descending->setText(i18nc("Group descending", "Largest First"));
+        ascending->setText(i18nc("Group ascending", "Smallest First"));
+    } else if (role == "modificationtime" || role == "creationtime" || role == "accesstime") {
+        descending->setText(i18nc("Group descending", "Newest First"));
+        ascending->setText(i18nc("Group ascending", "Oldest First"));
+    } else if (role == "rating") {
+        descending->setText(i18nc("Group descending", "Highest First"));
+        ascending->setText(i18nc("Group ascending", "Lowest First"));
+    } else {
+        descending->setText(i18nc("Group descending", "Descending"));
+        ascending->setText(i18nc("Group ascending", "Ascending"));
+    }
+
+    // Disable group order selector if grouping behavior does not support it
+    if (role == "none" || role == "followSort") {
+        descending->setEnabled(false);
+        ascending->setEnabled(false);
+    } else {
+        descending->setEnabled(true);
+        ascending->setEnabled(true);
+    }
+
+    slotGroupOrderChanged(m_currentView->groupOrder());
+}
+
 void DolphinViewActionHandler::slotZoomLevelChanged(int current, int previous)
 {
     Q_UNUSED(previous)
@@ -781,6 +883,32 @@ void DolphinViewActionHandler::slotSortTriggered(QAction *action)
     m_currentView->setSortRole(role);
 }
 
+void DolphinViewActionHandler::slotGroupTriggered(QAction *action)
+{
+    // The radiobuttons of the "Group By"-menu are split between the main-menu
+    // and several sub-menus. Because of this they don't have a common
+    // action-group that assures an exclusive toggle-state between the main-menu
+    // actions and the sub-menu-actions. If an action gets checked, it must
+    // be assured that all other actions get unchecked, except the ascending/
+    // descending actions
+    for (QAction *groupAction : std::as_const(m_groupByActions)) {
+        KActionMenu *actionMenu = qobject_cast<KActionMenu *>(groupAction);
+        if (actionMenu) {
+            const auto actions = actionMenu->menu()->actions();
+            for (QAction *subAction : actions) {
+                subAction->setChecked(false);
+            }
+        } else if (groupAction->actionGroup()) {
+            groupAction->setChecked(false);
+        }
+    }
+    action->setChecked(true);
+
+    // Apply the activated sort-role to the view
+    const QByteArray role = action->data().toByteArray();
+    m_currentView->setGroupRole(role);
+}
+
 void DolphinViewActionHandler::slotAdjustViewProperties()
 {
     Q_EMIT actionBeingHandled();
index 20d62cf64a0e61601495734f354315d9b222e10a..a589be9eaac7eb8e41efd7bd20b49fe75552a1b4 100644 (file)
@@ -165,6 +165,16 @@ private Q_SLOTS:
      */
     void slotSortRoleChanged(const QByteArray &role);
 
+    /**
+     * Updates the state of the 'Group Ascending/Descending' action.
+     */
+    void slotGroupOrderChanged(Qt::SortOrder order);
+
+    /**
+     * Updates the state of the 'Group by' actions.
+     */
+    void slotGroupRoleChanged(const QByteArray &role);
+
     /**
      * Updates the state of the 'Zoom In' and 'Zoom Out' actions.
      */
@@ -186,6 +196,11 @@ private Q_SLOTS:
      */
     void slotVisibleRolesChanged(const QList<QByteArray> &current, const QList<QByteArray> &previous);
 
+    /**
+     * Changes the grouping of the current view.
+     */
+    void slotGroupTriggered(QAction *);
+    
     /**
      * Switches between sorting by groups or not.
      */
@@ -281,6 +296,7 @@ private:
     DolphinView *m_currentView;
 
     QHash<QByteArray, KToggleAction *> m_sortByActions;
+    QHash<QByteArray, KToggleAction *> m_groupByActions;
     QHash<QByteArray, KToggleAction *> m_visibleRoles;
 };
 
index 8bf3b2531290a68d8be66928e310ada3820e5575..9c03cf598087b203d3882dbc3fea305b301c365c 100644 (file)
@@ -330,6 +330,32 @@ Qt::SortOrder ViewProperties::sortOrder() const
     return static_cast<Qt::SortOrder>(m_node->sortOrder());
 }
 
+void ViewProperties::setGroupRole(const QByteArray &role)
+{
+    if (m_node->groupRole() != role) {
+        m_node->setGroupRole(role);
+        update();
+    }
+}
+
+QByteArray ViewProperties::groupRole() const
+{
+    return m_node->groupRole().toLatin1();
+}
+
+void ViewProperties::setGroupOrder(Qt::SortOrder groupOrder)
+{
+    if (m_node->groupOrder() != groupOrder) {
+        m_node->setGroupOrder(groupOrder);
+        update();
+    }
+}
+
+Qt::SortOrder ViewProperties::groupOrder() const
+{
+    return static_cast<Qt::SortOrder>(m_node->groupOrder());
+}
+
 void ViewProperties::setSortFoldersFirst(bool foldersFirst)
 {
     if (m_node->sortFoldersFirst() != foldersFirst) {
@@ -474,6 +500,8 @@ void ViewProperties::setDirProperties(const ViewProperties &props)
     setGroupedSorting(props.groupedSorting());
     setSortRole(props.sortRole());
     setSortOrder(props.sortOrder());
+    setGroupRole(props.groupRole());
+    setGroupOrder(props.groupOrder());
     setSortFoldersFirst(props.sortFoldersFirst());
     setSortHiddenLast(props.sortHiddenLast());
     setVisibleRoles(props.visibleRoles());
index bee1e7330f3bf00161c6371d05829a76d17039a8..ac9ee4bb6082917a72215069a951a45c4c5806d5 100644 (file)
@@ -59,6 +59,12 @@ public:
     void setSortOrder(Qt::SortOrder sortOrder);
     Qt::SortOrder sortOrder() const;
 
+    void setGroupRole(const QByteArray &role);
+    QByteArray groupRole() const;
+
+    void setGroupOrder(Qt::SortOrder groupOrder);
+    Qt::SortOrder groupOrder() const;
+
     void setSortFoldersFirst(bool foldersFirst);
     bool sortFoldersFirst() const;