X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/0cad00c39aec4508688c87e520acc782b1001d1f..99cc976a8564cbdae63e4261fcb2292a33148dbc:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index c35d164e1..bf2597903 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,49 +1,40 @@ -/***************************************************************************** - * 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/kfileitemmodelsortalgorithm.h" +#include +#include #include #include -#include "dolphindebug.h" - -#include "private/kfileitemmodelsortalgorithm.h" -#include "private/kfileitemmodeldirlister.h" - +#include #include +#include #include #include +#include +#include -#include -#include +Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject* parent) : KItemModelBase("text", parent), - m_dirLister(0), + m_dirLister(nullptr), m_sortDirsFirst(true), + m_sortHiddenLast(false), m_sortRole(NameRole), m_sortingProgressPercent(-1), m_roles(), @@ -52,8 +43,8 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_filter(), m_filteredItems(), m_requestRole(), - m_maximumUpdateIntervalTimer(0), - m_resortAllItemsTimer(0), + m_maximumUpdateIntervalTimer(nullptr), + m_resortAllItemsTimer(nullptr), m_pendingItemsToInsert(), m_groups(), m_expandedDirs(), @@ -63,7 +54,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,17 +63,17 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_dirLister->setMainWindow(parentWidget->window()); } - connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted); - connect(m_dirLister, static_cast(&KFileItemModelDirLister::canceled), this, &KFileItemModel::slotCanceled); - connect(m_dirLister, static_cast(&KFileItemModelDirLister::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, static_cast(&KFileItemModelDirLister::clear), this, &KFileItemModel::slotClear); - connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage); - connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage); - connect(m_dirLister, static_cast(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection); - connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); + connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted); + connect(m_dirLister, QOverload<>::of(&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, QOverload<>::of(&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, QOverload::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection); + connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted); // Apply default roles that should be determined resetRoles(); @@ -91,6 +83,7 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_roles.insert("text"); m_roles.insert("isDir"); m_roles.insert("isLink"); + m_roles.insert("isHidden"); // For slow KIO-slaves like used for searching it makes sense to show results periodically even // before the completed() or canceled() signal has been emitted. @@ -215,6 +208,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); @@ -245,11 +251,10 @@ 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; - bool canUseMostLocalUrls = true; - const ItemData* lastAddedItem = 0; + const ItemData* lastAddedItem = nullptr; for (int index : indexes) { const ItemData* itemData = m_itemData.at(index); @@ -270,10 +275,7 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const urls << item.url(); bool isLocal; - mostLocalUrls << item.mostLocalUrl(isLocal); - if (!isLocal) { - canUseMostLocalUrls = false; - } + mostLocalUrls << item.mostLocalUrl(&isLocal); } } @@ -310,6 +312,9 @@ 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) { + continue; + } description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); } } @@ -327,7 +332,26 @@ QList > KFileItemModel::groups() const switch (typeForRole(sortRole())) { case NameRole: m_groups = nameRoleGroups(); break; case SizeRole: m_groups = sizeRoleGroups(); break; - case DateRole: m_groups = dateRoleGroups(); break; + case ModificationTimeRole: + m_groups = timeRoleGroups([](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }); + break; + case CreationTimeRole: + m_groups = timeRoleGroups([](const ItemData *item) { + return item->item.time(KFileItem::CreationTime); + }); + break; + case AccessTimeRole: + m_groups = timeRoleGroups([](const ItemData *item) { + return item->item.time(KFileItem::AccessTime); + }); + break; + case DeletionTimeRole: + m_groups = timeRoleGroups([](const ItemData *item) { + 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; @@ -413,7 +437,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; @@ -479,7 +504,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 @@ -517,7 +542,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 { @@ -608,16 +633,24 @@ void KFileItemModel::restoreExpandedDirectories(const QSet &urls) void KFileItemModel::expandParentDirectories(const QUrl &url) { - const int pos = m_dirLister->url().path().length(); // 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 // expanded. QUrl urlToExpand = m_dirLister->url(); - const QStringList subDirs = url.path().mid(pos).split(QDir::separator()); + const int pos = urlToExpand.path().length(); + + // 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(), Qt::SkipEmptyParts); for (int i = 0; i < subDirs.count() - 1; ++i) { - urlToExpand.setPath(urlToExpand.path() + '/' + subDirs.at(i)); + QString path = urlToExpand.path(); + if (!path.endsWith(QLatin1Char('/'))) { + path.append(QLatin1Char('/')); + } + urlToExpand.setPath(path + subDirs.at(i)); m_urlsToExpand.insert(urlToExpand); } @@ -713,7 +746,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)); } @@ -761,13 +794,13 @@ QList KFileItemModel::rolesInformation() void KFileItemModel::onGroupedSortingChanged(bool current) { - Q_UNUSED(current); + Q_UNUSED(current) m_groups.clear(); } -void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) +void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems) { - Q_UNUSED(previous); + Q_UNUSED(previous) m_sortRole = typeForRole(current); if (!m_requestRole[m_sortRole]) { @@ -776,13 +809,15 @@ void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArr setRoles(newRoles); } - resortAllItems(); + if (resortItems) { + resortAllItems(); + } } void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) resortAllItems(); } @@ -805,6 +840,9 @@ void KFileItemModel::loadSortingSettings() default: Q_UNREACHABLE(); } + // 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()); } void KFileItemModel::resortAllItems() @@ -828,7 +866,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()); } @@ -871,13 +909,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(); } } @@ -888,6 +926,7 @@ void KFileItemModel::resortAllItems() void KFileItemModel::slotCompleted() { + m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); if (!m_urlsToExpand.isEmpty()) { @@ -895,7 +934,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); @@ -912,7 +953,7 @@ void KFileItemModel::slotCompleted() m_urlsToExpand.clear(); } - emit directoryLoadingCompleted(); + Q_EMIT directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() @@ -920,7 +961,7 @@ void KFileItemModel::slotCanceled() m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); - emit directoryLoadingCanceled(); + Q_EMIT directoryLoadingCanceled(); } void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) @@ -961,7 +1002,7 @@ 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); @@ -969,7 +1010,7 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis // 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); } else { @@ -978,11 +1019,13 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis } } - if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { + if (!m_maximumUpdateIntervalTimer->isActive()) { // Assure that items get dispatched if no completed() or canceled() signal is // emitted during the maximum update interval. m_maximumUpdateIntervalTimer->start(); } + + Q_EMIT fileItemsChanged({KFileItem(directoryUrl)}); } void KFileItemModel::slotItemsDeleted(const KFileItemList& items) @@ -991,8 +1034,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); @@ -1004,6 +1048,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()); @@ -1014,7 +1063,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); @@ -1031,6 +1080,8 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); removeFilteredChildren(itemRanges); removeItems(itemRanges, DeleteItemData); + + Q_EMIT fileItemsChanged(dirsChanged); } void KFileItemModel::slotRefreshItems(const QList >& items) @@ -1045,6 +1096,15 @@ 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()) { @@ -1052,25 +1112,32 @@ void KFileItemModel::slotRefreshItems(const QList >& 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()); - m_items.insert(newItem.url(), indexForItem); - indexes.append(indexForItem); + if (newItemMatchesFilter) { + m_items.insert(newItem.url(), indexForItem); + 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); @@ -1083,11 +1150,22 @@ void KFileItemModel::slotRefreshItems(const QList >& itemData->values.clear(); m_filteredItems.erase(it); - m_filteredItems.insert(newItem, itemData); + if (newItemMatchesFilter) { + newVisibleItems.append(itemData); + } else { + m_filteredItems.insert(newItem, itemData); + } } } } + // Hide items, previously visible that should get hidden + const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); + removeItems(removedRanges, KeepItemData); + + // Show previously hidden items that should get visible + insertItems(newVisibleItems); + // 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()) { @@ -1095,9 +1173,11 @@ void KFileItemModel::slotRefreshItems(const QList >& } // Extract the item-ranges out of the changed indexes - qSort(indexes); + std::sort(indexes.begin(), indexes.end()); const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); + + Q_EMIT fileItemsChanged(changedFiles); } void KFileItemModel::slotClear() @@ -1121,7 +1201,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(); @@ -1157,12 +1237,20 @@ void KFileItemModel::insertItems(QList& newItems) m_groups.clear(); prepareItemsForSorting(newItems); - if (m_sortRole == NameRole && m_naturalSorting) { - // Natural sorting of items can be very slow. However, it becomes much - // faster if the input sequence is already mostly sorted. Therefore, we - // first sort 'newItems' according to the QStrings returned by - // KFileItem::text() using QString::operator<(), which is quite fast. - parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount()); + // Natural sorting of items can be very slow. However, it becomes much faster + // if the input sequence is already mostly sorted. Therefore, we first sort + // 'newItems' according to the QStrings using QString::operator<(), which is quite fast. + if (m_naturalSorting) { + 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) + { + const QByteArray role = roleForType(m_sortRole); + return a->values.value(role).toString() < b->values.value(role).toString(); + }; + parallelMergeSort(newItems.begin(), newItems.end(), lambdaLessThan, QThread::idealThreadCount()); + } } sort(newItems.begin(), newItems.end()); @@ -1184,7 +1272,7 @@ void KFileItemModel::insertItems(QList& newItems) } else { m_itemData.reserve(totalItemCount); for (int i = existingItemCount; i < totalItemCount; ++i) { - m_itemData.append(0); + m_itemData.append(nullptr); } // We build the new list m_itemData in reverse order to minimize @@ -1229,7 +1317,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(); @@ -1246,7 +1334,7 @@ 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) { @@ -1254,7 +1342,7 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe delete m_itemData.at(index); } - m_itemData[index] = 0; + m_itemData[index] = nullptr; } } @@ -1282,7 +1370,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 @@ -1295,12 +1383,12 @@ QList KFileItemModel::createItemDataList(const QUrl& } const int parentIndex = index(parentUrl); - ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); + ItemData* parentItem = parentIndex < 0 ? 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; @@ -1318,9 +1406,10 @@ void KFileItemModel::prepareItemsForSorting(QList& itemDataList) case GroupRole: case DestinationRole: case PathRole: + 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); } @@ -1329,7 +1418,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()) { @@ -1397,12 +1486,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; @@ -1470,6 +1559,7 @@ KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) con // with KFileItemModel::roleForType() in case if a change is done). roles.insert("isDir", IsDirRole); roles.insert("isLink", IsLinkRole); + roles.insert("isHidden", IsHiddenRole); roles.insert("isExpanded", IsExpandedRole); roles.insert("isExpandable", IsExpandableRole); roles.insert("expandedParentsCount", ExpandedParentsCountRole); @@ -1496,6 +1586,7 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const // with KFileItemModel::typeForRole() in case if a change is done). roles.insert(IsDirRole, "isDir"); roles.insert(IsLinkRole, "isLink"); + roles.insert(IsHiddenRole, "isHidden"); roles.insert(IsExpandedRole, "isExpanded"); roles.insert(IsExpandableRole, "isExpandable"); roles.insert(ExpandedParentsCountRole, "expandedParentsCount"); @@ -1523,6 +1614,10 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, data.insert(sharedValue("isLink"), true); } + if (m_requestRole[IsHiddenRole]) { + data.insert(sharedValue("isHidden"), item.isHidden()); + } + if (m_requestRole[NameRole]) { data.insert(sharedValue("text"), item.text()); } @@ -1531,12 +1626,28 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, data.insert(sharedValue("size"), item.size()); } - if (m_requestRole[DateRole]) { - // Don't use KFileItem::timeString() as this is too expensive when - // having several thousands of items. Instead the formatting of the - // date-time will be done on-demand by the view when the date will be shown. - const QDateTime dateTime = item.time(KFileItem::ModificationTime); - data.insert(sharedValue("date"), dateTime); + if (m_requestRole[ModificationTimeRole]) { + // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when + // having several thousands of items. Instead read the raw number from UDSEntry directly + // and the formatting of the date-time will be done on-demand by the view when the date will be shown. + const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); + data.insert(sharedValue("modificationtime"), dateTime); + } + + if (m_requestRole[CreationTimeRole]) { + // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when + // having several thousands of items. Instead read the raw number from UDSEntry directly + // and the formatting of the date-time will be done on-demand by the view when the date will be shown. + const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); + data.insert(sharedValue("creationtime"), dateTime); + } + + if (m_requestRole[AccessTimeRole]) { + // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when + // having several thousands of items. Instead read the raw number from UDSEntry directly + // and the formatting of the date-time will be done on-demand by the view when the date will be shown. + const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); + data.insert(sharedValue("accesstime"), dateTime); } if (m_requestRole[PermissionsRole]) { @@ -1554,7 +1665,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, if (m_requestRole[DestinationRole]) { QString destination = item.linkDest(); if (destination.isEmpty()) { - destination = QStringLiteral("-"); + destination = QLatin1Char('-'); } data.insert(sharedValue("destination"), destination); } @@ -1582,6 +1693,14 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, data.insert(sharedValue("path"), path); } + if (m_requestRole[DeletionTimeRole]) { + QDateTime deletionTime; + if (item.url().scheme() == QLatin1String("trash")) { + deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate); + } + data.insert(sharedValue("deletiontime"), deletionTime); + } + if (m_requestRole[IsExpandableRole] && isDir) { data.insert(sharedValue("isExpandable"), true); } @@ -1594,7 +1713,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()); @@ -1642,7 +1767,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) { @@ -1657,68 +1793,34 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QColla return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } -/** - * Helper class for KFileItemModel::sort(). - */ -class KFileItemModelLessThan +void KFileItemModel::sort(const QList::iterator &begin, + const QList::iterator &end) const { -public: - KFileItemModelLessThan(const KFileItemModel* model, const QCollator& collator) : - m_model(model), - m_collator(collator) + auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) { - } - - KFileItemModelLessThan(const KFileItemModelLessThan& other) : - m_model(other.m_model), - m_collator() - { - m_collator.setCaseSensitivity(other.m_collator.caseSensitivity()); - m_collator.setIgnorePunctuation(other.m_collator.ignorePunctuation()); - m_collator.setLocale(other.m_collator.locale()); - m_collator.setNumericMode(other.m_collator.numericMode()); - } - - ~KFileItemModelLessThan() = default; - //We do not delete m_model as the pointer was passed from outside ant it will be deleted elsewhere. - - KFileItemModelLessThan& operator=(const KFileItemModelLessThan& other) - { - m_model = other.m_model; - m_collator = other.m_collator; - return *this; - } - - bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const - { - return m_model->lessThan(a, b, m_collator); - } - -private: - const KFileItemModel* m_model; - QCollator m_collator; -}; - -void KFileItemModel::sort(QList::iterator begin, - QList::iterator end) const -{ - KFileItemModelLessThan lessThan(this, m_collator); + return lessThan(a, b, m_collator); + }; - if (m_sortRole == NameRole) { - // Sorting by name can be expensive, in particular if natural sorting is + if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) { + // Sorting by string can be expensive, in particular if natural sorting is // enabled. Use all CPU cores to speed up the sorting process. static const int numberOfThreads = QThread::idealThreadCount(); - parallelMergeSort(begin, end, lessThan, numberOfThreads); + parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads); } else { // Sorting by other roles is quite fast. Use only one thread to prevent // problems caused by non-reentrant comparison functions, see // https://bugs.kde.org/show_bug.cgi?id=312679 - mergeSort(begin, end, lessThan); + mergeSort(begin, end, lambdaLessThan); } } 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; @@ -1730,65 +1832,104 @@ 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()); - - const QVariant valueA = a->values.value("size"); - const QVariant 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 { - result = valueA.toInt() - valueB.toInt(); + if (valueA.toLongLong() < valueB.toLongLong()) { + return -1; + } 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; + } + + case ModificationTimeRole: { + 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) { + return -1; + } else if (dateTimeA > dateTimeB) { + return +1; } break; } - case DateRole: { - const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); - const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); + case CreationTimeRole: { + 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; } - case RatingRole: { - result = a->values.value("rating").toInt() - b->values.value("rating").toInt(); + case DeletionTimeRole: { + const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); + const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); + if (dateTimeA < dateTimeB) { + return -1; + } else if (dateTimeA > dateTimeB) { + return +1; + } break; } - case ImageSizeRole: { - // Alway use a natural comparing to interpret the numbers of a string like - // "1600 x 1200" for having a correct sorting. - result = collator.compare(a->values.value("imageSize").toString(), - b->values.value("imageSize").toString()); + case RatingRole: + case WidthRole: + case HeightRole: + case WordCountRole: + case LineCountRole: + case TrackRole: + case ReleaseYearRole: { + result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt(); break; } default: { const QByteArray role = roleForType(m_sortRole); - result = QString::compare(a->values.value(role).toString(), - b->values.value(role).toString()); + const QString roleValueA = a->values.value(role).toString(); + const QString roleValueB = b->values.value(role).toString(); + if (!roleValueA.isEmpty() && roleValueB.isEmpty()) { + return -1; + } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) { + return +1; + } else if (isRoleValueNatural(m_sortRole)) { + result = stringCompare(roleValueA, roleValueB, collator); + } else { + result = QString::compare(roleValueA, roleValueB); + } break; } @@ -1819,6 +1960,8 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const 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); } @@ -1834,11 +1977,6 @@ int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCol return QString::compare(a, b, Qt::CaseSensitive); } -bool KFileItemModel::useMaximumUpdateInterval() const -{ - return !m_dirLister->url().isLocalFile(); -} - QList > KFileItemModel::nameRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); @@ -1864,28 +2002,35 @@ QList > KFileItemModel::nameRoleGroups() const if (firstChar != newFirstChar) { QString newGroupValue; if (newFirstChar.isLetter()) { - // Try to find a matching group in the range 'A' to 'Z'. - static std::vector lettersAtoZ; - lettersAtoZ.reserve('Z' - 'A' + 1); - if (lettersAtoZ.empty()) { - for (char c = 'A'; c <= 'Z'; ++c) { - lettersAtoZ.push_back(QLatin1Char(c)); + + if (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)); + } } - } - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; + auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { + return m_collator.compare(c1, c2) < 0; + }; - std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); - if (it != lettersAtoZ.end()) { - if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) { - // newFirstChar belongs to the group preceding *it. - // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. - --it; + 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; } - newGroupValue = *it; + } else { + // Symbols from non Latin-based scripts newGroupValue = newFirstChar; } } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { @@ -1920,16 +2065,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) { @@ -1941,7 +2094,7 @@ QList > KFileItemModel::sizeRoleGroups() const return groups; } -QList > KFileItemModel::dateRoleGroups() const +QList > KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); @@ -1950,26 +2103,26 @@ QList > KFileItemModel::dateRoleGroups() const const QDate currentDate = QDate::currentDate(); - QDate previousModifiedDate; + QDate previousFileDate; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime); - const QDate modifiedDate = modifiedTime.date(); - if (modifiedDate == previousModifiedDate) { + const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); + const QDate fileDate = fileTime.date(); + if (fileDate == previousFileDate) { // The current item is in the same group as the previous item continue; } - previousModifiedDate = modifiedDate; + previousFileDate = fileDate; - const int daysDistance = modifiedDate.daysTo(currentDate); + const int daysDistance = fileDate.daysTo(currentDate); QString newGroupValue; - if (currentDate.year() == modifiedDate.year() && - currentDate.month() == modifiedDate.month()) { + if (currentDate.year() == fileDate.year() && + currentDate.month() == fileDate.month()) { switch (daysDistance / 7) { case 0: @@ -1977,7 +2130,7 @@ QList > KFileItemModel::dateRoleGroups() const case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; default: - newGroupValue = modifiedTime.toString( + 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); @@ -2001,55 +2154,95 @@ QList > KFileItemModel::dateRoleGroups() const } } else { const QDate lastMonthDate = currentDate.addMonths(-1); - if (lastMonthDate.year() == modifiedDate.year() && - lastMonthDate.month() == modifiedDate.month()) { + if (lastMonthDate.year() == fileDate.year() && + lastMonthDate.month() == fileDate.month()) { if (daysDistance == 1) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Yesterday' (MMMM, yyyy)")); - newGroupValue = i18nc("Can be used to script translation of " - "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + 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 " + "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", + "%1", newGroupValue); + } 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); + } } else if (daysDistance <= 7) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " + 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", "%1", newGroupValue); } else if (daysDistance <= 7 * 2) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'One Week Ago' (MMMM, yyyy)")); - newGroupValue = i18nc("Can be used to script translation of " - "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + 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 " + "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", + "%1", newGroupValue); + } 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); + } } else if (daysDistance <= 7 * 3) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Two Weeks Ago' (MMMM, yyyy)")); - newGroupValue = i18nc("Can be used to script translation of " - "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + 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 " + "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", + "%1", newGroupValue); + } 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); + } } else if (daysDistance <= 7 * 4) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Three Weeks Ago' (MMMM, yyyy)")); - newGroupValue = i18nc("Can be used to script translation of " - "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", - "%1", newGroupValue); + 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 " + "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", + "%1", newGroupValue); + } 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); + } } else { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: " - "MMMM is full month name in current locale, and yyyy is " - "full year number", "'Earlier on' MMMM, yyyy")); - newGroupValue = i18nc("Can be used to script translation of " - "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", - "%1", newGroupValue); + 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 " + "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", + "%1", newGroupValue); + } 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); + } } } else { - newGroupValue = modifiedTime.toString(i18nc("@title:group " + 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 " @@ -2201,14 +2394,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); } } } @@ -2216,30 +2409,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 - { 0, NoRole, 0, 0, 0, 0, false, false }, - { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), 0, 0, false, false }, - { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), 0, 0, false, false }, - { "date", DateRole, I18N_NOOP2_NOSTRIP("@label", "Date"), 0, 0, false, false }, - { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), 0, 0, false, false }, - { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), 0, 0, true, false }, - { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), 0, 0, true, false }, - { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), 0, 0, true, false }, - { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, - { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, - { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, - { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, - { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, - { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, - { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, - { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, - { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, - { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, - { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, - { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false }, - { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, - { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, - { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + // | 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 }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); @@ -2250,7 +2453,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 @@ -2289,7 +2492,7 @@ bool KFileItemModel::isConsistent() const return false; } - for (int i = 0; i < count(); ++i) { + for (int i = 0, iMax = count(); i < iMax; ++i) { // Check if m_items and m_itemData are consistent. const KFileItem item = fileItem(i); if (item.isNull()) { @@ -2329,3 +2532,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.")); + } +}