From 989c0f4f0319f7d8e573c8a88fe8011bd3315466 Mon Sep 17 00:00:00 2001 From: Ramil Nurmanov Date: Fri, 5 Jun 2026 00:06:12 +0400 Subject: Allow grouping by a separate criterion Add an explicit group role to Dolphin views so items can be grouped independently from the active sort role. Previously, Dolphin's grouped view always used the current sort role as the grouping criterion. This meant that grouping and sorting were tied together, for example grouping by type also required sorting by type. This change adds a separate Group By menu. It replaces the previous Show in Groups toggle and allows the user to disable grouping, keep the old Same as Sort behavior, or explicitly group by a selected criterion. When an explicit group role is set, the model keeps groups stable and uses the normal sort role as a secondary key inside each group. The roles updater also receives the active group role, so roles used only for grouping are still updated correctly. BUG: 416134 CCBUG: 510670 CCBUG: 513235 CCBUG: 46393 --- src/kitemviews/kfileitemmodel.cpp | 78 +++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 11 deletions(-) (limited to 'src/kitemviews/kfileitemmodel.cpp') diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index ba33254c0..f4b51ce72 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -639,7 +639,7 @@ QList> KFileItemModel::groups() const QElapsedTimer timer; timer.start(); #endif - switch (typeForRole(sortRole())) { + switch (typeForRole(groupRole())) { case NameRole: m_groups = nameRoleGroups(); break; @@ -673,7 +673,7 @@ QList> KFileItemModel::groups() const m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(groupRole()); break; } @@ -1232,6 +1232,24 @@ void KFileItemModel::onGroupedSortingChanged(bool current) m_groups.clear(); } +void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) +{ + Q_UNUSED(previous) + const RoleType roleType = typeForRole(current); + + if (!m_requestRole[roleType]) { + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); + } + + m_groups.clear(); + + if (resortItems) { + resortAllItems(); + } +} + void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) { Q_UNUSED(previous) @@ -1927,7 +1945,7 @@ void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBe QList KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const { - if (m_sortRole == TypeRole) { + if (m_sortRole == TypeRole || typeForRole(rawGroupRole()) == 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). @@ -1991,6 +2009,17 @@ void KFileItemModel::prepareItemsForSorting(QList &itemDataList) // DateRole). break; } + + if (typeForRole(rawGroupRole()) == TypeRole && m_sortRole != TypeRole) { + for (ItemData *itemData : std::as_const(itemDataList)) { + if (itemData->values.isEmpty()) { + const KFileItem &item = itemData->item; + if (item.isDir() || item.isMimeTypeKnown()) { + itemData->values = retrieveData(itemData->item, itemData->parent); + } + } + } + } } int KFileItemModel::expandedParentsCount(const ItemData *data) @@ -2046,7 +2075,8 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i // Trigger a resorting if necessary. Note that this can happen even if the sort // role has not changed at all because the file name can be used as a fallback. if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole)) - || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting. + || (changedRoles.contains("count") && sortRole() == "size") // "count" is used in the "size" sort role, so this might require a resorting. + || (groupedSorting() && !rawGroupRole().isEmpty() && changedRoles.contains(groupRole()))) { for (const KItemRange &range : itemRanges) { bool needsResorting = false; @@ -2077,7 +2107,7 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i } } - if (groupedSorting() && changedRoles.contains(sortRole())) { + if (groupedSorting() && (changedRoles.contains(sortRole()) || (!rawGroupRole().isEmpty() && changedRoles.contains(groupRole())))) { // The position is still correct, but the groups might have changed // if the changed item is either the first or the last item in a // group. @@ -2307,6 +2337,20 @@ QHash KFileItemModel::retrieveData(const KFileItem &item, return data; } +QString KFileItemModel::groupKeyForItem(const ItemData *item, const QByteArray &role) const +{ + switch (typeForRole(role)) { + case ModificationTimeRole: + case CreationTimeRole: + case AccessTimeRole: + return QDateTime::fromSecsSinceEpoch(item->values.value(role).toLongLong()).date().toString(Qt::ISODate); + case DeletionTimeRole: + return item->values.value("deletiontime").toDateTime().date().toString(Qt::ISODate); + default: + return item->values.value(role).toString(); + } +} + bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const { int result = 0; @@ -2364,6 +2408,17 @@ bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QColla } } + if (groupedSorting() && !rawGroupRole().isEmpty() && groupRole() != sortRole()) { + const QByteArray gRole = groupRole(); + const QString groupValueA = groupKeyForItem(a, gRole); + const QString groupValueB = groupKeyForItem(b, gRole); + const int groupResult = stringCompare(groupValueA, groupValueB, collator); + if (groupResult != 0) { + // Groups are always in ascending order regardless of the sort order setting. + return groupResult < 0; + } + } + result = sortRoleCompare(a, b, collator); return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; @@ -2375,15 +2430,16 @@ void KFileItemModel::sort(const QList::iterator &beg return lessThan(a, b, m_collator); }; - if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) { - // Sorting by string can be expensive, in particular if natural sorting is - // enabled. Use all CPU cores to speed up the sorting process. + const QByteArray rawRole = rawGroupRole(); + const bool groupKeyIsString = !rawRole.isEmpty() && groupRole() != sortRole() + && isRoleValueNatural(typeForRole(rawRole)); + // String-based sorts benefit from parallelism; non-string sorts use one thread + // to avoid non-reentrant comparison functions (bug 312679). + const bool primaryKeyIsString = m_sortRole == NameRole || isRoleValueNatural(m_sortRole) || groupKeyIsString; + if (primaryKeyIsString) { static const int numberOfThreads = QThread::idealThreadCount(); parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads); } else { - // Sorting by other roles is quite fast. Use only one thread to prevent - // problems caused by non-reentrant comparison functions, see - // https://bugs.kde.org/show_bug.cgi?id=312679 mergeSort(begin, end, lambdaLessThan); } } -- cgit v1.3.1