diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp index b4aade6b9a..bc00af7cc5 100644 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@ -305,10 +305,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"))); } diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index bc88d643ec..eca7534ad8 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -1496,6 +1496,7 @@ void DolphinMainWindow::updateHamburgerMenu() } 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"))); @@ -2316,7 +2317,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); diff --git a/src/dolphinpart.rc b/src/dolphinpart.rc index d13f4aaed7..13f0f2172b 100644 --- a/src/dolphinpart.rc +++ b/src/dolphinpart.rc @@ -23,6 +23,7 @@ &View + diff --git a/src/dolphinui.rc b/src/dolphinui.rc index 2f7ea857d1..d884fe3004 100644 --- a/src/dolphinui.rc +++ b/src/dolphinui.rc @@ -43,6 +43,7 @@ + diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp index d763fe2879..d58a1a8baf 100644 --- a/src/kitemviews/kfileitemlistview.cpp +++ b/src/kitemviews/kfileitemlistview.cpp @@ -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); diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index c694da9f25..5b7b781a8f 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -34,11 +34,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() @@ -387,7 +388,17 @@ QList> 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; @@ -421,7 +432,7 @@ QList> KFileItemModel::groups() const m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(role); break; } @@ -886,6 +897,39 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges) } } +KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role) +{ + static QHash 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::rolesInformation() { static QList rolesInfo; @@ -894,24 +938,7 @@ QList 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); } } @@ -920,6 +947,15 @@ QList KFileItemModel::rolesInformation() return rolesInfo; } +QList KFileItemModel::extraGroupingInformation() +{ + static QList 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) @@ -930,6 +966,13 @@ void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, 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 newRoles = m_roles; @@ -949,6 +992,36 @@ void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre resortAllItems(); } +void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, 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 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; @@ -971,6 +1044,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() @@ -1036,7 +1110,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> oldGroups = m_groups; m_groups.clear(); @@ -1621,7 +1696,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 || 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). @@ -1645,9 +1720,9 @@ QList KFileItemModel::createItemDataList(const QUrl return itemDataList; } -void KFileItemModel::prepareItemsForSorting(QList &itemDataList) +void KFileItemModel::prepareItemsWithRole(QList &itemDataList, RoleType roleType) { - switch (m_sortRole) { + switch (roleType) { case ExtensionRole: case PermissionsRole: case OwnerRole: @@ -1686,6 +1761,12 @@ void KFileItemModel::prepareItemsForSorting(QList &itemDataList) } } +void KFileItemModel::prepareItemsForSorting(QList &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" @@ -2019,31 +2100,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::iterator &begin, const QList::iterator &end) const @@ -2234,6 +2317,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()); @@ -2253,180 +2438,196 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol return QString::compare(a, b, Qt::CaseSensitive); } -QList> KFileItemModel::nameRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> 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(); + const QString name = itemData->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(); - } + QMutexLocker collatorLock(s_collatorMutex()); - 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. + // 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 lettersAtoZ; - lettersAtoZ.reserve('Z' - 'A' + 1); - if (lettersAtoZ.empty()) { - for (char c = 'A'; c <= 'Z'; ++c) { - lettersAtoZ.push_back(QLatin1Char(c)); - } - } - - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; - - std::vector::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; - } - - } else { - // Symbols from non Latin-based scripts - newGroupValue = newFirstChar; + // Try to find a matching group in the range 'A' to 'Z'. + static std::vector lettersAtoZ; + lettersAtoZ.reserve('Z' - 'A' + 1); + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); } - } 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 (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { + return m_collator.compare(c1, c2) < 0; + }; + + std::vector::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; + } + if (withString) { + groupInfo.text = *it; + } + groupInfo.comparable = (*it).unicode(); } - firstChar = newFirstChar; + } else { + // Symbols from non Latin-based scripts + if (withString) { + groupInfo.text = firstChar; + } + 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> 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> 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; + 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(); } - - 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"); - } - } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(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> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const +KFileItemModel::ItemGroupInfo +KFileItemModel::timeRoleGroup(const std::function &fileTimeCb, const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QDate oldFileDate; + ItemGroupInfo groupInfo; const QDate currentDate = QDate::currentDate(); + const QDateTime fileTime = fileTimeCb(itemData); + const QDate fileDate = fileTime.date(); + const int daysDistance = fileDate.daysTo(currentDate); - QDate previousFileDate; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; + 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 } - - const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); - const QDate fileDate = fileTime.date(); - if (fileDate == previousFileDate) { - // The current item is in the same group as the previous item - continue; + } 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 } - previousFileDate = fileDate; - - const int daysDistance = fileDate.daysTo(currentDate); - - QString newGroupValue; + } + 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 = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd")); - newGroupValue = i18nc( + groupInfo.text = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd")); + groupInfo.text = i18nc( "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); @@ -2443,30 +2644,30 @@ QList> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function(i, newGroupValue)); +KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const +{ + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; + + const QFileInfo info(itemData->item.url().toLocalFile()); + const QFileDevice::Permissions permissions = info.permissions(); + if (permissions == oldPermissions && withString == oldWithString) { + return oldGroupInfo; + } + groupInfo.comparable = (int)permissions; + + if (withString) { + // Set user string + QString user; + if (permissions & QFile::ReadUser) { + user = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (permissions & QFile::WriteUser) { + user += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (permissions & QFile::ExeUser) { + user += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2); + + // Set group string + QString group; + if (permissions & QFile::ReadGroup) { + group = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (permissions & QFile::WriteGroup) { + group += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (permissions & QFile::ExeGroup) { + group += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2); + + // Set others string + QString others; + if (permissions & QFile::ReadOther) { + others = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (permissions & QFile::WriteOther) { + others += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (permissions & QFile::ExeOther) { + others += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2); + groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); + } + oldWithString = withString; + oldPermissions = permissions; + oldGroupInfo = groupInfo; + return groupInfo; +} + +KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const +{ + ItemGroupInfo groupInfo; + groupInfo.comparable = itemData->values.value("rating", 0).toInt(); + if (withString) { + // Dolphin does not currently use string representation of star rating + // as stars are rendered as graphics in group headers. + groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable)); + } + return groupInfo; +} + +KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const +{ + return {0, itemData->values.value(role).toString()}; +} + +QList> KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); } } + return groups; +} +QList> KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } + return groups; +} + +KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const +{ + int priority = 0; + if (itemData->item.isDir() && m_sortDirsFirst) { + // Ensure folders stay first regardless of grouping order + if (groupOrder() == Qt::AscendingOrder) { + priority = -1; + } else { + priority = 1; + } + } + return {priority, itemData->values.value("type").toString()}; +} + +QList> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } @@ -2581,68 +2940,19 @@ QList> KFileItemModel::permissionRoleGroups() const const int maxIndex = count() - 1; QList> groups; - QString permissionsString; - QString groupValue; + ItemGroupInfo groupInfo; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const ItemData *itemData = m_itemData.at(i); - const QString newPermissionsString = itemData->values.value("permissions").toString(); - if (newPermissionsString == permissionsString) { - continue; - } - permissionsString = newPermissionsString; + ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i)); - const QFileInfo info(itemData->item.url().toLocalFile()); - - // Set user string - QString user; - if (info.permission(QFile::ReadUser)) { - user = i18nc("@item:intext Access permission, concatenated", "Read, "); - } - if (info.permission(QFile::WriteUser)) { - user += i18nc("@item:intext Access permission, concatenated", "Write, "); - } - if (info.permission(QFile::ExeUser)) { - user += i18nc("@item:intext Access permission, concatenated", "Execute, "); - } - user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2); - - // Set group string - QString group; - if (info.permission(QFile::ReadGroup)) { - group = i18nc("@item:intext Access permission, concatenated", "Read, "); - } - if (info.permission(QFile::WriteGroup)) { - group += i18nc("@item:intext Access permission, concatenated", "Write, "); - } - if (info.permission(QFile::ExeGroup)) { - group += i18nc("@item:intext Access permission, concatenated", "Execute, "); - } - group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2); - - // Set others string - QString others; - if (info.permission(QFile::ReadOther)) { - others = i18nc("@item:intext Access permission, concatenated", "Read, "); - } - if (info.permission(QFile::WriteOther)) { - others += i18nc("@item:intext Access permission, concatenated", "Write, "); - } - if (info.permission(QFile::ExeOther)) { - others += i18nc("@item:intext Access permission, concatenated", "Execute, "); - } - others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2); - - const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } - return groups; } @@ -2653,18 +2963,21 @@ QList> KFileItemModel::ratingRoleGroups() const const int maxIndex = count() - 1; QList> 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(i, newGroupValue)); + + ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + // Using the numeric representation because Dolphin has a special + // case for drawing stars. + groups.append(QPair(i, newGroupInfo.comparable)); } } - return groups; } @@ -2675,20 +2988,19 @@ QList> KFileItemModel::genericStringRoleGroups(const QByteA const int maxIndex = count() - 1; QList> 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(i, newGroupValue)); - isFirstGroupValue = false; + + ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } - return groups; } @@ -2725,7 +3037,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 }, diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index ce58f89acc..001c847012 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -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 rolesInformation(); + /** + * @return Provides static information for all available grouping + * behaviors supported by KFileItemModel but not directly + * mapped to roles of KFileItemModel. + */ + static QList 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 ¤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; + 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(); @@ -365,6 +381,15 @@ private: ItemData *parent; }; + 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 &items); @@ -378,6 +403,12 @@ private: */ QList 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 &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 @@ -445,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 &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> nameRoleGroups() const; QList> sizeRoleGroups() const; QList> timeRoleGroups(const std::function &fileTimeCb) const; QList> permissionRoleGroups() const; QList> ratingRoleGroups() const; + QList> typeRoleGroups() const; QList> genericStringRoleGroups(const QByteArray &typeForRole) const; /** @@ -542,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 m_roles; @@ -600,4 +651,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 diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 0ff431ac99..3828f0979c 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -368,7 +368,7 @@ 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())) { + if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) { int insertedCount = 0; for (const KItemRange &range : itemRanges) { const int lastIndex = insertedCount + range.index + range.count - 1; @@ -1218,13 +1218,13 @@ void KFileItemModelRolesUpdater::applySortRole(int index) QHash 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 { diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h index aa9ca5fc0e..df3a942262 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.h +++ b/src/kitemviews/kfileitemmodelrolesupdater.h @@ -315,6 +315,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); diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index afc392810e..45f5851bf5 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -1481,6 +1481,26 @@ void KItemListView::slotSortRoleChanged(const QByteArray ¤t, 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 ¤t, const QByteArray &previous) +{ + Q_UNUSED(current) + Q_UNUSED(previous) + if (m_grouped) { + updateVisibleGroupHeaders(); + doLayout(NoAnimation); + } +} + void KItemListView::slotCurrentChanged(int current, int previous) { Q_UNUSED(previous) @@ -1743,6 +1763,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())); } @@ -2185,7 +2207,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 8812eb8cc4..04d48bd477 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -430,6 +430,8 @@ 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 slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder 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); @@ -552,8 +554,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(); diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp index 9fdecafb86..17716f57f5 100644 --- a/src/kitemviews/kitemmodelbase.cpp +++ b/src/kitemviews/kitemmodelbase.cpp @@ -10,17 +10,21 @@ 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 ¤t, 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(); diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h index 42a9c54c94..bc8ab64427 100644 --- a/src/kitemviews/kitemmodelbase.h +++ b/src/kitemviews/kitemmodelbase.h @@ -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 ¤t, const QByteArray &previous); void sortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); + void groupRoleChanged(const QByteArray ¤t, 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 ¤t, 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 diff --git a/src/settings/applyviewpropsjob.cpp b/src/settings/applyviewpropsjob.cpp index 2a2b4bfe4f..21c30af022 100644 --- a/src/settings/applyviewpropsjob.cpp +++ b/src/settings/applyviewpropsjob.cpp @@ -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); diff --git a/src/settings/contextmenu/contextmenusettingspage.cpp b/src/settings/contextmenu/contextmenusettingspage.cpp index c401c2d6b6..64b78d2bdc 100644 --- a/src/settings/contextmenu/contextmenusettingspage.cpp +++ b/src/settings/contextmenu/contextmenusettingspage.cpp @@ -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") { diff --git a/src/settings/dolphin_contextmenusettings.kcfg b/src/settings/dolphin_contextmenusettings.kcfg index 6e45d9bcdf..ac7e8f9ae3 100644 --- a/src/settings/dolphin_contextmenusettings.kcfg +++ b/src/settings/dolphin_contextmenusettings.kcfg @@ -18,6 +18,10 @@ true + + + true + true diff --git a/src/settings/dolphin_directoryviewpropertysettings.kcfg b/src/settings/dolphin_directoryviewpropertysettings.kcfg index f4d2883696..7f34f70932 100644 --- a/src/settings/dolphin_directoryviewpropertysettings.kcfg +++ b/src/settings/dolphin_directoryviewpropertysettings.kcfg @@ -35,8 +35,8 @@ - When this option is enabled, the sorted items are categorized into groups. - false + When this option is enabled, the items are categorized into groups. + true @@ -52,6 +52,19 @@ Qt::DescendingOrder + + + This option defines which attribute (text, size, date, etc.) grouping is performed on. + none + + + + + Qt::AscendingOrder + Qt::AscendingOrder + Qt::DescendingOrder + + true @@ -84,5 +97,3 @@ - - diff --git a/src/settings/dolphinsettingsdialog.cpp b/src/settings/dolphinsettingsdialog.cpp index c564dd196c..0fd4328057 100644 --- a/src/settings/dolphinsettingsdialog.cpp +++ b/src/settings/dolphinsettingsdialog.cpp @@ -65,6 +65,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"), diff --git a/src/settings/viewpropertiesdialog.cpp b/src/settings/viewpropertiesdialog.cpp index 37c3d539b3..a13b0b117b 100644 --- a/src/settings/viewpropertiesdialog.cpp +++ b/src/settings/viewpropertiesdialog.cpp @@ -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 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 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()); diff --git a/src/settings/viewpropertiesdialog.h b/src/settings/viewpropertiesdialog.h index d1f056fbbf..49536dcc7b 100644 --- a/src/settings/viewpropertiesdialog.h +++ b/src/settings/viewpropertiesdialog.h @@ -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; diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index fad825dc01..ea05f840b7 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -164,7 +164,7 @@ void KFileItemModelTest::testDefaultSortRole() void KFileItemModelTest::testDefaultGroupedSorting() { - QCOMPARE(m_model->groupedSorting(), false); + QCOMPARE(m_model->groupedSorting(), true); } void KFileItemModelTest::testNewItems() @@ -2007,6 +2007,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(), @@ -2093,6 +2094,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(), @@ -2102,6 +2105,7 @@ void KFileItemModelTest::testNameRoleGroupsWithExpandedItems() QList> expectedGroups; expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(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"). diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index d42d9cfcd8..9dedb9661c 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -503,6 +503,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) { @@ -2114,6 +2150,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); diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index b55e2ee9be..f3c0189bf9 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -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; @@ -522,6 +538,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. diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index 2934e80058..815b0f63e6 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -33,6 +33,7 @@ DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection *collection , m_actionCollection(collection) , m_currentView(nullptr) , m_sortByActions() + , m_groupByActions() , m_visibleRoles() { Q_ASSERT(m_actionCollection); @@ -58,6 +59,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()); @@ -275,27 +278,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(QStringLiteral("ascending")); - ascendingAction->setActionGroup(group); - connect(ascendingAction, &QAction::triggered, this, [this] { + KToggleAction *sortAscendingAction = m_actionCollection->add(QStringLiteral("sort_ascending")); + sortAscendingAction->setActionGroup(groupForSort); + connect(sortAscendingAction, &QAction::triggered, this, [this] { m_currentView->setSortOrder(Qt::AscendingOrder); }); - KToggleAction *descendingAction = m_actionCollection->add(QStringLiteral("descending")); - descendingAction->setActionGroup(group); - connect(descendingAction, &QAction::triggered, this, [this] { + KToggleAction *sortDescendingAction = m_actionCollection->add(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(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(QStringLiteral("group_ascending")); + groupAscendingAction->setActionGroup(groupForGroup); + connect(groupAscendingAction, &QAction::triggered, this, [this] { + m_currentView->setGroupOrder(Qt::AscendingOrder); + }); + + KToggleAction *groupDescendingAction = m_actionCollection->add(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_")); @@ -344,12 +380,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); } @@ -364,9 +403,13 @@ QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QSt indexingEnabled = config.fileIndexingEnabled(); #endif - const QList rolesInfo = KFileItemModel::rolesInformation(); + QList 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; } @@ -384,9 +427,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); } @@ -404,6 +449,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); } @@ -508,6 +555,8 @@ void DolphinViewActionHandler::updateViewActions() slotVisibleRolesChanged(m_currentView->visibleRoles(), QList()); 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 @@ -548,13 +597,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); @@ -674,8 +732,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")); @@ -697,6 +755,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) @@ -738,6 +840,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(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(); diff --git a/src/views/dolphinviewactionhandler.h b/src/views/dolphinviewactionhandler.h index f36b3d1d04..93ed10e2be 100644 --- a/src/views/dolphinviewactionhandler.h +++ b/src/views/dolphinviewactionhandler.h @@ -158,6 +158,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. */ @@ -179,6 +189,11 @@ private Q_SLOTS: */ void slotVisibleRolesChanged(const QList ¤t, const QList &previous); + /** + * Changes the grouping of the current view. + */ + void slotGroupTriggered(QAction *); + /** * Switches between sorting by groups or not. */ @@ -274,6 +289,7 @@ private: DolphinView *m_currentView; QHash m_sortByActions; + QHash m_groupByActions; QHash m_visibleRoles; }; diff --git a/src/views/viewproperties.cpp b/src/views/viewproperties.cpp index cc1325fbb3..c5afe663d6 100644 --- a/src/views/viewproperties.cpp +++ b/src/views/viewproperties.cpp @@ -253,6 +253,32 @@ Qt::SortOrder ViewProperties::sortOrder() const return static_cast(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(m_node->groupOrder()); +} + void ViewProperties::setSortFoldersFirst(bool foldersFirst) { if (m_node->sortFoldersFirst() != foldersFirst) { @@ -384,6 +410,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()); diff --git a/src/views/viewproperties.h b/src/views/viewproperties.h index 29827c38b6..0c0452d7ae 100644 --- a/src/views/viewproperties.h +++ b/src/views/viewproperties.h @@ -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;