X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/dd9405bf2f628bd28580737cc46335343074be0d..b4e80645e8e39ef7fcc1545136bad06ab3dd5f3e:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 1335778e7..603c16e0d 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -8,76 +8,79 @@ #include "kfileitemmodel.h" +#include "dolphin_contentdisplaysettings.h" #include "dolphin_generalsettings.h" -#include "dolphin_detailsmodesettings.h" #include "dolphindebug.h" -#include "private/kfileitemmodeldirlister.h" #include "private/kfileitemmodelsortalgorithm.h" +#include "views/draganddrophelper.h" -#include +#include +#include +#include #include #include +#ifndef QT_NO_ACCESSIBILITY +#include +#endif #include +#include #include #include +#include #include #include -#include -#include +#include -Q_GLOBAL_STATIC_WITH_ARGS(QMutex, s_collatorMutex, (QMutex::Recursive)) +Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) // #define KFILEITEMMODEL_DEBUG -KFileItemModel::KFileItemModel(QObject* parent) : - KItemModelBase("text", parent), - m_dirLister(nullptr), - m_sortDirsFirst(true), - m_sortRole(NameRole), - m_sortingProgressPercent(-1), - m_roles(), - m_itemData(), - m_items(), - m_filter(), - m_filteredItems(), - m_requestRole(), - m_maximumUpdateIntervalTimer(nullptr), - m_resortAllItemsTimer(nullptr), - m_pendingItemsToInsert(), - m_groups(), - m_expandedDirs(), - m_urlsToExpand() +KFileItemModel::KFileItemModel(QObject *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() + , m_items() + , m_filter() + , m_filteredItems() + , m_requestRole() + , m_maximumUpdateIntervalTimer(nullptr) + , m_resortAllItemsTimer(nullptr) + , m_pendingItemsToInsert() + , m_groups() + , m_expandedDirs() + , m_urlsToExpand() { m_collator.setNumericMode(true); loadSortingSettings(); - m_dirLister = new KFileItemModelDirLister(this); + m_dirLister = new KDirLister(this); + m_dirLister->setAutoErrorHandlingEnabled(false); m_dirLister->setDelayedMimeTypes(true); - const QWidget* parentWidget = qobject_cast(parent); + const QWidget *parentWidget = qobject_cast(parent); if (parentWidget) { m_dirLister->setMainWindow(parentWidget->window()); } - connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted); - connect(m_dirLister, QOverload<>::of(&KCoreDirLister::canceled), this, &KFileItemModel::slotCanceled); - connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); - connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); - connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); - connect(m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, &KFileItemModel::slotClear); - connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage); - connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage); - connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); - connect(m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection); - connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); - -#if KIO_VERSION < QT_VERSION_CHECK(5, 79, 0) - connect(m_dirLister, QOverload::of(&KCoreDirLister::completed), this, &KFileItemModel::slotCompleted); -#else + connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted); + connect(m_dirLister, &KCoreDirLister::canceled, this, &KFileItemModel::slotCanceled); + connect(m_dirLister, &KCoreDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); + connect(m_dirLister, &KCoreDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); + connect(m_dirLister, &KCoreDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); + connect(m_dirLister, &KCoreDirLister::clear, this, &KFileItemModel::slotClear); + connect(m_dirLister, &KCoreDirLister::infoMessage, this, &KFileItemModel::infoMessage); + connect(m_dirLister, &KCoreDirLister::jobError, this, &KFileItemModel::slotListerError); + connect(m_dirLister, &KCoreDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); + connect(m_dirLister, &KCoreDirLister::redirection, this, &KFileItemModel::directoryRedirection); connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted); -#endif // Apply default roles that should be determined resetRoles(); @@ -101,11 +104,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() || !GeneralSettings::hideXTrashFile()); } KFileItemModel::~KFileItemModel() @@ -130,6 +136,8 @@ void KFileItemModel::refreshDirectory(const QUrl &url) } m_dirLister->openUrl(url, KDirLister::Reload); + + Q_EMIT directoryRefreshing(); } QUrl KFileItemModel::directory() const @@ -150,9 +158,21 @@ int KFileItemModel::count() const QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { - ItemData* data = m_itemData.at(index); + ItemData *data = m_itemData.at(index); if (data->values.isEmpty()) { data->values = retrieveData(data->item, data->parent); + } else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) { + // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing + // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values + // so this special request of different behavior can be identified here. + bool hasExpandedParentsCount = false; + const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); + + data->values = retrieveData(data->item, data->parent); + data->values.insert("isExpanded", true); + if (hasExpandedParentsCount) { + data->values.insert("expandedParentsCount", expandedParentsCount); + } } return data->values; @@ -160,7 +180,7 @@ QHash KFileItemModel::data(int index) const return QHash(); } -bool KFileItemModel::setData(int index, const QHash& values) +bool KFileItemModel::setData(int index, const QHash &values) { if (index < 0 || index >= count()) { return false; @@ -186,13 +206,20 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - m_itemData[index]->values = currentValues; if (changedRoles.contains("text")) { QUrl url = m_itemData[index]->item.url(); + m_items.remove(url); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + currentValues["text"].toString()); m_itemData[index]->item.setUrl(url); + m_items.insert(url, index); + + if (!changedRoles.contains("url")) { + changedRoles.insert("url"); + currentValues["url"] = url; + } } + m_itemData[index]->values = currentValues; emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); @@ -212,9 +239,44 @@ bool KFileItemModel::sortDirectoriesFirst() const return m_sortDirsFirst; } +void KFileItemModel::setSortHiddenLast(bool hiddenLast) +{ + if (hiddenLast != m_sortHiddenLast) { + m_sortHiddenLast = hiddenLast; + resortAllItems(); + } +} + +bool KFileItemModel::sortHiddenLast() const +{ + return m_sortHiddenLast; +} + +void KFileItemModel::setShowTrashMime(bool showTrashMime) +{ + const auto trashMime = QStringLiteral("application/x-trash"); + QStringList excludeFilter = m_filter.excludeMimeTypes(); + + if (showTrashMime) { + excludeFilter.removeAll(trashMime); + } else if (!excludeFilter.contains(trashMime)) { + excludeFilter.append(trashMime); + } + + setExcludeMimeTypeFilter(excludeFilter); +} + +void KFileItemModel::scheduleResortAllItems() +{ + if (!m_resortAllItemsTimer->isActive()) { + m_resortAllItemsTimer->start(); + } +} + void KFileItemModel::setShowHiddenFiles(bool show) { - m_dirLister->setShowingDotFiles(show); + m_dirLister->setShowHiddenFiles(show); + setShowTrashMime(show || !GeneralSettings::hideXTrashFile()); m_dirLister->emitChanges(); if (show) { dispatchPendingItemsToInsert(); @@ -223,7 +285,7 @@ void KFileItemModel::setShowHiddenFiles(bool show) bool KFileItemModel::showHiddenFiles() const { - return m_dirLister->showingDotFiles(); + return m_dirLister->showHiddenFiles(); } void KFileItemModel::setShowDirectoriesOnly(bool enabled) @@ -236,20 +298,20 @@ bool KFileItemModel::showDirectoriesOnly() const return m_dirLister->dirOnlyMode(); } -QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const +QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const { - QMimeData* data = new QMimeData(); + QMimeData *data = new QMimeData(); // The following code has been taken from KDirModel::mimeData() // (kdelibs/kio/kio/kdirmodel.cpp) // SPDX-FileCopyrightText: 2006 David Faure QList urls; QList mostLocalUrls; - const ItemData* lastAddedItem = nullptr; + const ItemData *lastAddedItem = nullptr; for (int index : indexes) { - const ItemData* itemData = m_itemData.at(index); - const ItemData* parent = itemData->parent; + const ItemData *itemData = m_itemData.at(index); + const ItemData *parent = itemData->parent; while (parent && parent != lastAddedItem) { parent = parent->parent; @@ -261,7 +323,7 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const } lastAddedItem = itemData; - const KFileItem& item = itemData->item; + const KFileItem &item = itemData->item; if (!item.isNull()) { urls << item.url(); @@ -274,16 +336,32 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const return data; } -int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const +namespace +{ +QString removeMarks(const QString &original) +{ + const auto normalized = original.normalized(QString::NormalizationForm_D); + QString res; + for (auto ch : normalized) { + if (!ch.isMark()) { + res.append(ch); + } + } + return res; +} +} + +int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const { + const auto noMarkText = removeMarks(text); startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } @@ -292,37 +370,67 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd bool KFileItemModel::supportsDropping(int index) const { - const KFileItem item = fileItem(index); + KFileItem item; + if (index == -1) { + item = rootItem(); + } else { + item = fileItem(index); + } + 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 +QString KFileItemModel::roleDescription(const QByteArray &role) const { static QHash description; if (description.isEmpty()) { int count = 0; - const RoleInfoMap* map = rolesInfoMap(count); + const RoleInfoMap *map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { - if (!map[i].roleTranslation) { + if (map[i].roleTranslation.isEmpty()) { continue; } - description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); + description.insert(map[i].role, map[i].roleTranslation.toString()); } } return description.value(role); } -QList > KFileItemModel::groups() const +QList> KFileItemModel::groups() const { if (!m_itemData.isEmpty() && m_groups.isEmpty()) { #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); #endif - switch (typeForRole(sortRole())) { - case NameRole: m_groups = nameRoleGroups(); break; - case SizeRole: m_groups = sizeRoleGroups(); break; + 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; + case SizeRole: + m_groups = sizeRoleGroups(); + break; case ModificationTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::ModificationTime); @@ -343,9 +451,15 @@ QList > KFileItemModel::groups() const return item->values.value("deletiontime").toDateTime(); }); break; - case PermissionsRole: m_groups = permissionRoleGroups(); break; - case RatingRole: m_groups = ratingRoleGroups(); break; - default: m_groups = genericStringRoleGroups(sortRole()); break; + case PermissionsRole: + m_groups = permissionRoleGroups(); + break; + case RatingRole: + m_groups = ratingRoleGroups(); + break; + default: + m_groups = genericStringRoleGroups(role); + break; } #ifdef KFILEITEMMODEL_DEBUG @@ -374,12 +488,12 @@ KFileItem KFileItemModel::fileItem(const QUrl &url) const return KFileItem(); } -int KFileItemModel::index(const KFileItem& item) const +int KFileItemModel::index(const KFileItem &item) const { return index(item.url()); } -int KFileItemModel::index(const QUrl& url) const +int KFileItemModel::index(const QUrl &url) const { const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash); @@ -429,13 +543,13 @@ int KFileItemModel::index(const QUrl& url) const } const auto uniqueKeys = indexesForUrl.uniqueKeys(); - for (const QUrl& url : uniqueKeys) { + for (const QUrl &url : uniqueKeys) { if (indexesForUrl.count(url) > 1) { qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; auto it = indexesForUrl.find(url); while (it != indexesForUrl.end() && it.key() == url) { - const ItemData* data = m_itemData.at(it.value()); + const ItemData *data = m_itemData.at(it.value()); qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item; if (data->parent) { qCWarning(DolphinDebug) << "parent" << data->parent->item; @@ -460,7 +574,7 @@ void KFileItemModel::clear() slotClear(); } -void KFileItemModel::setRoles(const QSet& roles) +void KFileItemModel::setRoles(const QSet &roles) { if (m_roles == roles) { return; @@ -484,7 +598,7 @@ void KFileItemModel::setRoles(const QSet& roles) QSetIterator it(roles); while (it.hasNext()) { - const QByteArray& role = it.next(); + const QByteArray &role = it.next(); m_requestRole[typeForRole(role)] = true; } @@ -500,8 +614,8 @@ void KFileItemModel::setRoles(const QSet& roles) // Clear the 'values' of all filtered items. They will be re-populated with the // correct roles the next time 'values' will be accessed via data(int). - QHash::iterator filteredIt = m_filteredItems.begin(); - const QHash::iterator filteredEnd = m_filteredItems.end(); + QHash::iterator filteredIt = m_filteredItems.begin(); + const QHash::iterator filteredEnd = m_filteredItems.end(); while (filteredIt != filteredEnd) { (*filteredIt)->values.clear(); ++filteredIt; @@ -533,7 +647,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded) m_dirLister->openUrl(url, KDirLister::Keep); const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); - for (const QVariant& var : previouslyExpandedChildren) { + for (const QVariant &var : previouslyExpandedChildren) { m_urlsToExpand.insert(var.toUrl()); } } else { @@ -551,6 +665,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded) m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); + m_dirLister->forgetDirs(url); const int parentLevel = expandedParentsCount(index); const int itemCount = m_itemData.count(); @@ -560,12 +675,13 @@ bool KFileItemModel::setExpanded(int index, bool expanded) int childIndex = firstChildIndex; while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { - ItemData* itemData = m_itemData.at(childIndex); + ItemData *itemData = m_itemData.at(childIndex); if (itemData->values.value("isExpanded").toBool()) { const QUrl targetUrl = itemData->item.targetUrl(); 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 + m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 + m_dirLister->forgetDirs(url); expandedChildren.append(targetUrl); } ++childIndex; @@ -624,7 +740,6 @@ void KFileItemModel::restoreExpandedDirectories(const QSet &urls) void KFileItemModel::expandParentDirectories(const QUrl &url) { - // Assure that each sub-path of the URL that should be // expanded is added to m_urlsToExpand. KDirLister // does not care whether the parent-URL has already been @@ -658,7 +773,7 @@ void KFileItemModel::expandParentDirectories(const QUrl &url) } } -void KFileItemModel::setNameFilter(const QString& nameFilter) +void KFileItemModel::setNameFilter(const QString &nameFilter) { if (m_filter.pattern() != nameFilter) { dispatchPendingItemsToInsert(); @@ -672,7 +787,7 @@ QString KFileItemModel::nameFilter() const return m_filter.pattern(); } -void KFileItemModel::setMimeTypeFilters(const QStringList& filters) +void KFileItemModel::setMimeTypeFilters(const QStringList &filters) { if (m_filter.mimeTypes() != filters) { dispatchPendingItemsToInsert(); @@ -686,49 +801,103 @@ QStringList KFileItemModel::mimeTypeFilters() const return m_filter.mimeTypes(); } +void KFileItemModel::setExcludeMimeTypeFilter(const QStringList &filters) +{ + if (m_filter.excludeMimeTypes() != filters) { + dispatchPendingItemsToInsert(); + m_filter.setExcludeMimeTypes(filters); + applyFilters(); + } +} + +QStringList KFileItemModel::excludeMimeTypeFilter() const +{ + return m_filter.excludeMimeTypes(); +} void KFileItemModel::applyFilters() { - // Check which shown items from m_itemData must get - // hidden and hence moved to m_filteredItems. - QVector newFilteredIndexes; + // ===STEP 1=== + // Check which previously shown items from m_itemData must now get + // hidden and hence moved from m_itemData into m_filteredItems. - const int itemCount = m_itemData.count(); - for (int index = 0; index < itemCount; ++index) { - ItemData* itemData = m_itemData.at(index); - - // Only filter non-expanded items as child items may never - // exist without a parent item - if (!itemData->values.value("isExpanded").toBool()) { - const KFileItem item = itemData->item; - if (!m_filter.matches(item)) { - newFilteredIndexes.append(index); - m_filteredItems.insert(item, itemData); - } + QList newFilteredIndexes; // This structure is good for prepending. We will want an ascending sorted Container at the end, this will do fine. + + // This pointer will refer to the next confirmed shown item from the point of + // view of the current "itemData" in the upcoming "for" loop. + ItemData *itemShownBelow = nullptr; + + // We will iterate backwards because it's convenient to know beforehand if the item just below is its child or not. + for (int index = m_itemData.count() - 1; index >= 0; --index) { + ItemData *itemData = m_itemData.at(index); + + if (m_filter.matches(itemData->item) || (itemShownBelow && itemShownBelow->parent == itemData)) { + // We could've entered here for two reasons: + // 1. This item passes the filter itself + // 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below + + // So this item must remain shown. + // Lets register this item as the next shown item from the point of view of the next iteration of this for loop + itemShownBelow = itemData; + } else { + // We hide this item for now, however, for expanded folders this is not final: + // if after the next "for" loop we discover that its children must now be shown with the newly applied fliter, we shall re-insert it + newFilteredIndexes.prepend(index); + m_filteredItems.insert(itemData->item, itemData); + // indexShownBelow doesn't get updated since this item will be hidden } } - const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); - removeItems(removedRanges, KeepItemData); + // This will remove the newly filtered items from m_itemData + removeItems(KItemRangeList::fromSortedContainer(newFilteredIndexes), KeepItemData); + // ===STEP 2=== // Check which hidden items from m_filteredItems should - // get visible again and hence removed from m_filteredItems. - QList newVisibleItems; + // become visible again and hence moved from m_filteredItems back into m_itemData. - QHash::iterator it = m_filteredItems.begin(); + QList newVisibleItems; + + QHash ancestorsOfNewVisibleItems; // We will make sure these also become visible in step 3. + + QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (m_filter.matches(it.key())) { newVisibleItems.append(it.value()); + + // If this is a child of an expanded folder, we must make sure that its whole parental chain will also be shown. + // We will go up through its parental chain until we either: + // 1 - reach the "root item" of the current view, i.e the currently opened folder on Dolphin. Their children have their ItemData::parent set to + // nullptr. or 2 - we reach an unfiltered parent or a previously discovered ancestor. + for (ItemData *parent = it.value()->parent; parent && !ancestorsOfNewVisibleItems.contains(parent->item) && m_filteredItems.contains(parent->item); + parent = parent->parent) { + // We wish we could remove this parent from m_filteredItems right now, but we are iterating over it + // and it would mess up the iteration. We will mark it to be removed in step 3. + ancestorsOfNewVisibleItems.insert(parent->item, parent); + } + it = m_filteredItems.erase(it); } else { + // Item remains filtered for now + // However, for expanded folders this is not final, we may discover later that it has unfiltered descendants. ++it; } } + // ===STEP 3=== + // Handles the ancestorsOfNewVisibleItems. + // Now that we are done iterating through m_filteredItems we can safely move the ancestorsOfNewVisibleItems from m_filteredItems to newVisibleItems. + for (it = ancestorsOfNewVisibleItems.begin(); it != ancestorsOfNewVisibleItems.end(); it++) { + if (m_filteredItems.remove(it.key())) { + // m_filteredItems still contained this ancestor until now so we can be sure that we aren't adding a duplicate ancestor to newVisibleItems. + newVisibleItems.append(it.value()); + } + } + + // This will insert the newly discovered unfiltered items into m_itemData insertItems(newVisibleItems); } -void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) +void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges) { if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { // There are either no filtered items, or it is not possible to expand @@ -736,14 +905,14 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) return; } - QSet parents; - for (const KItemRange& range : itemRanges) { + QSet parents; + for (const KItemRange &range : itemRanges) { for (int index = range.index; index < range.index + range.count; ++index) { parents.insert(m_itemData.at(index)); } } - QHash::iterator it = m_filteredItems.begin(); + QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (parents.contains(it.value()->parent)) { delete it.value(); @@ -754,27 +923,48 @@ 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; if (rolesInfo.isEmpty()) { int count = 0; - const RoleInfoMap* map = rolesInfoMap(count); + 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 = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); - if (map[i].groupTranslation) { - info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); - } 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; + RoleInfo info = roleInformation(map[i].role); rolesInfo.append(info); } } @@ -783,16 +973,32 @@ 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) m_groups.clear(); } -void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems) +void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) { 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; @@ -812,6 +1018,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; @@ -834,6 +1070,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() @@ -857,7 +1094,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()); } @@ -872,8 +1109,7 @@ void KFileItemModel::resortAllItems() // Determine the first index that has been moved. int firstMovedIndex = 0; - while (firstMovedIndex < itemCount - && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { + while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { ++firstMovedIndex; } @@ -882,8 +1118,7 @@ void KFileItemModel::resortAllItems() m_groups.clear(); int lastMovedIndex = itemCount - 1; - while (lastMovedIndex > firstMovedIndex - && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { + while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { --lastMovedIndex; } @@ -901,9 +1136,10 @@ 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; + const QList> oldGroups = m_groups; m_groups.clear(); if (groups() != oldGroups) { Q_EMIT groupsChanged(); @@ -927,7 +1163,7 @@ void KFileItemModel::slotCompleted() // -> we expand the first visible URL we find in m_restoredExpandedUrls. // Iterate over a const copy because items are deleted and inserted within the loop const auto urlsToExpand = m_urlsToExpand; - for(const QUrl &url : urlsToExpand) { + for (const QUrl &url : urlsToExpand) { const int indexForUrl = index(url); if (indexForUrl >= 0) { m_urlsToExpand.remove(url); @@ -955,16 +1191,11 @@ void KFileItemModel::slotCanceled() Q_EMIT directoryLoadingCanceled(); } -void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) +void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList &items) { Q_ASSERT(!items.isEmpty()); - QUrl parentUrl; - if (m_expandedDirs.contains(directoryUrl)) { - parentUrl = m_expandedDirs.value(directoryUrl); - } else { - parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); - } + const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash)); if (m_requestRole[ExpandedParentsCountRole]) { // If the expanding of items is enabled, the call @@ -993,21 +1224,33 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis } } - const QList itemDataList = createItemDataList(parentUrl, items); + const QList itemDataList = createItemDataList(parentUrl, items); if (!m_filter.hasSetFilters()) { m_pendingItemsToInsert.append(itemDataList); } else { + QSet parentsToEnsureVisible; + // The name or type filter is active. Hide filtered items // before inserting them into the model and remember // the filtered items in m_filteredItems. - for (ItemData* itemData : itemDataList) { + for (ItemData *itemData : itemDataList) { if (m_filter.matches(itemData->item)) { m_pendingItemsToInsert.append(itemData); + if (itemData->parent) { + parentsToEnsureVisible.insert(itemData->parent); + } } else { m_filteredItems.insert(itemData->item, itemData); } } + + // Entire parental chains must be shown + for (ItemData *parent : parentsToEnsureVisible) { + for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) { + m_pendingItemsToInsert.append(parent); + } + } } if (!m_maximumUpdateIntervalTimer->isActive()) { @@ -1015,27 +1258,78 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis // emitted during the maximum update interval. m_maximumUpdateIntervalTimer->start(); } + + Q_EMIT fileItemsChanged({KFileItem(directoryUrl)}); +} + +int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet &parentsToEnsureVisible) +{ + int filteredParentsCount = 0; + // The childless parents not yet removed will always be right above the start of a removed range. + // We iterate backwards to ensure the deepest folders are processed before their parents + for (int i = removedItemRanges.size() - 1; i >= 0; i--) { + KItemRange itemRange = removedItemRanges.at(i); + const ItemData *const firstInRange = m_itemData.at(itemRange.index); + ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr; + const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr; + + if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove) + && !parentsToEnsureVisible.contains(itemAbove)) { + // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible + // and this deleted range covers all of its descendents, so none will be left. + m_filteredItems.insert(itemAbove->item, itemAbove); + // This range's starting index will be extended to include the parent above: + --itemRange.index; + ++itemRange.count; + ++filteredParentsCount; + KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange(); + // We must check if this caused the range to touch the previous range, if that's the case they shall be merged + if (i > 0 && previousRange.index + previousRange.count == itemRange.index) { + previousRange.count += itemRange.count; + removedItemRanges.replace(i - 1, previousRange); + removedItemRanges.removeAt(i); + } else { + removedItemRanges.replace(i, itemRange); + // We must revisit this range in the next iteration since its starting index changed + ++i; + } + } + } + return filteredParentsCount; } -void KFileItemModel::slotItemsDeleted(const KFileItemList& items) +void KFileItemModel::slotItemsDeleted(const KFileItemList &items) { dispatchPendingItemsToInsert(); QVector indexesToRemove; indexesToRemove.reserve(items.count()); + KFileItemList dirsChanged; + + const auto currentDir = directory(); + + for (const KFileItem &item : items) { + if (item.url() == currentDir) { + Q_EMIT currentDirectoryRemoved(); + return; + } - for (const KFileItem& item : items) { const int indexForItem = index(item); if (indexForItem >= 0) { indexesToRemove.append(indexForItem); } else { // Probably the item has been filtered. - QHash::iterator it = m_filteredItems.find(item); + QHash::iterator it = m_filteredItems.find(item); if (it != m_filteredItems.end()) { delete it.value(); m_filteredItems.erase(it); } } + + QUrl parentUrl = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); + if (dirsChanged.findByUrl(parentUrl).isNull()) { + dirsChanged << KFileItem(parentUrl); + } } std::sort(indexesToRemove.begin(), indexesToRemove.end()); @@ -1046,7 +1340,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); @@ -1060,12 +1354,22 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) indexesToRemove = indexesToRemoveWithChildren; } - const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); + KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); removeFilteredChildren(itemRanges); - removeItems(itemRanges, DeleteItemData); + + // This call will update itemRanges to include the childless parents that have been filtered. + const int filteredParentsCount = filterChildlessParents(itemRanges); + + // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted + // mixed with expanded folders that are just being filtered out. + // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint + // so removeItems() will check m_filteredItems to differentiate which is which. + removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData); + + Q_EMIT fileItemsChanged(dirsChanged); } -void KFileItemModel::slotRefreshItems(const QList >& items) +void KFileItemModel::slotRefreshItems(const QList> &items) { Q_ASSERT(!items.isEmpty()); #ifdef KFILEITEMMODEL_DEBUG @@ -1077,59 +1381,151 @@ void KFileItemModel::slotRefreshItems(const QList >& indexes.reserve(items.count()); QSet changedRoles; + KFileItemList changedFiles; + + // Contains the indexes of the currently visible items + // that should get hidden and hence moved to m_filteredItems. + QVector newFilteredIndexes; + + // Contains currently hidden items that should + // get visible and hence removed from m_filteredItems + QList newVisibleItems; + + QListIterator> it(items); - QListIterator > it(items); while (it.hasNext()) { - const QPair& itemPair = it.next(); - const KFileItem& oldItem = itemPair.first; - const KFileItem& newItem = itemPair.second; + const QPair &itemPair = it.next(); + const KFileItem &oldItem = itemPair.first; + const KFileItem &newItem = itemPair.second; const int indexForItem = index(oldItem); + const bool newItemMatchesFilter = m_filter.matches(newItem); if (indexForItem >= 0) { m_itemData[indexForItem]->item = newItem; // Keep old values as long as possible if they could not retrieved synchronously yet. // The update of the values will be done asynchronously by KFileItemModelRolesUpdater. - QHashIterator it(retrieveData(newItem, m_itemData.at(indexForItem)->parent)); - QHash& values = m_itemData[indexForItem]->values; + ItemData *const itemData = m_itemData.at(indexForItem); + QHashIterator it(retrieveData(newItem, itemData->parent)); while (it.hasNext()) { it.next(); - const QByteArray& role = it.key(); - if (values.value(role) != it.value()) { - values.insert(role, it.value()); + const QByteArray &role = it.key(); + if (itemData->values.value(role) != it.value()) { + itemData->values.insert(role, it.value()); changedRoles.insert(role); } } m_items.remove(oldItem.url()); + // We must maintain m_items consistent with m_itemData for now, this very loop is using it. + // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated. m_items.insert(newItem.url(), indexForItem); - indexes.append(indexForItem); + if (newItemMatchesFilter + || (itemData->values.value("isExpanded").toBool() + && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) { + // We are lenient with expanded folders that originally had visible children. + // If they become childless now they will be caught by filterChildlessParents() + changedFiles.append(newItem); + indexes.append(indexForItem); + } else { + newFilteredIndexes.append(indexForItem); + m_filteredItems.insert(newItem, itemData); + } } else { // Check if 'oldItem' is one of the filtered items. - QHash::iterator it = m_filteredItems.find(oldItem); + QHash::iterator it = m_filteredItems.find(oldItem); if (it != m_filteredItems.end()) { - ItemData* itemData = it.value(); + ItemData *const itemData = it.value(); itemData->item = newItem; // The data stored in 'values' might have changed. Therefore, we clear // 'values' and re-populate it the next time it is requested via data(int). + // Before clearing, we must remember if it was expanded and the expanded parents count, + // otherwise these states would be lost. The data() method will deal with this special case. + const bool isExpanded = itemData->values.value("isExpanded").toBool(); + bool hasExpandedParentsCount = false; + const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); itemData->values.clear(); + if (isExpanded) { + itemData->values.insert("isExpanded", true); + if (hasExpandedParentsCount) { + itemData->values.insert("expandedParentsCount", expandedParentsCount); + } + } m_filteredItems.erase(it); - m_filteredItems.insert(newItem, itemData); + if (newItemMatchesFilter) { + newVisibleItems.append(itemData); + } else { + m_filteredItems.insert(newItem, itemData); + } + } + } + } + + std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end()); + + // We must keep track of parents of new visible items since they must be shown no matter what + // They will be considered "immune" to filterChildlessParents() + QSet parentsToEnsureVisible; + + for (ItemData *item : newVisibleItems) { + for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) { + parentsToEnsureVisible.insert(parent); + } + } + for (ItemData *parent : parentsToEnsureVisible) { + // We make sure they are all unfiltered. + if (m_filteredItems.remove(parent->item)) { + // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems + newVisibleItems.append(parent); + // It could be in newFilteredIndexes, we must remove it if it's there: + const int parentIndex = index(parent->item); + if (parentIndex >= 0) { + QVector::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex); + if (it != newFilteredIndexes.end() && *it == parentIndex) { + newFilteredIndexes.erase(it); + } } } } + KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); + + // This call will update itemRanges to include the childless parents that have been filtered. + filterChildlessParents(removedRanges, parentsToEnsureVisible); + + removeItems(removedRanges, KeepItemData); + + // Show previously hidden items that should get visible + insertItems(newVisibleItems); + + // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic. + // If the changed items have been created recently, they might not be in m_items yet. // In that case, the list 'indexes' might be empty. if (indexes.isEmpty()) { return; } + if (newVisibleItems.count() > 0 || removedRanges.count() > 0) { + // The original indexes have changed and are now worthless since items were removed and/or inserted. + indexes.clear(); + // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes. + const QSet changedFilesSet(changedFiles.cbegin(), changedFiles.cend()); + for (int i = 0; i < m_itemData.count(); i++) { + if (changedFilesSet.contains(m_itemData.at(i)->item)) { + indexes.append(i); + } + } + } else { + std::sort(indexes.begin(), indexes.end()); + } + // Extract the item-ranges out of the changed indexes - std::sort(indexes.begin(), indexes.end()); const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); + + Q_EMIT fileItemsChanged(changedFiles); } void KFileItemModel::slotClear() @@ -1173,7 +1569,7 @@ void KFileItemModel::dispatchPendingItemsToInsert() } } -void KFileItemModel::insertItems(QList& newItems) +void KFileItemModel::insertItems(QList &newItems) { if (newItems.isEmpty()) { return; @@ -1196,8 +1592,7 @@ void KFileItemModel::insertItems(QList& newItems) if (m_sortRole == NameRole) { parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount()); } else if (isRoleValueNatural(m_sortRole)) { - auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) - { + auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) { const QByteArray role = roleForType(m_sortRole); return a->values.value(role).toString() < b->values.value(role).toString(); }; @@ -1236,7 +1631,7 @@ void KFileItemModel::insertItems(QList& newItems) int rangeCount = 0; while (sourceIndexNewItems >= 0) { - ItemData* newItem = newItems.at(sourceIndexNewItems); + ItemData *newItem = newItems.at(sourceIndexNewItems); if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) { // Move an existing item to its new position. If any new items // are behind it, push the item range to itemRanges. @@ -1276,7 +1671,7 @@ void KFileItemModel::insertItems(QList& newItems) #endif } -void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) +void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior) { if (itemRanges.isEmpty()) { return; @@ -1286,11 +1681,11 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe // Step 1: Remove the items from m_itemData, and free the ItemData. int removedItemsCount = 0; - for (const KItemRange& range : itemRanges) { + for (const KItemRange &range : itemRanges) { removedItemsCount += range.count; for (int index = range.index; index < range.index + range.count; ++index) { - if (behavior == DeleteItemData) { + if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) { delete m_itemData.at(index); } @@ -1325,23 +1720,24 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe Q_EMIT itemsRemoved(itemRanges); } -QList KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const +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). determineMimeTypes(items, 200); } + // We search for the parent in m_itemData and then in m_filteredItems if necessary const int parentIndex = index(parentUrl); - ItemData* parentItem = parentIndex < 0 ? nullptr : m_itemData.at(parentIndex); + ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex); - QList itemDataList; + QList itemDataList; itemDataList.reserve(items.count()); - for (const KFileItem& item : items) { - ItemData* itemData = new ItemData(); + for (const KFileItem &item : items) { + ItemData *itemData = new ItemData(); itemData->item = item; itemData->parent = parentItem; itemDataList.append(itemData); @@ -1350,9 +1746,10 @@ 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: case GroupRole: @@ -1361,7 +1758,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); } @@ -1370,7 +1767,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()) { @@ -1390,11 +1787,17 @@ void KFileItemModel::prepareItemsForSorting(QList& itemDataList) } } -int KFileItemModel::expandedParentsCount(const ItemData* data) +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" // if the corresponding item is expanded, and it is not a top-level item. - const ItemData* parent = data->parent; + const ItemData *parent = data->parent; if (parent) { if (parent->parent) { Q_ASSERT(parent->values.contains("expandedParentsCount")); @@ -1413,7 +1816,7 @@ void KFileItemModel::removeExpandedItems() const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { - const ItemData* itemData = m_itemData.at(i); + const ItemData *itemData = m_itemData.at(i); if (itemData->parent) { indexesToRemove.append(i); } @@ -1423,8 +1826,8 @@ void KFileItemModel::removeExpandedItems() m_expandedDirs.clear(); // Also remove all filtered items which have a parent. - QHash::iterator it = m_filteredItems.begin(); - const QHash::iterator end = m_filteredItems.end(); + QHash::iterator it = m_filteredItems.begin(); + const QHash::iterator end = m_filteredItems.end(); while (it != end) { if (it.value()->parent) { @@ -1436,14 +1839,15 @@ void KFileItemModel::removeExpandedItems() } } -void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet& changedRoles) +void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &itemRanges, const QSet &changedRoles) { Q_EMIT itemsChanged(itemRanges, changedRoles); // 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))) { - for (const KItemRange& range : itemRanges) { + 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; const int first = range.index; @@ -1453,11 +1857,9 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& i // (a) The first item in the range is "lessThan" its predecessor, // (b) the successor of the last item is "lessThan" the last item, or // (c) the internal order of the items in the range is incorrect. - if (first > 0 - && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { + if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { needsResorting = true; - } else if (last < count() - 1 - && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { + } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { needsResorting = true; } else { for (int index = first; index < last; ++index) { @@ -1469,7 +1871,7 @@ void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& i } if (needsResorting) { - m_resortAllItemsTimer->start(); + scheduleResortAllItems(); return; } } @@ -1495,14 +1897,14 @@ void KFileItemModel::resetRoles() } } -KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const +KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray &role) const { static QHash roles; if (roles.isEmpty()) { // Insert user visible roles that can be accessed with // KFileItemModel::roleInformation() int count = 0; - const RoleInfoMap* map = rolesInfoMap(count); + const RoleInfoMap *map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { roles.insert(map[i].role, map[i].roleType); } @@ -1529,7 +1931,7 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const // Insert user visible roles that can be accessed with // KFileItemModel::roleInformation() int count = 0; - const RoleInfoMap* map = rolesInfoMap(count); + const RoleInfoMap *map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { roles.insert(map[i].roleType, map[i].role); } @@ -1549,7 +1951,7 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const return roles.value(roleType); } -QHash KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const +QHash KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const { // It is important to insert only roles that are fast to retrieve. E.g. // KFileItem::iconName() can be very expensive if the MIME-type is unknown @@ -1567,13 +1969,18 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, } if (m_requestRole[IsHiddenRole]) { - data.insert(sharedValue("isHidden"), item.isHidden()); + data.insert(sharedValue("isHidden"), item.isHidden() || item.mimetype() == QStringLiteral("application/x-trash")); } if (m_requestRole[NameRole]) { data.insert(sharedValue("text"), item.text()); } + if (m_requestRole[ExtensionRole] && !isDir) { + // TODO KF6 use KFileItem::suffix 464722 + data.insert(sharedValue("extension"), QFileInfo(item.name()).suffix()); + } + if (m_requestRole[SizeRole] && !isDir) { data.insert(sharedValue("size"), item.size()); } @@ -1603,7 +2010,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, } if (m_requestRole[PermissionsRole]) { - data.insert(sharedValue("permissions"), item.permissionsString()); + data.insert(sharedValue("permissions"), QVariantList() << item.permissionsString() << item.permissions()); } if (m_requestRole[OwnerRole]) { @@ -1684,7 +2091,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, return data; } -bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const +bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const { int result = 0; @@ -1719,26 +2126,38 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QColla } } - if (m_sortDirsFirst || 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 = 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; + } + } + 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 +void KFileItemModel::sort(const QList::iterator &begin, const QList::iterator &end) const { - auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) - { + auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) { return lessThan(a, b, m_collator); }; @@ -1755,10 +2174,15 @@ void KFileItemModel::sort(const QList::iterator &begi } } -int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const +int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const { - const KFileItem& itemA = a->item; - const KFileItem& itemB = b->item; + // This function must never return 0, because that would break stable + // sorting, which leads to all kinds of bugs. + // See: https://bugs.kde.org/show_bug.cgi?id=433247 + // If two items have equal sort values, let the fallbacks at the bottom of + // the function handle it. + const KFileItem &itemA = a->item; + const KFileItem &itemB = b->item; int result = 0; @@ -1768,44 +2192,43 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const break; case SizeRole: { - if (itemA.isDir()) { - // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): - Q_ASSERT(itemB.isDir()); - - QVariant valueA, valueB; - if (DetailsModeSettings::directorySizeCount()) { - valueA = a->values.value("count"); - valueB = b->values.value("count"); - } else { - // use dir size then - valueA = a->values.value("size"); - valueB = b->values.value("size"); - } - if (valueA.isNull() && valueB.isNull()) { - result = 0; - } else if (valueA.isNull()) { - result = -1; + 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"); + auto valueB = b->values.value("count"); + if (valueA.isNull()) { + if (!valueB.isNull()) { + return -1; + } } else if (valueB.isNull()) { - result = +1; + return +1; } else { if (valueA.toLongLong() < valueB.toLongLong()) { return -1; - } else { + } else if (valueA.toLongLong() > valueB.toLongLong()) { return +1; } } + break; + } + + KIO::filesize_t sizeA = 0; + if (itemA.isDir()) { + sizeA = a->values.value("size").toULongLong(); } else { - // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): - Q_ASSERT(!itemB.isDir()); - const KIO::filesize_t sizeA = itemA.size(); - const KIO::filesize_t sizeB = itemB.size(); - if (sizeA > sizeB) { - result = +1; - } else if (sizeA < sizeB) { - result = -1; - } else { - result = 0; - } + sizeA = itemA.size(); + } + KIO::filesize_t sizeB = 0; + if (itemB.isDir()) { + sizeB = b->values.value("size").toULongLong(); + } else { + sizeB = itemB.size(); + } + if (sizeA < sizeB) { + return -1; + } else if (sizeA > sizeB) { + return +1; } break; } @@ -1814,9 +2237,20 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); if (dateTimeA < dateTimeB) { - result = -1; + return -1; + } else if (dateTimeA > dateTimeB) { + return +1; + } + break; + } + + case AccessTimeRole: { + const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); + const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); + if (dateTimeA < dateTimeB) { + return -1; } else if (dateTimeA > dateTimeB) { - result = +1; + return +1; } break; } @@ -1825,9 +2259,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); if (dateTimeA < dateTimeB) { - result = -1; + return -1; } else if (dateTimeA > dateTimeB) { - result = +1; + return +1; } break; } @@ -1836,9 +2270,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); if (dateTimeA < dateTimeB) { - result = -1; + return -1; } else if (dateTimeA > dateTimeB) { - result = +1; + return +1; } break; } @@ -1846,6 +2280,8 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const case RatingRole: case WidthRole: case HeightRole: + case PublisherRole: + case PageCountRole: case WordCountRole: case LineCountRole: case TrackRole: @@ -1854,14 +2290,27 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const break; } + case DimensionsRole: { + const QByteArray role = roleForType(m_sortRole); + const QSize dimensionsA = a->values.value(role).toSize(); + const QSize dimensionsB = b->values.value(role).toSize(); + + if (dimensionsA.width() == dimensionsB.width()) { + result = dimensionsA.height() - dimensionsB.height(); + } else { + result = dimensionsA.width() - dimensionsB.width(); + } + break; + } + default: { const QByteArray role = roleForType(m_sortRole); const QString roleValueA = a->values.value(role).toString(); const QString roleValueB = b->values.value(role).toString(); if (!roleValueA.isEmpty() && roleValueB.isEmpty()) { - result = -1; + return -1; } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) { - result = +1; + return +1; } else if (isRoleValueNatural(m_sortRole)) { result = stringCompare(roleValueA, roleValueB, collator); } else { @@ -1869,7 +2318,6 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const } break; } - } if (result != 0) { @@ -1895,12 +2343,131 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } -int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const +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()); if (m_naturalSorting) { - return collator.compare(a, b); + // Split extension, taking into account it can be empty + constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep; + + // Sort by baseName first + const QString aBaseName = a.section('.', 0, 0, flags); + const QString bBaseName = b.section('.', 0, 0, flags); + + const int res = collator.compare(aBaseName, bBaseName); + if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) { + return res; + } + + // sliced() has undefined behavior when pos < 0 or pos > size(). + Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0); + Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0); + + // baseNames were equal, sort by extension + return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length())); } const int result = QString::compare(a, b, collator.caseSensitivity()); @@ -1914,121 +2481,122 @@ 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(); - // 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. - - // 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)); - } - } + QMutexLocker collatorLock(s_collatorMutex()); - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; + // 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)); + } + } - 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; + 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; - - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } + const KFileItem item = itemData->item; + fileSize = !item.isNull() ? item.size() : ~0U; - const KFileItem& item = m_itemData.at(i)->item; - const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; - QString newGroupValue; - if (!item.isNull() && item.isDir()) { - newGroupValue = i18nc("@title:group Size", "Folders"); - } else if (fileSize < 5 * 1024 * 1024) { - newGroupValue = i18nc("@title:group Size", "Small"); - } else if (fileSize < 10 * 1024 * 1024) { - newGroupValue = i18nc("@title:group Size", "Medium"); + groupInfo.comparable = -1; // None + if (!item.isNull() && item.isDir()) { + if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) { + groupInfo.comparable = 0; // Folders } else { - newGroupValue = i18nc("@title:group Size", "Big"); + fileSize = itemData->values.value("size").toULongLong(); } - - 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(); @@ -2039,6 +2607,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< continue; } + const QLocale locale; const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); const QDate fileDate = fileTime.date(); if (fileDate == previousFileDate) { @@ -2049,261 +2618,447 @@ QList > KFileItemModel::timeRoleGroups(const std::function< const int daysDistance = fileDate.daysTo(currentDate); - QString newGroupValue; - if (currentDate.year() == fileDate.year() && - currentDate.month() == fileDate.month()) { - + 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 + } + } 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 + } + } + 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"); break; - case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; + case 0: + groupInfo.text = i18nc("@title:group Date", "Today"); + break; + case 1: + groupInfo.text = i18nc("@title:group Date", "Yesterday"); + break; default: - newGroupValue = fileTime.toString( - i18nc("@title:group Date: The week day name: dddd", "dddd")); - newGroupValue = i18nc("Can be used to script translation of \"dddd\"" - "with context @title:group Date", "%1", newGroupValue); + newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd")); + newGroupValue = i18nc( + "Can be used to script translation of \"dddd\"" + "with context @title:group Date", + "%1", + 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); } } else { const QDate lastMonthDate = currentDate.addMonths(-1); - if (lastMonthDate.year() == fileDate.year() && - lastMonthDate.month() == fileDate.month()) { - + if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) { if (daysDistance == 1) { - const KLocalizedString format = ki18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Yesterday' (MMMM, yyyy)"); + const KLocalizedString format = ki18nc( + "@title:group Date: " + "MMMM is full month name in current locale, and yyyy is " + "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " + "part of the text that should not be formatted as a date", + "'Yesterday' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { - newGroupValue = fileTime.toString(translatedFormat); - newGroupValue = i18nc("Can be used to script translation of " + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( + "Can be used to script translation of " "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } else { - qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; - const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); - newGroupValue = fileTime.toString(untranslatedFormat); + qCWarning(DolphinDebug).nospace() + << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; + const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } } else if (daysDistance <= 7) { - newGroupValue = fileTime.toString(i18nc("@title:group Date: " - "The week day name: dddd, MMMM is full month name " - "in current locale, and yyyy is full year number", - "dddd (MMMM, yyyy)")); - newGroupValue = i18nc("Can be used to script translation of " + newGroupValue = locale.toString(fileTime, + i18nc("@title:group Date: " + "The week day name: dddd, MMMM is full month name " + "in current locale, and yyyy is full year number.", + "dddd (MMMM, yyyy)")); + newGroupValue = i18nc( + "Can be used to script translation of " "\"dddd (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); - } else if (daysDistance <= 7 * 2) { - const KLocalizedString format = ki18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'One Week Ago' (MMMM, yyyy)"); + "%1", + groupInfo.text); + } else if (daysDistance < 7 * 2) { + const KLocalizedString format = ki18nc( + "@title:group Date: " + "MMMM is full month name in current locale, and yyyy is " + "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " + "part of the text that should not be formatted as a date", + "'One Week Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { - newGroupValue = fileTime.toString(translatedFormat); - newGroupValue = i18nc("Can be used to script translation of " + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( + "Can be used to script translation of " "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } else { - qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; - const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); - newGroupValue = fileTime.toString(untranslatedFormat); + qCWarning(DolphinDebug).nospace() + << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; + const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } - } else if (daysDistance <= 7 * 3) { - const KLocalizedString format = ki18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Two Weeks Ago' (MMMM, yyyy)"); + } else if (daysDistance < 7 * 3) { + const KLocalizedString format = ki18nc( + "@title:group Date: " + "MMMM is full month name in current locale, and yyyy is " + "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " + "part of the text that should not be formatted as a date", + "'Two Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { - newGroupValue = fileTime.toString(translatedFormat); - newGroupValue = i18nc("Can be used to script translation of " + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( + "Can be used to script translation of " "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } else { - qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; - const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); - newGroupValue = fileTime.toString(untranslatedFormat); + qCWarning(DolphinDebug).nospace() + << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; + const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } - } else if (daysDistance <= 7 * 4) { - const KLocalizedString format = ki18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Three Weeks Ago' (MMMM, yyyy)"); + } else if (daysDistance < 7 * 4) { + const KLocalizedString format = ki18nc( + "@title:group Date: " + "MMMM is full month name in current locale, and yyyy is " + "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " + "part of the text that should not be formatted as a date", + "'Three Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { - newGroupValue = fileTime.toString(translatedFormat); - newGroupValue = i18nc("Can be used to script translation of " + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( + "Can be used to script translation of " "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } else { - qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; - const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); - newGroupValue = fileTime.toString(untranslatedFormat); + qCWarning(DolphinDebug).nospace() + << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; + const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } } else { - const KLocalizedString format = ki18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Earlier on' MMMM, yyyy"); + const KLocalizedString format = ki18nc( + "@title:group Date: " + "MMMM is full month name in current locale, and yyyy is " + "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " + "part of the text that should not be formatted as a date", + "'Earlier on' MMMM, yyyy"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { - newGroupValue = fileTime.toString(translatedFormat); - newGroupValue = i18nc("Can be used to script translation of " + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { + newGroupValue = locale.toString(fileTime, translatedFormat); + newGroupValue = i18nc( + "Can be used to script translation of " "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } else { - qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; - const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); - newGroupValue = fileTime.toString(untranslatedFormat); + qCWarning(DolphinDebug).nospace() + << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; + const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); + newGroupValue = locale.toString(fileTime, untranslatedFormat); } } } else { - newGroupValue = fileTime.toString(i18nc("@title:group " - "The month and year: MMMM is full month name in current locale, " - "and yyyy is full year number", "MMMM, yyyy")); - newGroupValue = i18nc("Can be used to script translation of " + newGroupValue = locale.toString(fileTime, + i18nc("@title:group " + "The month and year: MMMM is full month name in current locale, " + "and yyyy is full year number", + "MMMM, yyyy")); + newGroupValue = i18nc( + "Can be used to script translation of " "\"MMMM, yyyy\" with context @title:group Date", - "%1", newGroupValue); + "%1", + groupInfo.text); } } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); - } } - - return groups; + oldWithString = withString; + 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()); + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; - const int maxIndex = count() - 1; - QList > groups; - - QString permissionsString; - QString groupValue; - 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; - - const QFileInfo info(itemData->item.url().toLocalFile()); + 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 (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); + } + oldWithString = withString; + 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 %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; } -QList > KFileItemModel::ratingRoleGroups() const +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; + 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; + 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; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } @@ -2335,54 +3090,62 @@ void KFileItemModel::emitSortProgress(int resolvedCount) } } -const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) +const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count) { static const RoleInfoMap rolesInfoMap[] = { - // | role | roleType | role translation | group translation | requires Baloo | requires indexer - { nullptr, NoRole, nullptr, nullptr, nullptr, nullptr, false, false }, - { "text", NameRole, I18NC_NOOP("@label", "Name"), nullptr, nullptr, false, false }, - { "size", SizeRole, I18NC_NOOP("@label", "Size"), nullptr, nullptr, false, false }, - { "modificationtime", ModificationTimeRole, I18NC_NOOP("@label", "Modified"), nullptr, nullptr, false, false }, - { "creationtime", CreationTimeRole, I18NC_NOOP("@label", "Created"), nullptr, nullptr, false, false }, - { "accesstime", AccessTimeRole, I18NC_NOOP("@label", "Accessed"), nullptr, nullptr, false, false }, - { "type", TypeRole, I18NC_NOOP("@label", "Type"), nullptr, nullptr, false, false }, - { "rating", RatingRole, I18NC_NOOP("@label", "Rating"), nullptr, nullptr, true, false }, - { "tags", TagsRole, I18NC_NOOP("@label", "Tags"), nullptr, nullptr, true, false }, - { "comment", CommentRole, I18NC_NOOP("@label", "Comment"), nullptr, nullptr, true, false }, - { "title", TitleRole, I18NC_NOOP("@label", "Title"), I18NC_NOOP("@label", "Document"), true, true }, - { "wordCount", WordCountRole, I18NC_NOOP("@label", "Word Count"), I18NC_NOOP("@label", "Document"), true, true }, - { "lineCount", LineCountRole, I18NC_NOOP("@label", "Line Count"), I18NC_NOOP("@label", "Document"), true, true }, - { "imageDateTime", ImageDateTimeRole, I18NC_NOOP("@label", "Date Photographed"), I18NC_NOOP("@label", "Image"), true, true }, - { "width", WidthRole, I18NC_NOOP("@label", "Width"), I18NC_NOOP("@label", "Image"), true, true }, - { "height", HeightRole, I18NC_NOOP("@label", "Height"), I18NC_NOOP("@label", "Image"), true, true }, - { "orientation", OrientationRole, I18NC_NOOP("@label", "Orientation"), I18NC_NOOP("@label", "Image"), true, true }, - { "artist", ArtistRole, I18NC_NOOP("@label", "Artist"), I18NC_NOOP("@label", "Audio"), true, true }, - { "genre", GenreRole, I18NC_NOOP("@label", "Genre"), I18NC_NOOP("@label", "Audio"), true, true }, - { "album", AlbumRole, I18NC_NOOP("@label", "Album"), I18NC_NOOP("@label", "Audio"), true, true }, - { "duration", DurationRole, I18NC_NOOP("@label", "Duration"), I18NC_NOOP("@label", "Audio"), true, true }, - { "bitrate", BitrateRole, I18NC_NOOP("@label", "Bitrate"), I18NC_NOOP("@label", "Audio"), true, true }, - { "track", TrackRole, I18NC_NOOP("@label", "Track"), I18NC_NOOP("@label", "Audio"), true, true }, - { "releaseYear", ReleaseYearRole, I18NC_NOOP("@label", "Release Year"), I18NC_NOOP("@label", "Audio"), true, true }, - { "aspectRatio", AspectRatioRole, I18NC_NOOP("@label", "Aspect Ratio"), I18NC_NOOP("@label", "Video"), true, true }, - { "frameRate", FrameRateRole, I18NC_NOOP("@label", "Frame Rate"), I18NC_NOOP("@label", "Video"), true, true }, - { "path", PathRole, I18NC_NOOP("@label", "Path"), I18NC_NOOP("@label", "Other"), false, false }, - { "deletiontime", DeletionTimeRole, I18NC_NOOP("@label", "Deletion Time"), I18NC_NOOP("@label", "Other"), false, false }, - { "destination", DestinationRole, I18NC_NOOP("@label", "Link Destination"), I18NC_NOOP("@label", "Other"), false, false }, - { "originUrl", OriginUrlRole, I18NC_NOOP("@label", "Downloaded From"), I18NC_NOOP("@label", "Other"), true, false }, - { "permissions", PermissionsRole, I18NC_NOOP("@label", "Permissions"), I18NC_NOOP("@label", "Other"), false, false }, - { "owner", OwnerRole, I18NC_NOOP("@label", "Owner"), I18NC_NOOP("@label", "Other"), false, false }, - { "group", GroupRole, I18NC_NOOP("@label", "User Group"), I18NC_NOOP("@label", "Other"), false, false }, + // clang-format off + // | role | roleType | role translation | group translation | requires Baloo | requires indexer + { 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 }, + { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, + { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, + { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, + { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, + { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, + { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, + { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "publisher", PublisherRole, kli18nc("@label", "Publisher"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "pageCount", PageCountRole, kli18nc("@label", "Page Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, + { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, + { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, + { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, + { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, + { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, + { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, + { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, + { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, + { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, + { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, + { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, + { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, + { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, + { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), KLazyLocalizedString(), true, false }, + { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), kli18nc("@tooltip", "The permission format can be changed in settings. Options are Symbolic, Numeric (Octal) or Combined formats"), false, false }, + { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, + { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, }; + // clang-format on count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); return rolesInfoMap; } -void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) +void KFileItemModel::determineMimeTypes(const KFileItemList &items, int timeout) { QElapsedTimer timer; timer.start(); - for (const KFileItem& item : items) { + for (const KFileItem &item : items) { // Only determine mime types for files here. For directories, // KFileItem::determineMimeType() reads the .directory file inside to // load the icon, but this is not necessary at all if we just need the @@ -2400,7 +3163,7 @@ void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) } } -QByteArray KFileItemModel::sharedValue(const QByteArray& value) +QByteArray KFileItemModel::sharedValue(const QByteArray &value) { static QSet pool; const QSet::const_iterator it = pool.constFind(value); @@ -2437,14 +3200,13 @@ bool KFileItemModel::isConsistent() const // Check if the items are sorted correctly. if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) { - qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" - << fileItem(i - 1) << fileItem(i); + qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i); return false; } // Check if all parent-child relationships are consistent. - const ItemData* data = m_itemData.at(i); - const ItemData* parent = data->parent; + const ItemData *data = m_itemData.at(i); + const ItemData *parent = data->parent; if (parent) { if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; @@ -2453,7 +3215,8 @@ bool KFileItemModel::isConsistent() const const int parentIndex = index(parent->item); if (parentIndex >= i) { - qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item; + qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" + << data->item; return false; } } @@ -2461,3 +3224,18 @@ bool KFileItemModel::isConsistent() const return true; } + +void KFileItemModel::slotListerError(KIO::Job *job) +{ + const int jobError = job->error(); + if (jobError == KIO::ERR_IS_FILE) { + if (auto *listJob = qobject_cast(job)) { + Q_EMIT urlIsFileError(listJob->url()); + } + } else { + const QString errorString = job->errorString(); + Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError); + } +} + +#include "moc_kfileitemmodel.cpp"