X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/be8443bed87c1f27a9cd1f82d969cc06f91c2f62..64afe7b22622f79b34aafd54501b08120ab2fc5c:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 688826ee4..734950257 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,26 +1,26 @@ -/*************************************************************************** - * Copyright (C) 2011 by Peter Penz * - * Copyright (C) 2013 by Frank Reininghaus * - * * - * 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 @@ -34,6 +34,9 @@ #include #include +#include +#include + // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject* parent) : @@ -110,6 +113,7 @@ KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); qDeleteAll(m_filteredItems.values()); + qDeleteAll(m_pendingItemsToInsert); } void KFileItemModel::loadDirectory(const KUrl& url) @@ -119,6 +123,13 @@ void KFileItemModel::loadDirectory(const KUrl& url) void KFileItemModel::refreshDirectory(const KUrl& 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); } @@ -140,7 +151,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(); } @@ -151,14 +167,14 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - QHash currentValues = m_itemData.at(index)->values; + QHash currentValues = data(index); // Determine which roles have been changed QSet changedRoles; QHashIterator it(values); while (it.hasNext()) { it.next(); - const QByteArray role = it.key(); + const QByteArray role = sharedValue(it.key()); const QVariant value = it.value(); if (currentValues[role] != value) { @@ -178,11 +194,7 @@ bool KFileItemModel::setData(int index, const QHash& value m_itemData[index]->item.setUrl(url); } - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - - if (changedRoles.contains(sortRole())) { - m_resortAllItemsTimer->start(); - } + emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); return true; } @@ -224,7 +236,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(); @@ -234,13 +246,25 @@ 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.url(); + urls << item.targetUrl(); bool isLocal; mostLocalUrls << item.mostLocalUrl(isLocal); @@ -251,9 +275,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); @@ -266,12 +288,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; } } @@ -402,6 +424,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 @@ -416,28 +447,48 @@ bool KFileItemModel::setExpanded(int index, bool expanded) } QHash values; - values.insert("isExpanded", expanded); + values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } - const KUrl url = m_itemData.at(index)->item.url(); + const KFileItem item = m_itemData.at(index)->item; + const KUrl url = item.url(); + const KUrl targetUrl = item.targetUrl(); if (expanded) { - m_expandedDirs.insert(url); + 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 { - m_expandedDirs.remove(url); + m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); + const int parentLevel = expandedParentsCount(index); + const int itemCount = m_itemData.count(); + const int firstChildIndex = index + 1; - KFileItemList itemsToRemove; - const int expandedParentsCount = data(index)["expandedParentsCount"].toInt(); - ++index; - while (index < count() && data(index)["expandedParentsCount"].toInt() > expandedParentsCount) { - itemsToRemove.append(m_itemData.at(index)->item); - ++index; + 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(); + m_expandedDirs.remove(targetUrl); + expandedChildren.append(targetUrl); + } + ++childIndex; } - removeItems(itemsToRemove, DeleteItemData); + 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 true; @@ -454,7 +505,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; } @@ -462,17 +515,14 @@ 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; } QSet KFileItemModel::expandedDirectories() const { - return m_expandedDirs; + return m_expandedDirs.values().toSet(); } void KFileItemModel::restoreExpandedDirectories(const QSet& urls) @@ -541,21 +591,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. @@ -574,6 +628,32 @@ void KFileItemModel::applyFilters() insertItems(newVisibleItems); } +void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) +{ + if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { + // There are either no filtered items, or it is not possible to expand + // folders -> there cannot be any filtered children. + return; + } + + QSet parents; + foreach (const KItemRange& range, itemRanges) { + for (int index = range.index; index < range.index + range.count; ++index) { + parents.insert(m_itemData.at(index)); + } + } + + QHash::iterator it = m_filteredItems.begin(); + while (it != m_filteredItems.end()) { + if (parents.contains(it.value()->parent)) { + delete it.value(); + it = m_filteredItems.erase(it); + } else { + ++it; + } + } +} + QList KFileItemModel::rolesInformation() { static QList rolesInfo; @@ -593,7 +673,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); } @@ -614,11 +694,11 @@ void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArr Q_UNUSED(previous); m_sortRole = typeForRole(current); -#ifdef KFILEITEMMODEL_DEBUG if (!m_requestRole[m_sortRole]) { - kWarning() << "The sort-role has been changed to a role that has not been received yet"; + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); } -#endif resortAllItems(); } @@ -655,7 +735,6 @@ void KFileItemModel::resortAllItems() oldUrls.append(itemData->item.url()); } - m_groups.clear(); m_items.clear(); // Resort the items @@ -664,20 +743,45 @@ void KFileItemModel::resortAllItems() m_items.insert(m_itemData.at(i)->item.url(), i); } - // Determine the indexes that have been moved - QList movedToIndexes; - movedToIndexes.reserve(itemCount); - for (int i = 0; i < itemCount; i++) { - const int newIndex = m_items.value(oldUrls.at(i).url()); - movedToIndexes.append(newIndex); + // Determine the first index that has been moved. + int firstMovedIndex = 0; + while (firstMovedIndex < itemCount + && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { + ++firstMovedIndex; } - // Don't check whether items have really been moved and always emit a - // itemsMoved() signal after resorting: In case of grouped items - // the groups might change even if the items themselves don't change their - // position. Let the receiver of the signal decide whether a check for moved - // items makes sense. - emit itemsMoved(KItemRange(0, itemCount), movedToIndexes); + const bool itemsHaveMoved = firstMovedIndex < itemCount; + if (itemsHaveMoved) { + m_groups.clear(); + + int lastMovedIndex = itemCount - 1; + while (lastMovedIndex > firstMovedIndex + && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { + --lastMovedIndex; + } + + Q_ASSERT(firstMovedIndex <= lastMovedIndex); + + // Create a list movedToIndexes, which has the property that + // movedToIndexes[i] is the new index of the item with the old index + // firstMovedIndex + i. + const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1; + QList movedToIndexes; + movedToIndexes.reserve(movedItemsCount); + for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) { + const int newIndex = m_items.value(oldUrls.at(i)); + movedToIndexes.append(newIndex); + } + + emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); + } else if (groupedSorting()) { + // The groups might have changed even if the order of the items has not. + const QList > oldGroups = m_groups; + m_groups.clear(); + if (groups() != oldGroups) { + emit groupsChanged(); + } + } #ifdef KFILEITEMMODEL_DEBUG kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); @@ -725,14 +829,15 @@ void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemLis { Q_ASSERT(!items.isEmpty()); - KUrl parentUrl = directoryUrl; - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); + KUrl parentUrl; + if (m_expandedDirs.contains(directoryUrl)) { + parentUrl = m_expandedDirs.value(directoryUrl); + } else { + parentUrl = directoryUrl; + parentUrl.adjustPath(KUrl::RemoveTrailingSlash); + } if (m_requestRole[ExpandedParentsCountRole]) { - // To be able to compare whether the new items may be inserted as children - // of a parent item the pending items must be added to the model first. - dispatchPendingItemsToInsert(); - KFileItem item = items.first(); // If the expanding of items is enabled, the call @@ -746,6 +851,12 @@ void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemLis return; } + if (directoryUrl != directory()) { + // To be able to compare whether the new items may be inserted as children + // of a parent item the pending items must be added to the model first. + dispatchPendingItemsToInsert(); + } + // KDirLister keeps the children of items that got expanded once even if // they got collapsed again with KFileItemModel::setExpanded(false). So it must be // checked whether the parent for new items is still expanded. @@ -784,57 +895,49 @@ 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 KUrl url = item.url(); + const int index = m_items.value(url, -1); + if (index >= 0) { + indexesToRemove.append(index); + } 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]) { - // Remove all filtered children of deleted items. First, we put the - // deleted URLs into a set to provide fast lookup while iterating - // over m_filteredItems and prevent quadratic complexity if there - // are N removed items and N filtered items. - // - // TODO: This does currently *not* work if the parent-child - // relationships can not be determined just by using KUrl::upUrl(). - // This is the case, e.g., when browsing smb:/. - QSet urlsToRemove; - urlsToRemove.reserve(itemsToRemove.count()); - foreach (const KFileItem& item, itemsToRemove) { - KUrl url = item.url(); - url.adjustPath(KUrl::RemoveTrailingSlash); - urlsToRemove.insert(url); - } - - QHash::iterator it = m_filteredItems.begin(); - while (it != m_filteredItems.end()) { - const KUrl url = it.key().url(); - KUrl parentUrl = url.upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); + std::sort(indexesToRemove.begin(), indexesToRemove.end()); - if (urlsToRemove.contains(parentUrl)) { - delete it.value(); - it = m_filteredItems.erase(it); - } else { - ++it; - } + if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { + // Assure that removing a parent item also results in removing all children + QVector indexesToRemoveWithChildren; + indexesToRemoveWithChildren.reserve(m_items.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) @@ -844,12 +947,12 @@ void KFileItemModel::slotRefreshItems(const QList >& kDebug() << "Refreshing" << items.count() << "items"; #endif - m_groups.clear(); - // Get the indexes of all items that have been refreshed QList indexes; indexes.reserve(items.count()); + QSet changedRoles; + QListIterator > it(items); while (it.hasNext()) { const QPair& itemPair = it.next(); @@ -862,14 +965,33 @@ void KFileItemModel::slotRefreshItems(const QList >& // 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; while (it.hasNext()) { it.next(); - m_itemData[index]->values.insert(it.key(), it.value()); + const QByteArray& role = it.key(); + if (values.value(role) != it.value()) { + values.insert(role, it.value()); + changedRoles.insert(role); + } } m_items.remove(oldItem.url()); m_items.insert(newItem.url(), index); indexes.append(index); + } 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); + } } } @@ -881,33 +1003,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, m_roles); - - resortAllItems(); + const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); + emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); } void KFileItemModel::slotClear() @@ -922,6 +1019,8 @@ void KFileItemModel::slotClear() m_maximumUpdateIntervalTimer->stop(); m_resortAllItemsTimer->stop(); + + qDeleteAll(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); const int removedCount = m_itemData.count(); @@ -954,135 +1053,118 @@ void KFileItemModel::dispatchPendingItemsToInsert() } } -void KFileItemModel::insertItems(QList& items) +void KFileItemModel::insertItems(QList& newItems) { - if (items.isEmpty()) { + if (newItems.isEmpty()) { return; } - 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); - } - #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); kDebug() << "==========================================================="; - kDebug() << "Inserting" << items.count() << "items"; + kDebug() << "Inserting" << newItems.count() << "items"; #endif m_groups.clear(); + prepareItemsForSorting(newItems); - sort(items.begin(), items.end()); + 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 kDebug() << "[TIME] Sorting:" << timer.elapsed(); #endif KItemRangeList itemRanges; - int targetIndex = 0; - int sourceIndex = 0; - int insertedAtIndex = -1; // Index for the current item-range - int insertedCount = 0; // Count for the current item-range - int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges - while (sourceIndex < items.count()) { - // Find target index from m_items to insert the current item - // in a sorted order - const int previousTargetIndex = targetIndex; - while (targetIndex < m_itemData.count()) { - if (!lessThan(m_itemData.at(targetIndex), items.at(sourceIndex))) { - break; + const int existingItemCount = m_itemData.count(); + const int newItemCount = newItems.count(); + const int totalItemCount = existingItemCount + newItemCount; + + if (existingItemCount == 0) { + // Optimization for the common special case that there are no + // items in the model yet. Happens, e.g., when entering a folder. + m_itemData = newItems; + itemRanges << KItemRange(0, newItemCount); + } else { + m_itemData.reserve(totalItemCount); + for (int i = existingItemCount; i < totalItemCount; ++i) { + m_itemData.append(0); + } + + // We build the new list m_items 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))) { + // Move an existing item to its new position. If any new items + // are behind it, push the item range to itemRanges. + if (rangeCount > 0) { + itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); + rangeCount = 0; + } + + m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems); + --sourceIndexExistingItems; + } else { + // Insert a new item into the list. + ++rangeCount; + m_itemData[targetIndex] = newItem; + --sourceIndexNewItems; } - ++targetIndex; + --targetIndex; } - if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) { - itemRanges << KItemRange(insertedAtIndex, insertedCount); - previouslyInsertedCount += insertedCount; - insertedAtIndex = targetIndex - previouslyInsertedCount; - insertedCount = 0; + // Push the final item range to itemRanges. + if (rangeCount > 0) { + itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); } - // Insert item at the position targetIndex by transferring - // the ownership of the item-data from 'items' to m_itemData. - // m_items will be inserted after the loop (see comment below) - m_itemData.insert(targetIndex, items.at(sourceIndex)); - ++insertedCount; - - if (insertedAtIndex < 0) { - insertedAtIndex = targetIndex; - Q_ASSERT(previouslyInsertedCount == 0); - } - ++targetIndex; - ++sourceIndex; + // Note that itemRanges is still sorted in reverse order. + std::reverse(itemRanges.begin(), itemRanges.end()); } - // The indexes of all m_items must be adjusted, not only the index - // of the new items - const int itemDataCount = m_itemData.count(); - m_items.reserve(itemDataCount); - for (int i = 0; i < itemDataCount; ++i) { + // 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); } - itemRanges << KItemRange(insertedAtIndex, insertedCount); emit itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed(); + kDebug() << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); #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); + // Step 1: Remove the items from the hash m_items, 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) { + const KUrl url = m_itemData.at(index)->item.url(); // Prevent repeated expensive rehashing by using QHash::erase(), // rather than QHash::remove(). @@ -1097,14 +1179,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; @@ -1122,12 +1197,12 @@ 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. Note that all indexes - // might have been changed by the removal of the items. + // 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 = 0; i < newItemDataCount; ++i) { + for (int i = itemRanges.front().index; i < newItemDataCount; ++i) { m_items.insert(m_itemData.at(i)->item.url(), i); } @@ -1136,6 +1211,13 @@ void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior QList KFileItemModel::createItemDataList(const KUrl& 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 = m_items.value(parentUrl, -1); ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); @@ -1145,7 +1227,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); } @@ -1153,23 +1234,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() @@ -1237,28 +1436,23 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, // KFileItem::iconName() can be very expensive if the MIME-type is unknown // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. QHash data; - data.insert("url", item.url()); + data.insert(sharedValue("url"), item.url()); const bool isDir = item.isDir(); - if (m_requestRole[IsDirRole]) { - data.insert("isDir", isDir); + if (m_requestRole[IsDirRole] && isDir) { + data.insert(sharedValue("isDir"), true); } - if (m_requestRole[IsLinkRole]) { - const bool isLink = item.isLink(); - data.insert("isLink", isLink); + if (m_requestRole[IsLinkRole] && item.isLink()) { + data.insert(sharedValue("isLink"), true); } if (m_requestRole[NameRole]) { - data.insert("text", item.text()); + data.insert(sharedValue("text"), item.text()); } - if (m_requestRole[SizeRole]) { - if (isDir) { - data.insert("size", QVariant()); - } else { - data.insert("size", item.size()); - } + if (m_requestRole[SizeRole] && !isDir) { + data.insert(sharedValue("size"), item.size()); } if (m_requestRole[DateRole]) { @@ -1266,19 +1460,19 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, // 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()); + data.insert(sharedValue("date"), dateTime.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]) { @@ -1286,7 +1480,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, if (destination.isEmpty()) { destination = QLatin1String("-"); } - data.insert("destination", destination); + data.insert(sharedValue("destination"), destination); } if (m_requestRole[PathRole]) { @@ -1309,32 +1503,29 @@ QHash KFileItemModel::retrieveData(const KFileItem& item, const int index = path.lastIndexOf(item.text()); path = path.mid(0, index - 1); - data.insert("path", path); - } - - if (m_requestRole[IsExpandedRole]) { - data.insert("isExpanded", false); + data.insert(sharedValue("path"), path); } - if (m_requestRole[IsExpandableRole]) { - data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl()); + if (m_requestRole[IsExpandableRole] && isDir) { + data.insert(sharedValue("isExpandable"), true); } if (m_requestRole[ExpandedParentsCountRole]) { - int level = 0; if (parent) { - level = parent->values["expandedParentsCount"].toInt() + 1; + const int level = expandedParentsCount(parent) + 1; + data.insert(sharedValue("expandedParentsCount"), level); } - - data.insert("expandedParentsCount", level); } if (item.isMimeTypeKnown()) { - data.insert("iconName", item.iconName()); + data.insert(sharedValue("iconName"), item.iconName()); if (m_requestRole[TypeRole]) { - data.insert("type", item.mimeComment()); + data.insert(sharedValue("type"), item.mimeComment()); } + } else if (m_requestRole[TypeRole] && isDir) { + static const QString folderMimeType = item.mimeComment(); + data.insert(sharedValue("type"), folderMimeType); } return data; @@ -1345,8 +1536,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. @@ -1366,7 +1557,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) { @@ -1557,6 +1748,11 @@ bool KFileItemModel::useMaximumUpdateInterval() const return !m_dirLister->url().isLocalFile(); } +static bool localeAwareLessThan(const QChar& c1, const QChar& c2) +{ + return QString::localeAwareCompare(c1, c2) < 0; +} + QList > KFileItemModel::nameRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); @@ -1566,13 +1762,12 @@ QList > KFileItemModel::nameRoleGroups() const QString groupValue; QChar firstChar; - bool isLetter = false; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString name = m_itemData.at(i)->values.value("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(); @@ -1582,31 +1777,31 @@ QList > KFileItemModel::nameRoleGroups() const if (firstChar != newFirstChar) { QString newGroupValue; - if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) { - // Apply group 'A' - 'Z' - newGroupValue = newFirstChar; - isLetter = true; + if (newFirstChar.isLetter()) { + // Try to find a matching group in the range 'A' to 'Z'. + static std::vector lettersAtoZ; + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } + + std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); + if (it != lettersAtoZ.end()) { + if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) { + // newFirstChar belongs to the group preceding *it. + // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. + --it; + } + newGroupValue = *it; + } else { + newGroupValue = newFirstChar; + } } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { // Apply group '0 - 9' for any name that starts with a digit newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); - isLetter = false; } else { - if (isLetter) { - // If the current group is 'A' - 'Z' check whether a locale character - // fits into the existing group. - // TODO: This does not work in the case if e.g. the group 'O' starts with - // an umlaut 'O' -> provide unit-test to document this known issue - const QChar prevChar(firstChar.unicode() - ushort(1)); - const QChar nextChar(firstChar.unicode() + ushort(1)); - const QString currChar(newFirstChar); - const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 && - currChar.localeAwareCompare(nextChar) < 0; - if (partOfCurrentGroup) { - continue; - } - } newGroupValue = i18nc("@title:group", "Others"); - isLetter = false; } if (newGroupValue != groupValue) { @@ -1855,23 +2050,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. @@ -1903,7 +2081,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 }, @@ -1932,12 +2110,20 @@ const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) return rolesInfoMap; } -void KFileItemModel::determineMimeTypes(const QList& items, int timeout) +void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { QElapsedTimer timer; timer.start(); - foreach (const ItemData* itemData, items) { // krazy:exclude=foreach - itemData->item.determineMimeType(); + 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. @@ -1946,6 +2132,19 @@ void KFileItemModel::determineMimeTypes(const QList& items, int timeo } } +QByteArray KFileItemModel::sharedValue(const QByteArray& value) +{ + static QSet pool; + const QSet::const_iterator it = pool.constFind(value); + + if (it != pool.constEnd()) { + return *it; + } else { + pool.insert(value); + return value; + } +} + bool KFileItemModel::isConsistent() const { if (m_items.count() != m_itemData.count()) { @@ -1977,7 +2176,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; }