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/kfileitemlistview.cpp | 14 +++++ src/kitemviews/kfileitemlistview.h | 1 + src/kitemviews/kfileitemmodel.cpp | 78 +++++++++++++++++++++++---- src/kitemviews/kfileitemmodel.h | 3 ++ src/kitemviews/kfileitemmodelrolesupdater.cpp | 8 +++ src/kitemviews/kitemlistview.cpp | 18 +++++-- src/kitemviews/kitemlistview.h | 1 + src/kitemviews/kitemmodelbase.cpp | 41 ++++++++++++-- src/kitemviews/kitemmodelbase.h | 9 ++++ 9 files changed, 156 insertions(+), 17 deletions(-) (limited to 'src/kitemviews') diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp index 3a02d537d..585a972a6 100644 --- a/src/kitemviews/kfileitemlistview.cpp +++ b/src/kitemviews/kfileitemlistview.cpp @@ -354,6 +354,15 @@ void KFileItemListView::slotSortRoleChanged(const QByteArray ¤t, const QBy KStandardItemListView::slotSortRoleChanged(current, previous); } +void KFileItemListView::slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous) +{ + if (!visibleRoles().contains(current)) { + applyRolesToModel(); + } + + KItemListView::slotGroupRoleChanged(current, previous); +} + void KFileItemListView::triggerVisibleIndexRangeUpdate() { if (!model()) { @@ -441,6 +450,11 @@ void KFileItemListView::applyRolesToModel() // Assure that the role that is used for sorting will be determined roles.insert(fileItemModel->sortRole()); + const QByteArray rawGroupRole = fileItemModel->rawGroupRole(); + if (!rawGroupRole.isEmpty()) { + roles.insert(rawGroupRole); + } + fileItemModel->setRoles(roles); m_modelRolesUpdater->setRoles(roles); } diff --git a/src/kitemviews/kfileitemlistview.h b/src/kitemviews/kfileitemlistview.h index eae00f1f0..203daca09 100644 --- a/src/kitemviews/kfileitemlistview.h +++ b/src/kitemviews/kfileitemlistview.h @@ -93,6 +93,7 @@ protected: protected Q_SLOTS: void slotItemsRemoved(const KItemRangeList &itemRanges) override; void slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous) override; + void slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous) override; private Q_SLOTS: void triggerVisibleIndexRangeUpdate(); 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); } } diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index 2a6e710c3..0ac5d9261 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -293,6 +293,7 @@ protected: void onGroupedSortingChanged(bool current) override; void onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override; void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override; + void onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override; private Q_SLOTS: /** @@ -447,6 +448,8 @@ private: */ bool lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const; + QString groupKeyForItem(const ItemData *item, const QByteArray &role) const; + /** * Sorts the items between \a begin and \a end using the comparison * function lessThan(). diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 38fa58436..33cf6341c 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -281,6 +281,14 @@ void KFileItemModelRolesUpdater::setPaused(bool paused) void KFileItemModelRolesUpdater::setRoles(const QSet &roles) { if (m_roles != roles) { + const QSet addedRoles = roles - m_roles; + for (const QByteArray &role : addedRoles) { + if (m_resolvableRoles.contains(role)) { + m_finishedItems.clear(); + break; + } + } + m_roles = roles; #if HAVE_BALOO diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index b87216d4b..d24c2426d 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -1477,8 +1477,8 @@ void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSe } } - if (m_grouped && roles.contains(m_model->sortRole())) { - // The sort-role has been changed which might result + if (m_grouped && roles.contains(m_model->groupRole())) { + // The group-role data has been changed which might result // in modified group headers updateVisibleGroupHeaders(); doLayout(NoAnimation); @@ -1543,6 +1543,16 @@ void KItemListView::slotSortRoleChanged(const QByteArray ¤t, const QByteAr } } +void KItemListView::slotGroupRoleChanged(const QByteArray ¤t, 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 @@ -1826,6 +1836,7 @@ 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::groupRoleChanged, this, &KItemListView::slotGroupRoleChanged); m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count())); } @@ -1843,6 +1854,7 @@ void KItemListView::setModel(KItemModelBase *model) connect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); + connect(m_model, &KItemModelBase::groupRoleChanged, this, &KItemListView::slotGroupRoleChanged); const int itemCount = m_model->count(); if (itemCount > 0) { @@ -2251,7 +2263,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); diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 8dc83d528..20c34ceea 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -441,6 +441,7 @@ protected Q_SLOTS: virtual void slotGroupedSortingChanged(bool current); virtual void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); virtual void slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous); + virtual void slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous); virtual void slotCurrentChanged(int current, int previous); virtual void slotSelectionChanged(const KItemSet ¤t, const KItemSet &previous); diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp index 82e491220..a4f7b0890 100644 --- a/src/kitemviews/kitemmodelbase.cpp +++ b/src/kitemviews/kitemmodelbase.cpp @@ -50,13 +50,41 @@ bool KItemModelBase::groupedSorting() const void KItemModelBase::setSortRole(const QByteArray &role, bool resortItems) { if (role != m_sortRole) { - const QByteArray previous = m_sortRole; + const QByteArray previousSortRole = m_sortRole; m_sortRole = role; - onSortRoleChanged(role, previous, resortItems); - Q_EMIT sortRoleChanged(role, previous); + onSortRoleChanged(role, previousSortRole, resortItems); + Q_EMIT sortRoleChanged(role, previousSortRole); + + if (m_groupRole.isEmpty()) { + onGroupRoleChanged(role, previousSortRole, false); + Q_EMIT groupRoleChanged(role, previousSortRole); + } + } +} + +void KItemModelBase::setGroupRole(const QByteArray &role) +{ + if (role != m_groupRole) { + const QByteArray previousEffective = groupRole(); + m_groupRole = role; + const QByteArray currentEffective = groupRole(); + if (currentEffective != previousEffective) { + onGroupRoleChanged(currentEffective, previousEffective); + Q_EMIT groupRoleChanged(currentEffective, previousEffective); + } } } +QByteArray KItemModelBase::groupRole() const +{ + return m_groupRole.isEmpty() ? m_sortRole : m_groupRole; +} + +QByteArray KItemModelBase::rawGroupRole() const +{ + return m_groupRole; +} + QByteArray KItemModelBase::sortRole() const { return m_sortRole; @@ -149,6 +177,13 @@ void KItemModelBase::onSortRoleChanged(const QByteArray ¤t, const QByteArr Q_UNUSED(resortItems) } +void KItemModelBase::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) +{ + Q_UNUSED(current) + Q_UNUSED(previous) + Q_UNUSED(resortItems) +} + void KItemModelBase::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { Q_UNUSED(current) diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h index 42a9c54c9..68d1d5d8a 100644 --- a/src/kitemviews/kitemmodelbase.h +++ b/src/kitemviews/kitemmodelbase.h @@ -76,6 +76,10 @@ public: void setSortRole(const QByteArray &role, bool resortItems = true); QByteArray sortRole() const; + void setGroupRole(const QByteArray &role); + QByteArray groupRole() const; + QByteArray rawGroupRole() const; + /** * Sets the sort order to \a order. The method KItemModelBase::onSortOrderChanged() will be * called so that model-implementations can react on the sort order change. Afterwards the @@ -248,6 +252,8 @@ Q_SIGNALS: void sortRoleChanged(const QByteArray ¤t, const QByteArray &previous); void sortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); + void groupRoleChanged(const QByteArray ¤t, const QByteArray &previous); + protected: /** * Is invoked if the grouped sorting has been changed by KItemModelBase::setGroupedSorting(). Allows @@ -266,6 +272,8 @@ protected: */ virtual void onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true); + virtual void onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = 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. @@ -279,6 +287,7 @@ protected: private: bool m_groupedSorting; QByteArray m_sortRole; + QByteArray m_groupRole; Qt::SortOrder m_sortOrder; }; -- cgit v1.3.1