]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Merge branch 'release/21.12'
authorNate Graham <nate@kde.org>
Mon, 29 Nov 2021 17:25:06 +0000 (10:25 -0700)
committerNate Graham <nate@kde.org>
Mon, 29 Nov 2021 17:25:06 +0000 (10:25 -0700)
.kde-ci.yml
CMakeLists.txt
src/kitemviews/kfileitemmodel.cpp
src/kitemviews/kfileitemmodel.h
src/kitemviews/kfileitemmodelrolesupdater.cpp
src/settings/dolphinsettingsdialog.cpp
src/tests/kfileitemmodeltest.cpp
src/views/dolphinremoteencoding.cpp

index bcf24537fe16216c34753ef02d1b50141b15bb13..3f3b7f8bc423ad2ed63c2106e5205408d4d6704e 100644 (file)
@@ -26,6 +26,6 @@ Dependencies:
     'frameworks/baloo': '@stable'
     'frameworks/kwindowsystem': '@stable'
     'frameworks/kfilemetadata': '@stable'
-    'libraries/baloo-widgets': '@stable'
+    'libraries/baloo-widgets': '@same'
     'libraries/kuserfeedback': '@stable'
     'libraries/phonon': '@stable'
index abffbab33a9dcd95eea05306b27ee3791e14aaa8..a0d99a941bc840a08cc86b18a8c1d294c6b8bf55 100644 (file)
@@ -1,9 +1,9 @@
 cmake_minimum_required(VERSION 3.16)
 
 # KDE Application Version, managed by release script
-set (RELEASE_SERVICE_VERSION_MAJOR "21")
-set (RELEASE_SERVICE_VERSION_MINOR "11")
-set (RELEASE_SERVICE_VERSION_MICRO "90")
+set (RELEASE_SERVICE_VERSION_MAJOR "22")
+set (RELEASE_SERVICE_VERSION_MINOR "03")
+set (RELEASE_SERVICE_VERSION_MICRO "70")
 set (RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
 project(Dolphin VERSION ${RELEASE_SERVICE_VERSION})
 
index d1235510061bf430d95e9095313433df9fe6be57..80c3baab597da2ee7a023c59a9a140c337dcc02f 100644 (file)
@@ -25,6 +25,7 @@
 #include <QWidget>
 #include <QRecursiveMutex>
 #include <QIcon>
+#include <algorithm>
 
 Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
 
@@ -149,6 +150,18 @@ QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
         ItemData* data = m_itemData.at(index);
         if (data->values.isEmpty()) {
             data->values = retrieveData(data->item, data->parent);
+        } else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) {
+            // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing
+            // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values
+            // so this special request of different behavior can be identified here.
+            bool hasExpandedParentsCount = false;
+            const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
+
+            data->values = retrieveData(data->item, data->parent);
+            data->values.insert("isExpanded", true);
+            if (hasExpandedParentsCount) {
+                data->values.insert("expandedParentsCount", expandedParentsCount);
+            }
         }
 
         return data->values;
@@ -712,7 +725,7 @@ void KFileItemModel::applyFilters()
         ItemData *itemData = m_itemData.at(index);
 
         if (m_filter.matches(itemData->item)
-            || (itemShownBelow && itemShownBelow->parent == itemData && itemData->values.value("isExpanded").toBool())) {
+            || (itemShownBelow && itemShownBelow->parent == itemData)) {
             // We could've entered here for two reasons:
             // 1. This item passes the filter itself
             // 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below
@@ -1010,12 +1023,7 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis
 {
     Q_ASSERT(!items.isEmpty());
 
-    QUrl parentUrl;
-    if (m_expandedDirs.contains(directoryUrl)) {
-        parentUrl = m_expandedDirs.value(directoryUrl);
-    } else {
-        parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash);
-    }
+    const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash));
 
     if (m_requestRole[ExpandedParentsCountRole]) {
         // If the expanding of items is enabled, the call
@@ -1049,16 +1057,28 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis
     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()) {
@@ -1070,6 +1090,42 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis
     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();
@@ -1119,9 +1175,17 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
         indexesToRemove = indexesToRemoveWithChildren;
     }
 
-    const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
+    KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
     removeFilteredChildren(itemRanges);
-    removeItems(itemRanges, DeleteItemData);
+
+    // 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);
 }
@@ -1149,6 +1213,7 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >&
     QList<ItemData*> newVisibleItems;
 
     QListIterator<QPair<KFileItem, KFileItem> > it(items);
+
     while (it.hasNext()) {
         const QPair<KFileItem, KFileItem>& itemPair = it.next();
         const KFileItem& oldItem = itemPair.first;
@@ -1172,8 +1237,14 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >&
             }
 
             m_items.remove(oldItem.url());
-            if (newItemMatchesFilter) {
-                m_items.insert(newItem.url(), indexForItem);
+            // 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 {
@@ -1184,12 +1255,23 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >&
             // Check if 'oldItem' is one of the filtered items.
             QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
             if (it != m_filteredItems.end()) {
-                ItemData* itemData = it.value();
+                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) {
@@ -1201,21 +1283,66 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >&
         }
     }
 
-    // Hide items, previously visible that should get hidden
-    const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
+    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
-    std::sort(indexes.begin(), indexes.end());
     const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
     emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
 
@@ -1380,7 +1507,7 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe
         removedItemsCount += range.count;
 
         for (int index = range.index; index < range.index + range.count; ++index) {
-            if (behavior == DeleteItemData) {
+            if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) {
                 delete m_itemData.at(index);
             }
 
@@ -1424,8 +1551,9 @@ QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl&
         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 ? nullptr : m_itemData.at(parentIndex);
+    ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex);
 
     QList<ItemData*> itemDataList;
     itemDataList.reserve(items.count());
index 161f6a0e25e3c0d6ae8b7ad794262ffe2f45252d..471cfc2d27575578ff0fb259ab45de87009f5d9c 100644 (file)
@@ -309,7 +309,8 @@ private:
 
     enum RemoveItemsBehavior {
         KeepItemData,
-        DeleteItemData
+        DeleteItemData,
+        DeleteItemDataIfUnfiltered
     };
 
     void insertItems(QList<ItemData*>& items);
@@ -469,6 +470,15 @@ private:
      */
     bool isConsistent() const;
 
+    /**
+     * Filters out the expanded folders that don't pass the filter themselves and don't have any filter-passing children.
+     * Will update the removedItemRanges arguments to include the parents that have been filtered.
+     * @returns the number of parents that have been filtered.
+     * @param removedItemRanges The ranges of items being deleted/filtered, will get updated
+     * @param parentsToEnsureVisible Parents that must be visible no matter what due to being ancestors of newly visible items
+     */
+    int filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible = QSet<ItemData *>());
+
 private:
     KDirLister *m_dirLister = nullptr;
 
index 978f5df6e0b35abee929ad86be36019557e44100..3804a19074a19dd042543413e5cda0befac2de43 100644 (file)
@@ -1405,10 +1405,19 @@ QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
                                (2 * m_maximumVisibleItems)));
 
     // Add visible items.
+    // Resolve files first, their previews are quicker.
+    QList<int> visibleDirs;
     for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
-        result.append(i);
+        const KFileItem item = m_model->fileItem(i);
+        if (item.isDir()) {
+            visibleDirs.append(i);
+        } else {
+            result.append(i);
+        }
     }
 
+    result.append(visibleDirs);
+
     // We need a reasonable upper limit for number of items to resolve after
     // and before the visible range. m_maximumVisibleItems can be quite large
     // when using Compact View.
index 3b7d4b26706832129eafe583e08bbb7d19fb460f..d699ef894bc342b236a478133557fcda80a7508b 100644 (file)
@@ -90,7 +90,7 @@ DolphinSettingsDialog::DolphinSettingsDialog(const QUrl& url, QWidget* parent, K
     });
     KPageWidgetItem* contextMenuSettingsFrame = addPage(contextMenuSettingsPage,
                                                         i18nc("@title:group", "Context Menu"));
-    contextMenuSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("application-menu")));
+    contextMenuSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-menu-edit")));
     connect(contextMenuSettingsPage, &ContextMenuSettingsPage::changed, this, &DolphinSettingsDialog::enableApply);
 
     // Trash
index 7a22a1a7f7eb75dd50b5923a0ab513706f9cb9c6..385a8fe758be5a75fdc13653e7461f13f882c70b 100644 (file)
@@ -73,6 +73,10 @@ private Q_SLOTS:
     void testNameFilter();
     void testEmptyPath();
     void testRefreshExpandedItem();
+    void testAddItemToFilteredExpandedFolder();
+    void testDeleteItemsWithExpandedFolderWithFilter();
+    void testRefreshItemsWithFilter();
+    void testRefreshExpandedFolderWithFilter();
     void testRemoveHiddenItems();
     void collapseParentOfHiddenItems();
     void removeParentOfHiddenItems();
@@ -1143,6 +1147,219 @@ void KFileItemModelTest::testRefreshExpandedItem()
     QVERIFY(m_model->isExpanded(0));
 }
 
+/**
+ * Verifies that adding an item to an expanded folder that's filtered makes the parental chain visible.
+ */
+void KFileItemModelTest::testAddItemToFilteredExpandedFolder()
+{
+    QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+    QSignalSpy fileItemsChangedSpy(m_model, &KFileItemModel::fileItemsChanged);
+
+    QSet<QByteArray> modelRoles = m_model->roles();
+    modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+    m_model->setRoles(modelRoles);
+
+    m_testDir->createFile("a/b/file");
+
+    m_model->loadDirectory(m_testDir->url());
+    QVERIFY(itemsInsertedSpy.wait());
+    QCOMPARE(m_model->count(), 1); // "a
+
+    // Expand "a/".
+    m_model->setExpanded(0, true);
+    QVERIFY(itemsInsertedSpy.wait());   
+
+    // Expand "a/b/".
+    m_model->setExpanded(1, true);
+    QVERIFY(itemsInsertedSpy.wait());
+
+    QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/file"
+
+    const QUrl urlB = m_model->fileItem(1).url();
+
+    // Set a filter that matches ".txt" extension
+    m_model->setNameFilter("*.txt");
+    QCOMPARE(m_model->count(), 0); // Everything got hidden since we don't have a .txt file yet
+
+    m_model->slotItemsAdded(urlB, KFileItemList() << KFileItem(QUrl("a/b/newItem.txt")));
+    m_model->slotCompleted();
+
+    // Entire parental chain should now be shown
+    QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/newItem.txt"
+    QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "newItem.txt");
+
+    // Items should be indented in hierarchy
+    QCOMPARE(m_model->expandedParentsCount(0), 0);
+    QCOMPARE(m_model->expandedParentsCount(1), 1);
+    QCOMPARE(m_model->expandedParentsCount(2), 2);
+}
+
+/**
+ * Verifies that deleting the last filter-passing child from expanded folders
+ * makes the parental chain hidden.
+ */
+void KFileItemModelTest::testDeleteItemsWithExpandedFolderWithFilter()
+{
+    QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+    QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+    
+    QSet<QByteArray> modelRoles = m_model->roles();
+    modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+    m_model->setRoles(modelRoles);
+
+    m_testDir->createFile("a/b/file");
+
+    m_model->loadDirectory(m_testDir->url());
+    QVERIFY(itemsInsertedSpy.wait());
+    QCOMPARE(m_model->count(), 1); // "a
+
+    // Expand "a/".
+    m_model->setExpanded(0, true);
+    QVERIFY(itemsInsertedSpy.wait());   
+
+    // Expand "a/b/".
+    m_model->setExpanded(1, true);
+    QVERIFY(itemsInsertedSpy.wait());
+
+    QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/file"
+
+    // Set a filter that matches "file" extension
+    m_model->setNameFilter("file");
+    QCOMPARE(m_model->count(), 3); // Everything is still shown
+
+    // Delete "file"
+    QCOMPARE(itemsRemovedSpy.count(), 0);
+    m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(2));
+    QCOMPARE(itemsRemovedSpy.count(), 1);
+
+    // Entire parental chain should now be filtered
+    QCOMPARE(m_model->count(), 0);
+    QCOMPARE(m_model->m_filteredItems.size(), 2);
+}
+
+/**
+ * Verifies that the fileItemsChanged signal is raised with the correct index after renaming files with filter set.
+ * The rename operation will cause one item to be filtered out and another item to be reordered.
+ */
+void KFileItemModelTest::testRefreshItemsWithFilter()
+{
+    QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+    QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+    QSignalSpy itemsChangedSpy(m_model, &KFileItemModel::itemsChanged);
+    QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved);
+
+    // Creates three .txt files
+    m_testDir->createFiles({"b.txt", "c.txt", "d.txt"});
+
+    m_model->loadDirectory(m_testDir->url());
+    QVERIFY(itemsInsertedSpy.wait());
+
+    QCOMPARE(m_model->count(), 3); // "b.txt", "c.txt", "d.txt"
+
+    // Set a filter that matches ".txt" extension
+    m_model->setNameFilter("*.txt");
+    QCOMPARE(m_model->count(), 3); // Still all items are shown
+    QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt");
+
+    // Objects used to rename
+    const KFileItem fileItemC_txt = m_model->fileItem(1);
+    KFileItem fileItemC_cfg = fileItemC_txt;
+    fileItemC_cfg.setUrl(QUrl("c.cfg"));
+
+    const KFileItem fileItemD_txt = m_model->fileItem(2);
+    KFileItem fileItemA_txt = fileItemD_txt;
+    fileItemA_txt.setUrl(QUrl("a.txt"));
+
+    // Rename "c.txt" to "c.cfg"; and rename "d.txt" to "a.txt"
+    QCOMPARE(itemsRemovedSpy.count(), 0);
+    QCOMPARE(itemsChangedSpy.count(), 0);
+    m_model->slotRefreshItems({qMakePair(fileItemC_txt, fileItemC_cfg), qMakePair(fileItemD_txt, fileItemA_txt)});
+    QCOMPARE(itemsRemovedSpy.count(), 1);
+    QCOMPARE(itemsChangedSpy.count(), 1);
+    QCOMPARE(m_model->count(), 2); // Only "a.txt" and "b.txt". "c.cfg" got filtered out
+
+    QList<QVariant> arguments = itemsChangedSpy.takeLast();
+    KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>();
+
+    // We started with the order "b.txt", "c.txt", "d.txt"
+    // "d.txt" started with index "2"
+    // "c.txt" got renamed and got filtered out
+    // "d.txt" index shifted from index "2" to "1"
+    // So we expect index "1" in this argument, meaning "d.txt" was renamed
+    QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1));
+
+    // Re-sorting is done asynchronously:
+    QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "a.txt"); // Files should still be in the incorrect order
+    QVERIFY(itemsMovedSpy.wait());
+    QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt"); // Files were re-sorted and should now be in the correct order
+}
+
+
+/**
+ * Verifies that parental chains are hidden and shown as needed while their children get filtered/unfiltered due to renaming.
+ * Also verifies that the "isExpanded" and "expandedParentsCount" values are kept for expanded folders that get refreshed.
+ */
+void KFileItemModelTest::testRefreshExpandedFolderWithFilter() {
+    QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+    QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved);
+
+    QSet<QByteArray> modelRoles = m_model->roles();
+    modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+    m_model->setRoles(modelRoles);
+
+    m_testDir->createFile("a/b/someFolder/someFile");
+
+    m_model->loadDirectory(m_testDir->url());
+    QVERIFY(itemsInsertedSpy.wait());
+
+    QCOMPARE(m_model->count(), 1); // Only "a/"
+
+    // Expand "a/".
+    m_model->setExpanded(0, true);
+    QVERIFY(itemsInsertedSpy.wait());   
+
+    // Expand "a/b/".
+    m_model->setExpanded(1, true);
+    QVERIFY(itemsInsertedSpy.wait());
+
+    // Expand "a/b/someFolder/".
+    m_model->setExpanded(2, true);
+    QVERIFY(itemsInsertedSpy.wait());
+    QCOMPARE(m_model->count(), 4); // 4 items: "a/", "a/b/", "a/b/someFolder", "a/b/someFolder/someFile"
+
+    // Set a filter that matches the expanded folder "someFolder"
+    m_model->setNameFilter("someFolder");
+    QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/someFolder"
+
+    // Objects used to rename
+    const KFileItem fileItemA = m_model->fileItem(0);
+    KFileItem fileItemARenamed = fileItemA;
+    fileItemARenamed.setUrl(QUrl("a_renamed"));    
+
+    const KFileItem fileItemSomeFolder = m_model->fileItem(2);
+    KFileItem fileItemRenamedFolder = fileItemSomeFolder;
+    fileItemRenamedFolder.setUrl(QUrl("/a_renamed/b/renamedFolder"));
+
+    // Rename "a" to "a_renamed"
+    // This way we test if the algorithm is sane as to NOT hide "a_renamed" since it will have visible children
+    m_model->slotRefreshItems({qMakePair(fileItemA, fileItemARenamed)});
+    QCOMPARE(m_model->count(), 3); // Entire parental chain must still be shown
+    QCOMPARE(itemsInModel(), QStringList() << "a_renamed" << "b" << "someFolder");
+
+    // Rename "a_renamed" back to "a"; and "someFolder" to "renamedFolder"
+    m_model->slotRefreshItems({qMakePair(fileItemARenamed, fileItemA), qMakePair(fileItemSomeFolder, fileItemRenamedFolder)});
+    QCOMPARE(m_model->count(), 0); // Entire parental chain became hidden
+
+    // Rename "renamedFolder" back to "someFolder". Filter is passing again
+    m_model->slotRefreshItems({qMakePair(fileItemRenamedFolder, fileItemSomeFolder)});
+    QCOMPARE(m_model->count(), 3); // Entire parental chain is shown again
+    QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "someFolder");
+
+    // slotRefreshItems() should preserve "isExpanded" and "expandedParentsCount" values explicitly in this case
+    QCOMPARE(m_model->m_itemData.at(2)->values.value("isExpanded").toBool(), true);
+    QCOMPARE(m_model->m_itemData.at(2)->values.value("expandedParentsCount"), 2);
+}
+
 /**
  * Verify that removing hidden files and folders from the model does not
  * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046
@@ -1298,8 +1515,7 @@ void KFileItemModelTest::removeParentOfHiddenItems()
     // Simulate the deletion of the directory "a/b/".
     m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1));
     QCOMPARE(itemsRemovedSpy.count(), 2);
-    QCOMPARE(m_model->count(), 1);
-    QCOMPARE(itemsInModel(), QStringList() << "a");
+    QCOMPARE(m_model->count(), 0); // "a" will be filtered out since it doesn't pass the filter and doesn't have visible children
 
     // Remove the filter -> only the file "a/1" should appear.
     m_model->setNameFilter(QString());
index 41b3b6890db29a2886737ae5b5cd80aaeb743066..c7c8b09d1f8d7205818bd69c7eae2beeab0247bd 100644 (file)
@@ -90,7 +90,7 @@ void DolphinRemoteEncoding::fillMenu()
     QMenu* menu = m_menu->menu();
     menu->clear();
 
-
+    menu->addAction(i18n("Default"), this, SLOT(slotDefault()), 0)->setCheckable(true);
     for (int i = 0; i < m_encodingDescriptions.size();i++) {
         QAction* action = new QAction(m_encodingDescriptions.at(i), this);
         action->setCheckable(true);
@@ -100,7 +100,6 @@ void DolphinRemoteEncoding::fillMenu()
     menu->addSeparator();
 
     menu->addAction(i18n("Reload"), this, SLOT(slotReload()), 0);
-    menu->addAction(i18n("Default"), this, SLOT(slotDefault()), 0)->setCheckable(true);
     m_idDefault = m_encodingDescriptions.size() + 2;
 
     connect(menu, &QMenu::triggered, this, &DolphinRemoteEncoding::slotItemSelected);