X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/3c83d8f14defbec5f09aeeb78de3885535d2dc71..848abc5922167a467bb73107ee6b72e9af3c8317:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index e0ae03302..70014e1a7 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,91 +1,104 @@ -/*************************************************************************** - * Copyright (C) 2011 by Peter Penz * - * * - * 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 * - ***************************************************************************/ +/***************************************************************************** + * 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 * + *****************************************************************************/ #include "kfileitemmodel.h" -#include -#include -#include -#include -#include -#include +#include "dolphin_generalsettings.h" +#include +#include + +#include "dolphindebug.h" + +#include "private/kfileitemmodelsortalgorithm.h" +#include "private/kfileitemmodeldirlister.h" + +#include #include #include +#include // #define KFILEITEMMODEL_DEBUG -KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : - KItemModelBase("name", parent), - m_dirLister(dirLister), - m_naturalSorting(KGlobalSettings::naturalSorting()), - m_sortFoldersFirst(true), +KFileItemModel::KFileItemModel(QObject* parent) : + KItemModelBase("text", parent), + m_dirLister(nullptr), + m_sortDirsFirst(true), m_sortRole(NameRole), + m_sortingProgressPercent(-1), m_roles(), - m_caseSensitivity(Qt::CaseInsensitive), m_itemData(), m_items(), m_filter(), m_filteredItems(), m_requestRole(), - m_minimumUpdateIntervalTimer(0), - m_maximumUpdateIntervalTimer(0), - m_resortAllItemsTimer(0), + m_maximumUpdateIntervalTimer(nullptr), + m_resortAllItemsTimer(nullptr), m_pendingItemsToInsert(), - m_pendingEmitLoadingCompleted(false), m_groups(), - m_rootExpansionLevel(UninitializedRootExpansionLevel), - m_expandedUrls(), + m_expandedDirs(), m_urlsToExpand() { + m_collator.setNumericMode(true); + + loadSortingSettings(); + + m_dirLister = new KFileItemModelDirLister(this); + m_dirLister->setDelayedMimeTypes(true); + + const QWidget* parentWidget = qobject_cast(parent); + if (parentWidget) { + 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, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); + connect(m_dirLister, static_cast(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection); + connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); + // Apply default roles that should be determined resetRoles(); m_requestRole[NameRole] = true; m_requestRole[IsDirRole] = true; - m_roles.insert("name"); + m_requestRole[IsLinkRole] = true; + m_roles.insert("text"); m_roles.insert("isDir"); - - Q_ASSERT(dirLister); - - connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); - connect(dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted())); - connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); - connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); - connect(dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); - connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear())); - connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); - - // Although the layout engine of KItemListView is fast it is very inefficient to e.g. - // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates - // are done in 1 second intervals for equal operations. - m_minimumUpdateIntervalTimer = new QTimer(this); - m_minimumUpdateIntervalTimer->setInterval(1000); - m_minimumUpdateIntervalTimer->setSingleShot(true); - connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); + 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. m_maximumUpdateIntervalTimer = new QTimer(this); m_maximumUpdateIntervalTimer->setInterval(2000); m_maximumUpdateIntervalTimer->setSingleShot(true); - connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); - + connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert); + // When changing the value of an item which represents the sort-role a resorting must be // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done // for a lot of items within a quite small timeslot. To prevent expensive resortings the @@ -93,17 +106,43 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : m_resortAllItemsTimer = new QTimer(this); m_resortAllItemsTimer->setInterval(500); m_resortAllItemsTimer->setSingleShot(true); - connect(m_resortAllItemsTimer, SIGNAL(timeout()), this, SLOT(resortAllItems())); + connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); - Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval()); - - connect(KGlobalSettings::self(), SIGNAL(naturalSortingChanged()), this, SLOT(slotNaturalSortingChanged())); + connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); } KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); - m_itemData.clear(); + qDeleteAll(m_filteredItems); + qDeleteAll(m_pendingItemsToInsert); +} + +void KFileItemModel::loadDirectory(const QUrl &url) +{ + m_dirLister->openUrl(url); +} + +void KFileItemModel::refreshDirectory(const QUrl &url) +{ + // Refresh all expanded directories first (Bug 295300) + QHashIterator expandedDirs(m_expandedDirs); + while (expandedDirs.hasNext()) { + expandedDirs.next(); + m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload); + } + + m_dirLister->openUrl(url, KDirLister::Reload); +} + +QUrl KFileItemModel::directory() const +{ + return m_dirLister->url(); +} + +void KFileItemModel::cancelDirectoryLoading() +{ + m_dirLister->stop(); } int KFileItemModel::count() const @@ -114,7 +153,12 @@ int KFileItemModel::count() const QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { - return m_itemData.at(index)->values; + ItemData* data = m_itemData.at(index); + if (data->values.isEmpty()) { + data->values = retrieveData(data->item, data->parent); + } + + return data->values; } return QHash(); } @@ -125,14 +169,14 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - QHash currentValues = m_itemData.at(index)->values; + QHash currentValues = data(index); // Determine which roles have been changed QSet changedRoles; QHashIterator it(values); while (it.hasNext()) { it.next(); - const QByteArray role = it.key(); + const QByteArray role = sharedValue(it.key()); const QVariant value = it.value(); if (currentValues[role] != value) { @@ -146,75 +190,82 @@ bool KFileItemModel::setData(int index, const QHash& value } m_itemData[index]->values = currentValues; - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - - if (changedRoles.contains(sortRole())) { - m_resortAllItemsTimer->start(); + if (changedRoles.contains("text")) { + QUrl url = m_itemData[index]->item.url(); + url = url.adjusted(QUrl::RemoveFilename); + url.setPath(url.path() + currentValues["text"].toString()); + m_itemData[index]->item.setUrl(url); } - + + emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); + return true; } -void KFileItemModel::setSortFoldersFirst(bool foldersFirst) +void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) { - if (foldersFirst != m_sortFoldersFirst) { - m_sortFoldersFirst = foldersFirst; + if (dirsFirst != m_sortDirsFirst) { + m_sortDirsFirst = dirsFirst; resortAllItems(); } } -bool KFileItemModel::sortFoldersFirst() const +bool KFileItemModel::sortDirectoriesFirst() const { - return m_sortFoldersFirst; + return m_sortDirsFirst; } void KFileItemModel::setShowHiddenFiles(bool show) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - dirLister->setShowingDotFiles(show); - dirLister->emitChanges(); - if (show) { - slotCompleted(); - } + m_dirLister->setShowingDotFiles(show); + m_dirLister->emitChanges(); + if (show) { + dispatchPendingItemsToInsert(); } } bool KFileItemModel::showHiddenFiles() const { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister ? dirLister->showingDotFiles() : false; + return m_dirLister->showingDotFiles(); } -void KFileItemModel::setShowFoldersOnly(bool enabled) +void KFileItemModel::setShowDirectoriesOnly(bool enabled) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - dirLister->setDirOnlyMode(enabled); - } + m_dirLister->setDirOnlyMode(enabled); } -bool KFileItemModel::showFoldersOnly() const +bool KFileItemModel::showDirectoriesOnly() const { - KDirLister* dirLister = m_dirLister.data(); - return dirLister ? dirLister->dirOnlyMode() : false; + return m_dirLister->dirOnlyMode(); } -QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const +QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const { QMimeData* data = new QMimeData(); // The following code has been taken from KDirModel::mimeData() // (kdelibs/kio/kio/kdirmodel.cpp) // Copyright (C) 2006 David Faure - KUrl::List urls; - KUrl::List mostLocalUrls; + QList urls; + QList mostLocalUrls; bool canUseMostLocalUrls = true; + const ItemData* lastAddedItem = nullptr; - QSetIterator it(indexes); - while (it.hasNext()) { - const int index = it.next(); - const KFileItem item = fileItem(index); + for (int index : indexes) { + const ItemData* itemData = m_itemData.at(index); + const ItemData* parent = itemData->parent; + + while (parent && parent != lastAddedItem) { + parent = parent->parent; + } + + if (parent && parent == lastAddedItem) { + // A parent of 'itemData' has been added already. + continue; + } + + lastAddedItem = itemData; + const KFileItem& item = itemData->item; if (!item.isNull()) { urls << item.url(); @@ -226,15 +277,7 @@ QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const } } - const bool different = canUseMostLocalUrls && mostLocalUrls != urls; - urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0 - if (different) { - mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls); - urls.populateMimeData(mostLocalUrls, data); - } else { - urls.populateMimeData(data); - } - + KUrlMimeData::setUrls(urls, mostLocalUrls, data); return data; } @@ -242,12 +285,12 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } @@ -257,34 +300,21 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd bool KFileItemModel::supportsDropping(int index) const { const KFileItem item = fileItem(index); - return item.isNull() ? false : item.isDir() || item.isDesktopFile(); + return !item.isNull() && (item.isDir() || item.isDesktopFile()); } QString KFileItemModel::roleDescription(const QByteArray& role) const { - QString descr; - - switch (roleIndex(role)) { - case NameRole: descr = i18nc("@item:intable", "Name"); break; - case SizeRole: descr = i18nc("@item:intable", "Size"); break; - case DateRole: descr = i18nc("@item:intable", "Date"); break; - case PermissionsRole: descr = i18nc("@item:intable", "Permissions"); break; - case OwnerRole: descr = i18nc("@item:intable", "Owner"); break; - case GroupRole: descr = i18nc("@item:intable", "Group"); break; - case TypeRole: descr = i18nc("@item:intable", "Type"); break; - case DestinationRole: descr = i18nc("@item:intable", "Destination"); break; - case PathRole: descr = i18nc("@item:intable", "Path"); break; - case CommentRole: descr = i18nc("@item:intable", "Comment"); break; - case TagsRole: descr = i18nc("@item:intable", "Tags"); break; - case RatingRole: descr = i18nc("@item:intable", "Rating"); break; - case NoRole: break; - case IsDirRole: break; - case IsExpandedRole: break; - case ExpansionLevelRole: break; - default: Q_ASSERT(false); break; + static QHash description; + if (description.isEmpty()) { + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); + } } - return descr; + return description.value(role); } QList > KFileItemModel::groups() const @@ -294,28 +324,36 @@ QList > KFileItemModel::groups() const QElapsedTimer timer; timer.start(); #endif - switch (roleIndex(sortRole())) { - case NameRole: m_groups = nameRoleGroups(); break; - case SizeRole: m_groups = sizeRoleGroups(); break; - case DateRole: m_groups = dateRoleGroups(); break; - case PermissionsRole: m_groups = permissionRoleGroups(); break; - case OwnerRole: m_groups = genericStringRoleGroups("owner"); break; - case GroupRole: m_groups = genericStringRoleGroups("group"); break; - case TypeRole: m_groups = genericStringRoleGroups("type"); break; - case DestinationRole: m_groups = genericStringRoleGroups("destination"); break; - case PathRole: m_groups = genericStringRoleGroups("path"); break; - case CommentRole: m_groups = genericStringRoleGroups("comment"); break; - case TagsRole: m_groups = genericStringRoleGroups("tags"); break; - case RatingRole: m_groups = ratingRoleGroups(); break; - case NoRole: break; - case IsDirRole: break; - case IsExpandedRole: break; - case ExpansionLevelRole: break; - default: Q_ASSERT(false); break; + switch (typeForRole(sortRole())) { + case NameRole: m_groups = nameRoleGroups(); break; + case SizeRole: m_groups = sizeRoleGroups(); break; + case ModificationTimeRole: + m_groups = timeRoleGroups([](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }); + 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; } #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); + qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); #endif } @@ -331,38 +369,93 @@ KFileItem KFileItemModel::fileItem(int index) const return KFileItem(); } -KFileItem KFileItemModel::fileItem(const KUrl& url) const +KFileItem KFileItemModel::fileItem(const QUrl &url) const { - const int index = m_items.value(url, -1); - if (index >= 0) { - return m_itemData.at(index)->item; + const int indexForUrl = index(url); + if (indexForUrl >= 0) { + return m_itemData.at(indexForUrl)->item; } return KFileItem(); } int KFileItemModel::index(const KFileItem& item) const { - if (item.isNull()) { - return -1; - } - - return m_items.value(item.url(), -1); + return index(item.url()); } -int KFileItemModel::index(const KUrl& url) const +int KFileItemModel::index(const QUrl& url) const { - KUrl urlToFind = url; - urlToFind.adjustPath(KUrl::RemoveTrailingSlash); - return m_items.value(urlToFind, -1); + const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash); + + const int itemCount = m_itemData.count(); + int itemsInHash = m_items.count(); + + int index = m_items.value(urlToFind, -1); + while (index < 0 && itemsInHash < itemCount) { + // Not all URLs are stored yet in m_items. We grow m_items until either + // urlToFind is found, or all URLs have been stored in m_items. + // Note that we do not add the URLs to m_items one by one, but in + // larger blocks. After each block, we check if urlToFind is in + // m_items. We could in principle compare urlToFind with each URL while + // we are going through m_itemData, but comparing two QUrls will, + // unlike calling qHash for the URLs, trigger a parsing of the URLs + // which costs both CPU cycles and memory. + const int blockSize = 1000; + const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount); + for (int i = itemsInHash; i < currentBlockEnd; ++i) { + const QUrl nextUrl = m_itemData.at(i)->item.url(); + m_items.insert(nextUrl, i); + } + + itemsInHash = currentBlockEnd; + index = m_items.value(urlToFind, -1); + } + + if (index < 0) { + // The item could not be found, even though all items from m_itemData + // should be in m_items now. We print some diagnostic information which + // might help to find the cause of the problem, but only once. This + // prevents that obtaining and printing the debugging information + // wastes CPU cycles and floods the shell or .xsession-errors. + static bool printDebugInfo = true; + + if (m_items.count() != m_itemData.count() && printDebugInfo) { + printDebugInfo = false; + + qCWarning(DolphinDebug) << "The model is in an inconsistent state."; + qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count(); + qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count(); + + // Check if there are multiple items with the same URL. + QMultiHash indexesForUrl; + for (int i = 0; i < m_itemData.count(); ++i) { + indexesForUrl.insert(m_itemData.at(i)->item.url(), i); + } + + foreach (const QUrl& url, indexesForUrl.uniqueKeys()) { + if (indexesForUrl.count(url) > 1) { + qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; + + auto it = indexesForUrl.find(url); + while (it != indexesForUrl.end() && it.key() == url) { + const ItemData* data = m_itemData.at(it.value()); + qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item; + if (data->parent) { + qCWarning(DolphinDebug) << "parent" << data->parent->item; + } + ++it; + } + } + } + } + } + + return index; } KFileItem KFileItemModel::rootItem() const { - const KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - return dirLister->rootItem(); - } - return KFileItem(); + return m_dirLister->rootItem(); } void KFileItemModel::clear() @@ -372,11 +465,16 @@ void KFileItemModel::clear() void KFileItemModel::setRoles(const QSet& roles) { + if (m_roles == roles) { + return; + } + + const QSet changedRoles = (roles - m_roles) + (m_roles - roles); m_roles = roles; if (count() > 0) { - const bool supportedExpanding = m_requestRole[ExpansionLevelRole]; - const bool willSupportExpanding = roles.contains("expansionLevel"); + const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole]; + const bool willSupportExpanding = roles.contains("expandedParentsCount"); if (supportedExpanding && !willSupportExpanding) { // No expanding is supported anymore. Take care to delete all items that have an expansion level // that is not 0 (and hence are part of an expanded item). @@ -390,18 +488,26 @@ void KFileItemModel::setRoles(const QSet& roles) QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); - m_requestRole[roleIndex(role)] = true; + m_requestRole[typeForRole(role)] = true; } if (count() > 0) { // Update m_data with the changed requested roles const int maxIndex = count() - 1; for (int i = 0; i <= maxIndex; ++i) { - m_itemData[i]->values = retrieveData(m_itemData.at(i)->item); + m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); } - kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!"; - emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet()); + emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); + } + + // Clear the 'values' of all filtered items. They will be re-populated with the + // correct roles the next time 'values' will be accessed via data(int). + QHash::iterator filteredIt = m_filteredItems.begin(); + const QHash::iterator filteredEnd = m_filteredItems.end(); + while (filteredIt != filteredEnd) { + (*filteredIt)->values.clear(); + ++filteredIt; } } @@ -417,39 +523,65 @@ bool KFileItemModel::setExpanded(int index, bool expanded) } QHash values; - values.insert("isExpanded", expanded); + values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } - KDirLister* dirLister = m_dirLister.data(); - const KUrl url = m_itemData.at(index)->item.url(); + const KFileItem item = m_itemData.at(index)->item; + const QUrl url = item.url(); + const QUrl targetUrl = item.targetUrl(); if (expanded) { - m_expandedUrls.insert(url); + m_expandedDirs.insert(targetUrl, url); + m_dirLister->openUrl(url, KDirLister::Keep); - if (dirLister) { - dirLister->openUrl(url, KDirLister::Keep); - return true; + const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); + foreach (const QVariant& var, previouslyExpandedChildren) { + m_urlsToExpand.insert(var.toUrl()); } } else { - m_expandedUrls.remove(url); + // Note that there might be (indirect) children of the folder which is to be collapsed in + // m_pendingItemsToInsert. To prevent that they will be inserted into the model later, + // possibly without a parent, which might result in a crash, we insert all pending items + // right now. All new items which would be without a parent will then be removed. + dispatchPendingItemsToInsert(); - if (dirLister) { - dirLister->stop(url); + // Check if the index of the collapsed folder has changed. If that is the case, then items + // were inserted before the collapsed folder, and its index needs to be updated. + if (m_itemData.at(index)->item != item) { + index = this->index(item); } - KFileItemList itemsToRemove; - const int expansionLevel = data(index)["expansionLevel"].toInt(); - ++index; - while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) { - itemsToRemove.append(m_itemData.at(index)->item); - ++index; + m_expandedDirs.remove(targetUrl); + m_dirLister->stop(url); + + const int parentLevel = expandedParentsCount(index); + const int itemCount = m_itemData.count(); + const int firstChildIndex = index + 1; + + QVariantList expandedChildren; + + int childIndex = firstChildIndex; + while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { + ItemData* itemData = m_itemData.at(childIndex); + if (itemData->values.value("isExpanded").toBool()) { + const QUrl targetUrl = itemData->item.targetUrl(); + const QUrl url = itemData->item.url(); + m_expandedDirs.remove(targetUrl); + m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 + expandedChildren.append(targetUrl); + } + ++childIndex; } - removeItems(itemsToRemove); - return true; + const int childrenCount = childIndex - firstChildIndex; + + removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount)); + removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData); + + m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren); } - return false; + return true; } bool KFileItemModel::isExpanded(int index) const @@ -463,45 +595,63 @@ bool KFileItemModel::isExpanded(int index) const bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { - return m_itemData.at(index)->values.value("isExpandable").toBool(); + // Call data (instead of accessing m_itemData directly) + // to ensure that the value is initialized. + return data(index).value("isExpandable").toBool(); } return false; } -QSet KFileItemModel::expandedUrls() const +int KFileItemModel::expandedParentsCount(int index) const { - return m_expandedUrls; + if (index >= 0 && index < count()) { + return expandedParentsCount(m_itemData.at(index)); + } + return 0; } -void KFileItemModel::restoreExpandedUrls(const QSet& urls) +QSet KFileItemModel::expandedDirectories() const { - m_urlsToExpand = urls; + QSet result; + const auto dirs = m_expandedDirs; + for (const auto &dir : dirs) { + result.insert(dir); + } + return result; } -void KFileItemModel::expandParentItems(const KUrl& url) +void KFileItemModel::restoreExpandedDirectories(const QSet &urls) { - const KDirLister* dirLister = m_dirLister.data(); - if (!dirLister) { - return; - } + m_urlsToExpand = urls; +} - const int pos = dirLister->url().path().length(); +void KFileItemModel::expandParentDirectories(const QUrl &url) +{ // Assure that each sub-path of the URL that should be // expanded is added to m_urlsToExpand. KDirLister // does not care whether the parent-URL has already been // expanded. - KUrl urlToExpand = dirLister->url(); - const QStringList subDirs = url.path().mid(pos).split(QDir::separator()); + QUrl urlToExpand = m_dirLister->url(); + 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(), QString::SkipEmptyParts); for (int i = 0; i < subDirs.count() - 1; ++i) { - urlToExpand.addPath(subDirs.at(i)); + QString path = urlToExpand.path(); + if (!path.endsWith(QLatin1Char('/'))) { + path.append(QLatin1Char('/')); + } + urlToExpand.setPath(path + subDirs.at(i)); m_urlsToExpand.insert(urlToExpand); } // KDirLister::open() must called at least once to trigger an initial // loading. The pending URLs that must be restored are handled // in slotCompleted(). - QSetIterator it2(m_urlsToExpand); + QSetIterator it2(m_urlsToExpand); while (it2.hasNext()) { const int idx = index(it2.next()); if (idx >= 0 && !isExpanded(idx)) { @@ -515,46 +665,125 @@ void KFileItemModel::setNameFilter(const QString& nameFilter) { if (m_filter.pattern() != nameFilter) { dispatchPendingItemsToInsert(); - m_filter.setPattern(nameFilter); + applyFilters(); + } +} - // Check which shown items from m_itemData must get - // hidden and hence moved to m_filteredItems. - KFileItemList newFilteredItems; - - foreach (ItemData* itemData, m_itemData) { - if (!m_filter.matches(itemData->item)) { - // Only filter non-expanded items as child items may never - // exist without a parent item - if (!itemData->values.value("isExpanded").toBool()) { - newFilteredItems.append(itemData->item); - m_filteredItems.insert(itemData->item); - } +QString KFileItemModel::nameFilter() const +{ + return m_filter.pattern(); +} + +void KFileItemModel::setMimeTypeFilters(const QStringList& filters) +{ + if (m_filter.mimeTypes() != filters) { + dispatchPendingItemsToInsert(); + m_filter.setMimeTypes(filters); + applyFilters(); + } +} + +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; + + 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); } } + } - removeItems(newFilteredItems); + const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); + removeItems(removedRanges, KeepItemData); - // Check which hidden items from m_filteredItems should - // get visible again and hence removed from m_filteredItems. - KFileItemList newVisibleItems; + // Check which hidden items from m_filteredItems should + // get visible again and hence removed from m_filteredItems. + QList newVisibleItems; - QMutableSetIterator it(m_filteredItems); - while (it.hasNext()) { - const KFileItem item = it.next(); - if (m_filter.matches(item)) { - newVisibleItems.append(item); - m_filteredItems.remove(item); - } + QHash::iterator it = m_filteredItems.begin(); + while (it != m_filteredItems.end()) { + if (m_filter.matches(it.key())) { + newVisibleItems.append(it.value()); + it = m_filteredItems.erase(it); + } else { + ++it; } + } + + insertItems(newVisibleItems); +} - insertItems(newVisibleItems); +void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) +{ + if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { + // There are either no filtered items, or it is not possible to expand + // folders -> there cannot be any filtered children. + return; + } + + QSet parents; + foreach (const KItemRange& range, itemRanges) { + for (int index = range.index; index < range.index + range.count; ++index) { + parents.insert(m_itemData.at(index)); + } + } + + QHash::iterator it = m_filteredItems.begin(); + while (it != m_filteredItems.end()) { + if (parents.contains(it.value()->parent)) { + delete it.value(); + it = m_filteredItems.erase(it); + } else { + ++it; + } } } -QString KFileItemModel::nameFilter() const +QList KFileItemModel::rolesInformation() { - return m_filter.pattern(); + static QList rolesInfo; + if (rolesInfo.isEmpty()) { + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + if (map[i].roleType != NoRole) { + RoleInfo info; + info.role = map[i].role; + info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); + if (map[i].groupTranslation) { + info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); + } else { + // For top level roles, groupTranslation is 0. We must make sure that + // info.group is an empty string then because the code that generates + // menus tries to put the actions into sub menus otherwise. + info.group = QString(); + } + info.requiresBaloo = map[i].requiresBaloo; + info.requiresIndexer = map[i].requiresIndexer; + rolesInfo.append(info); + } + } + } + + return rolesInfo; } void KFileItemModel::onGroupedSortingChanged(bool current) @@ -566,13 +795,13 @@ void KFileItemModel::onGroupedSortingChanged(bool current) void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) { Q_UNUSED(previous); - m_sortRole = roleIndex(current); + m_sortRole = typeForRole(current); -#ifdef KFILEITEMMODEL_DEBUG if (!m_requestRole[m_sortRole]) { - kWarning() << "The sort-role has been changed to a role that has not been received yet"; + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); } -#endif resortAllItems(); } @@ -581,13 +810,34 @@ void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre { Q_UNUSED(current); Q_UNUSED(previous); - resortAllItems(); + resortAllItems(); +} + +void KFileItemModel::loadSortingSettings() +{ + using Choice = GeneralSettings::EnumSortingChoice; + switch (GeneralSettings::sortingChoice()) { + case Choice::NaturalSorting: + m_naturalSorting = true; + m_collator.setCaseSensitivity(Qt::CaseInsensitive); + break; + case Choice::CaseSensitiveSorting: + m_naturalSorting = false; + m_collator.setCaseSensitivity(Qt::CaseSensitive); + break; + case Choice::CaseInsensitiveSorting: + m_naturalSorting = false; + m_collator.setCaseSensitivity(Qt::CaseInsensitive); + break; + default: + Q_UNREACHABLE(); + } } void KFileItemModel::resortAllItems() { m_resortAllItemsTimer->stop(); - + const int itemCount = count(); if (itemCount <= 0) { return; @@ -596,58 +846,75 @@ void KFileItemModel::resortAllItems() #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); - kDebug() << "==========================================================="; - kDebug() << "Resorting" << itemCount << "items"; + qCDebug(DolphinDebug) << "==========================================================="; + qCDebug(DolphinDebug) << "Resorting" << itemCount << "items"; #endif // Remember the order of the current URLs so // that it can be determined which indexes have // been moved because of the resorting. - QList oldUrls; + QList oldUrls; oldUrls.reserve(itemCount); foreach (const ItemData* itemData, m_itemData) { oldUrls.append(itemData->item.url()); } - - m_groups.clear(); + m_items.clear(); - + m_items.reserve(itemCount); + // Resort the items - sort(m_itemData.begin(), m_itemData.end()); + sort(m_itemData.begin(), m_itemData.end()); for (int i = 0; i < itemCount; ++i) { m_items.insert(m_itemData.at(i)->item.url(), i); } - - // Determine the indexes that have been moved - QList movedToIndexes; - movedToIndexes.reserve(itemCount); - for (int i = 0; i < itemCount; i++) { - const int newIndex = m_items.value(oldUrls.at(i).url()); - movedToIndexes.append(newIndex); - } - - // Don't check whether items have really been moved and always emit a - // itemsMoved() signal after resorting: In case of grouped items - // the groups might change even if the items themselves don't change their - // position. Let the receiver of the signal decide whether a check for moved - // items makes sense. - emit itemsMoved(KItemRange(0, itemCount), movedToIndexes); - + + // Determine the first index that has been moved. + int firstMovedIndex = 0; + while (firstMovedIndex < itemCount + && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { + ++firstMovedIndex; + } + + const bool itemsHaveMoved = firstMovedIndex < itemCount; + if (itemsHaveMoved) { + m_groups.clear(); + + int lastMovedIndex = itemCount - 1; + while (lastMovedIndex > firstMovedIndex + && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { + --lastMovedIndex; + } + + Q_ASSERT(firstMovedIndex <= lastMovedIndex); + + // Create a list movedToIndexes, which has the property that + // movedToIndexes[i] is the new index of the item with the old index + // firstMovedIndex + i. + const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1; + QList movedToIndexes; + movedToIndexes.reserve(movedItemsCount); + for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) { + const int newIndex = m_items.value(oldUrls.at(i)); + movedToIndexes.append(newIndex); + } + + 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(); + } + } + #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); -#endif + qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); +#endif } void KFileItemModel::slotCompleted() { - if (m_urlsToExpand.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) { - // dispatchPendingItems() will be called when the timer - // has been expired. - m_pendingEmitLoadingCompleted = true; - return; - } - - m_pendingEmitLoadingCompleted = false; dispatchPendingItemsToInsert(); if (!m_urlsToExpand.isEmpty()) { @@ -655,11 +922,11 @@ 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 KUrl& url, m_urlsToExpand) { - const int index = m_items.value(url, -1); - if (index >= 0) { + foreach (const QUrl& url, m_urlsToExpand) { + const int indexForUrl = index(url); + if (indexForUrl >= 0) { m_urlsToExpand.remove(url); - if (setExpanded(index, true)) { + if (setExpanded(indexForUrl, true)) { // The dir lister has been triggered. This slot will be called // again after the directory has been expanded. return; @@ -672,67 +939,70 @@ void KFileItemModel::slotCompleted() m_urlsToExpand.clear(); } - emit loadingCompleted(); - m_minimumUpdateIntervalTimer->start(); + emit directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() { - m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); + + emit directoryLoadingCanceled(); } -void KFileItemModel::slotNewItems(const KFileItemList& items) +void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) { Q_ASSERT(!items.isEmpty()); - if (m_requestRole[ExpansionLevelRole] && m_rootExpansionLevel >= 0) { - // To be able to compare whether the new items may be inserted as children - // of a parent item the pending items must be added to the model first. - dispatchPendingItemsToInsert(); - - KFileItem item = items.first(); + QUrl parentUrl; + if (m_expandedDirs.contains(directoryUrl)) { + parentUrl = m_expandedDirs.value(directoryUrl); + } else { + parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); + } + if (m_requestRole[ExpandedParentsCountRole]) { // If the expanding of items is enabled, the call // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded() // might result in emitting the same items twice due to the Keep-parameter. // This case happens if an item gets expanded, collapsed and expanded again // before the items could be loaded for the first expansion. - const int index = m_items.value(item.url(), -1); - if (index >= 0) { + if (index(items.first().url()) >= 0) { // The items are already part of the model. return; } + if (directoryUrl != directory()) { + // To be able to compare whether the new items may be inserted as children + // of a parent item the pending items must be added to the model first. + dispatchPendingItemsToInsert(); + } + // KDirLister keeps the children of items that got expanded once even if // they got collapsed again with KFileItemModel::setExpanded(false). So it must be // checked whether the parent for new items is still expanded. - KUrl parentUrl = item.url().upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); - const int parentIndex = m_items.value(parentUrl, -1); + const int parentIndex = index(parentUrl); if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { // The parent is not expanded. return; } } - if (m_filter.pattern().isEmpty()) { - m_pendingItemsToInsert.append(items); + QList itemDataList = createItemDataList(parentUrl, items); + + if (!m_filter.hasSetFilters()) { + m_pendingItemsToInsert.append(itemDataList); } else { - // The name-filter is active. Hide filtered items + // The name or type filter is active. Hide filtered items // before inserting them into the model and remember // the filtered items in m_filteredItems. - KFileItemList filteredItems; - foreach (const KFileItem& item, items) { - if (m_filter.matches(item)) { - filteredItems.append(item); + foreach (ItemData* itemData, itemDataList) { + if (m_filter.matches(itemData->item)) { + m_pendingItemsToInsert.append(itemData); } else { - m_filteredItems.insert(item); + m_filteredItems.insert(itemData->item, itemData); } } - - m_pendingItemsToInsert.append(filteredItems); } if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { @@ -746,48 +1016,102 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { dispatchPendingItemsToInsert(); - KFileItemList itemsToRemove = items; - if (m_requestRole[ExpansionLevelRole] && m_rootExpansionLevel >= 0) { - // Assure that removing a parent item also results in removing all children - foreach (const KFileItem& item, items) { - itemsToRemove.append(childItems(item)); + QVector indexesToRemove; + indexesToRemove.reserve(items.count()); + + foreach (const KFileItem& item, items) { + const int indexForItem = index(item); + if (indexForItem >= 0) { + indexesToRemove.append(indexForItem); + } else { + // Probably the item has been filtered. + QHash::iterator it = m_filteredItems.find(item); + if (it != m_filteredItems.end()) { + delete it.value(); + m_filteredItems.erase(it); + } } } - if (!m_filteredItems.isEmpty()) { - foreach (const KFileItem& item, itemsToRemove) { - m_filteredItems.remove(item); + std::sort(indexesToRemove.begin(), indexesToRemove.end()); + + if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { + // Assure that removing a parent item also results in removing all children + QVector indexesToRemoveWithChildren; + indexesToRemoveWithChildren.reserve(m_itemData.count()); + + const int itemCount = m_itemData.count(); + foreach (int index, indexesToRemove) { + indexesToRemoveWithChildren.append(index); + + const int parentLevel = expandedParentsCount(index); + int childIndex = index + 1; + while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { + indexesToRemoveWithChildren.append(childIndex); + ++childIndex; + } } + + indexesToRemove = indexesToRemoveWithChildren; } - removeItems(itemsToRemove); + const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); + removeFilteredChildren(itemRanges); + removeItems(itemRanges, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList >& items) { Q_ASSERT(!items.isEmpty()); #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Refreshing" << items.count() << "items"; + qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items"; #endif - m_groups.clear(); - // Get the indexes of all items that have been refreshed QList indexes; indexes.reserve(items.count()); + QSet changedRoles; + QListIterator > it(items); while (it.hasNext()) { const QPair& itemPair = it.next(); const KFileItem& oldItem = itemPair.first; const KFileItem& newItem = itemPair.second; - const int index = m_items.value(oldItem.url(), -1); - if (index >= 0) { - m_itemData[index]->item = newItem; - m_itemData[index]->values = retrieveData(newItem); + const int indexForItem = index(oldItem); + 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; + while (it.hasNext()) { + it.next(); + const QByteArray& role = it.key(); + if (values.value(role) != it.value()) { + values.insert(role, it.value()); + changedRoles.insert(role); + } + } + m_items.remove(oldItem.url()); - m_items.insert(newItem.url(), index); - indexes.append(index); + m_items.insert(newItem.url(), indexForItem); + indexes.append(indexForItem); + } 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->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). + itemData->values.clear(); + + m_filteredItems.erase(it); + m_filteredItems.insert(newItem, itemData); + } } } @@ -799,50 +1123,25 @@ void KFileItemModel::slotRefreshItems(const QList >& // Extract the item-ranges out of the changed indexes qSort(indexes); - - KItemRangeList itemRangeList; - int previousIndex = indexes.at(0); - int rangeIndex = previousIndex; - int rangeCount = 1; - - const int maxIndex = indexes.count() - 1; - for (int i = 1; i <= maxIndex; ++i) { - const int currentIndex = indexes.at(i); - if (currentIndex == previousIndex + 1) { - ++rangeCount; - } else { - itemRangeList.append(KItemRange(rangeIndex, rangeCount)); - - rangeIndex = currentIndex; - rangeCount = 1; - } - previousIndex = currentIndex; - } - - if (rangeCount > 0) { - itemRangeList.append(KItemRange(rangeIndex, rangeCount)); - } - - emit itemsChanged(itemRangeList, m_roles); - - resortAllItems(); + const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); + emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); } void KFileItemModel::slotClear() { #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Clearing all items"; + qCDebug(DolphinDebug) << "Clearing all items"; #endif + qDeleteAll(m_filteredItems); m_filteredItems.clear(); m_groups.clear(); - m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); m_resortAllItemsTimer->stop(); - m_pendingItemsToInsert.clear(); - m_rootExpansionLevel = UninitializedRootExpansionLevel; + qDeleteAll(m_pendingItemsToInsert); + m_pendingItemsToInsert.clear(); const int removedCount = m_itemData.count(); if (removedCount > 0) { @@ -852,17 +1151,12 @@ void KFileItemModel::slotClear() emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } - m_expandedUrls.clear(); + m_expandedDirs.clear(); } -void KFileItemModel::slotClear(const KUrl& url) +void KFileItemModel::slotSortingChoiceChanged() { - Q_UNUSED(url); -} - -void KFileItemModel::slotNaturalSortingChanged() -{ - m_naturalSorting = KGlobalSettings::naturalSorting(); + loadSortingSettings(); resortAllItems(); } @@ -872,221 +1166,313 @@ void KFileItemModel::dispatchPendingItemsToInsert() insertItems(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); } - - if (m_pendingEmitLoadingCompleted) { - emit loadingCompleted(); - } } -void KFileItemModel::insertItems(const KFileItemList& items) +void KFileItemModel::insertItems(QList& newItems) { - if (items.isEmpty()) { + if (newItems.isEmpty()) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); - kDebug() << "==========================================================="; - kDebug() << "Inserting" << items.count() << "items"; + qCDebug(DolphinDebug) << "==========================================================="; + qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items"; #endif 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()); + } - QList sortedItems = createItemDataList(items); - sort(sortedItems.begin(), sortedItems.end()); + sort(newItems.begin(), newItems.end()); #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Sorting:" << timer.elapsed(); + qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed(); #endif KItemRangeList itemRanges; - int targetIndex = 0; - int sourceIndex = 0; - int insertedAtIndex = -1; // Index for the current item-range - int insertedCount = 0; // Count for the current item-range - int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges - while (sourceIndex < sortedItems.count()) { - // Find target index from m_items to insert the current item - // in a sorted order - const int previousTargetIndex = targetIndex; - while (targetIndex < m_itemData.count()) { - if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) { - break; + const int existingItemCount = m_itemData.count(); + const int newItemCount = newItems.count(); + const int totalItemCount = existingItemCount + newItemCount; + + if (existingItemCount == 0) { + // Optimization for the common special case that there are no + // items in the model yet. Happens, e.g., when entering a folder. + m_itemData = newItems; + itemRanges << KItemRange(0, newItemCount); + } else { + m_itemData.reserve(totalItemCount); + for (int i = existingItemCount; i < totalItemCount; ++i) { + m_itemData.append(0); + } + + // We build the new list m_itemData in reverse order to minimize + // the number of moves and guarantee O(N) complexity. + int targetIndex = totalItemCount - 1; + int sourceIndexExistingItems = existingItemCount - 1; + int sourceIndexNewItems = newItemCount - 1; + + int rangeCount = 0; + + while (sourceIndexNewItems >= 0) { + ItemData* newItem = newItems.at(sourceIndexNewItems); + if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) { + // Move an existing item to its new position. If any new items + // are behind it, push the item range to itemRanges. + if (rangeCount > 0) { + itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); + rangeCount = 0; + } + + m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems); + --sourceIndexExistingItems; + } else { + // Insert a new item into the list. + ++rangeCount; + m_itemData[targetIndex] = newItem; + --sourceIndexNewItems; } - ++targetIndex; + --targetIndex; } - if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) { - itemRanges << KItemRange(insertedAtIndex, insertedCount); - previouslyInsertedCount += insertedCount; - insertedAtIndex = targetIndex - previouslyInsertedCount; - insertedCount = 0; + // Push the final item range to itemRanges. + if (rangeCount > 0) { + itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); } - // Insert item at the position targetIndex by transfering - // the ownership of the item-data from sortedItems to m_itemData. - // m_items will be inserted after the loop (see comment below) - m_itemData.insert(targetIndex, sortedItems.at(sourceIndex)); - ++insertedCount; - - if (insertedAtIndex < 0) { - insertedAtIndex = targetIndex; - Q_ASSERT(previouslyInsertedCount == 0); - } - ++targetIndex; - ++sourceIndex; + // Note that itemRanges is still sorted in reverse order. + std::reverse(itemRanges.begin(), itemRanges.end()); } - // The indexes of all m_items must be adjusted, not only the index - // of the new items - const int itemDataCount = m_itemData.count(); - for (int i = 0; i < itemDataCount; ++i) { - m_items.insert(m_itemData.at(i)->item.url(), i); - } + // The indexes in m_items are not correct anymore. Therefore, we clear m_items. + // It will be re-populated with the updated indices if index(const QUrl&) is called. + m_items.clear(); - itemRanges << KItemRange(insertedAtIndex, insertedCount); emit itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed(); + qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); #endif } -void KFileItemModel::removeItems(const KFileItemList& items) +void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) { - if (items.isEmpty()) { + if (itemRanges.isEmpty()) { return; } -#ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Removing " << items.count() << "items"; -#endif - m_groups.clear(); - QList sortedItems; - sortedItems.reserve(items.count()); - foreach (const KFileItem& item, items) { - const int index = m_items.value(item.url(), -1); - if (index >= 0) { - sortedItems.append(m_itemData.at(index)); - } - } - sort(sortedItems.begin(), sortedItems.end()); + // Step 1: Remove the items from m_itemData, and free the ItemData. + int removedItemsCount = 0; + foreach (const KItemRange& range, itemRanges) { + removedItemsCount += range.count; - QList indexesToRemove; - indexesToRemove.reserve(items.count()); - - // Calculate the item ranges that will get deleted - KItemRangeList itemRanges; - int removedAtIndex = -1; - int removedCount = 0; - int targetIndex = 0; - foreach (const ItemData* itemData, sortedItems) { - const KFileItem& itemToRemove = itemData->item; - - const int previousTargetIndex = targetIndex; - while (targetIndex < m_itemData.count()) { - if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) { - break; + for (int index = range.index; index < range.index + range.count; ++index) { + if (behavior == DeleteItemData) { + delete m_itemData.at(index); } - ++targetIndex; - } - if (targetIndex >= m_itemData.count()) { - kWarning() << "Item that should be deleted has not been found!"; - return; - } - - if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) { - itemRanges << KItemRange(removedAtIndex, removedCount); - removedAtIndex = targetIndex; - removedCount = 0; - } - indexesToRemove.append(targetIndex); - if (removedAtIndex < 0) { - removedAtIndex = targetIndex; + m_itemData[index] = 0; } - ++removedCount; - ++targetIndex; } - // Delete the items - for (int i = indexesToRemove.count() - 1; i >= 0; --i) { - const int indexToRemove = indexesToRemove.at(i); - ItemData* data = m_itemData.at(indexToRemove); + // Step 2: Remove the ItemData pointers from the list m_itemData. + int target = itemRanges.at(0).index; + int source = itemRanges.at(0).index + itemRanges.at(0).count; + int nextRange = 1; - m_items.remove(data->item.url()); + const int oldItemDataCount = m_itemData.count(); + while (source < oldItemDataCount) { + m_itemData[target] = m_itemData[source]; + ++target; + ++source; - delete data; - m_itemData.removeAt(indexToRemove); + if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { + // Skip the items in the next removed range. + source += itemRanges.at(nextRange).count; + ++nextRange; + } } - // The indexes of all m_items must be adjusted, not only the index - // of the removed items - const int itemDataCount = m_itemData.count(); - for (int i = 0; i < itemDataCount; ++i) { - m_items.insert(m_itemData.at(i)->item.url(), i); - } + m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); - if (count() <= 0) { - m_rootExpansionLevel = UninitializedRootExpansionLevel; - } + // The indexes in m_items are not correct anymore. Therefore, we clear m_items. + // It will be re-populated with the updated indices if index(const QUrl&) is called. + m_items.clear(); - itemRanges << KItemRange(removedAtIndex, removedCount); emit itemsRemoved(itemRanges); } -QList KFileItemModel::createItemDataList(const KFileItemList& items) const +QList KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const { + if (m_sortRole == TypeRole) { + // Try to resolve the MIME-types synchronously to prevent a reordering of + // the items when sorting by type (per default MIME-types are resolved + // asynchronously by KFileItemModelRolesUpdater). + determineMimeTypes(items, 200); + } + + const int parentIndex = index(parentUrl); + ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); + QList itemDataList; itemDataList.reserve(items.count()); foreach (const KFileItem& item, items) { ItemData* itemData = new ItemData(); itemData->item = item; - itemData->values = retrieveData(item); - itemData->parent = 0; - - const bool determineParent = m_requestRole[ExpansionLevelRole] - && itemData->values["expansionLevel"].toInt() > 0; - if (determineParent) { - KUrl parentUrl = item.url().upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); - const int parentIndex = m_items.value(parentUrl, -1); - if (parentIndex >= 0) { - itemData->parent = m_itemData.at(parentIndex); - } else { - kWarning() << "Parent item not found for" << item.url(); + itemData->parent = parentItem; + itemDataList.append(itemData); + } + + return itemDataList; +} + +void KFileItemModel::prepareItemsForSorting(QList& itemDataList) +{ + switch (m_sortRole) { + case PermissionsRole: + case OwnerRole: + 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) { + if (itemData->values.isEmpty()) { + itemData->values = retrieveData(itemData->item, itemData->parent); + } + } + break; + + case TypeRole: + // At least store the data including the file type for items with known MIME type. + foreach (ItemData* itemData, itemDataList) { + if (itemData->values.isEmpty()) { + const KFileItem item = itemData->item; + if (item.isDir() || item.isMimeTypeKnown()) { + itemData->values = retrieveData(itemData->item, itemData->parent); + } } } + break; - itemDataList.append(itemData); + default: + // The other roles are either resolved by KFileItemModelRolesUpdater + // (this includes the SizeRole for directories), or they do not need + // to be stored in the QHash "values" for sorting because the data can + // be retrieved directly from the KFileItem (NameRole, SizeRole for files, + // DateRole). + break; + } +} + +int KFileItemModel::expandedParentsCount(const ItemData* data) +{ + // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" + // if the corresponding item is expanded, and it is not a top-level item. + const ItemData* parent = data->parent; + if (parent) { + if (parent->parent) { + Q_ASSERT(parent->values.contains("expandedParentsCount")); + return parent->values.value("expandedParentsCount").toInt() + 1; + } else { + return 1; + } + } else { + return 0; } - - return itemDataList; } void KFileItemModel::removeExpandedItems() { - KFileItemList expandedItems; + QVector indexesToRemove; const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { const ItemData* itemData = m_itemData.at(i); - if (itemData->values.value("expansionLevel").toInt() > 0) { - expandedItems.append(itemData->item); + if (itemData->parent) { + indexesToRemove.append(i); + } + } + + removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData); + m_expandedDirs.clear(); + + // Also remove all filtered items which have a parent. + QHash::iterator it = m_filteredItems.begin(); + const QHash::iterator end = m_filteredItems.end(); + + while (it != end) { + if (it.value()->parent) { + delete it.value(); + it = m_filteredItems.erase(it); + } else { + ++it; } } +} + +void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet& changedRoles) +{ + 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) { + bool needsResorting = false; + + const int first = range.index; + const int last = range.index + range.count - 1; + + // Resorting the model is necessary if + // (a) The first item in the range is "lessThan" its predecessor, + // (b) the successor of the last item is "lessThan" the last item, or + // (c) the internal order of the items in the range is incorrect. + if (first > 0 + && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { + needsResorting = true; + } else if (last < count() - 1 + && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { + needsResorting = true; + } else { + for (int index = first; index < last; ++index) { + if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) { + needsResorting = true; + break; + } + } + } - // The m_rootExpansionLevel may not get reset before all items with - // a bigger expansionLevel have been removed. - Q_ASSERT(m_rootExpansionLevel >= 0); - removeItems(expandedItems); + if (needsResorting) { + m_resortAllItemsTimer->start(); + return; + } + } + } - m_rootExpansionLevel = UninitializedRootExpansionLevel; - m_expandedUrls.clear(); + if (groupedSorting() && changedRoles.contains(sortRole())) { + // The position is still correct, but the groups might have changed + // if the changed item is either the first or the last item in a + // group. + // In principle, we could try to find out if the item really is the + // first or last one in its group and then update the groups + // (possibly with a delayed timer to make sure that we don't + // re-calculate the groups very often if items are updated one by + // one), but starting m_resortAllItemsTimer is easier. + m_resortAllItemsTimer->start(); + } } void KFileItemModel::resetRoles() @@ -1096,178 +1482,225 @@ void KFileItemModel::resetRoles() } } -KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const -{ - static QHash rolesHash; - if (rolesHash.isEmpty()) { - rolesHash.insert("name", NameRole); - rolesHash.insert("size", SizeRole); - rolesHash.insert("date", DateRole); - rolesHash.insert("permissions", PermissionsRole); - rolesHash.insert("owner", OwnerRole); - rolesHash.insert("group", GroupRole); - rolesHash.insert("type", TypeRole); - rolesHash.insert("destination", DestinationRole); - rolesHash.insert("path", PathRole); - rolesHash.insert("comment", CommentRole); - rolesHash.insert("tags", TagsRole); - rolesHash.insert("rating", RatingRole); - rolesHash.insert("isDir", IsDirRole); - rolesHash.insert("isExpanded", IsExpandedRole); - rolesHash.insert("isExpandable", IsExpandableRole); - rolesHash.insert("expansionLevel", ExpansionLevelRole); - } - return rolesHash.value(role, NoRole); -} - -QByteArray KFileItemModel::roleByteArray(Role role) const -{ - static const char* const roles[RolesCount] = { - 0, // NoRole - "name", - "size", - "date", - "permissions", - "owner", - "group", - "type", - "destination", - "path", - "comment", - "tags", - "rating", - "isDir", - "isExpanded", - "isExpandable", - "expansionLevel" +KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const +{ + static QHash roles; + if (roles.isEmpty()) { + // Insert user visible roles that can be accessed with + // KFileItemModel::roleInformation() + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + roles.insert(map[i].role, map[i].roleType); + } + + // Insert internal roles (take care to synchronize the implementation + // 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); + + Q_ASSERT(roles.count() == RolesCount); + } + + return roles.value(role, NoRole); +} + +QByteArray KFileItemModel::roleForType(RoleType roleType) const +{ + static QHash roles; + if (roles.isEmpty()) { + // Insert user visible roles that can be accessed with + // KFileItemModel::roleInformation() + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + roles.insert(map[i].roleType, map[i].role); + } + + // Insert internal roles (take care to synchronize the implementation + // 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"); + + Q_ASSERT(roles.count() == RolesCount); }; - return roles[role]; + + return roles.value(roleType); } -QHash KFileItemModel::retrieveData(const KFileItem& item) const -{ +QHash KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const +{ // It is important to insert only roles that are fast to retrieve. E.g. // KFileItem::iconName() can be very expensive if the MIME-type is unknown // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. QHash data; - data.insert("iconPixmap", QPixmap()); - data.insert("url", item.url()); + data.insert(sharedValue("url"), item.url()); const bool isDir = item.isDir(); - if (m_requestRole[IsDirRole]) { - data.insert("isDir", isDir); + if (m_requestRole[IsDirRole] && isDir) { + data.insert(sharedValue("isDir"), true); + } + + if (m_requestRole[IsLinkRole] && item.isLink()) { + data.insert(sharedValue("isLink"), true); + } + + if (m_requestRole[IsHiddenRole] && item.isHidden()) { + data.insert(sharedValue("isHidden"), true); } if (m_requestRole[NameRole]) { - data.insert("name", item.text()); + data.insert(sharedValue("text"), item.text()); } - if (m_requestRole[SizeRole]) { - if (isDir) { - data.insert("size", QVariant()); - } else { - data.insert("size", item.size()); - } + if (m_requestRole[SizeRole] && !isDir) { + data.insert(sharedValue("size"), item.size()); + } + + if (m_requestRole[ModificationTimeRole]) { + // 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("modificationtime"), dateTime); } - if (m_requestRole[DateRole]) { + if (m_requestRole[CreationTimeRole]) { // 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 KDateTime dateTime = item.time(KFileItem::ModificationTime); - data.insert("date", dateTime.dateTime()); + const QDateTime dateTime = item.time(KFileItem::CreationTime); + data.insert(sharedValue("creationtime"), dateTime); + } + + if (m_requestRole[AccessTimeRole]) { + // 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::AccessTime); + data.insert(sharedValue("accesstime"), dateTime); } if (m_requestRole[PermissionsRole]) { - data.insert("permissions", item.permissionsString()); + data.insert(sharedValue("permissions"), item.permissionsString()); } if (m_requestRole[OwnerRole]) { - data.insert("owner", item.user()); + data.insert(sharedValue("owner"), item.user()); } if (m_requestRole[GroupRole]) { - data.insert("group", item.group()); + data.insert(sharedValue("group"), item.group()); } if (m_requestRole[DestinationRole]) { QString destination = item.linkDest(); if (destination.isEmpty()) { - destination = i18nc("@item:intable", "No destination"); + destination = QStringLiteral("-"); } - data.insert("destination", destination); + data.insert(sharedValue("destination"), destination); } if (m_requestRole[PathRole]) { QString path; - if (item.url().protocol() == QLatin1String("trash")) { + if (item.url().scheme() == QLatin1String("trash")) { path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA); } else { + // For performance reasons cache the home-path in a static QString + // (see QDir::homePath() for more details) + static QString homePath; + if (homePath.isEmpty()) { + homePath = QDir::homePath(); + } + path = item.localPath(); + if (path.startsWith(homePath)) { + path.replace(0, homePath.length(), QLatin1Char('~')); + } } const int index = path.lastIndexOf(item.text()); path = path.mid(0, index - 1); - data.insert("path", path); + data.insert(sharedValue("path"), path); } - if (m_requestRole[IsExpandedRole]) { - data.insert("isExpanded", false); + 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]) { - data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl()); + if (m_requestRole[IsExpandableRole] && isDir) { + data.insert(sharedValue("isExpandable"), true); } - if (m_requestRole[ExpansionLevelRole]) { - if (m_rootExpansionLevel == UninitializedRootExpansionLevel && m_dirLister.data()) { - const KUrl rootUrl = m_dirLister.data()->url(); - const QString protocol = rootUrl.protocol(); - const bool forceRootExpansionLevel = (protocol == QLatin1String("trash") || - protocol == QLatin1String("nepomuk") || - protocol == QLatin1String("remote") || - protocol.contains(QLatin1String("search"))); - if (forceRootExpansionLevel) { - m_rootExpansionLevel = ForceRootExpansionLevel; - } else { - const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash); - m_rootExpansionLevel = rootDir.count('/'); - } - } - - if (m_rootExpansionLevel == ForceRootExpansionLevel) { - data.insert("expansionLevel", -1); - } else { - const QString dir = item.url().directory(KUrl::AppendTrailingSlash); - const int level = dir.count('/') - m_rootExpansionLevel; - data.insert("expansionLevel", level); + if (m_requestRole[ExpandedParentsCountRole]) { + if (parent) { + const int level = expandedParentsCount(parent) + 1; + data.insert(sharedValue("expandedParentsCount"), level); } } if (item.isMimeTypeKnown()) { - data.insert("iconName", item.iconName()); + data.insert(sharedValue("iconName"), item.iconName()); if (m_requestRole[TypeRole]) { - data.insert("type", item.mimeComment()); + data.insert(sharedValue("type"), item.mimeComment()); } + } else if (m_requestRole[TypeRole] && isDir) { + static const QString folderMimeType = item.mimeComment(); + data.insert(sharedValue("type"), folderMimeType); } return data; } -bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const +bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const { int result = 0; - if (m_rootExpansionLevel >= 0) { - result = expansionLevelsCompare(a, b); - if (result != 0) { - // The items have parents with different expansion levels - return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; + if (a->parent != b->parent) { + const int expansionLevelA = expandedParentsCount(a); + const int expansionLevelB = expandedParentsCount(b); + + // If b has a higher expansion level than a, check if a is a parent + // of b, and make sure that both expansion levels are equal otherwise. + for (int i = expansionLevelB; i > expansionLevelA; --i) { + if (b->parent == a) { + return true; + } + b = b->parent; + } + + // If a has a higher expansion level than a, check if b is a parent + // of a, and make sure that both expansion levels are equal otherwise. + for (int i = expansionLevelA; i > expansionLevelB; --i) { + if (a->parent == b) { + return false; + } + a = a->parent; + } + + Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b)); + + // Compare the last parents of a and b which are different. + while (a->parent != b->parent) { + a = a->parent; + b = b->parent; } } - if (m_sortFoldersFirst || m_sortRole == SizeRole) { + if (m_sortDirsFirst || m_sortRole == SizeRole) { const bool isDirA = a->item.isDir(); const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { @@ -1277,12 +1710,72 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const } } - result = sortRoleCompare(a, b); + result = sortRoleCompare(a, b, collator); return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } -int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const +/** + * Helper class for KFileItemModel::sort(). + */ +class KFileItemModelLessThan +{ +public: + KFileItemModelLessThan(const KFileItemModel* model, const QCollator& collator) : + m_model(model), + m_collator(collator) + { + } + + 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); + + if (m_sortRole == NameRole) { + // Sorting by name 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); + } 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); + } +} + +int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const { const KFileItem& itemA = a->item; const KFileItem& itemB = b->item; @@ -1293,7 +1786,7 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const case NameRole: // The name role is handled as default fallback after the switch break; - + case SizeRole: { if (itemA.isDir()) { // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): @@ -1326,9 +1819,31 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const break; } - case DateRole: { - const KDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); - const KDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); + case ModificationTimeRole: { + const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); + const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); + if (dateTimeA < dateTimeB) { + result = -1; + } else if (dateTimeA > dateTimeB) { + result = +1; + } + break; + } + + case CreationTimeRole: { + const QDateTime dateTimeA = itemA.time(KFileItem::CreationTime); + const QDateTime dateTimeB = itemB.time(KFileItem::CreationTime); + if (dateTimeA < dateTimeB) { + result = -1; + } else if (dateTimeA > dateTimeB) { + result = +1; + } + break; + } + + case DeletionTimeRole: { + const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); + const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { @@ -1336,28 +1851,27 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const } break; } - + case RatingRole: { result = a->values.value("rating").toInt() - b->values.value("rating").toInt(); break; } - - case PermissionsRole: - case OwnerRole: - case GroupRole: - case TypeRole: - case DestinationRole: - case PathRole: - case CommentRole: - case TagsRole: { - const QByteArray role = roleByteArray(m_sortRole); + + 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()); + break; + } + + default: { + const QByteArray role = roleForType(m_sortRole); result = QString::compare(a->values.value(role).toString(), b->values.value(role).toString()); break; } - - default: - break; + } if (result != 0) { @@ -1366,14 +1880,13 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const } // Fallback #1: Compare the text of the items - result = stringCompare(itemA.text(), itemB.text()); + result = stringCompare(itemA.text(), itemB.text(), collator); if (result != 0) { return result; } // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used - result = stringCompare(itemA.name(m_caseSensitivity == Qt::CaseInsensitive), - itemB.name(m_caseSensitivity == Qt::CaseInsensitive)); + result = stringCompare(itemA.name(), itemB.name(), collator); if (result != 0) { return result; } @@ -1384,234 +1897,26 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } -void KFileItemModel::sort(QList::iterator begin, - QList::iterator end) -{ - // The implementation is based on qStableSortHelper() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - // In opposite to qStableSort() it allows to use a member-function for the comparison of elements. - - const int span = end - begin; - if (span < 2) { - return; - } - - const QList::iterator middle = begin + span / 2; - sort(begin, middle); - sort(middle, end); - merge(begin, middle, end); -} - -void KFileItemModel::merge(QList::iterator begin, - QList::iterator pivot, - QList::iterator end) -{ - // The implementation is based on qMerge() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - const int len1 = pivot - begin; - const int len2 = end - pivot; - - if (len1 == 0 || len2 == 0) { - return; - } - - if (len1 + len2 == 2) { - if (lessThan(*(begin + 1), *(begin))) { - qSwap(*begin, *(begin + 1)); - } - return; - } - - QList::iterator firstCut; - QList::iterator secondCut; - int len2Half; - if (len1 > len2) { - const int len1Half = len1 / 2; - firstCut = begin + len1Half; - secondCut = lowerBound(pivot, end, *firstCut); - len2Half = secondCut - pivot; - } else { - len2Half = len2 / 2; - secondCut = pivot + len2Half; - firstCut = upperBound(begin, pivot, *secondCut); - } - - reverse(firstCut, pivot); - reverse(pivot, secondCut); - reverse(firstCut, secondCut); - - const QList::iterator newPivot = firstCut + len2Half; - merge(begin, firstCut, newPivot); - merge(newPivot, secondCut, end); -} - -QList::iterator KFileItemModel::lowerBound(QList::iterator begin, - QList::iterator end, - const ItemData* value) -{ - // The implementation is based on qLowerBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList::iterator middle; - int n = int(end - begin); - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (lessThan(*middle, value)) { - begin = middle + 1; - n -= half + 1; - } else { - n = half; - } - } - return begin; -} - -QList::iterator KFileItemModel::upperBound(QList::iterator begin, - QList::iterator end, - const ItemData* value) -{ - // The implementation is based on qUpperBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList::iterator middle; - int n = end - begin; - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (lessThan(value, *middle)) { - n = half; - } else { - begin = middle + 1; - n -= half + 1; - } - } - return begin; -} - -void KFileItemModel::reverse(QList::iterator begin, - QList::iterator end) -{ - // The implementation is based on qReverse() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - --end; - while (begin < end) { - qSwap(*begin++, *end--); - } -} - -int KFileItemModel::stringCompare(const QString& a, const QString& b) const -{ - // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*) - // Copyright (C) 2006 by Peter Penz - // Copyright (C) 2006 by Dominic Battre - // Copyright (C) 2006 by Martin Pool - - if (m_caseSensitivity == Qt::CaseInsensitive) { - const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive) - : QString::compare(a, b, Qt::CaseInsensitive); - if (result != 0) { - // Only return the result, if the strings are not equal. If they are equal by a case insensitive - // comparison, still a deterministic sort order is required. A case sensitive - // comparison is done as fallback. - return result; - } - } - - return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive) - : QString::compare(a, b, Qt::CaseSensitive); -} - -int KFileItemModel::expansionLevelsCompare(const ItemData* a, const ItemData* b) const +int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const { - const KUrl urlA = a->item.url(); - const KUrl urlB = b->item.url(); - if (urlA.directory() == urlB.directory()) { - // Both items have the same directory as parent - return 0; - } - - // Check whether one item is the parent of the other item - if (urlA.isParentOf(urlB)) { - return (sortOrder() == Qt::AscendingOrder) ? -1 : +1; - } else if (urlB.isParentOf(urlA)) { - return (sortOrder() == Qt::AscendingOrder) ? +1 : -1; - } - - // Determine the maximum common path of both items and - // remember the index in 'index' - const QString pathA = urlA.path(); - const QString pathB = urlB.path(); - - const int maxIndex = qMin(pathA.length(), pathB.length()) - 1; - int index = 0; - while (index <= maxIndex && pathA.at(index) == pathB.at(index)) { - ++index; - } - if (index > maxIndex) { - index = maxIndex; - } - while ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) { - --index; + if (m_naturalSorting) { + return collator.compare(a, b); } - // Determine the first sub-path after the common path and - // check whether it represents a directory or already a file - bool isDirA = true; - const QString subPathA = subPath(a->item, pathA, index, &isDirA); - bool isDirB = true; - const QString subPathB = subPath(b->item, pathB, index, &isDirB); - - if (isDirA && !isDirB) { - return (sortOrder() == Qt::AscendingOrder) ? -1 : +1; - } else if (!isDirA && isDirB) { - return (sortOrder() == Qt::AscendingOrder) ? +1 : -1; - } - - // Compare the items of the parents that represent the first - // different path after the common path. - const QString parentPathA = pathA.left(index) + subPathA; - const QString parentPathB = pathB.left(index) + subPathB; - - const ItemData* parentA = a; - while (parentA && parentA->item.url().path() != parentPathA) { - parentA = parentA->parent; - } - - const ItemData* parentB = b; - while (parentB && parentB->item.url().path() != parentPathB) { - parentB = parentB->parent; - } - - if (parentA && parentB) { - return sortRoleCompare(parentA, parentB); + const int result = QString::compare(a, b, collator.caseSensitivity()); + if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) { + // Only return the result, if the strings are not equal. If they are equal by a case insensitive + // comparison, still a deterministic sort order is required. A case sensitive + // comparison is done as fallback. + return result; } - kWarning() << "Child items without parent detected:" << a->item.url() << b->item.url(); - return QString::compare(urlA.url(), urlB.url(), Qt::CaseSensitive); -} - -QString KFileItemModel::subPath(const KFileItem& item, - const QString& itemPath, - int start, - bool* isDir) const -{ - Q_ASSERT(isDir); - const int pathIndex = itemPath.indexOf('/', start + 1); - *isDir = (pathIndex > 0) || item.isDir(); - return itemPath.mid(start, pathIndex - start); + return QString::compare(a, b, Qt::CaseSensitive); } bool KFileItemModel::useMaximumUpdateInterval() const { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister && !dirLister->url().isLocalFile(); + return !m_dirLister->url().isLocalFile(); } QList > KFileItemModel::nameRoleGroups() const @@ -1623,13 +1928,12 @@ QList > KFileItemModel::nameRoleGroups() const QString groupValue; QChar firstChar; - bool isLetter = false; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString name = m_itemData.at(i)->values.value("name").toString(); + const QString name = m_itemData.at(i)->item.text(); // Use the first character of the name as group indication QChar newFirstChar = name.at(0).toUpper(); @@ -1639,31 +1943,36 @@ QList > KFileItemModel::nameRoleGroups() const if (firstChar != newFirstChar) { QString newGroupValue; - if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) { - // Apply group 'A' - 'Z' - newGroupValue = newFirstChar; - isLetter = true; + 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)); + } + } + + 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; + } + newGroupValue = *it; + } else { + newGroupValue = newFirstChar; + } } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { // Apply group '0 - 9' for any name that starts with a digit newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); - isLetter = false; } else { - if (isLetter) { - // If the current group is 'A' - 'Z' check whether a locale character - // fits into the existing group. - // TODO: This does not work in the case if e.g. the group 'O' starts with - // an umlaut 'O' -> provide unit-test to document this known issue - const QChar prevChar(firstChar.unicode() - ushort(1)); - const QChar nextChar(firstChar.unicode() + ushort(1)); - const QString currChar(newFirstChar); - const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 && - currChar.localeAwareCompare(nextChar) < 0; - if (partOfCurrentGroup) { - continue; - } - } newGroupValue = i18nc("@title:group", "Others"); - isLetter = false; } if (newGroupValue != groupValue) { @@ -1712,61 +2021,50 @@ QList > KFileItemModel::sizeRoleGroups() const return groups; } -QList > KFileItemModel::dateRoleGroups() const +QList > KFileItemModel::timeRoleGroups(std::function fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; - const QDate currentDate = KDateTime::currentLocalDateTime().date(); - - int yearForCurrentWeek = 0; - int currentWeek = currentDate.weekNumber(&yearForCurrentWeek); - if (yearForCurrentWeek == currentDate.year() + 1) { - currentWeek = 53; - } + const QDate currentDate = QDate::currentDate(); - QDate previousModifiedDate; + QDate previousFileDate; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const KDateTime 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; - - const int daysDistance = modifiedDate.daysTo(currentDate); + previousFileDate = fileDate; - int yearForModifiedWeek = 0; - int modifiedWeek = modifiedDate.weekNumber(&yearForModifiedWeek); - if (yearForModifiedWeek == modifiedDate.year() + 1) { - modifiedWeek = 53; - } + const int daysDistance = fileDate.daysTo(currentDate); QString newGroupValue; - if (currentDate.year() == modifiedDate.year() && currentDate.month() == modifiedDate.month()) { - if (modifiedWeek > currentWeek) { - // Usecase: modified date = 2010-01-01, current date = 2010-01-22 - // modified week = 53, current week = 3 - modifiedWeek = 0; - } - switch (currentWeek - modifiedWeek) { + if (currentDate.year() == fileDate.year() && + currentDate.month() == fileDate.month()) { + + switch (daysDistance / 7) { case 0: switch (daysDistance) { case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; - default: newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A", "%A")); + default: + newGroupValue = fileTime.toString( + i18nc("@title:group Date: The week day name: dddd", "dddd")); + newGroupValue = i18nc("Can be used to script translation of \"dddd\"" + "with context @title:group Date", "%1", newGroupValue); } break; case 1: - newGroupValue = i18nc("@title:group Date", "Last Week"); + newGroupValue = i18nc("@title:group Date", "One Week Ago"); break; case 2: newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); @@ -1783,22 +2081,60 @@ 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: %B is full month name in current locale, and %Y is full year number", "Yesterday (%B, %Y)")); + newGroupValue = fileTime.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); } else if (daysDistance <= 7) { - newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A, %B is full month name in current locale, and %Y is full year number", "%A (%B, %Y)")); + newGroupValue = fileTime.toString(i18nc("@title:group Date: " + "The week day name: dddd, MMMM is full month name " + "in current locale, and yyyy is full year number", + "dddd (MMMM, yyyy)")); + newGroupValue = i18nc("Can be used to script translation of " + "\"dddd (MMMM, yyyy)\" with context @title:group Date", + "%1", newGroupValue); } else if (daysDistance <= 7 * 2) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Last Week (%B, %Y)")); + newGroupValue = fileTime.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); } else if (daysDistance <= 7 * 3) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Two Weeks Ago (%B, %Y)")); + newGroupValue = fileTime.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); } else if (daysDistance <= 7 * 4) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Three Weeks Ago (%B, %Y)")); + newGroupValue = fileTime.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); } else { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Earlier on %B, %Y")); + newGroupValue = fileTime.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); } } else { - newGroupValue = modifiedTime.toString(i18nc("@title:group The month and year: %B is full month name in current locale, and %Y is full year number", "%B, %Y")); + 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 " + "\"MMMM, yyyy\" with context @title:group Date", + "%1", newGroupValue); } } @@ -1832,7 +2168,7 @@ QList > KFileItemModel::permissionRoleGroups() const } permissionsString = newPermissionsString; - const QFileInfo info(itemData->item.url().pathOrUrl()); + const QFileInfo info(itemData->item.url().toLocalFile()); // Set user string QString user; @@ -1929,21 +2265,154 @@ QList > KFileItemModel::genericStringRoleGroups(const QByte return groups; } -KFileItemList KFileItemModel::childItems(const KFileItem& item) const +void KFileItemModel::emitSortProgress(int resolvedCount) { - KFileItemList items; + // Be tolerant against a resolvedCount with a wrong range. + // Although there should not be a case where KFileItemModelRolesUpdater + // (= caller) provides a wrong range, it is important to emit + // a useful progress information even if there is an unexpected + // implementation issue. - int index = m_items.value(item.url(), -1); - if (index >= 0) { - const int parentLevel = m_itemData.at(index)->values.value("expansionLevel").toInt(); - ++index; - while (index < m_itemData.count() && m_itemData.at(index)->values.value("expansionLevel").toInt() > parentLevel) { - items.append(m_itemData.at(index)->item); - ++index; + const int itemCount = count(); + if (resolvedCount >= itemCount) { + m_sortingProgressPercent = -1; + if (m_resortAllItemsTimer->isActive()) { + m_resortAllItemsTimer->stop(); + resortAllItems(); + } + + 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); + } + } +} + +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, I18N_NOOP2_NOSTRIP("@label", "Name"), nullptr, nullptr, false, false }, + { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), nullptr, nullptr, false, false }, + { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), nullptr, nullptr, false, false }, + { "creationtime", CreationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Created"), nullptr, nullptr, false, false }, + { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), nullptr, nullptr, false, false }, + { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), nullptr, nullptr, false, false }, + { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), nullptr, nullptr, true, false }, + { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), nullptr, nullptr, true, false }, + { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), nullptr, nullptr, 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 }, + { "imageDateTime", ImageDateTimeRole, I18N_NOOP2_NOSTRIP("@label", "Date Photographed"), I18N_NOOP2_NOSTRIP("@label", "Image"), 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 }, + { "genre", GenreRole, I18N_NOOP2_NOSTRIP("@label", "Genre"), 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 }, + { "bitrate", BitrateRole, I18N_NOOP2_NOSTRIP("@label", "Bitrate"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "releaseYear", ReleaseYearRole, I18N_NOOP2_NOSTRIP("@label", "Release Year"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "deletiontime",DeletionTimeRole,I18N_NOOP2_NOSTRIP("@label", "Deletion Time"), 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 }, + }; + + count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); + return rolesInfoMap; +} + +void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) +{ + QElapsedTimer timer; + timer.start(); + foreach (const KFileItem& item, items) { // krazy:exclude=foreach + // 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 + // type. Some special code for setting the correct mime type for + // directories is in retrieveData(). + if (!item.isDir()) { + item.determineMimeType(); + } + + if (timer.elapsed() > timeout) { + // Don't block the user interface, let the remaining items + // be resolved asynchronously. + return; } } +} + +QByteArray KFileItemModel::sharedValue(const QByteArray& value) +{ + static QSet pool; + const QSet::const_iterator it = pool.constFind(value); - return items; + if (it != pool.constEnd()) { + return *it; + } else { + pool.insert(value); + return value; + } } -#include "kfileitemmodel.moc" +bool KFileItemModel::isConsistent() const +{ + // m_items may contain less items than m_itemData because m_items + // is populated lazily, see KFileItemModel::index(const QUrl& url). + if (m_items.count() > m_itemData.count()) { + return false; + } + + 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()) { + qCWarning(DolphinDebug) << "Item" << i << "is null"; + return false; + } + + const int itemIndex = index(item); + if (itemIndex != i) { + qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex; + return false; + } + + // Check if the items are sorted correctly. + if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) { + qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" + << fileItem(i - 1) << fileItem(i); + return false; + } + + // Check if all parent-child relationships are consistent. + const ItemData* data = m_itemData.at(i); + const ItemData* parent = data->parent; + if (parent) { + if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { + qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; + return false; + } + + const int parentIndex = index(parent->item); + if (parentIndex >= i) { + qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item; + return false; + } + } + } + + return true; +}