+ m_resortAllItemsTimer->stop();
+
+ const int itemCount = count();
+ if (itemCount <= 0) {
+ return;
+ }
+
+#ifdef KFILEITEMMODEL_DEBUG
+ QElapsedTimer timer;
+ timer.start();
+ qCDebug(DolphinDebug) << "===========================================================";
+ qCDebug(DolphinDebug) << "Resorting" << itemCount << "items";
+#endif
+
+ // Remember the order of the current URLs so
+ // that it can be determined which indexes have
+ // been moved because of the resorting.
+ QList<QUrl> oldUrls;
+ oldUrls.reserve(itemCount);
+ for (const ItemData* itemData : qAsConst(m_itemData)) {
+ oldUrls.append(itemData->item.url());
+ }
+
+ m_items.clear();
+ m_items.reserve(itemCount);
+
+ // 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);
+ }
+
+ // Determine the first index that has been moved.
+ int firstMovedIndex = 0;
+ while (firstMovedIndex < itemCount
+ && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
+ ++firstMovedIndex;
+ }
+
+ const bool itemsHaveMoved = firstMovedIndex < itemCount;
+ if (itemsHaveMoved) {
+ m_groups.clear();
+
+ int lastMovedIndex = itemCount - 1;
+ while (lastMovedIndex > firstMovedIndex
+ && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
+ --lastMovedIndex;
+ }
+
+ Q_ASSERT(firstMovedIndex <= lastMovedIndex);
+
+ // Create a list movedToIndexes, which has the property that
+ // movedToIndexes[i] is the new index of the item with the old index
+ // firstMovedIndex + i.
+ const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1;
+ QList<int> movedToIndexes;
+ movedToIndexes.reserve(movedItemsCount);
+ for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) {
+ const int newIndex = m_items.value(oldUrls.at(i));
+ movedToIndexes.append(newIndex);
+ }
+
+ Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
+ } else if (groupedSorting()) {
+ // The groups might have changed even if the order of the items has not.
+ const QList<QPair<int, QVariant> > oldGroups = m_groups;
+ m_groups.clear();
+ if (groups() != oldGroups) {
+ Q_EMIT groupsChanged();
+ }
+ }
+
+#ifdef KFILEITEMMODEL_DEBUG
+ qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
+#endif
+}
+
+void KFileItemModel::slotCompleted()
+{
+ m_maximumUpdateIntervalTimer->stop();
+ dispatchPendingItemsToInsert();
+
+ 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.
+ // Iterate over a const copy because items are deleted and inserted within the loop
+ const auto urlsToExpand = m_urlsToExpand;
+ for(const QUrl &url : urlsToExpand) {
+ const int indexForUrl = index(url);
+ if (indexForUrl >= 0) {
+ m_urlsToExpand.remove(url);
+ if (setExpanded(indexForUrl, true)) {
+ // The dir lister has been triggered. This slot will be called
+ // again after the directory has been expanded.
+ return;
+ }
+ }
+ }
+
+ // 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();
+ }
+
+ Q_EMIT directoryLoadingCompleted();
+}
+
+void KFileItemModel::slotCanceled()
+{
+ m_maximumUpdateIntervalTimer->stop();
+ dispatchPendingItemsToInsert();
+
+ Q_EMIT directoryLoadingCanceled();
+}
+
+void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
+{
+ Q_ASSERT(!items.isEmpty());
+
+ const QUrl parentUrl = m_expandedDirs.value(directoryUrl, 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();
+ }
+
+ // 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;
+ }
+ }
+
+ const QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
+
+ if (!m_filter.hasSetFilters()) {
+ m_pendingItemsToInsert.append(itemDataList);
+ } else {
+ QSet<ItemData *> parentsToEnsureVisible;
+
+ // The name or type filter is active. Hide filtered items
+ // before inserting them into the model and remember
+ // the filtered items in m_filteredItems.
+ for (ItemData* itemData : itemDataList) {
+ if (m_filter.matches(itemData->item)) {
+ m_pendingItemsToInsert.append(itemData);
+ if (itemData->parent) {
+ parentsToEnsureVisible.insert(itemData->parent);
+ }
+ } else {
+ m_filteredItems.insert(itemData->item, itemData);
+ }
+ }
+
+ // Entire parental chains must be shown
+ for (ItemData *parent : parentsToEnsureVisible) {
+ for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) {
+ m_pendingItemsToInsert.append(parent);
+ }
+ }
+ }
+
+ if (!m_maximumUpdateIntervalTimer->isActive()) {
+ // Assure that items get dispatched if no completed() or canceled() signal is
+ // emitted during the maximum update interval.
+ m_maximumUpdateIntervalTimer->start();
+ }
+
+ Q_EMIT fileItemsChanged({KFileItem(directoryUrl)});
+}
+
+int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible)
+{
+ int filteredParentsCount = 0;
+ // The childless parents not yet removed will always be right above the start of a removed range.
+ // We iterate backwards to ensure the deepest folders are processed before their parents
+ for (int i = removedItemRanges.size() - 1; i >= 0; i--) {
+ KItemRange itemRange = removedItemRanges.at(i);
+ const ItemData *const firstInRange = m_itemData.at(itemRange.index);
+ ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr;
+ const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr;
+
+ if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove)
+ && !parentsToEnsureVisible.contains(itemAbove)) {
+ // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible
+ // and this deleted range covers all of its descendents, so none will be left.
+ m_filteredItems.insert(itemAbove->item, itemAbove);
+ // This range's starting index will be extended to include the parent above:
+ --itemRange.index;
+ ++itemRange.count;
+ ++filteredParentsCount;
+ KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange();
+ // We must check if this caused the range to touch the previous range, if that's the case they shall be merged
+ if (i > 0 && previousRange.index + previousRange.count == itemRange.index) {
+ previousRange.count += itemRange.count;
+ removedItemRanges.replace(i - 1, previousRange);
+ removedItemRanges.removeAt(i);
+ } else {
+ removedItemRanges.replace(i, itemRange);
+ // We must revisit this range in the next iteration since its starting index changed
+ ++i;
+ }
+ }
+ }
+ return filteredParentsCount;
+}
+
+void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
+{
+ dispatchPendingItemsToInsert();
+
+ QVector<int> indexesToRemove;
+ indexesToRemove.reserve(items.count());
+ KFileItemList dirsChanged;
+
+ for (const KFileItem& item : items) {
+ const int indexForItem = index(item);
+ if (indexForItem >= 0) {
+ indexesToRemove.append(indexForItem);
+ } else {
+ // Probably the item has been filtered.
+ QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
+ if (it != m_filteredItems.end()) {
+ delete it.value();
+ m_filteredItems.erase(it);
+ }
+ }
+
+ QUrl parentUrl = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ if (dirsChanged.findByUrl(parentUrl).isNull()) {
+ dirsChanged << KFileItem(parentUrl);
+ }
+ }
+
+ std::sort(indexesToRemove.begin(), indexesToRemove.end());
+
+ if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) {
+ // Assure that removing a parent item also results in removing all children
+ QVector<int> indexesToRemoveWithChildren;
+ indexesToRemoveWithChildren.reserve(m_itemData.count());
+
+ const int itemCount = m_itemData.count();
+ for (int index : qAsConst(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;
+ }
+
+ KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
+ removeFilteredChildren(itemRanges);
+
+ // This call will update itemRanges to include the childless parents that have been filtered.
+ const int filteredParentsCount = filterChildlessParents(itemRanges);
+
+ // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted
+ // mixed with expanded folders that are just being filtered out.
+ // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint
+ // so removeItems() will check m_filteredItems to differentiate which is which.
+ removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData);
+
+ Q_EMIT fileItemsChanged(dirsChanged);
+}
+
+void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& 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<int> indexes;
+ indexes.reserve(items.count());
+
+ QSet<QByteArray> changedRoles;
+ KFileItemList changedFiles;
+
+ // Contains the indexes of the currently visible items
+ // that should get hidden and hence moved to m_filteredItems.
+ QVector<int> newFilteredIndexes;
+
+ // Contains currently hidden items that should
+ // get visible and hence removed from m_filteredItems
+ QList<ItemData*> newVisibleItems;
+
+ QListIterator<QPair<KFileItem, KFileItem> > it(items);
+
+ while (it.hasNext()) {
+ const QPair<KFileItem, KFileItem>& itemPair = it.next();
+ const KFileItem& oldItem = itemPair.first;
+ const KFileItem& newItem = itemPair.second;
+ const int indexForItem = index(oldItem);
+ const bool newItemMatchesFilter = m_filter.matches(newItem);
+ if (indexForItem >= 0) {
+ m_itemData[indexForItem]->item = newItem;
+
+ // Keep old values as long as possible if they could not retrieved synchronously yet.
+ // The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
+ ItemData * const itemData = m_itemData.at(indexForItem);
+ QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent));
+ while (it.hasNext()) {
+ it.next();
+ const QByteArray& role = it.key();
+ if (itemData->values.value(role) != it.value()) {
+ itemData->values.insert(role, it.value());
+ changedRoles.insert(role);
+ }
+ }
+
+ m_items.remove(oldItem.url());
+ // We must maintain m_items consistent with m_itemData for now, this very loop is using it.
+ // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated.
+ m_items.insert(newItem.url(), indexForItem);
+ if (newItemMatchesFilter
+ || (itemData->values.value("isExpanded").toBool()
+ && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) {
+ // We are lenient with expanded folders that originally had visible children.
+ // If they become childless now they will be caught by filterChildlessParents()
+ changedFiles.append(newItem);
+ indexes.append(indexForItem);
+ } else {
+ newFilteredIndexes.append(indexForItem);
+ m_filteredItems.insert(newItem, itemData);
+ }
+ } else {
+ // Check if 'oldItem' is one of the filtered items.
+ QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
+ if (it != m_filteredItems.end()) {
+ ItemData *const 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).
+ // Before clearing, we must remember if it was expanded and the expanded parents count,
+ // otherwise these states would be lost. The data() method will deal with this special case.
+ const bool isExpanded = itemData->values.value("isExpanded").toBool();
+ bool hasExpandedParentsCount = false;
+ const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
+ itemData->values.clear();
+ if (isExpanded) {
+ itemData->values.insert("isExpanded", true);
+ if (hasExpandedParentsCount) {
+ itemData->values.insert("expandedParentsCount", expandedParentsCount);
+ }
+ }
+
+ m_filteredItems.erase(it);
+ if (newItemMatchesFilter) {
+ newVisibleItems.append(itemData);
+ } else {
+ m_filteredItems.insert(newItem, itemData);
+ }
+ }
+ }
+ }
+
+ std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end());
+
+ // We must keep track of parents of new visible items since they must be shown no matter what
+ // They will be considered "immune" to filterChildlessParents()
+ QSet<ItemData *> parentsToEnsureVisible;
+
+ for (ItemData *item : newVisibleItems) {
+ for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) {
+ parentsToEnsureVisible.insert(parent);
+ }
+ }
+ for (ItemData *parent : parentsToEnsureVisible) {
+ // We make sure they are all unfiltered.
+ if (m_filteredItems.remove(parent->item)) {
+ // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems
+ newVisibleItems.append(parent);
+ // It could be in newFilteredIndexes, we must remove it if it's there:
+ const int parentIndex = index(parent->item);
+ if (parentIndex >= 0) {
+ QVector<int>::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex);
+ if (it != newFilteredIndexes.end() && *it == parentIndex) {
+ newFilteredIndexes.erase(it);
+ }
+ }
+ }
+ }
+
+ KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
+
+ // This call will update itemRanges to include the childless parents that have been filtered.
+ filterChildlessParents(removedRanges, parentsToEnsureVisible);
+
+ removeItems(removedRanges, KeepItemData);
+
+ // Show previously hidden items that should get visible
+ insertItems(newVisibleItems);
+
+ // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic.
+
+ // 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;
+ }
+
+ if (newVisibleItems.count() > 0 || removedRanges.count() > 0) {
+ // The original indexes have changed and are now worthless since items were removed and/or inserted.
+ indexes.clear();
+ // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes.
+ const QSet<const KFileItem> changedFilesSet(changedFiles.cbegin(), changedFiles.cend());
+ for (int i = 0; i < m_itemData.count(); i++) {
+ if (changedFilesSet.contains(m_itemData.at(i)->item)) {
+ indexes.append(i);
+ }
+ }
+ } else {
+ std::sort(indexes.begin(), indexes.end());
+ }
+
+ // Extract the item-ranges out of the changed indexes
+ const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
+ emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
+
+ Q_EMIT fileItemsChanged(changedFiles);
+}
+
+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();
+ Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
+ }
+
+ m_expandedDirs.clear();
+}
+
+void KFileItemModel::slotSortingChoiceChanged()
+{
+ loadSortingSettings();
+ resortAllItems();
+}
+
+void KFileItemModel::dispatchPendingItemsToInsert()
+{
+ if (!m_pendingItemsToInsert.isEmpty()) {
+ insertItems(m_pendingItemsToInsert);
+ m_pendingItemsToInsert.clear();
+ }
+}
+
+void KFileItemModel::insertItems(QList<ItemData*>& newItems)
+{
+ if (newItems.isEmpty()) {
+ return;
+ }
+
+#ifdef KFILEITEMMODEL_DEBUG
+ QElapsedTimer timer;
+ timer.start();
+ qCDebug(DolphinDebug) << "===========================================================";
+ qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items";
+#endif
+
+ m_groups.clear();
+ prepareItemsForSorting(newItems);
+
+ // Natural sorting of items can be very slow. However, it becomes much faster
+ // if the input sequence is already mostly sorted. Therefore, we first sort
+ // 'newItems' according to the QStrings using QString::operator<(), which is quite fast.
+ if (m_naturalSorting) {
+ if (m_sortRole == NameRole) {
+ parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
+ } else if (isRoleValueNatural(m_sortRole)) {
+ auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b)
+ {
+ const QByteArray role = roleForType(m_sortRole);
+ return a->values.value(role).toString() < b->values.value(role).toString();
+ };
+ parallelMergeSort(newItems.begin(), newItems.end(), lambdaLessThan, QThread::idealThreadCount());
+ }
+ }
+
+ sort(newItems.begin(), newItems.end());
+
+#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(nullptr);
+ }
+
+ // 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();
+
+ Q_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;
+ for (const KItemRange& range : itemRanges) {
+ removedItemsCount += range.count;
+
+ for (int index = range.index; index < range.index + range.count; ++index) {
+ if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) {
+ delete m_itemData.at(index);
+ }
+
+ m_itemData[index] = nullptr;
+ }
+ }
+
+ // 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();
+
+ Q_EMIT itemsRemoved(itemRanges);
+}
+
+QList<KFileItemModel::ItemData*> 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);
+ }
+
+ // We search for the parent in m_itemData and then in m_filteredItems if necessary
+ const int parentIndex = index(parentUrl);
+ ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex);
+
+ QList<ItemData*> itemDataList;
+ itemDataList.reserve(items.count());
+
+ for (const KFileItem& item : items) {
+ ItemData* itemData = new ItemData();
+ itemData->item = item;
+ itemData->parent = parentItem;
+ itemDataList.append(itemData);
+ }
+
+ return itemDataList;
+}
+
+void KFileItemModel::prepareItemsForSorting(QList<ItemData*>& itemDataList)
+{
+ switch (m_sortRole) {
+ case PermissionsRole:
+ case OwnerRole:
+ case GroupRole:
+ case DestinationRole:
+ case PathRole:
+ case DeletionTimeRole:
+ // These roles can be determined with retrieveData, and they have to be stored
+ // in the QHash "values" for the sorting.
+ for (ItemData* itemData : qAsConst(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.
+ for (ItemData* itemData : qAsConst(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<int> indexesToRemove;
+
+ const int maxIndex = m_itemData.count() - 1;
+ for (int i = 0; i <= maxIndex; ++i) {
+ 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<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
+ const QHash<KFileItem, ItemData*>::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<QByteArray>& changedRoles)
+{
+ Q_EMIT itemsChanged(itemRanges, changedRoles);
+
+ // Trigger a resorting if necessary. Note that this can happen even if the sort
+ // role has not changed at all because the file name can be used as a fallback.
+ if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
+ for (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;
+ }
+ }
+ }
+
+ 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()
+{
+ for (int i = 0; i < RolesCount; ++i) {
+ m_requestRole[i] = false;
+ }
+}
+
+KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const
+{
+ static QHash<QByteArray, RoleType> roles;
+ if (roles.isEmpty()) {
+ // Insert user visible roles that can be accessed with
+ // KFileItemModel::roleInformation()
+ int count = 0;
+ const RoleInfoMap* map = rolesInfoMap(count);
+ for (int i = 0; i < count; ++i) {
+ roles.insert(map[i].role, map[i].roleType);
+ }
+
+ // Insert internal roles (take care to synchronize the implementation
+ // with KFileItemModel::roleForType() in case if a change is done).
+ roles.insert("isDir", IsDirRole);
+ roles.insert("isLink", IsLinkRole);
+ roles.insert("isHidden", IsHiddenRole);
+ roles.insert("isExpanded", IsExpandedRole);
+ roles.insert("isExpandable", IsExpandableRole);
+ roles.insert("expandedParentsCount", ExpandedParentsCountRole);
+
+ Q_ASSERT(roles.count() == RolesCount);
+ }
+
+ return roles.value(role, NoRole);
+}
+
+QByteArray KFileItemModel::roleForType(RoleType roleType) const
+{
+ static QHash<RoleType, QByteArray> 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<QByteArray, QVariant> 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