X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/9e6d73aef89453c1c251a8678d730e3dea2f8d54..6dcbb8127c:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 33dc9979b..41ddb43b4 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,171 +1,273 @@ -/*************************************************************************** - * 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 "dolphin_generalsettings.h" + +#include +#include + +#include "dolphindebug.h" + +#include "private/kfileitemmodelsortalgorithm.h" +#include "private/kfileitemmodeldirlister.h" #include #include +#include + +#include +#include -#define KFILEITEMMODEL_DEBUG +// #define KFILEITEMMODEL_DEBUG -KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : - KItemModelBase(QByteArray(), "name", parent), - m_dirLister(dirLister), - m_naturalSorting(true), - m_sortFoldersFirst(true), - m_groupRole(NoRole), +KFileItemModel::KFileItemModel(QObject* parent) : + KItemModelBase("text", parent), + m_dirLister(0), + m_sortDirsFirst(true), m_sortRole(NameRole), - m_caseSensitivity(Qt::CaseInsensitive), - m_sortedItems(), + m_sortingProgressPercent(-1), + m_roles(), + m_itemData(), m_items(), - m_data(), + m_filter(), + m_filteredItems(), m_requestRole(), - m_minimumUpdateIntervalTimer(0), m_maximumUpdateIntervalTimer(0), + m_resortAllItemsTimer(0), m_pendingItemsToInsert(), - m_rootExpansionLevel(-1) + m_groups(), + m_expandedDirs(), + m_urlsToExpand() { - resetRoles(); - m_requestRole[NameRole] = true; - m_requestRole[IsDirRole] = true; + m_collator.setNumericMode(true); + + loadSortingSettings(); - Q_ASSERT(dirLister); + m_dirLister = new KFileItemModelDirLister(this); + m_dirLister->setDelayedMimeTypes(true); - connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); - connect(dirLister, SIGNAL(completed()), this, SLOT(slotCompleted())); - connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); - connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); - connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear())); - connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); + const QWidget* parentWidget = qobject_cast(parent); + if (parentWidget) { + m_dirLister->setMainWindow(parentWidget->window()); + } - // 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())); + 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_requestRole[IsLinkRole] = true; + m_roles.insert("text"); + m_roles.insert("isDir"); + m_roles.insert("isLink"); + m_roles.insert("isHidden"); // For slow KIO-slaves like used for searching it makes sense to show results periodically even // before the completed() or canceled() signal has been emitted. m_maximumUpdateIntervalTimer = new QTimer(this); m_maximumUpdateIntervalTimer->setInterval(2000); m_maximumUpdateIntervalTimer->setSingleShot(true); - connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); - - Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval()); + 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 + // resorting is postponed until the timer has been exceeded. + m_resortAllItemsTimer = new QTimer(this); + m_resortAllItemsTimer->setInterval(500); + m_resortAllItemsTimer->setSingleShot(true); + connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); + + connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); } KFileItemModel::~KFileItemModel() { + qDeleteAll(m_itemData); + 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 { - return m_data.count(); + return m_itemData.count(); } QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { - return m_data.at(index); + ItemData* data = m_itemData.at(index); + if (data->values.isEmpty()) { + data->values = retrieveData(data->item, data->parent); + } + + return data->values; } return QHash(); } bool KFileItemModel::setData(int index, const QHash& values) { - if (index >= 0 && index < count()) { - QHash currentValue = m_data.at(index); - - QSet changedRoles; - QHashIterator it(values); - while (it.hasNext()) { - it.next(); - const QByteArray role = it.key(); - const QVariant value = it.value(); - - if (currentValue[role] != value) { - currentValue[role] = value; - changedRoles.insert(role); - } - } + if (index < 0 || index >= count()) { + return false; + } + + QHash currentValues = data(index); + + // Determine which roles have been changed + QSet changedRoles; + QHashIterator it(values); + while (it.hasNext()) { + it.next(); + const QByteArray role = sharedValue(it.key()); + const QVariant value = it.value(); - if (!changedRoles.isEmpty()) { - m_data[index] = currentValue; - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); + if (currentValues[role] != value) { + currentValues[role] = value; + changedRoles.insert(role); } + } - return true; + if (changedRoles.isEmpty()) { + return false; } - return false; + + m_itemData[index]->values = currentValues; + 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; } -int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const +void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) { - startFromIndex = qMax(0, startFromIndex); - for (int i = startFromIndex; i < count(); i++) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { - kDebug() << data(i)["name"].toString(); - return i; - } + if (dirsFirst != m_sortDirsFirst) { + m_sortDirsFirst = dirsFirst; + resortAllItems(); } - for (int i = 0; i < startFromIndex; i++) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { - kDebug() << data(i)["name"].toString(); - return i; - } +} + +bool KFileItemModel::sortDirectoriesFirst() const +{ + return m_sortDirsFirst; +} + +void KFileItemModel::setShowHiddenFiles(bool show) +{ + m_dirLister->setShowingDotFiles(show); + m_dirLister->emitChanges(); + if (show) { + dispatchPendingItemsToInsert(); } - return -1; } -bool KFileItemModel::supportsGrouping() const +bool KFileItemModel::showHiddenFiles() const { - return true; + return m_dirLister->showingDotFiles(); } -bool KFileItemModel::supportsSorting() const +void KFileItemModel::setShowDirectoriesOnly(bool enabled) { - return true; + m_dirLister->setDirOnlyMode(enabled); } -QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const +bool KFileItemModel::showDirectoriesOnly() const +{ + return m_dirLister->dirOnlyMode(); +} + +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 = 0; - 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(); @@ -177,34 +279,167 @@ 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; +} + +int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const +{ + startFromIndex = qMax(0, startFromIndex); + for (int i = startFromIndex; i < count(); ++i) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + return i; + } + } + for (int i = 0; i < startFromIndex; ++i) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + return i; + } } + return -1; +} - return data; +bool KFileItemModel::supportsDropping(int index) const +{ + const KFileItem item = fileItem(index); + return !item.isNull() && (item.isDir() || item.isDesktopFile()); +} + +QString KFileItemModel::roleDescription(const QByteArray& role) const +{ + 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 description.value(role); +} + +QList > KFileItemModel::groups() const +{ + if (!m_itemData.isEmpty() && m_groups.isEmpty()) { +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); +#endif + switch (typeForRole(sortRole())) { + case NameRole: m_groups = nameRoleGroups(); break; + case SizeRole: m_groups = sizeRoleGroups(); break; + case ModificationTimeRole: m_groups = timeRoleGroups(KFileItem::ModificationTime); break; + case AccessTimeRole: m_groups = timeRoleGroups(KFileItem::AccessTime); break; + case PermissionsRole: m_groups = permissionRoleGroups(); break; + case RatingRole: m_groups = ratingRoleGroups(); break; + default: m_groups = genericStringRoleGroups(sortRole()); break; + } + +#ifdef KFILEITEMMODEL_DEBUG + qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); +#endif + } + + return m_groups; } KFileItem KFileItemModel::fileItem(int index) const { if (index >= 0 && index < count()) { - return m_sortedItems.at(index); + return m_itemData.at(index)->item; } return KFileItem(); } +KFileItem KFileItemModel::fileItem(const QUrl &url) const +{ + 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 index(item.url()); +} + +int KFileItemModel::index(const QUrl& url) const +{ + 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); } - return m_items.value(item, -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 +{ + return m_dirLister->rootItem(); } void KFileItemModel::clear() @@ -214,9 +449,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[IsExpandedRole] && m_requestRole[ExpansionLevelRole]; - const bool willSupportExpanding = roles.contains("isExpanded") && 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). @@ -224,89 +466,112 @@ void KFileItemModel::setRoles(const QSet& roles) } } + m_groups.clear(); resetRoles(); + 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_data[i] = retrieveData(m_sortedItems.at(i)); + 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; } } QSet KFileItemModel::roles() const { - QSet roles; - for (int i = 0; i < RolesCount; ++i) { - if (m_requestRole[i]) { - switch (i) { - case NoRole: break; - case NameRole: roles.insert("name"); break; - case SizeRole: roles.insert("size"); break; - case DateRole: roles.insert("date"); break; - case PermissionsRole: roles.insert("permissions"); break; - case OwnerRole: roles.insert("owner"); break; - case GroupRole: roles.insert("group"); break; - case TypeRole: roles.insert("type"); break; - case DestinationRole: roles.insert("destination"); break; - case PathRole: roles.insert("path"); break; - case IsDirRole: roles.insert("isDir"); break; - case IsExpandedRole: roles.insert("isExpanded"); break; - case ExpansionLevelRole: roles.insert("expansionLevel"); break; - default: Q_ASSERT(false); break; - } - } - } - return roles; + return m_roles; } bool KFileItemModel::setExpanded(int index, bool expanded) { - if (isExpanded(index) == expanded || index < 0 || index >= count()) { + if (!isExpandable(index) || isExpanded(index) == expanded) { return false; } QHash values; - values.insert("isExpanded", expanded); + values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } + const KFileItem item = m_itemData.at(index)->item; + const QUrl url = item.url(); + const QUrl targetUrl = item.targetUrl(); if (expanded) { - const KUrl url = m_sortedItems.at(index).url(); - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - dirLister->openUrl(url, KDirLister::Keep); - return true; + m_expandedDirs.insert(targetUrl, url); + m_dirLister->openUrl(url, KDirLister::Keep); + + const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); + foreach (const QVariant& var, previouslyExpandedChildren) { + m_urlsToExpand.insert(var.toUrl()); } } else { - KFileItemList itemsToRemove; - const int expansionLevel = data(index)["expansionLevel"].toInt(); - ++index; - while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) { - itemsToRemove.append(m_sortedItems.at(index)); - ++index; + // 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(); + + // 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); + } + + 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 { if (index >= 0 && index < count()) { - return m_data.at(index).value("isExpanded").toBool(); + return m_itemData.at(index)->values.value("isExpanded").toBool(); } return false; } @@ -314,281 +579,875 @@ bool KFileItemModel::isExpanded(int index) const bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { - return m_sortedItems.at(index).isDir(); + // Call data (instead of accessing m_itemData directly) + // to ensure that the value is initialized. + return data(index).value("isExpandable").toBool(); } return false; } -void KFileItemModel::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous) +int KFileItemModel::expandedParentsCount(int index) const { - Q_UNUSED(previous); - m_groupRole = roleIndex(current); + if (index >= 0 && index < count()) { + return expandedParentsCount(m_itemData.at(index)); + } + return 0; } -void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) +QSet KFileItemModel::expandedDirectories() const { - Q_UNUSED(previous); - const int itemCount = count(); - if (itemCount <= 0) { - return; + QSet result; + const auto dirs = m_expandedDirs; + for (const auto &dir : dirs) { + result.insert(dir); } + return result; +} - m_sortRole = roleIndex(current); +void KFileItemModel::restoreExpandedDirectories(const QSet &urls) +{ + m_urlsToExpand = urls; +} - KFileItemList sortedItems = m_sortedItems; - m_sortedItems.clear(); - m_items.clear(); - m_data.clear(); - emit itemsRemoved(KItemRangeList() << KItemRange(0, itemCount)); +void KFileItemModel::expandParentDirectories(const QUrl &url) +{ + const int pos = m_dirLister->url().path().length(); + + // Assure that each sub-path of the URL that should be + // expanded is added to m_urlsToExpand. KDirLister + // does not care whether the parent-URL has already been + // expanded. + QUrl urlToExpand = m_dirLister->url(); + const QStringList subDirs = url.path().mid(pos).split(QDir::separator()); + for (int i = 0; i < subDirs.count() - 1; ++i) { + urlToExpand.setPath(urlToExpand.path() + '/' + subDirs.at(i)); + m_urlsToExpand.insert(urlToExpand); + } - sort(sortedItems.begin(), sortedItems.end()); - int index = 0; - foreach (const KFileItem& item, sortedItems) { - m_sortedItems.append(item); - m_items.insert(item, index); - m_data.append(retrieveData(item)); + // 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); + while (it2.hasNext()) { + const int idx = index(it2.next()); + if (idx >= 0 && !isExpanded(idx)) { + setExpanded(idx, true); + break; + } + } +} - ++index; +void KFileItemModel::setNameFilter(const QString& nameFilter) +{ + if (m_filter.pattern() != nameFilter) { + dispatchPendingItemsToInsert(); + m_filter.setPattern(nameFilter); + applyFilters(); } +} - emit itemsInserted(KItemRangeList() << KItemRange(0, itemCount)); +QString KFileItemModel::nameFilter() const +{ + return m_filter.pattern(); } -void KFileItemModel::slotCompleted() +void KFileItemModel::setMimeTypeFilters(const QStringList& filters) { - if (m_minimumUpdateIntervalTimer->isActive()) { - // dispatchPendingItems() will be called when the timer - // has been expired. - return; + if (m_filter.mimeTypes() != filters) { + dispatchPendingItemsToInsert(); + m_filter.setMimeTypes(filters); + applyFilters(); } - - dispatchPendingItemsToInsert(); - m_minimumUpdateIntervalTimer->start(); } -void KFileItemModel::slotCanceled() +QStringList KFileItemModel::mimeTypeFilters() const { - m_minimumUpdateIntervalTimer->stop(); - m_maximumUpdateIntervalTimer->stop(); - dispatchPendingItemsToInsert(); + return m_filter.mimeTypes(); } -void KFileItemModel::slotNewItems(const KFileItemList& items) + +void KFileItemModel::applyFilters() { - m_pendingItemsToInsert.append(items); + // 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); + } + } + } - if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { - // Assure that items get dispatched if no completed() or canceled() signal is - // emitted during the maximum update interval. - m_maximumUpdateIntervalTimer->start(); + 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. + QList newVisibleItems; + + 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); } -void KFileItemModel::slotItemsDeleted(const KFileItemList& items) +void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) { - if (!m_pendingItemsToInsert.isEmpty()) { - insertItems(m_pendingItemsToInsert); - m_pendingItemsToInsert.clear(); + 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; + } } - removeItems(items); } -void KFileItemModel::slotClear() +QList KFileItemModel::rolesInformation() { -#ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Clearing all items"; -#endif + 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); + } + } + } - m_minimumUpdateIntervalTimer->stop(); - m_maximumUpdateIntervalTimer->stop(); - m_pendingItemsToInsert.clear(); + return rolesInfo; +} - m_rootExpansionLevel = -1; +void KFileItemModel::onGroupedSortingChanged(bool current) +{ + Q_UNUSED(current); + m_groups.clear(); +} - const int removedCount = m_data.count(); - if (removedCount > 0) { - m_sortedItems.clear(); - m_items.clear(); - m_data.clear(); - emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); +void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(previous); + m_sortRole = typeForRole(current); + + if (!m_requestRole[m_sortRole]) { + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); } + + resortAllItems(); } -void KFileItemModel::slotClear(const KUrl& url) +void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(url); + Q_UNUSED(current); + Q_UNUSED(previous); + resortAllItems(); } -void KFileItemModel::dispatchPendingItemsToInsert() +void KFileItemModel::loadSortingSettings() { - if (!m_pendingItemsToInsert.isEmpty()) { - insertItems(m_pendingItemsToInsert); - m_pendingItemsToInsert.clear(); + 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::insertItems(const KFileItemList& items) +void KFileItemModel::resortAllItems() { - if (items.isEmpty()) { + m_resortAllItemsTimer->stop(); + + const int itemCount = count(); + if (itemCount <= 0) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); - kDebug() << "==========================================================="; - kDebug() << "Inserting" << items.count() << "items"; + qCDebug(DolphinDebug) << "==========================================================="; + qCDebug(DolphinDebug) << "Resorting" << itemCount << "items"; #endif - KFileItemList sortedItems = items; - sort(sortedItems.begin(), sortedItems.end()); + // Remember the order of the current URLs so + // that it can be determined which indexes have + // been moved because of the resorting. + QList oldUrls; + oldUrls.reserve(itemCount); + foreach (const ItemData* itemData, m_itemData) { + oldUrls.append(itemData->item.url()); + } -#ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Sorting:" << timer.elapsed(); -#endif + m_items.clear(); + m_items.reserve(itemCount); - 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_sortedItems.count()) { - if (!lessThan(m_sortedItems.at(targetIndex), sortedItems.at(sourceIndex))) { - break; - } - ++targetIndex; - } + // Resort the items + sort(m_itemData.begin(), m_itemData.end()); + for (int i = 0; i < itemCount; ++i) { + m_items.insert(m_itemData.at(i)->item.url(), i); + } - if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) { - itemRanges << KItemRange(insertedAtIndex, insertedCount); - previouslyInsertedCount += insertedCount; - insertedAtIndex = targetIndex - previouslyInsertedCount; - insertedCount = 0; - } + // Determine the first index that has been moved. + int firstMovedIndex = 0; + while (firstMovedIndex < itemCount + && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { + ++firstMovedIndex; + } - // Insert item at the position targetIndex - const KFileItem item = sortedItems.at(sourceIndex); - m_sortedItems.insert(targetIndex, item); - m_data.insert(targetIndex, retrieveData(item)); - // m_items will be inserted after the loop (see comment below) - ++insertedCount; + const bool itemsHaveMoved = firstMovedIndex < itemCount; + if (itemsHaveMoved) { + m_groups.clear(); - if (insertedAtIndex < 0) { - insertedAtIndex = targetIndex; - Q_ASSERT(previouslyInsertedCount == 0); + int lastMovedIndex = itemCount - 1; + while (lastMovedIndex > firstMovedIndex + && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { + --lastMovedIndex; } - ++targetIndex; - ++sourceIndex; - } - // The indexes of all m_items must be adjusted, not only the index - // of the new items - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i), i); - } + 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); + } - itemRanges << KItemRange(insertedAtIndex, insertedCount); - emit itemsInserted(itemRanges); + 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] Inserting of" << items.count() << "items:" << timer.elapsed(); + qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); #endif } -void KFileItemModel::removeItems(const KFileItemList& items) +void KFileItemModel::slotCompleted() { - if (items.isEmpty()) { - return; - } + dispatchPendingItemsToInsert(); -#ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Removing " << items.count() << "items"; -#endif + if (!m_urlsToExpand.isEmpty()) { + // Try to find a URL that can be expanded. + // Note that the parent folder must be expanded before any of its subfolders become visible. + // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet + // -> we expand the first visible URL we find in m_restoredExpandedUrls. + foreach (const QUrl& url, m_urlsToExpand) { + const int indexForUrl = index(url); + if (indexForUrl >= 0) { + m_urlsToExpand.remove(url); + if (setExpanded(indexForUrl, true)) { + // The dir lister has been triggered. This slot will be called + // again after the directory has been expanded. + return; + } + } + } - KFileItemList sortedItems = items; - sort(sortedItems.begin(), sortedItems.end()); + // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen + // if these URLs have been deleted in the meantime. + m_urlsToExpand.clear(); + } - QList indexesToRemove; - indexesToRemove.reserve(items.count()); + emit directoryLoadingCompleted(); +} - // Calculate the item ranges that will get deleted - KItemRangeList itemRanges; - int removedAtIndex = -1; - int removedCount = 0; - int targetIndex = 0; - foreach (const KFileItem& itemToRemove, sortedItems) { - const int previousTargetIndex = targetIndex; - while (targetIndex < m_sortedItems.count()) { - if (m_sortedItems.at(targetIndex).url() == itemToRemove.url()) { - break; - } - ++targetIndex; +void KFileItemModel::slotCanceled() +{ + m_maximumUpdateIntervalTimer->stop(); + dispatchPendingItemsToInsert(); + + emit directoryLoadingCanceled(); +} + +void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) +{ + Q_ASSERT(!items.isEmpty()); + + QUrl parentUrl; + if (m_expandedDirs.contains(directoryUrl)) { + parentUrl = m_expandedDirs.value(directoryUrl); + } else { + parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); + } + + 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. + 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(); } - if (targetIndex >= m_sortedItems.count()) { - kWarning() << "Item that should be deleted has not been found!"; + + // 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. + const int parentIndex = index(parentUrl); + if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { + // The parent is not expanded. return; } + } + + QList itemDataList = createItemDataList(parentUrl, items); + + if (!m_filter.hasSetFilters()) { + m_pendingItemsToInsert.append(itemDataList); + } else { + // The name or type filter is active. Hide filtered items + // before inserting them into the model and remember + // the filtered items in m_filteredItems. + foreach (ItemData* itemData, itemDataList) { + if (m_filter.matches(itemData->item)) { + m_pendingItemsToInsert.append(itemData); + } else { + m_filteredItems.insert(itemData->item, itemData); + } + } + } - if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) { - itemRanges << KItemRange(removedAtIndex, removedCount); - removedAtIndex = targetIndex; - removedCount = 0; + if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { + // Assure that items get dispatched if no completed() or canceled() signal is + // emitted during the maximum update interval. + m_maximumUpdateIntervalTimer->start(); + } +} + +void KFileItemModel::slotItemsDeleted(const KFileItemList& items) +{ + dispatchPendingItemsToInsert(); + + 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); + } + } + } + + 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.append(targetIndex); - if (removedAtIndex < 0) { - removedAtIndex = targetIndex; + indexesToRemove = indexesToRemoveWithChildren; + } + + const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); + removeFilteredChildren(itemRanges); + removeItems(itemRanges, DeleteItemData); +} + +void KFileItemModel::slotRefreshItems(const QList >& items) +{ + Q_ASSERT(!items.isEmpty()); +#ifdef KFILEITEMMODEL_DEBUG + qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items"; +#endif + + // 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 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(), 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); + } } - ++removedCount; - ++targetIndex; } - // Delete the items - for (int i = indexesToRemove.count() - 1; i >= 0; --i) { - const int indexToRemove = indexesToRemove.at(i); - m_items.remove(m_sortedItems.at(indexToRemove)); - m_sortedItems.removeAt(indexToRemove); - m_data.removeAt(indexToRemove); + // If the changed items have been created recently, they might not be in m_items yet. + // In that case, the list 'indexes' might be empty. + if (indexes.isEmpty()) { + return; + } + + // Extract the item-ranges out of the changed indexes + qSort(indexes); + const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); + emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); +} + +void KFileItemModel::slotClear() +{ +#ifdef KFILEITEMMODEL_DEBUG + qCDebug(DolphinDebug) << "Clearing all items"; +#endif + + qDeleteAll(m_filteredItems); + m_filteredItems.clear(); + m_groups.clear(); + + m_maximumUpdateIntervalTimer->stop(); + m_resortAllItemsTimer->stop(); + + qDeleteAll(m_pendingItemsToInsert); + m_pendingItemsToInsert.clear(); + + const int removedCount = m_itemData.count(); + if (removedCount > 0) { + qDeleteAll(m_itemData); + m_itemData.clear(); + m_items.clear(); + emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } - // The indexes of all m_items must be adjusted, not only the index - // of the removed items - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i), i); + m_expandedDirs.clear(); +} + +void KFileItemModel::slotSortingChoiceChanged() +{ + loadSortingSettings(); + resortAllItems(); +} + +void KFileItemModel::dispatchPendingItemsToInsert() +{ + if (!m_pendingItemsToInsert.isEmpty()) { + insertItems(m_pendingItemsToInsert); + m_pendingItemsToInsert.clear(); } +} - if (count() <= 0) { - m_rootExpansionLevel = -1; +void KFileItemModel::insertItems(QList& newItems) +{ + if (newItems.isEmpty()) { + return; } - itemRanges << KItemRange(removedAtIndex, removedCount); +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); + 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()); + } + + sort(newItems.begin(), newItems.end()); + +#ifdef KFILEITEMMODEL_DEBUG + qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed(); +#endif + + KItemRangeList itemRanges; + 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; + } + + // Push the final item range to itemRanges. + if (rangeCount > 0) { + itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); + } + + // Note that itemRanges is still sorted in reverse order. + std::reverse(itemRanges.begin(), itemRanges.end()); + } + + // 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(); + + emit itemsInserted(itemRanges); + +#ifdef KFILEITEMMODEL_DEBUG + qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); +#endif +} + +void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) +{ + if (itemRanges.isEmpty()) { + return; + } + + m_groups.clear(); + + // Step 1: Remove the items from m_itemData, and free the ItemData. + int removedItemsCount = 0; + foreach (const KItemRange& range, itemRanges) { + removedItemsCount += range.count; + + for (int index = range.index; index < range.index + range.count; ++index) { + if (behavior == DeleteItemData) { + delete m_itemData.at(index); + } + + m_itemData[index] = 0; + } + } + + // 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; + + const int oldItemDataCount = m_itemData.count(); + while (source < oldItemDataCount) { + m_itemData[target] = m_itemData[source]; + ++target; + ++source; + + if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { + // Skip the items in the next removed range. + source += itemRanges.at(nextRange).count; + ++nextRange; + } + } + + m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); + + // 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(); + emit itemsRemoved(itemRanges); } -void KFileItemModel::removeExpandedItems() +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()); - KFileItemList expandedItems; + foreach (const KFileItem& item, items) { + ItemData* itemData = new ItemData(); + itemData->item = item; + itemData->parent = parentItem; + itemDataList.append(itemData); + } + + return itemDataList; +} - const int maxIndex = m_data.count() - 1; +void KFileItemModel::prepareItemsForSorting(QList& itemDataList) +{ + switch (m_sortRole) { + case PermissionsRole: + case OwnerRole: + case GroupRole: + case DestinationRole: + case PathRole: + // 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; + + 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; + } +} + +void KFileItemModel::removeExpandedItems() +{ + QVector indexesToRemove; + + const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { - if (m_data.at(i).value("expansionLevel").toInt() > 0) { - const KFileItem fileItem = m_sortedItems.at(i); - expandedItems.append(fileItem); + const ItemData* itemData = m_itemData.at(i); + 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 = -1; + 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() @@ -598,126 +1457,211 @@ void KFileItemModel::resetRoles() } } -KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const +KFileItemModel::RoleType KFileItemModel::typeForRole(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("isDir", IsDirRole); - rolesHash.insert("isExpanded", IsExpandedRole); - rolesHash.insert("expansionLevel", ExpansionLevelRole); + 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 rolesHash.value(role, NoRole); + + return roles.value(role, NoRole); } -QHash KFileItemModel::retrieveData(const KFileItem& item) const +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.value(roleType); +} + +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(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.name()); + 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[DateRole]) { + 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 KDateTime dateTime = item.time(KFileItem::ModificationTime); - data.insert("date", dateTime.dateTime()); + const QDateTime dateTime = item.time(KFileItem::ModificationTime); + data.insert(sharedValue("modificationtime"), 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]) { - data.insert("path", item.localPath()); + QString path; + 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(sharedValue("path"), path); } - if (m_requestRole[IsExpandedRole]) { - data.insert("isExpanded", false); + if (m_requestRole[IsExpandableRole] && isDir) { + data.insert(sharedValue("isExpandable"), true); } - if (m_requestRole[ExpansionLevelRole]) { - if (m_rootExpansionLevel < 0) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash); - m_rootExpansionLevel = rootDir.count('/'); - } + if (m_requestRole[ExpandedParentsCountRole]) { + if (parent) { + const int level = expandedParentsCount(parent) + 1; + data.insert(sharedValue("expandedParentsCount"), level); } - const QString dir = item.url().directory(KUrl::AppendTrailingSlash); - const int level = dir.count('/') - m_rootExpansionLevel - 1; - data.insert("expansionLevel", 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 KFileItem& a, const KFileItem& 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 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) { - const bool isDirA = a.isDir(); - const bool isDirB = b.isDir(); + if (m_sortDirsFirst || m_sortRole == SizeRole) { + const bool isDirA = a->item.isDir(); + const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { return true; } else if (!isDirA && isDirB) { @@ -725,20 +1669,118 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const } } + result = sortRoleCompare(a, b, collator); + + return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; +} + +/** + * 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; + + int result = 0; + switch (m_sortRole) { - case NameRole: { - result = stringCompare(a.text(), b.text()); - if (result == 0) { - // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used - result = stringCompare(a.name(m_caseSensitivity == Qt::CaseInsensitive), - b.name(m_caseSensitivity == Qt::CaseInsensitive)); + 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(): + Q_ASSERT(itemB.isDir()); + + const QVariant valueA = a->values.value("size"); + const QVariant valueB = b->values.value("size"); + if (valueA.isNull() && valueB.isNull()) { + result = 0; + } else if (valueA.isNull()) { + result = -1; + } else if (valueB.isNull()) { + result = +1; + } else { + result = valueA.toInt() - valueB.toInt(); + } + } else { + // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): + Q_ASSERT(!itemB.isDir()); + const KIO::filesize_t sizeA = itemA.size(); + const KIO::filesize_t sizeB = itemB.size(); + if (sizeA > sizeB) { + result = +1; + } else if (sizeA < sizeB) { + result = -1; + } else { + result = 0; + } } break; } - case DateRole: { - const KDateTime dateTimeA = a.time(KFileItem::ModificationTime); - const KDateTime dateTimeB = b.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) { @@ -747,172 +1789,561 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const break; } - default: + case RatingRole: { + result = a->values.value("rating").toInt() - b->values.value("rating").toInt(); break; } - if (result == 0) { - // It must be assured that the sort order is always unique even if two values have been - // equal. In this case a comparison of the URL is done which is unique in all cases - // within KDirLister. - result = QString::compare(a.url().url(), b.url().url(), Qt::CaseSensitive); + 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; } - return result < 0; + default: { + const QByteArray role = roleForType(m_sortRole); + result = QString::compare(a->values.value(role).toString(), + b->values.value(role).toString()); + break; + } + + } + + if (result != 0) { + // The current sort role was sufficient to define an order + return result; + } + + // Fallback #1: Compare the text of the items + 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(), itemB.name(), collator); + if (result != 0) { + return result; + } + + // Fallback #3: It must be assured that the sort order is always unique even if two values have been + // equal. In this case a comparison of the URL is done which is unique in all cases + // within KDirLister. + return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } -void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator) +int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const { - KFileItemList::iterator start = startIterator; - KFileItemList::iterator end = endIterator; + if (m_naturalSorting) { + return collator.compare(a, b); + } - // The implementation is based on qSortHelper() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - // In opposite to qSort() it allows to use a member-function for the comparison of elements. - while (1) { - int span = int(end - start); - if (span < 2) { - return; + 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; + } + + return QString::compare(a, b, Qt::CaseSensitive); +} + +bool KFileItemModel::useMaximumUpdateInterval() const +{ + return !m_dirLister->url().isLocalFile(); +} + +QList > KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString groupValue; + QChar firstChar; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; } - --end; - KFileItemList::iterator low = start, high = end - 1; - KFileItemList::iterator pivot = start + span / 2; + const QString name = m_itemData.at(i)->item.text(); - if (lessThan(*end, *start)) { - qSwap(*end, *start); + // Use the first character of the name as group indication + QChar newFirstChar = name.at(0).toUpper(); + if (newFirstChar == QLatin1Char('~') && name.length() > 1) { + newFirstChar = name.at(1).toUpper(); } - if (span == 2) { - return; + + if (firstChar != newFirstChar) { + QString newGroupValue; + if (newFirstChar.isLetter()) { + // Try to find a matching group in the range 'A' to 'Z'. + static std::vector lettersAtoZ; + lettersAtoZ.reserve('Z' - 'A' + 1); + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } + + 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"); + } else { + newGroupValue = i18nc("@title:group", "Others"); + } + + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + + firstChar = newFirstChar; } + } + return groups; +} + +QList > KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; - if (lessThan(*pivot, *start)) { - qSwap(*pivot, *start); + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; } - if (lessThan(*end, *pivot)) { - qSwap(*end, *pivot); + + const KFileItem& item = m_itemData.at(i)->item; + const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; + QString newGroupValue; + if (!item.isNull() && item.isDir()) { + newGroupValue = i18nc("@title:group Size", "Folders"); + } else if (fileSize < 5 * 1024 * 1024) { + newGroupValue = i18nc("@title:group Size", "Small"); + } else if (fileSize < 10 * 1024 * 1024) { + newGroupValue = i18nc("@title:group Size", "Medium"); + } else { + newGroupValue = i18nc("@title:group Size", "Big"); } - if (span == 3) { - return; + + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); } + } - qSwap(*pivot, *end); + return groups; +} - while (low < high) { - while (low < high && lessThan(*low, *end)) { - ++low; - } +QList > KFileItemModel::timeRoleGroups(KFileItem::FileTimes which) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; - while (high > low && lessThan(*end, *high)) { - --high; + const QDate currentDate = QDate::currentDate(); + + QDate previousFileDate; + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + const QDateTime fileTime = m_itemData.at(i)->item.time(which); + const QDate fileDate = fileTime.date(); + if (fileDate == previousFileDate) { + // The current item is in the same group as the previous item + continue; + } + previousFileDate = fileDate; + + const int daysDistance = fileDate.daysTo(currentDate); + + QString newGroupValue; + 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 = 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", "One Week Ago"); + break; + case 2: + newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); + break; + case 3: + newGroupValue = i18nc("@title:group Date", "Three Weeks Ago"); + break; + case 4: + case 5: + newGroupValue = i18nc("@title:group Date", "Earlier this Month"); + break; + default: + Q_ASSERT(false); } - if (low < high) { - qSwap(*low, *high); - ++low; - --high; + } else { + const QDate lastMonthDate = currentDate.addMonths(-1); + if (lastMonthDate.year() == fileDate.year() && + lastMonthDate.month() == fileDate.month()) { + + if (daysDistance == 1) { + 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 = 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 = 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 = 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 = 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 = 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 { - break; + 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); } } - if (lessThan(*low, *end)) { - ++low; + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + +QList > KFileItemModel::permissionRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString permissionsString; + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + const ItemData* itemData = m_itemData.at(i); + const QString newPermissionsString = itemData->values.value("permissions").toString(); + if (newPermissionsString == permissionsString) { + continue; } + permissionsString = newPermissionsString; + + const QFileInfo info(itemData->item.url().toLocalFile()); - qSwap(*end, *low); - sort(start, low); + // Set user string + QString user; + if (info.permission(QFile::ReadUser)) { + user = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteUser)) { + user += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeUser)) { + user += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2); - start = low + 1; - ++end; + // Set group string + QString group; + if (info.permission(QFile::ReadGroup)) { + group = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteGroup)) { + group += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeGroup)) { + group += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2); + + // Set others string + QString others; + if (info.permission(QFile::ReadOther)) { + others = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteOther)) { + others += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeOther)) { + others += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2); + + const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } } + + return groups; } -int KFileItemModel::stringCompare(const QString& a, const QString& b) const +QList > KFileItemModel::ratingRoleGroups() 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 + Q_ASSERT(!m_itemData.isEmpty()); - 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; + const int maxIndex = count() - 1; + QList > groups; + + int groupValue = -1; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); } } - return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive) - : QString::compare(a, b, Qt::CaseSensitive); + return groups; } -int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const +QList > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const { - const KUrl urlA = a.url(); - const KUrl urlB = b.url(); - if (urlA.directory() == urlB.directory()) { - // Both items have the same directory as parent - return 0; - } + Q_ASSERT(!m_itemData.isEmpty()); - // Check whether one item is the parent of the other item - if (urlA.isParentOf(urlB)) { - return -1; - } else if (urlB.isParentOf(urlA)) { - return +1; + const int maxIndex = count() - 1; + QList > groups; + + bool isFirstGroupValue = true; + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); + if (newGroupValue != groupValue || isFirstGroupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + isFirstGroupValue = false; + } } - // Determine the maximum common path of both items and - // remember the index in 'index' - const QString pathA = urlA.path(); - const QString pathB = urlB.path(); + return groups; +} - 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; - } +void KFileItemModel::emitSortProgress(int resolvedCount) +{ + // 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. - // 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, pathA, index, &isDirA); - bool isDirB = true; - const QString subPathB = subPath(b, pathB, index, &isDirB); + const int itemCount = count(); + if (resolvedCount >= itemCount) { + m_sortingProgressPercent = -1; + if (m_resortAllItemsTimer->isActive()) { + m_resortAllItemsTimer->stop(); + resortAllItems(); + } - if (isDirA && !isDirB) { - return -1; - } else if (!isDirA && isDirB) { - return +1; + 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); + } } +} - return stringCompare(subPathA, subPathB); +const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) +{ + static const RoleInfoMap rolesInfoMap[] = { + // | role | roleType | role translation | group translation | requires Baloo | requires indexer + { 0, NoRole, 0, 0, 0, 0, false, false }, + { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), 0, 0, false, false }, + { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), 0, 0, false, false }, + { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), 0, 0, false, false }, + { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), 0, 0, false, false }, + { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), 0, 0, false, false }, + { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), 0, 0, true, false }, + { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), 0, 0, true, false }, + { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), 0, 0, true, false }, + { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, + { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, + { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, + { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, + { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, + { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false }, + { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + }; + + count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); + return rolesInfoMap; } -QString KFileItemModel::subPath(const KFileItem& item, - const QString& itemPath, - int start, - bool* isDir) const +void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { - Q_ASSERT(isDir); - const int pathIndex = itemPath.indexOf('/', start + 1); - *isDir = (pathIndex > 0) || item.isDir(); - return itemPath.mid(start, pathIndex - start); + 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; + } + } } -bool KFileItemModel::useMaximumUpdateInterval() const +QByteArray KFileItemModel::sharedValue(const QByteArray& value) { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister && !dirLister->url().isLocalFile(); + static QSet pool; + const QSet::const_iterator it = pool.constFind(value); + + 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; i < count(); ++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; +}