#include <QTimer>
#include <QMimeData>
+#include <KDirLister>
#include <kio/job.h>
#include "kitemviews/kfileitemmodel.h"
-#include "kitemviews/private/kfileitemmodeldirlister.h"
#include "testdir.h"
void myMessageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg)
void testNameFilter();
void testEmptyPath();
void testRefreshExpandedItem();
+ void testAddItemToFilteredExpandedFolder();
+ void testDeleteItemsWithExpandedFolderWithFilter();
+ void testRefreshItemsWithFilter();
+ void testRefreshExpandedFolderWithFilter();
void testRemoveHiddenItems();
void collapseParentOfHiddenItems();
void removeParentOfHiddenItems();
// Test expanding subfolders in a folder with the items "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1".
// Besides testing the basic item expansion functionality, the test makes sure that
// KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b)
- // yields the correct result for "a/a/1" and "a/a-1/", whis is non-trivial because they share the
+ // yields the correct result for "a/a/1" and "a/a-1/", which is non-trivial because they share the
// first three characters.
QSet<QByteArray> originalModelRoles = m_model->roles();
QSet<QByteArray> modelRoles = originalModelRoles;
void KFileItemModelTest::testSorting()
{
+ // testDir structure is as follows
+ // ./
+ // ├─ .g/
+ // ├─ a
+ // ├─ b
+ // ├─ c/
+ // │ ├─ c-2/
+ // │ │ ├─ c-3
+ // │ ├─ c-1
+ // ├─ .f
+ // ├─ d
+ // ├─ e
+
QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved);
QVERIFY(itemsMovedSpy.isValid());
m_testDir->createFile("d", "The largest file in this directory", now.addDays(-1));
m_testDir->createFile("e", "An even larger file", now.addDays(-4));
m_testDir->createFile(".f");
+ m_testDir->createDir(".g");
m_model->loadDirectory(m_testDir->url());
QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ KItemRangeList itemRangeList = itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 5));
int index = m_model->index(QUrl(m_testDir->url().url() + "/c"));
m_model->setExpanded(index, true);
QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ itemRangeList = itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2));
index = m_model->index(QUrl(m_testDir->url().url() + "/c/c-2"));
m_model->setExpanded(index, true);
QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ itemRangeList = itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>();
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 1));
// Default: Sort by Name, ascending
QCOMPARE(m_model->sortRole(), QByteArray("text"));
QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 6 << 5 << 4);
- // TODO: Sort by other roles; show/hide hidden files
+ // 'Show Hidden Files' enabled
+ m_model->setShowHiddenFiles(true);
+ QVERIFY(m_model->showHiddenFiles());
+ QVERIFY(!m_model->sortHiddenLast());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << ".g" << "d" << "e" << "b" << "a" << ".f");
+ QCOMPARE(itemsMovedSpy.count(), 0);
+ QCOMPARE(itemsInsertedSpy.count(), 1);
+ QCOMPARE(itemsInsertedSpy.takeFirst().at(0).value<KItemRangeList>(), KItemRangeList() << KItemRange(4, 1) << KItemRange(8, 1));
+
+ // 'Sort Hidden Files Last' enabled
+ m_model->setSortHiddenLast(true);
+ QVERIFY(m_model->sortHiddenLast());
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "d" << "e" << "b" << "a" << ".g" << ".f");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsInsertedSpy.count(), 0);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 5));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 8 << 4 << 5 << 6 << 7);
+
+ // Sort by Name
+ m_model->setSortRole("text");
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "d" << "b" << "a" << ".g" << ".f");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 2));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 5 << 4);
+
+ // Sort ascending
+ m_model->setSortOrder(Qt::AscendingOrder);
+ QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "d" << "e" << ".g" << ".f");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(4, 4));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 7 << 6 << 5 << 4);
+
+ // 'Sort Folders First' disabled
+ m_model->setSortDirectoriesFirst(false);
+ QVERIFY(!m_model->sortDirectoriesFirst());
+ QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e" << ".f" << ".g");
+ QCOMPARE(itemsMovedSpy.count(), 1);
+ QCOMPARE(itemsMovedSpy.first().at(0).value<KItemRange>(), KItemRange(0, 10));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int> >(), QList<int>() << 2 << 4 << 5 << 3 << 0 << 1 << 6 << 7 << 9 << 8);
+
}
void KFileItemModelTest::testIndexForKeyboardSearch()
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
QVERIFY(itemsInsertedSpy.wait());
QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1"
- // Set a name filter that matches nothing -> only the expanded folders remain.
+ // Set a name filter that matches nothing -> nothing should remain.
m_model->setNameFilter("xyz");
QCOMPARE(itemsRemovedSpy.count(), 1);
- QCOMPARE(m_model->count(), 3);
- QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c");
+ QCOMPARE(m_model->count(), 0); //Everything is hidden
+ QCOMPARE(itemsInModel(), QStringList());
+
+ //Filter by the file names. Folder "d" will be hidden since it was collapsed
+ m_model->setNameFilter("1");
+ QCOMPARE(itemsRemovedSpy.count(), 1); // nothing was removed, itemsRemovedSpy count will remain the same:
+ QCOMPARE(m_model->count(), 6); // 6 items: "a/", "a/b/", "a/b/c", "a/b/c/1", "a/b/1", "a/1"
// Collapse the folder "a/".
m_model->setExpanded(0, false);
QCOMPARE(m_model->count(), 1);
QCOMPARE(itemsInModel(), QStringList() << "a");
- // Remove the filter -> no files should appear (and we should not get a crash).
+ // Remove the filter -> "a" should still appear (and we should not get a crash).
m_model->setNameFilter(QString());
+ QCOMPARE(itemsRemovedSpy.count(), 2); // nothing was removed, itemsRemovedSpy count will remain the same:
QCOMPARE(m_model->count(), 1);
+ QCOMPARE(itemsInModel(), QStringList() << "a");
}
/**
QVERIFY(itemsInsertedSpy.wait());
QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1"
- // Set a name filter that matches nothing -> only the expanded folders remain.
+ // Set a name filter that matches nothing -> nothing should remain.
m_model->setNameFilter("xyz");
QCOMPARE(itemsRemovedSpy.count(), 1);
+ QCOMPARE(m_model->count(), 0);
+ QCOMPARE(itemsInModel(), QStringList());
+
+ // Filter by "c". Folder "b" will also be shown because it is its parent.
+ m_model->setNameFilter("c");
+ QCOMPARE(itemsRemovedSpy.count(), 1); // nothing was removed, itemsRemovedSpy count will remain the same:
QCOMPARE(m_model->count(), 3);
QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c");
// 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());
m_model->slotCompleted();
QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2");
- m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown));
- m_model->slotCompleted();
- QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2");
-
m_model->slotItemsAdded(realChild2, KFileItemList() << KFileItem(QUrl("grandChild2"), QString(), KFileItem::Unknown));
m_model->slotCompleted();
QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "grandChild2" << "realGrandChild2" << "child2");
- // Set a name filter that matches nothing -> only expanded folders remain.
+ // Set a name filter that matches nothing -> nothing will remain.
m_model->setNameFilter("xyz");
- QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2");
+ QCOMPARE(itemsInModel(), QStringList());
QCOMPARE(itemsRemovedSpy.count(), 1);
QList<QVariant> arguments = itemsRemovedSpy.takeFirst();
KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>();
- QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 3) << KItemRange(7, 3));
+ QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 10));
+
+ // Set a name filter that matches only "realChild". Their prarents should still show.
+ m_model->setNameFilter("realChild");
+ QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2");
+ QCOMPARE(itemsRemovedSpy.count(), 0); // nothing was removed, itemsRemovedSpy will not be called this time
// Collapse "parent1".
m_model->setExpanded(0, false);