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/views/dolphinview.cpp | 33 ++++++++++ src/views/dolphinview.h | 10 ++++ src/views/dolphinviewactionhandler.cpp | 106 ++++++++++++++++++++++++++++----- src/views/dolphinviewactionhandler.h | 22 ++++--- src/views/viewproperties.cpp | 14 +++++ src/views/viewproperties.h | 3 + 6 files changed, 160 insertions(+), 28 deletions(-) (limited to 'src/views') diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index 6882df5d3..e14227198 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -549,6 +549,33 @@ QByteArray DolphinView::sortRole() const return model->sortRole(); } +void DolphinView::setGroupRole(const QByteArray &role) +{ + KItemModelBase *model = m_container->controller()->model(); + if (role == model->rawGroupRole()) { + return; + } + + ViewProperties props(viewPropertiesUrl()); + props.setGroupRole(role); + + model->setGroupRole(role); + + Q_EMIT groupRoleChanged(role); +} + +QByteArray DolphinView::groupRole() const +{ + const KItemModelBase *model = m_container->controller()->model(); + return model->groupRole(); +} + +QByteArray DolphinView::rawGroupRole() const +{ + const KItemModelBase *model = m_container->controller()->model(); + return model->rawGroupRole(); +} + void DolphinView::setSortOrder(Qt::SortOrder order) { if (sortOrder() != order) { @@ -2405,6 +2432,12 @@ void DolphinView::applyViewProperties(const ViewProperties &props) Q_EMIT sortRoleChanged(sortRole); } + const QByteArray groupRole = props.groupRole(); + if (groupRole != m_model->rawGroupRole()) { + m_model->setGroupRole(groupRole); + Q_EMIT groupRoleChanged(groupRole); + } + const Qt::SortOrder sortOrder = props.sortOrder(); if (sortOrder != m_model->sortOrder()) { m_model->setSortOrder(sortOrder); diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index 35276f9ac..b0ab6ab6a 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -218,6 +218,10 @@ public: void setSortRole(const QByteArray &role); QByteArray sortRole() const; + void setGroupRole(const QByteArray &role); + QByteArray groupRole() const; + QByteArray rawGroupRole() const; + /** * Updates the view properties of the current URL to the * sort order given by \a order. @@ -578,6 +582,12 @@ Q_SIGNALS: /** Is emitted if the sorting by name, size or date has been changed. */ void sortRoleChanged(const QByteArray &role); + /** + * Is emitted when the explicit group role changes. Passes the raw role — + * an empty byte array means grouping follows the sort role (see rawGroupRole()). + */ + void groupRoleChanged(const QByteArray &role); + /** Is emitted if the sort order (ascending or descending) has been changed. */ void sortOrderChanged(Qt::SortOrder order); diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index 7317ca79c..2aed32d8e 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -59,6 +59,7 @@ void DolphinViewActionHandler::setCurrentView(DolphinView *view) connect(view, &DolphinView::sortHiddenLastChanged, this, &DolphinViewActionHandler::slotSortHiddenLastChanged); connect(view, &DolphinView::visibleRolesChanged, this, &DolphinViewActionHandler::slotVisibleRolesChanged); connect(view, &DolphinView::groupedSortingChanged, this, &DolphinViewActionHandler::slotGroupedSortingChanged); + connect(view, &DolphinView::groupRoleChanged, this, &DolphinViewActionHandler::slotGroupRoleChanged); connect(view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewActionHandler::slotHiddenFilesShownChanged); connect(view, &DolphinView::sortRoleChanged, this, &DolphinViewActionHandler::slotSortRoleChanged); connect(view, &DolphinView::zoomLevelChanged, this, &DolphinViewActionHandler::slotZoomLevelChanged); @@ -327,11 +328,40 @@ void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *ac visibleRolesMenu->addAction(action); } - KToggleAction *showInGroups = m_actionCollection->add(QStringLiteral("show_in_groups")); - showInGroups->setIcon(QIcon::fromTheme(QStringLiteral("view-group"))); - showInGroups->setText(i18nc("@action:inmenu View", "Show in Groups")); - showInGroups->setWhatsThis(i18nc("@info:whatsthis", "This groups files and folders by their first letter.")); - connect(showInGroups, &KToggleAction::triggered, this, &DolphinViewActionHandler::toggleGroupedSorting); + KActionMenu *groupByActionMenu = m_actionCollection->add(QStringLiteral("group_by")); + groupByActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-group"))); + groupByActionMenu->setText(i18nc("@action:inmenu View", "Group By")); + groupByActionMenu->setWhatsThis(i18nc("@info:whatsthis", + "This groups files and folders by a chosen attribute. " + "\"None\" disables grouping. " + "\"Same as Sort\" groups by the active sort criterion.")); + groupByActionMenu->setPopupMode(QToolButton::InstantPopup); + + QActionGroup *groupByActionGroup = new QActionGroup(m_actionCollection); + groupByActionGroup->setExclusive(true); + connect(groupByActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupByTriggered); + + KToggleAction *groupByNone = m_actionCollection->add(QStringLiteral("group_by_none")); + groupByNone->setActionGroup(groupByActionGroup); + groupByNone->setText(i18nc("@action:inmenu Group By", "None")); + groupByNone->setData(QByteArray()); + m_groupByActions.insert(QByteArray(), groupByNone); + groupByActionMenu->addAction(groupByNone); + + KToggleAction *groupBySameAsSort = m_actionCollection->add(QStringLiteral("group_by_same_as_sort")); + groupBySameAsSort->setActionGroup(groupByActionGroup); + groupBySameAsSort->setText(i18nc("@action:inmenu Group By", "Same as Sort")); + groupBySameAsSort->setData(QByteArray("same_as_sort")); + m_groupByActions.insert(QByteArray("same_as_sort"), groupBySameAsSort); + groupByActionMenu->addAction(groupBySameAsSort); + + groupByActionMenu->addSeparator(); + + QActionGroup *groupRolesGroup = createFileItemRolesActionGroup(QStringLiteral("group_")); + const auto groupRolesGroupActions = groupRolesGroup->actions(); + for (QAction *action : groupRolesGroupActions) { + groupByActionMenu->addAction(action); + } KToggleAction *showHiddenFiles = m_actionCollection->add(QStringLiteral("show_hidden_files")); showHiddenFiles->setIcon(QIcon::fromTheme(QStringLiteral("view-visible"))); @@ -370,9 +400,9 @@ void DolphinViewActionHandler::createActions(SelectionMode::ActionTextHelper *ac viewSettings->addSeparator(); viewSettings->addAction(zoomWidgetAction); viewSettings->addAction(sortByActionMenu); + viewSettings->addAction(groupByActionMenu); viewSettings->addAction(visibleRolesMenu); viewSettings->addAction(showPreview); - viewSettings->addAction(showInGroups); viewSettings->addAction(showHiddenFiles); viewSettings->addAction(adjustViewProps); viewSettings->setPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup); @@ -384,14 +414,18 @@ 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_")); + Q_ASSERT(isSortGroup || isGroupGroup || groupPrefix == QLatin1String("show_")); QActionGroup *rolesActionGroup = new QActionGroup(m_actionCollection); - rolesActionGroup->setExclusive(isSortGroup); + rolesActionGroup->setExclusive(isSortGroup || isGroupGroup); KActionCategory *category; if (isSortGroup) { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered); category = new KActionCategory(i18nc("@item:intable, Heading of a list of fields", "Sort by Field"), m_actionCollection); + } else if (isGroupGroup) { + connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupByTriggered); + category = new KActionCategory(i18nc("@item:intable, Heading of a list of fields", "Group by Field"), m_actionCollection); } else { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); category = new KActionCategory(i18nc("@item:intable, Heading of a list of fields", "Show Field"), m_actionCollection); @@ -409,7 +443,7 @@ QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt const QList rolesInfo = KFileItemModel::rolesInformation(); 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; } @@ -427,9 +461,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::slotGroupByTriggered); } else { connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); } @@ -447,6 +483,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); } @@ -555,7 +593,7 @@ void DolphinViewActionHandler::updateViewActions() slotSortFoldersFirstChanged(m_currentView->sortFoldersFirst()); slotSortHiddenLastChanged(m_currentView->sortHiddenLast()); slotVisibleRolesChanged(m_currentView->visibleRoles(), QList()); - slotGroupedSortingChanged(m_currentView->groupedSorting()); + updateGroupByActions(); slotSortRoleChanged(m_currentView->sortRole()); slotZoomLevelChanged(m_currentView->zoomLevel(), -1); @@ -652,15 +690,51 @@ void DolphinViewActionHandler::slotVisibleRolesChanged(const QList & } } -void DolphinViewActionHandler::toggleGroupedSorting(bool grouped) +void DolphinViewActionHandler::slotGroupByTriggered(QAction *action) +{ + const QByteArray data = action->data().toByteArray(); + if (data.isEmpty()) { + m_currentView->setGroupedSorting(false); + } else if (data == "same_as_sort") { + m_currentView->setGroupedSorting(true); + m_currentView->setGroupRole(QByteArray()); + } else { + m_currentView->setGroupedSorting(true); + m_currentView->setGroupRole(data); + } +} + +void DolphinViewActionHandler::slotGroupedSortingChanged(bool /*groupedSorting*/) +{ + updateGroupByActions(); +} + +void DolphinViewActionHandler::slotGroupRoleChanged(const QByteArray & /*role*/) { - m_currentView->setGroupedSorting(grouped); + updateGroupByActions(); } -void DolphinViewActionHandler::slotGroupedSortingChanged(bool groupedSorting) +void DolphinViewActionHandler::updateGroupByActions() { - QAction *showInGroupsAction = m_actionCollection->action(QStringLiteral("show_in_groups")); - showInGroupsAction->setChecked(groupedSorting); + if (!m_currentView) { + return; + } + + const bool grouped = m_currentView->groupedSorting(); + const QByteArray rawRole = m_currentView->rawGroupRole(); + + QByteArray checkedKey; + if (!grouped) { + checkedKey = QByteArray(); + } else if (rawRole.isEmpty()) { + checkedKey = "same_as_sort"; + } else { + checkedKey = rawRole; + } + + for (const auto &[key, action] : m_groupByActions.asKeyValueRange()) { + action->setChecked(key == checkedKey); + } } void DolphinViewActionHandler::toggleShowHiddenFiles(bool show) diff --git a/src/views/dolphinviewactionhandler.h b/src/views/dolphinviewactionhandler.h index 20d62cf64..b15a73933 100644 --- a/src/views/dolphinviewactionhandler.h +++ b/src/views/dolphinviewactionhandler.h @@ -186,15 +186,9 @@ private Q_SLOTS: */ void slotVisibleRolesChanged(const QList ¤t, const QList &previous); - /** - * Switches between sorting by groups or not. - */ - void toggleGroupedSorting(bool); - - /** - * Updates the state of the 'Categorized sorting' menu action. - */ - void slotGroupedSortingChanged(bool sortCategorized); + void slotGroupByTriggered(QAction *action); + void slotGroupedSortingChanged(bool groupedSorting); + void slotGroupRoleChanged(const QByteArray &role); /** * Switches between showing and hiding of hidden marked files @@ -252,10 +246,11 @@ private: /** * Creates an action-group out of all roles from KFileItemModel. * Dependent on the group-prefix either a radiobutton-group is - * created for sorting (prefix is "sort_by_") or a checkbox-group + * created for sorting (prefix is "sort_by_"), a radiobutton-group + * is created for group-by (prefix is "group_"), or a checkbox-group * is created for additional information (prefix is "show_"). - * The changes of actions are reported to slotSortTriggered() or - * toggleAdditionalInfo(). + * The changes of actions are reported to slotSortTriggered(), + * slotGroupByTriggered(), or toggleAdditionalInfo() respectively. */ QActionGroup *createFileItemRolesActionGroup(const QString &groupPrefix); @@ -277,11 +272,14 @@ private: */ KToggleAction *detailsModeAction(); + void updateGroupByActions(); + KActionCollection *m_actionCollection; DolphinView *m_currentView; QHash m_sortByActions; QHash m_visibleRoles; + QHash m_groupByActions; }; #endif /* DOLPHINVIEWACTIONHANDLER_H */ diff --git a/src/views/viewproperties.cpp b/src/views/viewproperties.cpp index 9cd834991..f392f45c5 100644 --- a/src/views/viewproperties.cpp +++ b/src/views/viewproperties.cpp @@ -353,6 +353,19 @@ QByteArray ViewProperties::sortRole() const return m_node->sortRole().toLatin1(); } +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::setSortOrder(Qt::SortOrder sortOrder) { if (m_node->sortOrder() != sortOrder) { @@ -510,6 +523,7 @@ void ViewProperties::setDirProperties(const ViewProperties &props) setHiddenFilesShown(props.hiddenFilesShown()); setGroupedSorting(props.groupedSorting()); setSortRole(props.sortRole()); + setGroupRole(props.groupRole()); setSortOrder(props.sortOrder()); setSortFoldersFirst(props.sortFoldersFirst()); setSortHiddenLast(props.sortHiddenLast()); diff --git a/src/views/viewproperties.h b/src/views/viewproperties.h index 622a1c87e..801d33845 100644 --- a/src/views/viewproperties.h +++ b/src/views/viewproperties.h @@ -60,6 +60,9 @@ public: void setSortRole(const QByteArray &role); QByteArray sortRole() const; + void setGroupRole(const QByteArray &role); + QByteArray groupRole() const; + void setSortOrder(Qt::SortOrder sortOrder); Qt::SortOrder sortOrder() const; -- cgit v1.3.1