X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/ebfcb5e19b345a0fbb2425f537232f45d3b3d62a..a46121dc510f987f2d164b43eaf5f84ea8c83cb8:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 37a30519a..51bf546f9 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -21,11 +21,11 @@ #include "kfileitemmodel.h" -#include #include #include #include #include +#include //TODO: port to QCollator #include "private/kfileitemmodelsortalgorithm.h" #include "private/kfileitemmodeldirlister.h" @@ -69,18 +69,17 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_dirLister->setMainWindow(parentWidget->window()); } - connect(m_dirLister, SIGNAL(started(KUrl)), this, SIGNAL(directoryLoadingStarted())); - connect(m_dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); - connect(m_dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted())); - connect(m_dirLister, SIGNAL(itemsAdded(KUrl,KFileItemList)), this, SLOT(slotItemsAdded(KUrl,KFileItemList))); - connect(m_dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); - connect(m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); - connect(m_dirLister, SIGNAL(clear()), this, SLOT(slotClear())); - connect(m_dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); - connect(m_dirLister, SIGNAL(infoMessage(QString)), this, SIGNAL(infoMessage(QString))); - connect(m_dirLister, SIGNAL(errorMessage(QString)), this, SIGNAL(errorMessage(QString))); - connect(m_dirLister, SIGNAL(redirection(KUrl,KUrl)), this, SIGNAL(directoryRedirection(KUrl,KUrl))); - connect(m_dirLister, SIGNAL(urlIsFileError(KUrl)), this, SIGNAL(urlIsFileError(KUrl))); + connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted); + connect(m_dirLister, static_cast(&KFileItemModelDirLister::canceled), this, &KFileItemModel::slotCanceled); + connect(m_dirLister, static_cast(&KFileItemModelDirLister::completed), this, &KFileItemModel::slotCompleted); + connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); + connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); + connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); + connect(m_dirLister, static_cast(&KFileItemModelDirLister::clear), this, &KFileItemModel::slotClear); + connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage); + connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage); + connect(m_dirLister, static_cast(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection); + connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); // Apply default roles that should be determined resetRoles(); @@ -96,7 +95,7 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_maximumUpdateIntervalTimer = new QTimer(this); m_maximumUpdateIntervalTimer->setInterval(2000); m_maximumUpdateIntervalTimer->setSingleShot(true); - connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); + connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert); // When changing the value of an item which represents the sort-role a resorting must be // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done @@ -105,9 +104,10 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_resortAllItemsTimer = new QTimer(this); m_resortAllItemsTimer->setInterval(500); m_resortAllItemsTimer->setSingleShot(true); - connect(m_resortAllItemsTimer, SIGNAL(timeout()), this, SLOT(resortAllItems())); + connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); - connect(KGlobalSettings::self(), SIGNAL(naturalSortingChanged()), this, SLOT(slotNaturalSortingChanged())); + connect(KGlobalSettings::self(), &KGlobalSettings::naturalSortingChanged, + this, &KFileItemModel::slotNaturalSortingChanged); } KFileItemModel::~KFileItemModel() @@ -152,7 +152,12 @@ int KFileItemModel::count() const QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { - return m_itemData.at(index)->values; + ItemData* data = m_itemData.at(index); + if (data->values.isEmpty()) { + data->values = retrieveData(data->item, data->parent); + } + + return data->values; } return QHash(); } @@ -163,7 +168,7 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - QHash currentValues = m_itemData.at(index)->values; + QHash currentValues = data(index); // Determine which roles have been changed QSet changedRoles; @@ -190,33 +195,7 @@ bool KFileItemModel::setData(int index, const QHash& value m_itemData[index]->item.setUrl(url); } - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - - // Trigger a resorting if the item's correct position has changed. 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))) { - // Compare the changed item with its neighbors to see - // if an expensive resorting is needed at all. - const ItemData* changedItem = m_itemData.at(index); - const ItemData* previousItem = (index == 0) ? 0 : m_itemData.at(index - 1); - const ItemData* nextItem = (index == m_itemData.count() - 1) ? 0 : m_itemData.at(index + 1); - - if ((previousItem && lessThan(changedItem, previousItem)) - || (nextItem && lessThan(nextItem, changedItem))) { - m_resortAllItemsTimer->start(); - } else 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(); - } - } + emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); return true; } @@ -239,7 +218,7 @@ void KFileItemModel::setShowHiddenFiles(bool show) m_dirLister->setShowingDotFiles(show); m_dirLister->emitChanges(); if (show) { - slotCompleted(); + dispatchPendingItemsToInsert(); } } @@ -258,7 +237,7 @@ bool KFileItemModel::showDirectoriesOnly() const return m_dirLister->dirOnlyMode(); } -QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const +QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const { QMimeData* data = new QMimeData(); @@ -268,11 +247,23 @@ QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const KUrl::List urls; KUrl::List mostLocalUrls; bool canUseMostLocalUrls = true; + const ItemData* lastAddedItem = 0; - QSetIterator it(indexes); - while (it.hasNext()) { - const int index = it.next(); - const KFileItem item = fileItem(index); + foreach (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.targetUrl(); @@ -285,9 +276,7 @@ QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const } const bool different = canUseMostLocalUrls && mostLocalUrls != urls; - urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0 if (different) { - mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls); urls.populateMimeData(mostLocalUrls, data); } else { urls.populateMimeData(data); @@ -300,12 +289,12 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (data(i)["text"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (data(i)["text"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } @@ -367,27 +356,50 @@ KFileItem KFileItemModel::fileItem(int index) const KFileItem KFileItemModel::fileItem(const KUrl& url) const { - const int index = m_items.value(url, -1); - if (index >= 0) { - return m_itemData.at(index)->item; + const int indexForUrl = index(url); + if (indexForUrl >= 0) { + return m_itemData.at(indexForUrl)->item; } return KFileItem(); } int KFileItemModel::index(const KFileItem& item) const { - if (item.isNull()) { - return -1; - } - - return m_items.value(item.url(), -1); + return index(KUrl(item.url())); } int KFileItemModel::index(const KUrl& url) const { KUrl urlToFind = url; urlToFind.adjustPath(KUrl::RemoveTrailingSlash); - return m_items.value(urlToFind, -1); + + 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 KUrl nextUrl = m_itemData.at(i)->item.url(); + m_items.insert(nextUrl, i); + } + + itemsInHash = currentBlockEnd; + index = m_items.value(urlToFind, -1); + } + + Q_ASSERT(index >= 0 || m_items.count() == m_itemData.count()); + + return index; } KFileItem KFileItemModel::rootItem() const @@ -436,6 +448,15 @@ void KFileItemModel::setRoles(const QSet& roles) kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!"; emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet()); } + + // 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 @@ -461,15 +482,51 @@ bool KFileItemModel::setExpanded(int index, bool expanded) if (expanded) { m_expandedDirs.insert(targetUrl, url); m_dirLister->openUrl(url, KDirLister::Keep); + + const KUrl::List previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); + foreach (const KUrl& url, previouslyExpandedChildren) { + m_urlsToExpand.insert(url); + } } else { + // 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); - removeFilteredChildren(KFileItemList() << item); + const int parentLevel = expandedParentsCount(index); + const int itemCount = m_itemData.count(); + const int firstChildIndex = index + 1; + + KUrl::List expandedChildren; + + int childIndex = firstChildIndex; + while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { + ItemData* itemData = m_itemData.at(childIndex); + if (itemData->values.value("isExpanded").toBool()) { + const KUrl targetUrl = itemData->item.targetUrl(); + const KUrl 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; + } + const int childrenCount = childIndex - firstChildIndex; - const KFileItemList itemsToRemove = childItems(item); - removeFilteredChildren(itemsToRemove); - removeItems(itemsToRemove, DeleteItemData); + removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount)); + removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData); + + m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren); } return true; @@ -486,7 +543,9 @@ bool KFileItemModel::isExpanded(int index) const bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { - return m_itemData.at(index)->values.value("isExpandable").toBool(); + // Call data (instead of accessing m_itemData directly) + // to ensure that the value is initialized. + return data(index).value("isExpandable").toBool(); } return false; } @@ -494,10 +553,7 @@ bool KFileItemModel::isExpandable(int index) const int KFileItemModel::expandedParentsCount(int index) const { if (index >= 0 && index < count()) { - const int parentsCount = m_itemData.at(index)->values.value("expandedParentsCount").toInt(); - if (parentsCount > 0) { - return parentsCount; - } + return expandedParentsCount(m_itemData.at(index)); } return 0; } @@ -573,21 +629,25 @@ void KFileItemModel::applyFilters() { // Check which shown items from m_itemData must get // hidden and hence moved to m_filteredItems. - KFileItemList newFilteredItems; + QVector newFilteredIndexes; + + const int itemCount = m_itemData.count(); + for (int index = 0; index < itemCount; ++index) { + ItemData* itemData = m_itemData.at(index); - foreach (ItemData* itemData, m_itemData) { // 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)) { - newFilteredItems.append(item); + newFilteredIndexes.append(index); m_filteredItems.insert(item, itemData); } } } - removeItems(newFilteredItems, KeepItemData); + 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. @@ -606,22 +666,24 @@ void KFileItemModel::applyFilters() insertItems(newVisibleItems); } -void KFileItemModel::removeFilteredChildren(const KFileItemList& parentsList) +void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) { - if (m_filteredItems.isEmpty()) { + 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; } - // First, we put the parent items into a set to provide fast lookup - // while iterating over m_filteredItems and prevent quadratic - // complexity if there are N parents and N filtered items. - const QSet parents = parentsList.toSet(); + 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()) { - const ItemData* parent = it.value()->parent; - - if (parent && parents.contains(parent->item)) { + if (parents.contains(it.value()->parent)) { delete it.value(); it = m_filteredItems.erase(it); } else { @@ -649,7 +711,7 @@ QList KFileItemModel::rolesInformation() // menus tries to put the actions into sub menus otherwise. info.group = QString(); } - info.requiresNepomuk = map[i].requiresNepomuk; + info.requiresBaloo = map[i].requiresBaloo; info.requiresIndexer = map[i].requiresIndexer; rolesInfo.append(info); } @@ -712,6 +774,7 @@ void KFileItemModel::resortAllItems() } m_items.clear(); + m_items.reserve(itemCount); // Resort the items sort(m_itemData.begin(), m_itemData.end()); @@ -774,10 +837,10 @@ void KFileItemModel::slotCompleted() // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet // -> we expand the first visible URL we find in m_restoredExpandedUrls. foreach (const KUrl& url, m_urlsToExpand) { - const int index = m_items.value(url, -1); - if (index >= 0) { + const int indexForUrl = index(url); + if (indexForUrl >= 0) { m_urlsToExpand.remove(url); - if (setExpanded(index, true)) { + if (setExpanded(indexForUrl, true)) { // The dir lister has been triggered. This slot will be called // again after the directory has been expanded. return; @@ -814,15 +877,12 @@ void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemLis } if (m_requestRole[ExpandedParentsCountRole]) { - KFileItem item = items.first(); - // If the expanding of items is enabled, the call // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded() // might result in emitting the same items twice due to the Keep-parameter. // This case happens if an item gets expanded, collapsed and expanded again // before the items could be loaded for the first expansion. - const int index = m_items.value(item.url(), -1); - if (index >= 0) { + if (index(KUrl(items.first().url())) >= 0) { // The items are already part of the model. return; } @@ -836,7 +896,7 @@ void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemLis // 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 = m_items.value(parentUrl, -1); + const int parentIndex = index(parentUrl); if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { // The parent is not expanded. return; @@ -871,29 +931,48 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { dispatchPendingItemsToInsert(); - KFileItemList itemsToRemove = items; - if (m_requestRole[ExpandedParentsCountRole]) { - // Assure that removing a parent item also results in removing all children - foreach (const KFileItem& item, items) { - itemsToRemove.append(childItems(item)); - } - } + QVector indexesToRemove; + indexesToRemove.reserve(items.count()); - if (!m_filteredItems.isEmpty()) { - foreach (const KFileItem& item, itemsToRemove) { + foreach (const KFileItem& item, items) { + const int indexForItem = index(item); + if (indexForItem >= 0) { + indexesToRemove.append(indexForItem); + } else { + // Probably the item has been filtered. QHash::iterator it = m_filteredItems.find(item); if (it != m_filteredItems.end()) { delete it.value(); m_filteredItems.erase(it); } } + } - if (m_requestRole[ExpandedParentsCountRole]) { - removeFilteredChildren(itemsToRemove); + std::sort(indexesToRemove.begin(), indexesToRemove.end()); + + if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { + // Assure that removing a parent item also results in removing all children + QVector indexesToRemoveWithChildren; + indexesToRemoveWithChildren.reserve(m_itemData.count()); + + const int itemCount = m_itemData.count(); + foreach (int index, indexesToRemove) { + indexesToRemoveWithChildren.append(index); + + const int parentLevel = expandedParentsCount(index); + int childIndex = index + 1; + while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { + indexesToRemoveWithChildren.append(childIndex); + ++childIndex; + } } + + indexesToRemove = indexesToRemoveWithChildren; } - removeItems(itemsToRemove, DeleteItemData); + const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); + removeFilteredChildren(itemRanges); + removeItems(itemRanges, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList >& items) @@ -914,14 +993,14 @@ void KFileItemModel::slotRefreshItems(const QList >& const QPair& itemPair = it.next(); const KFileItem& oldItem = itemPair.first; const KFileItem& newItem = itemPair.second; - const int index = m_items.value(oldItem.url(), -1); - if (index >= 0) { - m_itemData[index]->item = newItem; + 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(index)->parent)); - QHash& values = m_itemData[index]->values; + 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(); @@ -932,8 +1011,22 @@ void KFileItemModel::slotRefreshItems(const QList >& } m_items.remove(oldItem.url()); - m_items.insert(newItem.url(), index); - indexes.append(index); + m_items.insert(newItem.url(), indexForItem); + indexes.append(indexForItem); + } else { + // Check if 'oldItem' is one of the filtered items. + QHash::iterator it = m_filteredItems.find(oldItem); + if (it != m_filteredItems.end()) { + ItemData* itemData = it.value(); + itemData->item = newItem; + + // The data stored in 'values' might have changed. Therefore, we clear + // 'values' and re-populate it the next time it is requested via data(int). + itemData->values.clear(); + + m_filteredItems.erase(it); + m_filteredItems.insert(newItem, itemData); + } } } @@ -945,35 +1038,8 @@ void KFileItemModel::slotRefreshItems(const QList >& // Extract the item-ranges out of the changed indexes qSort(indexes); - - KItemRangeList itemRangeList; - int previousIndex = indexes.at(0); - int rangeIndex = previousIndex; - int rangeCount = 1; - - const int maxIndex = indexes.count() - 1; - for (int i = 1; i <= maxIndex; ++i) { - const int currentIndex = indexes.at(i); - if (currentIndex == previousIndex + 1) { - ++rangeCount; - } else { - itemRangeList.append(KItemRange(rangeIndex, rangeCount)); - - rangeIndex = currentIndex; - rangeCount = 1; - } - previousIndex = currentIndex; - } - - if (rangeCount > 0) { - itemRangeList.append(KItemRange(rangeIndex, rangeCount)); - } - - emit itemsChanged(itemRangeList, changedRoles); - - if (changedRoles.contains(sortRole())) { - m_resortAllItemsTimer->start(); - } + const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); + emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); } void KFileItemModel::slotClear() @@ -1003,11 +1069,6 @@ void KFileItemModel::slotClear() m_expandedDirs.clear(); } -void KFileItemModel::slotClear(const KUrl& url) -{ - Q_UNUSED(url); -} - void KFileItemModel::slotNaturalSortingChanged() { m_naturalSorting = KGlobalSettings::naturalSorting(); @@ -1036,6 +1097,15 @@ void KFileItemModel::insertItems(QList& newItems) #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()); @@ -1059,7 +1129,7 @@ void KFileItemModel::insertItems(QList& newItems) m_itemData.append(0); } - // We build the new list m_items in reverse order to minimize + // 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; @@ -1097,11 +1167,9 @@ void KFileItemModel::insertItems(QList& newItems) std::reverse(itemRanges.begin(), itemRanges.end()); } - // The indexes starting from the first inserted item must be adjusted. - m_items.reserve(totalItemCount); - for (int i = itemRanges.front().index; i < totalItemCount; ++i) { - m_items.insert(m_itemData.at(i)->item.url(), i); - } + // The indexes in m_items are not correct anymore. Therefore, we clear m_items. + // It will be re-populated with the updated indices if index(const KUrl&) is called. + m_items.clear(); emit itemsInserted(itemRanges); @@ -1110,59 +1178,20 @@ void KFileItemModel::insertItems(QList& newItems) #endif } -static KItemRangeList sortedIndexesToKItemRangeList(const QList& sortedNumbers) +void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) { - if (sortedNumbers.empty()) { - return KItemRangeList(); - } - - KItemRangeList result; - - QList::const_iterator it = sortedNumbers.begin(); - int index = *it; - int count = 1; - - ++it; - - QList::const_iterator end = sortedNumbers.end(); - while (it != end) { - if (*it == index + count) { - ++count; - } else { - result << KItemRange(index, count); - index = *it; - count = 1; - } - ++it; + if (itemRanges.isEmpty()) { + return; } - result << KItemRange(index, count); - return result; -} - -void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior behavior) -{ -#ifdef KFILEITEMMODEL_DEBUG - kDebug() << "Removing " << items.count() << "items"; -#endif - m_groups.clear(); - // Step 1: Determine the indexes of the removed items, remove them from - // the hash m_items, and free the ItemData. - QList indexesToRemove; - indexesToRemove.reserve(items.count()); - foreach (const KFileItem& item, items) { - const KUrl url = item.url(); - const int index = m_items.value(url, -1); - if (index >= 0) { - indexesToRemove.append(index); - - // Prevent repeated expensive rehashing by using QHash::erase(), - // rather than QHash::remove(). - QHash::iterator it = m_items.find(url); - m_items.erase(it); + // 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); } @@ -1171,14 +1200,7 @@ void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior } } - if (indexesToRemove.isEmpty()) { - return; - } - - std::sort(indexesToRemove.begin(), indexesToRemove.end()); - // Step 2: Remove the ItemData pointers from the list m_itemData. - const KItemRangeList itemRanges = sortedIndexesToKItemRangeList(indexesToRemove); int target = itemRanges.at(0).index; int source = itemRanges.at(0).index + itemRanges.at(0).count; int nextRange = 1; @@ -1196,14 +1218,11 @@ void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior } } - m_itemData.erase(m_itemData.end() - indexesToRemove.count(), m_itemData.end()); + m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); - // Step 3: Adjust indexes in the hash m_items, starting from the - // index of the first removed item. - const int newItemDataCount = m_itemData.count(); - for (int i = itemRanges.front().index; i < newItemDataCount; ++i) { - m_items.insert(m_itemData.at(i)->item.url(), i); - } + // The indexes in m_items are not correct anymore. Therefore, we clear m_items. + // It will be re-populated with the updated indices if index(const KUrl&) is called. + m_items.clear(); emit itemsRemoved(itemRanges); } @@ -1217,7 +1236,7 @@ QList KFileItemModel::createItemDataList(const KUrl& determineMimeTypes(items, 200); } - const int parentIndex = m_items.value(parentUrl, -1); + const int parentIndex = index(parentUrl); ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); QList itemDataList; @@ -1226,7 +1245,6 @@ QList KFileItemModel::createItemDataList(const KUrl& foreach (const KFileItem& item, items) { ItemData* itemData = new ItemData(); itemData->item = item; - itemData->values = retrieveData(item, parentItem); itemData->parent = parentItem; itemDataList.append(itemData); } @@ -1234,23 +1252,141 @@ QList KFileItemModel::createItemDataList(const KUrl& return itemDataList; } +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() { - KFileItemList expandedItems; + QVector indexesToRemove; const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { const ItemData* itemData = m_itemData.at(i); - if (itemData->values.value("expandedParentsCount").toInt() > 0) { - expandedItems.append(itemData->item); + if (itemData->parent) { + indexesToRemove.append(i); } } - // The m_expandedParentsCountRoot may not get reset before all items with - // a bigger count have been removed. - removeItems(expandedItems, DeleteItemData); - + 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))) { + needsResorting = true; + } else if (last < count() - 1 + && lessThan(m_itemData.at(last + 1), m_itemData.at(last))) { + needsResorting = true; + } else { + for (int index = first; index < last; ++index) { + if (lessThan(m_itemData.at(index + 1), m_itemData.at(index))) { + needsResorting = true; + break; + } + } + } + + if (needsResorting) { + m_resortAllItemsTimer->start(); + return; + } + } + } + + 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() @@ -1341,8 +1477,8 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, // 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(sharedValue("date"), dateTime.dateTime()); + const QDateTime dateTime = item.time(KFileItem::ModificationTime); + data.insert(sharedValue("date"), dateTime); } if (m_requestRole[PermissionsRole]) { @@ -1367,7 +1503,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, if (m_requestRole[PathRole]) { QString path; - if (item.url().protocol() == QLatin1String("trash")) { + if (item.url().scheme() == QLatin1String("trash")) { path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA); } else { // For performance reasons cache the home-path in a static QString @@ -1394,7 +1530,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, if (m_requestRole[ExpandedParentsCountRole]) { if (parent) { - const int level = parent->values["expandedParentsCount"].toInt() + 1; + const int level = expandedParentsCount(parent) + 1; data.insert(sharedValue("expandedParentsCount"), level); } } @@ -1418,8 +1554,8 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const int result = 0; if (a->parent != b->parent) { - const int expansionLevelA = a->values.value("expandedParentsCount").toInt(); - const int expansionLevelB = b->values.value("expandedParentsCount").toInt(); + 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. @@ -1439,7 +1575,7 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const a = a->parent; } - Q_ASSERT(a->values.value("expandedParentsCount").toInt() == b->values.value("expandedParentsCount").toInt()); + Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b)); // Compare the last parents of a and b which are different. while (a->parent != b->parent) { @@ -1546,8 +1682,8 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const } case DateRole: { - const KDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); - const KDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); + const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); + const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { @@ -1649,7 +1785,7 @@ QList > KFileItemModel::nameRoleGroups() const continue; } - const QString name = m_itemData.at(i)->values.value("text").toString(); + const QString name = m_itemData.at(i)->item.text(); // Use the first character of the name as group indication QChar newFirstChar = name.at(0).toUpper(); @@ -1739,7 +1875,7 @@ QList > KFileItemModel::dateRoleGroups() const const int maxIndex = count() - 1; QList > groups; - const QDate currentDate = KDateTime::currentLocalDateTime().date(); + const QDate currentDate = QDate::currentDate(); QDate previousModifiedDate; QString groupValue; @@ -1748,7 +1884,7 @@ QList > KFileItemModel::dateRoleGroups() const continue; } - const KDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime); + const QDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime); const QDate modifiedDate = modifiedTime.date(); if (modifiedDate == previousModifiedDate) { // The current item is in the same group as the previous item @@ -1835,7 +1971,7 @@ QList > KFileItemModel::permissionRoleGroups() const } permissionsString = newPermissionsString; - const QFileInfo info(itemData->item.url().pathOrUrl()); + const QFileInfo info(itemData->item.url().toLocalFile()); // Set user string QString user; @@ -1932,23 +2068,6 @@ QList > KFileItemModel::genericStringRoleGroups(const QByte return groups; } -KFileItemList KFileItemModel::childItems(const KFileItem& item) const -{ - KFileItemList items; - - int index = m_items.value(item.url(), -1); - if (index >= 0) { - const int parentLevel = m_itemData.at(index)->values.value("expandedParentsCount").toInt(); - ++index; - while (index < m_itemData.count() && m_itemData.at(index)->values.value("expandedParentsCount").toInt() > parentLevel) { - items.append(m_itemData.at(index)->item); - ++index; - } - } - - return items; -} - void KFileItemModel::emitSortProgress(int resolvedCount) { // Be tolerant against a resolvedCount with a wrong range. @@ -1980,7 +2099,7 @@ void KFileItemModel::emitSortProgress(int resolvedCount) const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) { static const RoleInfoMap rolesInfoMap[] = { - // | role | roleType | role translation | group translation | requires Nepomuk | requires indexer + // | 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 }, @@ -2046,7 +2165,9 @@ QByteArray KFileItemModel::sharedValue(const QByteArray& value) bool KFileItemModel::isConsistent() const { - if (m_items.count() != m_itemData.count()) { + // m_items may contain less items than m_itemData because m_items + // is populated lazily, see KFileItemModel::index(const KUrl& url). + if (m_items.count() > m_itemData.count()) { return false; } @@ -2075,7 +2196,7 @@ bool KFileItemModel::isConsistent() const const ItemData* data = m_itemData.at(i); const ItemData* parent = data->parent; if (parent) { - if (data->values.value("expandedParentsCount").toInt() != parent->values.value("expandedParentsCount").toInt() + 1) { + if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { qWarning() << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; return false; }