X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/a058c64eba30f11bfa87ac097371c04437d0471e..8bc7f72a731904f4cc0669333e5db39613246e9e:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index ac3c33e22..d4d0be0e8 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,42 +1,35 @@ -/***************************************************************************** - * Copyright (C) 2011 by Peter Penz * - * Copyright (C) 2013 by Frank Reininghaus * - * Copyright (C) 2013 by Emmanuel Pescosta * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * - *****************************************************************************/ +/* + * SPDX-FileCopyrightText: 2011 Peter Penz + * SPDX-FileCopyrightText: 2013 Frank Reininghaus + * SPDX-FileCopyrightText: 2013 Emmanuel Pescosta + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ #include "kfileitemmodel.h" #include "dolphin_generalsettings.h" #include "dolphin_detailsmodesettings.h" #include "dolphindebug.h" -#include "private/kfileitemmodeldirlister.h" #include "private/kfileitemmodelsortalgorithm.h" +#include +#include #include +#include #include #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 @@ -44,6 +37,7 @@ KFileItemModel::KFileItemModel(QObject* parent) : KItemModelBase("text", parent), m_dirLister(nullptr), m_sortDirsFirst(true), + m_sortHiddenLast(false), m_sortRole(NameRole), m_sortingProgressPercent(-1), m_roles(), @@ -63,7 +57,8 @@ KFileItemModel::KFileItemModel(QObject* parent) : 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); @@ -71,18 +66,17 @@ KFileItemModel::KFileItemModel(QObject* parent) : 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, QOverload::of(&KCoreDirLister::completed), this, &KFileItemModel::slotCompleted); - 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); + 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); // Apply default roles that should be determined resetRoles(); @@ -158,6 +152,18 @@ QHash KFileItemModel::data(int index) const 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; @@ -217,6 +223,19 @@ 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::setShowHiddenFiles(bool show) { m_dirLister->setShowingDotFiles(show); @@ -247,7 +266,7 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const // The following code has been taken from KDirModel::mimeData() // (kdelibs/kio/kio/kdirmodel.cpp) - // Copyright (C) 2006 David Faure + // SPDX-FileCopyrightText: 2006 David Faure QList urls; QList mostLocalUrls; const ItemData* lastAddedItem = nullptr; @@ -308,10 +327,10 @@ QString KFileItemModel::roleDescription(const QByteArray& role) const int count = 0; 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()); } } @@ -433,7 +452,8 @@ int KFileItemModel::index(const QUrl& url) const indexesForUrl.insert(m_itemData.at(i)->item.url(), i); } - foreach (const QUrl& url, indexesForUrl.uniqueKeys()) { + const auto uniqueKeys = indexesForUrl.uniqueKeys(); + for (const QUrl& url : uniqueKeys) { if (indexesForUrl.count(url) > 1) { qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; @@ -499,7 +519,7 @@ void KFileItemModel::setRoles(const QSet& roles) m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); } - emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); + Q_EMIT itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); } // Clear the 'values' of all filtered items. They will be re-populated with the @@ -537,7 +557,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(); - foreach (const QVariant& var, previouslyExpandedChildren) { + for (const QVariant& var : previouslyExpandedChildren) { m_urlsToExpand.insert(var.toUrl()); } } else { @@ -639,7 +659,7 @@ void KFileItemModel::expandParentDirectories(const QUrl &url) // first subdir can be empty, if m_dirLister->url().path() does not end with '/' // this happens if baseUrl is not root but a home directory, see FoldersPanel, // so using QString::SkipEmptyParts - const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), QString::SkipEmptyParts); + const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), Qt::SkipEmptyParts); for (int i = 0; i < subDirs.count() - 1; ++i) { QString path = urlToExpand.path(); if (!path.endsWith(QLatin1Char('/'))) { @@ -690,45 +710,87 @@ QStringList KFileItemModel::mimeTypeFilters() const return m_filter.mimeTypes(); } - 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); } @@ -741,7 +803,7 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) } QSet parents; - foreach (const KItemRange& range, itemRanges) { + for (const KItemRange& range : itemRanges) { for (int index = range.index; index < range.index + range.count; ++index) { parents.insert(m_itemData.at(index)); } @@ -768,9 +830,9 @@ QList KFileItemModel::rolesInformation() 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); + 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 @@ -861,7 +923,7 @@ void KFileItemModel::resortAllItems() // been moved because of the resorting. QList oldUrls; oldUrls.reserve(itemCount); - foreach (const ItemData* itemData, m_itemData) { + for (const ItemData* itemData : qAsConst(m_itemData)) { oldUrls.append(itemData->item.url()); } @@ -904,13 +966,13 @@ void KFileItemModel::resortAllItems() movedToIndexes.append(newIndex); } - emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); + Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); } else if (groupedSorting()) { // The groups might have changed even if the order of the items has not. const QList > oldGroups = m_groups; m_groups.clear(); if (groups() != oldGroups) { - emit groupsChanged(); + Q_EMIT groupsChanged(); } } @@ -929,7 +991,9 @@ void KFileItemModel::slotCompleted() // Note that the parent folder must be expanded before any of its subfolders become visible. // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet // -> we expand the first visible URL we find in m_restoredExpandedUrls. - foreach (const QUrl& url, m_urlsToExpand) { + // Iterate over a const copy because items are deleted and inserted within the loop + const auto urlsToExpand = m_urlsToExpand; + for(const QUrl &url : urlsToExpand) { const int indexForUrl = index(url); if (indexForUrl >= 0) { m_urlsToExpand.remove(url); @@ -946,7 +1010,7 @@ void KFileItemModel::slotCompleted() m_urlsToExpand.clear(); } - emit directoryLoadingCompleted(); + Q_EMIT directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() @@ -954,19 +1018,14 @@ void KFileItemModel::slotCanceled() m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); - emit directoryLoadingCanceled(); + Q_EMIT directoryLoadingCanceled(); } 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 @@ -995,21 +1054,33 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis } } - 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. - foreach (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()) { @@ -1017,6 +1088,44 @@ 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) @@ -1025,8 +1134,9 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) QVector indexesToRemove; indexesToRemove.reserve(items.count()); + KFileItemList dirsChanged; - foreach (const KFileItem& item, items) { + for (const KFileItem& item : items) { const int indexForItem = index(item); if (indexForItem >= 0) { indexesToRemove.append(indexForItem); @@ -1038,6 +1148,11 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) 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()); @@ -1048,7 +1163,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) indexesToRemoveWithChildren.reserve(m_itemData.count()); const int itemCount = m_itemData.count(); - foreach (int index, indexesToRemove) { + for (int index : qAsConst(indexesToRemove)) { indexesToRemoveWithChildren.append(index); const int parentLevel = expandedParentsCount(index); @@ -1062,9 +1177,19 @@ 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) @@ -1079,59 +1204,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); + while (it.hasNext()) { 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()); + 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); 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() @@ -1155,7 +1372,7 @@ void KFileItemModel::slotClear() qDeleteAll(m_itemData); m_itemData.clear(); m_items.clear(); - emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); + Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } m_expandedDirs.clear(); @@ -1271,7 +1488,7 @@ void KFileItemModel::insertItems(QList& newItems) // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); - emit itemsInserted(itemRanges); + Q_EMIT itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); @@ -1288,11 +1505,11 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe // Step 1: Remove the items from m_itemData, and free the ItemData. int removedItemsCount = 0; - foreach (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); } @@ -1324,7 +1541,7 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); - emit itemsRemoved(itemRanges); + Q_EMIT itemsRemoved(itemRanges); } QList KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const @@ -1336,13 +1553,14 @@ QList KFileItemModel::createItemDataList(const QUrl& 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; itemDataList.reserve(items.count()); - foreach (const KFileItem& item, items) { + for (const KFileItem& item : items) { ItemData* itemData = new ItemData(); itemData->item = item; itemData->parent = parentItem; @@ -1363,7 +1581,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. - foreach (ItemData* itemData, itemDataList) { + for (ItemData* itemData : qAsConst(itemDataList)) { if (itemData->values.isEmpty()) { itemData->values = retrieveData(itemData->item, itemData->parent); } @@ -1372,7 +1590,7 @@ void KFileItemModel::prepareItemsForSorting(QList& itemDataList) case TypeRole: // At least store the data including the file type for items with known MIME type. - foreach (ItemData* itemData, itemDataList) { + for (ItemData* itemData : qAsConst(itemDataList)) { if (itemData->values.isEmpty()) { const KFileItem item = itemData->item; if (item.isDir() || item.isMimeTypeKnown()) { @@ -1440,12 +1658,12 @@ void KFileItemModel::removeExpandedItems() void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet& changedRoles) { - emit itemsChanged(itemRanges, 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))) { - foreach (const KItemRange& range, itemRanges) { + for (const KItemRange& range : itemRanges) { bool needsResorting = false; const int first = range.index; @@ -1667,7 +1885,13 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, } if (item.isMimeTypeKnown()) { - data.insert(sharedValue("iconName"), item.iconName()); + QString iconName = item.iconName(); + if (!QIcon::hasThemeIcon(iconName)) { + QMimeType mimeType = QMimeDatabase().mimeTypeForName(item.mimetype()); + iconName = mimeType.genericIconName(); + } + + data.insert(sharedValue("iconName"), iconName); if (m_requestRole[TypeRole]) { data.insert(sharedValue("type"), item.mimeComment()); @@ -1715,7 +1939,18 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QColla } } - if (m_sortDirsFirst || m_sortRole == SizeRole) { + // 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 || (DetailsModeSettings::directorySizeCount() && m_sortRole == SizeRole)) { const bool isDirA = a->item.isDir(); const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { @@ -1753,6 +1988,11 @@ void KFileItemModel::sort(const QList::iterator &begi int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const { + // 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; @@ -1764,44 +2004,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 (DetailsModeSettings::directorySizeCount() && 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 < valueB) { + 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; } @@ -1810,9 +2049,9 @@ 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) { - result = +1; + return +1; } break; } @@ -1821,9 +2060,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; } @@ -1832,9 +2071,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; } @@ -1855,9 +2094,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const 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 { @@ -1998,16 +2237,24 @@ QList > KFileItemModel::sizeRoleGroups() const } const KFileItem& item = m_itemData.at(i)->item; - const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; + 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"); - } else { - newGroupValue = i18nc("@title:group Size", "Big"); + if (DetailsModeSettings::directorySizeCount() || m_sortDirsFirst) { + newGroupValue = i18nc("@title:group Size", "Folders"); + } else { + fileSize = m_itemData.at(i)->values.value("size").toULongLong(); + } + } + + if (newGroupValue.isEmpty()) { + if (fileSize < 5 * 1024 * 1024) { // < 5 MB + newGroupValue = i18nc("@title:group Size", "Small"); + } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB + newGroupValue = i18nc("@title:group Size", "Medium"); + } else { + newGroupValue = i18nc("@title:group Size", "Big"); + } } if (newGroupValue != groupValue) { @@ -2085,7 +2332,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< 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)"); + "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); @@ -2100,7 +2347,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< } 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", + "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", @@ -2108,7 +2355,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< } 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)"); + "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); @@ -2123,7 +2370,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< } 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)"); + "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); @@ -2138,7 +2385,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< } 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)"); + "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); @@ -2153,7 +2400,7 @@ QList > KFileItemModel::timeRoleGroups(const std::function< } 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"); + "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); @@ -2319,14 +2566,14 @@ void KFileItemModel::emitSortProgress(int resolvedCount) resortAllItems(); } - emit directorySortingProgress(100); + Q_EMIT directorySortingProgress(100); } else if (itemCount > 0) { resolvedCount = qBound(0, resolvedCount, itemCount); const int progress = resolvedCount * 100 / itemCount; if (m_sortingProgressPercent != progress) { m_sortingProgressPercent = progress; - emit directorySortingProgress(progress); + Q_EMIT directorySortingProgress(progress); } } } @@ -2334,40 +2581,40 @@ void KFileItemModel::emitSortProgress(int resolvedCount) 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 }, + // | role | roleType | role translation | group translation | requires Baloo | requires indexer + { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), false, false }, + { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), false, false }, + { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), false, false }, + { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), false, false }, + { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), false, false }, + { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), false, false }, + { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), false, false }, + { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), true, false }, + { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), true, false }, + { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), true, false }, + { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), true, true }, + { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), true, true }, + { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), true, true }, + { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), true, true }, + { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), true, true }, + { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), true, true }, + { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), true, true }, + { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), true, true }, + { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), true, true }, + { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), true, true }, + { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), true, true }, + { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), true, true }, + { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), true, true }, + { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), true, true }, + { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), true, true }, + { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), true, true }, + { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), false, false }, + { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), false, false }, + { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), false, false }, + { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), true, false }, + { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), false, false }, + { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), false, false }, + { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), false, false }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); @@ -2378,7 +2625,7 @@ void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { QElapsedTimer timer; timer.start(); - foreach (const KFileItem& item, items) { // krazy:exclude=foreach + 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 @@ -2457,3 +2704,15 @@ bool KFileItemModel::isConsistent() const return true; } + +void KFileItemModel::slotListerError(KIO::Job *job) +{ + if (job->error() == 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.")); + } +}