+ }
+
+#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;
+
+ const auto currentDir = directory();
+
+ for (const KFileItem &item : items) {
+ if (item.url() == currentDir) {
+ Q_EMIT currentDirectoryRemoved();
+ return;
+ }
+
+ 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 : std::as_const(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);
+ }
+ }
+ }
+ }