X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/ed2d352c42a6d517d4f29b3582c0e00aa34fe647..e9b056dcf080a12a09fa1c1a55eab79edb2164b5:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index bbe725dfd..562046059 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -12,12 +12,13 @@ #include "dolphin_generalsettings.h" #include "dolphindebug.h" #include "private/kfileitemmodelsortalgorithm.h" +#include "views/draganddrophelper.h" #include #include +#include #include #include -#include #include #include @@ -26,7 +27,6 @@ #include #include #include -#include #include Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) @@ -34,7 +34,7 @@ Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject *parent) - : KItemModelBase("text", parent) + : KItemModelBase("text", "text", parent) , m_dirLister(nullptr) , m_sortDirsFirst(true) , m_sortHiddenLast(false) @@ -100,13 +100,14 @@ KFileItemModel::KFileItemModel(QObject *parent) // for a lot of items within a quite small timeslot. To prevent expensive resortings the // resorting is postponed until the timer has been exceeded. m_resortAllItemsTimer = new QTimer(this); - m_resortAllItemsTimer->setInterval(500); + m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable + // and sorting too infrequently which leads to users seeing an outdated sort order. m_resortAllItemsTimer->setSingleShot(true); connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); - setShowTrashMime(m_dirLister->showHiddenFiles()); + setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile()); } KFileItemModel::~KFileItemModel() @@ -240,35 +241,31 @@ bool KFileItemModel::sortHiddenLast() const return m_sortHiddenLast; } -void KFileItemModel::setShowTrashMime(bool show) +void KFileItemModel::setShowTrashMime(bool showTrashMime) { const auto trashMime = QStringLiteral("application/x-trash"); QStringList excludeFilter = m_filter.excludeMimeTypes(); - bool wasShown = !excludeFilter.contains(trashMime); - if (show) { - if (!wasShown) { - excludeFilter.removeAll(trashMime); - } - } else { - if (wasShown) { - excludeFilter.append(trashMime); - } + if (showTrashMime) { + excludeFilter.removeAll(trashMime); + } else if (!excludeFilter.contains(trashMime)) { + excludeFilter.append(trashMime); } - if (wasShown != show) { - setExcludeMimeTypeFilter(excludeFilter); + setExcludeMimeTypeFilter(excludeFilter); +} + +void KFileItemModel::scheduleResortAllItems() +{ + if (!m_resortAllItemsTimer->isActive()) { + m_resortAllItemsTimer->start(); } } void KFileItemModel::setShowHiddenFiles(bool show) { -#if KIO_VERSION < QT_VERSION_CHECK(5, 100, 0) - m_dirLister->setShowingDotFiles(show); -#else m_dirLister->setShowHiddenFiles(show); -#endif - setShowTrashMime(show); + setShowTrashMime(show || !GeneralSettings::hideXTrashFile()); m_dirLister->emitChanges(); if (show) { dispatchPendingItemsToInsert(); @@ -277,11 +274,7 @@ void KFileItemModel::setShowHiddenFiles(bool show) bool KFileItemModel::showHiddenFiles() const { -#if KIO_VERSION < QT_VERSION_CHECK(5, 100, 0) - return m_dirLister->showingDotFiles(); -#else return m_dirLister->showHiddenFiles(); -#endif } void KFileItemModel::setShowDirectoriesOnly(bool enabled) @@ -356,7 +349,18 @@ bool KFileItemModel::supportsDropping(int index) const } else { item = fileItem(index); } - return !item.isNull() && ((item.isDir() && item.isWritable()) || item.isDesktopFile()); + return !item.isNull() && DragAndDropHelper::supportsDropping(item); +} + +bool KFileItemModel::canEnterOnHover(int index) const +{ + KFileItem item; + if (index == -1) { + item = rootItem(); + } else { + item = fileItem(index); + } + return !item.isNull() && (item.isDir() || item.isDesktopFile()); } QString KFileItemModel::roleDescription(const QByteArray &role) const @@ -383,7 +387,10 @@ QList> KFileItemModel::groups() const QElapsedTimer timer; timer.start(); #endif - switch (typeForRole(sortRole())) { + switch (typeForRole(groupRole())) { + case NoRole: + m_groups.clear(); + break; case NameRole: m_groups = nameRoleGroups(); break; @@ -417,7 +424,7 @@ QList> KFileItemModel::groups() const m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(groupRole()); break; } @@ -624,9 +631,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded) m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); -#if KIO_VERSION >= QT_VERSION_CHECK(5, 92, 0) m_dirLister->forgetDirs(url); -#endif const int parentLevel = expandedParentsCount(index); const int itemCount = m_itemData.count(); @@ -642,9 +647,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded) const QUrl url = itemData->item.url(); m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 -#if KIO_VERSION >= QT_VERSION_CHECK(5, 92, 0) m_dirLister->forgetDirs(url); -#endif expandedChildren.append(targetUrl); } ++childIndex; @@ -886,6 +889,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 +930,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); } } @@ -942,11 +961,40 @@ void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArr } } -void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) +void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems) { Q_UNUSED(current) Q_UNUSED(previous) - resortAllItems(); + + if (resortItems) { + resortAllItems(); + } +} + +void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) +{ + Q_UNUSED(previous) + m_groupRole = typeForRole(current); + + if (!m_requestRole[m_sortRole]) { + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); + } + + if (resortItems) { + resortAllItems(); + } +} + +void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems) +{ + Q_UNUSED(current) + Q_UNUSED(previous) + + if (resortItems) { + resortAllItems(); + } } void KFileItemModel::loadSortingSettings() @@ -994,7 +1042,7 @@ void KFileItemModel::resortAllItems() // been moved because of the resorting. QList oldUrls; oldUrls.reserve(itemCount); - for (const ItemData *itemData : qAsConst(m_itemData)) { + for (const ItemData *itemData : std::as_const(m_itemData)) { oldUrls.append(itemData->item.url()); } @@ -1239,7 +1287,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList &items) indexesToRemoveWithChildren.reserve(m_itemData.count()); const int itemCount = m_itemData.count(); - for (int index : qAsConst(indexesToRemove)) { + for (int index : std::as_const(indexesToRemove)) { indexesToRemoveWithChildren.append(index); const int parentLevel = expandedParentsCount(index); @@ -1657,7 +1705,7 @@ void KFileItemModel::prepareItemsForSorting(QList &itemDataList) case DeletionTimeRole: // These roles can be determined with retrieveData, and they have to be stored // in the QHash "values" for the sorting. - for (ItemData *itemData : qAsConst(itemDataList)) { + for (ItemData *itemData : std::as_const(itemDataList)) { if (itemData->values.isEmpty()) { itemData->values = retrieveData(itemData->item, itemData->parent); } @@ -1666,7 +1714,7 @@ void KFileItemModel::prepareItemsForSorting(QList &itemDataList) case TypeRole: // At least store the data including the file type for items with known MIME type. - for (ItemData *itemData : qAsConst(itemDataList)) { + for (ItemData *itemData : std::as_const(itemDataList)) { if (itemData->values.isEmpty()) { const KFileItem item = itemData->item; if (item.isDir() || item.isMimeTypeKnown()) { @@ -1738,7 +1786,8 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i // Trigger a resorting if necessary. Note that this can happen even if the sort // role has not changed at all because the file name can be used as a fallback. - if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) { + if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole)) + || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting. for (const KItemRange &range : itemRanges) { bool needsResorting = false; @@ -1763,7 +1812,7 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &i } if (needsResorting) { - m_resortAllItemsTimer->start(); + scheduleResortAllItems(); return; } } @@ -2018,30 +2067,34 @@ 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::directorySizeCount() && 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 @@ -2081,7 +2134,7 @@ int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const break; case SizeRole: { - if (ContentDisplaySettings::directorySizeCount() && itemA.isDir()) { + if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && itemA.isDir()) { // folders first then // items A and B are folders thanks to lessThan checks auto valueA = a->values.value("count"); @@ -2232,6 +2285,112 @@ 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; + + int groupA, groupB; + switch (m_groupRole) { + case NoRole: + break; + case NameRole: + groupA = nameRoleGroup(a, false).comparable; + groupB = nameRoleGroup(b, false).comparable; + break; + case SizeRole: + groupA = sizeRoleGroup(a, false).comparable; + groupB = sizeRoleGroup(b, false).comparable; + break; + case ModificationTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }, + a, + false) + .comparable; + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }, + b, + false) + .comparable; + break; + case CreationTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::CreationTime); + }, + a, + false) + .comparable; + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::CreationTime); + }, + b, + false) + .comparable; + break; + case AccessTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::AccessTime); + }, + a, + false) + .comparable; + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::AccessTime); + }, + b, + false) + .comparable; + break; + case DeletionTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->values.value("deletiontime").toDateTime(); + }, + a, + false) + .comparable; + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->values.value("deletiontime").toDateTime(); + }, + b, + false) + .comparable; + break; + case PermissionsRole: + groupA = permissionRoleGroup(a, false).comparable; + groupB = permissionRoleGroup(b, false).comparable; + break; + case RatingRole: + groupA = ratingRoleGroup(a, false).comparable; + groupB = ratingRoleGroup(b, false).comparable; + break; + default: { + QString strGroupA = genericStringRoleGroup(groupRole(), a); + QString strGroupB = genericStringRoleGroup(groupRole(), b); + result = stringCompare(strGroupA, strGroupB, collator); + break; + } + } + if (result == 0) { + if (groupA < groupB) { + result = -1; + } else if (groupA > groupB) { + result = 1; + } + } + return result; +} + int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const { QMutexLocker collatorLock(s_collatorMutex()); @@ -2251,180 +2410,195 @@ 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 ItemGroupInfo oldGroupInfo; + static QChar oldFirstChar; + ItemGroupInfo groupInfo; QChar firstChar; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const QString name = m_itemData.at(i)->item.text(); - // Use the first character of the name as group indication - QChar newFirstChar = name.at(0).toUpper(); - if (newFirstChar == QLatin1Char('~') && name.length() > 1) { - newFirstChar = name.at(1).toUpper(); - } + const QString name = itemData->item.text(); - 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(); - // 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)); - } - } + if (firstChar == oldFirstChar) { + 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. - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; + // 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)); + } + } - 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; - } + auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { + return m_collator.compare(c1, c2) < 0; + }; - } else { - // Symbols from non Latin-based scripts - newGroupValue = newFirstChar; + std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan); + if (it != lettersAtoZ.end()) { + if (localeAwareLessThan(firstChar, *it)) { + // newFirstChar belongs to the group preceding *it. + // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. + --it; } - } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { - // Apply group '0 - 9' for any name that starts with a digit - newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); - } else { - newGroupValue = i18nc("@title:group", "Others"); + if (withString) { + groupInfo.text = *it; + } + groupInfo.comparable = (*it).unicode(); } - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } else { + // Symbols from non Latin-based scripts + if (withString) { + groupInfo.text = firstChar; } - - firstChar = newFirstChar; + groupInfo.comparable = firstChar.unicode(); + } + } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) { + // Apply group '0 - 9' for any name that starts with a digit + if (withString) { + groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9"); + } + groupInfo.comparable = (int)'0'; + } else { + if (withString) { + groupInfo.text = i18nc("@title:group", "Others"); } + groupInfo.comparable = (int)'.'; } - return groups; + 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()); + static ItemGroupInfo oldGroupInfo; + static KIO::filesize_t oldFileSize; + ItemGroupInfo groupInfo; + KIO::filesize_t fileSize; - const int maxIndex = count() - 1; - QList> groups; - - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const KFileItem &item = m_itemData.at(i)->item; - KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; - QString newGroupValue; - if (!item.isNull() && item.isDir()) { - if (ContentDisplaySettings::directorySizeCount() || m_sortDirsFirst) { - newGroupValue = i18nc("@title:group Size", "Folders"); - } else { - fileSize = m_itemData.at(i)->values.value("size").toULongLong(); - } - } + const KFileItem item = itemData->item; + fileSize = !item.isNull() ? item.size() : ~0U; - if (newGroupValue.isEmpty()) { - if (fileSize < 5 * 1024 * 1024) { // < 5 MB - newGroupValue = i18nc("@title:group Size", "Small"); - } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB - newGroupValue = i18nc("@title:group Size", "Medium"); - } else { - newGroupValue = i18nc("@title:group Size", "Big"); - } + groupInfo.comparable = -1; // None + if (!item.isNull() && item.isDir()) { + if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) { + groupInfo.comparable = 0; // Folders + } else { + fileSize = itemData->values.value("size").toULongLong(); } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } + if (fileSize == oldFileSize) { + return oldGroupInfo; + } + 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]); + } + oldFileSize = fileSize; + oldGroupInfo = groupInfo; + 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 ItemGroupInfo oldGroupInfo; + static QDate oldFileDate; + ItemGroupInfo groupInfo; const QDate currentDate = QDate::currentDate(); - - QDate previousFileDate; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; + const QDateTime fileTime = fileTimeCb(itemData); + const QDate fileDate = fileTime.date(); + const int daysDistance = fileDate.daysTo(currentDate); + + // 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); @@ -2441,30 +2615,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)); - } } - - return groups; + oldFileDate = fileDate; + oldGroupInfo = groupInfo; + return groupInfo; } -QList> KFileItemModel::permissionRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; - - QString permissionsString; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; - const ItemData *itemData = m_itemData.at(i); - const QString newPermissionsString = itemData->values.value("permissions").toString(); - if (newPermissionsString == permissionsString) { - continue; - } - permissionsString = newPermissionsString; - - const QFileInfo info(itemData->item.url().toLocalFile()); + const QFileInfo info(itemData->item.url().toLocalFile()); + const QFileDevice::Permissions permissions = info.permissions(); + if (permissions == oldPermissions) { + return oldGroupInfo; + } + groupInfo.comparable = (int)permissions; + if (withString) { // Set user string QString user; - if (info.permission(QFile::ReadUser)) { + if (permissions & QFile::ReadUser) { user = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteUser)) { + if (permissions & QFile::WriteUser) { user += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeUser)) { + if (permissions & QFile::ExeUser) { user += i18nc("@item:intext Access permission, concatenated", "Execute, "); } - user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2); + user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2); // Set group string QString group; - if (info.permission(QFile::ReadGroup)) { + if (permissions & QFile::ReadGroup) { group = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteGroup)) { + if (permissions & QFile::WriteGroup) { group += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeGroup)) { + if (permissions & QFile::ExeGroup) { group += i18nc("@item:intext Access permission, concatenated", "Execute, "); } - group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2); + group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2); // Set others string QString others; - if (info.permission(QFile::ReadOther)) { + if (permissions & QFile::ReadOther) { others = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteOther)) { + if (permissions & QFile::WriteOther) { others += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeOther)) { + if (permissions & QFile::ExeOther) { others += i18nc("@item:intext Access permission, concatenated", "Execute, "); } - others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2); + 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); + } + oldPermissions = permissions; + oldGroupInfo = groupInfo; + return groupInfo; +} - const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); +KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const +{ + ItemGroupInfo groupInfo; + groupInfo.comparable = itemData->values.value("rating", 0).toInt(); + if (withString) { + // Dolphin does not currently use string representation of star rating + // as stars are rendered as graphics in group headers. + groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated ") + QString::number(groupInfo.comparable); + } + return groupInfo; +} + +QString KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const +{ + return 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; } -QList> KFileItemModel::ratingRoleGroups() const +QList> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); 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 = timeRoleGroup(fileTimeCb, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; +} +QList> KFileItemModel::permissionRoleGroups() 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 = permissionRoleGroup(m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } -QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +QList> KFileItemModel::ratingRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); 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 = 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; +} + +QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + QString groupText; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + QString newGroupText = genericStringRoleGroup(role, m_itemData.at(i)); + + if (newGroupText != groupText) { + groupText = newGroupText; + groups.append(QPair(i, newGroupText)); + } + } return groups; } @@ -2723,7 +2991,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 },