diff --git a/autotests/kdirmodeltest.cpp b/autotests/kdirmodeltest.cpp index ed51afbd..db28530e 100644 --- a/autotests/kdirmodeltest.cpp +++ b/autotests/kdirmodeltest.cpp @@ -1,1522 +1,1583 @@ /* This file is part of the KDE project Copyright 2006 - 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdirmodeltest.h" #include #include #include #include #include #include //TODO #include "../../kdeui/tests/proxymodeltestsuite/modelspy.h" #include #ifdef Q_OS_UNIX #include #endif #include #include #include #include #include "kiotesthelper.h" #include QTEST_MAIN(KDirModelTest) #ifndef USE_QTESTEVENTLOOP #define exitLoop quit #endif #ifndef Q_OS_WIN #define SPECIALCHARS " special chars%:.pdf" #else #define SPECIALCHARS " special chars%.pdf" #endif Q_DECLARE_METATYPE(KFileItemList) void KDirModelTest::initTestCase() { qputenv("LC_ALL", "en_US.UTF-8"); // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); qRegisterMetaType("KFileItemList"); m_dirModelForExpand = nullptr; m_dirModel = nullptr; s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago m_tempDir = nullptr; m_topLevelFileNames << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << SPECIALCHARS ; recreateTestData(); fillModel(false); } void KDirModelTest::recreateTestData() { if (m_tempDir) { qDebug() << "Deleting old tempdir" << m_tempDir->path(); delete m_tempDir; qApp->processEvents(); // process inotify events so they don't pollute us later on } m_tempDir = new QTemporaryDir; qDebug() << "new tmp dir:" << m_tempDir->path(); // Create test data: /* * PATH/toplevelfile_1 * PATH/toplevelfile_2 * PATH/toplevelfile_3 * PATH/special chars%:.pdf * PATH/.hiddenfile * PATH/.hiddenfile2 * PATH/subdir * PATH/subdir/testfile * PATH/subdir/testsymlink * PATH/subdir/subsubdir * PATH/subdir/subsubdir/testfile */ const QString path = m_tempDir->path() + '/'; for (const QString &f : qAsConst(m_topLevelFileNames)) { createTestFile(path + f); } createTestFile(path + ".hiddenfile"); createTestFile(path + ".hiddenfile2"); createTestDirectory(path + "subdir"); createTestDirectory(path + "subdir/subsubdir", NoSymlink); m_dirIndex = QModelIndex(); m_fileIndex = QModelIndex(); m_secondFileIndex = QModelIndex(); } void KDirModelTest::cleanupTestCase() { delete m_tempDir; m_tempDir = nullptr; delete m_dirModel; m_dirModel = nullptr; delete m_dirModelForExpand; m_dirModelForExpand = nullptr; } void KDirModelTest::fillModel(bool reload, bool expectAllIndexes) { if (!m_dirModel) { m_dirModel = new KDirModel; } m_dirModel->dirLister()->setAutoErrorHandlingEnabled(false, nullptr); const QString path = m_tempDir->path() + '/'; KDirLister *dirLister = m_dirModel->dirLister(); qDebug() << "Calling openUrl"; - dirLister->openUrl(QUrl::fromLocalFile(path), reload ? KDirLister::Reload : KDirLister::NoFlags); + m_dirModel->openUrl(QUrl::fromLocalFile(path), reload ? KDirModel::Reload : KDirModel::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); qDebug() << "enterLoop, waiting for completed()"; enterLoop(); if (expectAllIndexes) { collectKnownIndexes(); } disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } // Called after test function void KDirModelTest::cleanup() { if (m_dirModel) { disconnect(m_dirModel, nullptr, &m_eventLoop, nullptr); disconnect(m_dirModel->dirLister(), nullptr, this, nullptr); m_dirModel->dirLister()->setNameFilter(QString()); m_dirModel->dirLister()->setMimeFilter(QStringList()); m_dirModel->dirLister()->emitChanges(); } } void KDirModelTest::collectKnownIndexes() { m_dirIndex = QModelIndex(); m_fileIndex = QModelIndex(); m_secondFileIndex = QModelIndex(); // Create the indexes once and for all // The trouble is that the order of listing is undefined, one can get 1/2/3/subdir or subdir/3/2/1 for instance. for (int row = 0; row < m_topLevelFileNames.count() + 1 /*subdir*/; ++row) { QModelIndex idx = m_dirModel->index(row, 0, QModelIndex()); QVERIFY(idx.isValid()); KFileItem item = m_dirModel->itemForIndex(idx); qDebug() << item.url() << "isDir=" << item.isDir(); QString fileName = item.url().fileName(); if (item.isDir()) { m_dirIndex = idx; } else if (fileName == QLatin1String("toplevelfile_1")) { m_fileIndex = idx; } else if (fileName == QLatin1String("toplevelfile_2")) { m_secondFileIndex = idx; } else if (fileName.startsWith(QLatin1String(" special"))) { m_specialFileIndex = idx; } } QVERIFY(m_dirIndex.isValid()); QVERIFY(m_fileIndex.isValid()); QVERIFY(m_secondFileIndex.isValid()); QVERIFY(m_specialFileIndex.isValid()); // Now list subdir/ QVERIFY(m_dirModel->canFetchMore(m_dirIndex)); m_dirModel->fetchMore(m_dirIndex); qDebug() << "Listing subdir/"; enterLoop(); // Index of a file inside a directory (subdir/testfile) QModelIndex subdirIndex; m_fileInDirIndex = QModelIndex(); for (int row = 0; row < 3; ++row) { QModelIndex idx = m_dirModel->index(row, 0, m_dirIndex); if (m_dirModel->itemForIndex(idx).isDir()) { subdirIndex = idx; } else if (m_dirModel->itemForIndex(idx).name() == QLatin1String("testfile")) { m_fileInDirIndex = idx; } } // List subdir/subsubdir QVERIFY(m_dirModel->canFetchMore(subdirIndex)); qDebug() << "Listing subdir/subsubdir"; m_dirModel->fetchMore(subdirIndex); enterLoop(); // Index of ... well, subdir/subsubdir/testfile m_fileInSubdirIndex = m_dirModel->index(0, 0, subdirIndex); } void KDirModelTest::enterLoop() { #ifdef USE_QTESTEVENTLOOP m_eventLoop.enterLoop(10 /*seconds max*/); QVERIFY(!m_eventLoop.timeout()); #else m_eventLoop.exec(); #endif } void KDirModelTest::slotListingCompleted() { qDebug(); #ifdef USE_QTESTEVENTLOOP m_eventLoop.exitLoop(); #else m_eventLoop.quit(); #endif } void KDirModelTest::testRowCount() { const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, m_topLevelFileNames.count() + 1 /*subdir*/); const int subdirRowCount = m_dirModel->rowCount(m_dirIndex); QCOMPARE(subdirRowCount, 3); QVERIFY(m_fileIndex.isValid()); const int fileRowCount = m_dirModel->rowCount(m_fileIndex); // #176555 QCOMPARE(fileRowCount, 0); } void KDirModelTest::testIndex() { QVERIFY(m_dirModel->hasChildren()); // Index of the first file QVERIFY(m_fileIndex.isValid()); QCOMPARE(m_fileIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_fileIndex.row(), 0); QCOMPARE(m_fileIndex.column(), 0); QVERIFY(!m_fileIndex.parent().isValid()); QVERIFY(!m_dirModel->hasChildren(m_fileIndex)); // Index of a directory QVERIFY(m_dirIndex.isValid()); QCOMPARE(m_dirIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_dirIndex.row(), 3); // ordering isn't guaranteed QCOMPARE(m_dirIndex.column(), 0); QVERIFY(!m_dirIndex.parent().isValid()); QVERIFY(m_dirModel->hasChildren(m_dirIndex)); // Index of a file inside a directory (subdir/testfile) QVERIFY(m_fileInDirIndex.isValid()); QCOMPARE(m_fileInDirIndex.model(), static_cast(m_dirModel)); //QCOMPARE(m_fileInDirIndex.row(), 0); // ordering isn't guaranteed QCOMPARE(m_fileInDirIndex.column(), 0); QVERIFY(m_fileInDirIndex.parent() == m_dirIndex); QVERIFY(!m_dirModel->hasChildren(m_fileInDirIndex)); // Index of subdir/subsubdir/testfile QVERIFY(m_fileInSubdirIndex.isValid()); QCOMPARE(m_fileInSubdirIndex.model(), static_cast(m_dirModel)); QCOMPARE(m_fileInSubdirIndex.row(), 0); // we can check it because it's the only file there QCOMPARE(m_fileInSubdirIndex.column(), 0); QVERIFY(m_fileInSubdirIndex.parent().parent() == m_dirIndex); QVERIFY(!m_dirModel->hasChildren(m_fileInSubdirIndex)); // Test sibling() by going from subdir/testfile to subdir/subsubdir const QModelIndex subsubdirIndex = m_fileInSubdirIndex.parent(); QVERIFY(subsubdirIndex.isValid()); QModelIndex sibling1 = m_dirModel->sibling(subsubdirIndex.row(), 0, m_fileInDirIndex); QVERIFY(sibling1.isValid()); QVERIFY(sibling1 == subsubdirIndex); // Invalid sibling call QVERIFY(!m_dirModel->sibling(1, 0, m_fileInSubdirIndex).isValid()); // Test index() with a valid parent (dir). QModelIndex index2 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), subsubdirIndex); QVERIFY(index2.isValid()); QVERIFY(index2 == m_fileInSubdirIndex); // Test index() with a non-parent (file). QModelIndex index3 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), m_fileIndex); QVERIFY(!index3.isValid()); } void KDirModelTest::testNames() { QString fileName = m_dirModel->data(m_fileIndex, Qt::DisplayRole).toString(); QCOMPARE(fileName, QString("toplevelfile_1")); QString specialFileName = m_dirModel->data(m_specialFileIndex, Qt::DisplayRole).toString(); QCOMPARE(specialFileName, QString(SPECIALCHARS)); QString dirName = m_dirModel->data(m_dirIndex, Qt::DisplayRole).toString(); QCOMPARE(dirName, QString("subdir")); QString fileInDirName = m_dirModel->data(m_fileInDirIndex, Qt::DisplayRole).toString(); QCOMPARE(fileInDirName, QString("testfile")); QString fileInSubdirName = m_dirModel->data(m_fileInSubdirIndex, Qt::DisplayRole).toString(); QCOMPARE(fileInSubdirName, QString("testfile")); } void KDirModelTest::testItemForIndex() { // root item KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); QVERIFY(!rootItem.isNull()); QCOMPARE(rootItem.name(), QString(".")); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QVERIFY(!fileItem.isNull()); QCOMPARE(fileItem.name(), QString("toplevelfile_1")); QVERIFY(!fileItem.isDir()); QCOMPARE(fileItem.url().toLocalFile(), QString(m_tempDir->path() + "/toplevelfile_1")); KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex); QVERIFY(!dirItem.isNull()); QCOMPARE(dirItem.name(), QString("subdir")); QVERIFY(dirItem.isDir()); QCOMPARE(dirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir")); KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex); QVERIFY(!fileInDirItem.isNull()); QCOMPARE(fileInDirItem.name(), QString("testfile")); QVERIFY(!fileInDirItem.isDir()); QCOMPARE(fileInDirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/testfile")); KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex); QVERIFY(!fileInSubdirItem.isNull()); QCOMPARE(fileInSubdirItem.name(), QString("testfile")); QVERIFY(!fileInSubdirItem.isDir()); QCOMPARE(fileInSubdirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/subsubdir/testfile")); } void KDirModelTest::testIndexForItem() { KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); QModelIndex rootIndex = m_dirModel->indexForItem(rootItem); QVERIFY(!rootIndex.isValid()); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QModelIndex fileIndex = m_dirModel->indexForItem(fileItem); QCOMPARE(fileIndex, m_fileIndex); KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex); QModelIndex dirIndex = m_dirModel->indexForItem(dirItem); QCOMPARE(dirIndex, m_dirIndex); KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex); QModelIndex fileInDirIndex = m_dirModel->indexForItem(fileInDirItem); QCOMPARE(fileInDirIndex, m_fileInDirIndex); KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex); QModelIndex fileInSubdirIndex = m_dirModel->indexForItem(fileInSubdirItem); QCOMPARE(fileInSubdirIndex, m_fileInSubdirIndex); } void KDirModelTest::testData() { // First file QModelIndex idx1col0 = m_dirModel->index(m_fileIndex.row(), 0, QModelIndex()); QCOMPARE(idx1col0.data().toString(), QString("toplevelfile_1")); QModelIndex idx1col1 = m_dirModel->index(m_fileIndex.row(), 1, QModelIndex()); QString size1 = m_dirModel->data(idx1col1, Qt::DisplayRole).toString(); QCOMPARE(size1, QString("11 B")); KFileItem item = m_dirModel->data(m_fileIndex, KDirModel::FileItemRole).value(); KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex); QCOMPARE(item, fileItem); QCOMPARE(m_dirModel->data(m_fileIndex, KDirModel::ChildCountRole).toInt(), (int)KDirModel::ChildCountUnknown); // Second file QModelIndex idx2col0 = m_dirModel->index(m_secondFileIndex.row(), 0, QModelIndex()); QString display2 = m_dirModel->data(idx2col0, Qt::DisplayRole).toString(); QCOMPARE(display2, QString("toplevelfile_2")); // Subdir: check child count QCOMPARE(m_dirModel->data(m_dirIndex, KDirModel::ChildCountRole).toInt(), 3); // Subsubdir: check child count QCOMPARE(m_dirModel->data(m_fileInSubdirIndex.parent(), KDirModel::ChildCountRole).toInt(), 1); } void KDirModelTest::testReload() { fillModel(true); testItemForIndex(); } // We want more info than just "the values differ", if they do. #define COMPARE_INDEXES(a, b) \ QCOMPARE(a.row(), b.row()); \ QCOMPARE(a.column(), b.column()); \ QCOMPARE(a.model(), b.model()); \ QCOMPARE(a.parent().isValid(), b.parent().isValid()); \ QCOMPARE(a, b); void KDirModelTest::testModifyFile() { const QString file = m_tempDir->path() + "/toplevelfile_2"; const QUrl url = QUrl::fromLocalFile(file); #if 1 QSignalSpy spyDataChanged(m_dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex))); #else ModelSpy modelSpy(m_dirModel); #endif connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); // "Touch" the file setTimeStamp(file, s_referenceTimeStamp.addSecs(20)); // In stat mode, kdirwatch doesn't notice file changes; we need to trigger it // by creating a file. //createTestFile(m_tempDir->path() + "/toplevelfile_5"); KDirWatch::self()->setDirty(m_tempDir->path()); // Wait for KDirWatch to notify the change (especially when using Stat) enterLoop(); // If we come here, then dataChanged() was emitted - all good. #if 0 QCOMPARE(modelSpy.count(), 1); const QVariantList dataChanged = modelSpy.first(); #else const QVariantList dataChanged = spyDataChanged[0]; #endif QModelIndex receivedIndex = dataChanged[0].value(); COMPARE_INDEXES(receivedIndex, m_secondFileIndex); receivedIndex = dataChanged[1].value(); QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1 disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } void KDirModelTest::testRenameFile() { const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2"); const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2_renamed"); QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); // If we come here, then dataChanged() was emitted - all good. QCOMPARE(spyDataChanged.count(), 1); COMPARE_INDEXES(spyDataChanged[0][0].value(), m_secondFileIndex); QModelIndex receivedIndex = spyDataChanged[0][1].value(); QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1 // check renaming happened QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), newUrl.toString()); // check that KDirLister::cachedItemForUrl won't give a bad name if copying that item (#195385) KFileItem cachedItem = KDirLister::cachedItemForUrl(newUrl); QVERIFY(!cachedItem.isNull()); QCOMPARE(cachedItem.name(), QString("toplevelfile_2_renamed")); QCOMPARE(cachedItem.entry().stringValue(KIO::UDSEntry::UDS_NAME), QString("toplevelfile_2_renamed")); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString()); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } void KDirModelTest::testMoveDirectory() { testMoveDirectory(QStringLiteral("subdir")); } void KDirModelTest::testMoveDirectory(const QString &dir /*just a dir name, no slash*/) { const QString path = m_tempDir->path() + '/'; const QString srcdir = path + dir; QVERIFY(QDir(srcdir).exists()); QTemporaryDir destDir; const QString dest = destDir.path() + '/'; QVERIFY(QDir(dest).exists()); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); // Move qDebug() << "Moving" << srcdir << "to" << dest; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(srcdir), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->exec()); // wait for kdirnotify enterLoop(); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); // Move back qDebug() << "Moving" << dest + dir << "back to" << srcdir; job = KIO::move(QUrl::fromLocalFile(dest + dir), QUrl::fromLocalFile(srcdir), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->exec()); enterLoop(); QVERIFY(QDir(srcdir).exists()); disconnect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); // m_dirIndex is invalid after the above... fillModel(true); } void KDirModelTest::testRenameDirectory() // #172945, #174703, (and #180156) { const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path + "subdir"); const QUrl newUrl = QUrl::fromLocalFile(path + "subdir_renamed"); // For #180156 we need a second kdirmodel, viewing the subdir being renamed. // I'm abusing m_dirModelForExpand for that purpose. delete m_dirModelForExpand; m_dirModelForExpand = new KDirModel; KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister(); connect(dirListerForExpand, QOverload<>::of(&KDirLister::completed), this, &KDirModelTest::slotListingCompleted); dirListerForExpand->openUrl(url); // async enterLoop(); // Now do the renaming QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); // If we come here, then dataChanged() was emitted - all good. //QCOMPARE(spyDataChanged.count(), 1); // it was in fact emitted 5 times... //COMPARE_INDEXES(spyDataChanged[0][0].value(), m_dirIndex); //QModelIndex receivedIndex = spyDataChanged[0][1].value(); //QCOMPARE(receivedIndex.row(), m_dirIndex.row()); // only compare row; column is count-1 // check renaming happened QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), newUrl.toString()); qDebug() << newUrl << "indexForUrl=" << m_dirModel->indexForUrl(newUrl) << "m_dirIndex=" << m_dirIndex; QCOMPARE(m_dirModel->indexForUrl(newUrl), m_dirIndex); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid()); // Check the other kdirmodel got redirected QCOMPARE(dirListerForExpand->url().toLocalFile(), QString(path + "subdir_renamed")); qDebug() << "calling testMoveDirectory(subdir_renamed)"; // Test moving the renamed directory; if something inside KDirModel // wasn't properly updated by the renaming, this would detect it and crash (#180673) testMoveDirectory(QStringLiteral("subdir_renamed")); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged enterLoop(); QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString()); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString()); QCOMPARE(m_dirModel->indexForUrl(url), m_dirIndex); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/testfile")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")).isValid()); QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir/testfile")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid()); QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid()); // TODO INVESTIGATE // QCOMPARE(dirListerForExpand->url().toLocalFile(), path+"subdir"); delete m_dirModelForExpand; m_dirModelForExpand = nullptr; } void KDirModelTest::testRenameDirectoryInCache() // #188807 { // Ensure the stuff is in cache. fillModel(true); const QString path = m_tempDir->path() + '/'; QVERIFY(!m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)).isNull()); // No more dirmodel nor dirlister. delete m_dirModel; m_dirModel = nullptr; // Now let's rename a directory that is in KCoreDirListerCache const QUrl url = QUrl::fromLocalFile(path); QUrl newUrl = url.adjusted(QUrl::StripTrailingSlash); newUrl.setPath(newUrl.path() + "_renamed"); qDebug() << newUrl; KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Put things back to normal job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // KDirNotify emits FileRenamed for each rename() above, which in turn // re-lists the directory. We need to wait for both signals to be emitted // otherwise the dirlister will not be in the state we expect. QTest::qWait(200); fillModel(true); QVERIFY(m_dirIndex.isValid()); KFileItem rootItem = m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)); QVERIFY(!rootItem.isNull()); } void KDirModelTest::testChmodDirectory() // #53397 { QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged); connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); const QString path = m_tempDir->path() + '/'; KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex()); const mode_t origPerm = rootItem.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; //const QFile::Permissions origPerm = rootItem.filePermissions(); //QVERIFY(origPerm & QFile::ReadOwner); //const QFile::Permissions newPerm = origPerm ^ QFile::WriteGroup; QVERIFY(newPerm != origPerm); KFileItemList items; items << rootItem; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(job->exec()); // ChmodJob doesn't talk to KDirNotify, kpropertiesdialog does. // [this allows to group notifications after all the changes one can make in the dialog] org::kde::KDirNotify::emitFilesChanged(QList() << QUrl::fromLocalFile(path)); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then dataChanged() was emitted - all good. QCOMPARE(spyDataChanged.count(), 1); QModelIndex receivedIndex = spyDataChanged[0][0].value(); qDebug() << receivedIndex; QVERIFY(!receivedIndex.isValid()); const KFileItem newRootItem = m_dirModel->itemForIndex(QModelIndex()); QVERIFY(!newRootItem.isNull()); QCOMPARE(QString::number(newRootItem.permissions(), 16), QString::number(newPerm, 16)); disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop); } enum { NoFlag = 0, NewDir = 1, // whether to re-create a new QTemporaryDir completely, to avoid cached fileitems ListFinalDir = 2, // whether to list the target dir at the same time, like k3b, for #193364 Recreate = 4, CacheSubdir = 8, // put subdir in the cache before expandToUrl // flags, next item is 16! }; void KDirModelTest::testExpandToUrl_data() { QTest::addColumn("flags"); // see enum above QTest::addColumn("expandToPath"); // relative path QTest::addColumn("expectedExpandSignals"); QTest::newRow("the root, nothing to do") << int(NoFlag) << QString() << QStringList(); QTest::newRow(".") << int(NoFlag) << "." << (QStringList()); QTest::newRow("subdir") << int(NoFlag) << "subdir" << (QStringList() << QStringLiteral("subdir")); QTest::newRow("subdir/.") << int(NoFlag) << "subdir/." << (QStringList() << QStringLiteral("subdir")); const QString subsubdir = QStringLiteral("subdir/subsubdir"); // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir. QTest::newRow("subdir/subsubdir") << int(NoFlag) << subsubdir << (QStringList() << QStringLiteral("subdir") << subsubdir); // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir, list subsubdir. const QString subsubdirfile = subsubdir + "/testfile"; QTest::newRow("subdir/subsubdir/testfile sync") << int(NoFlag) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); #ifndef Q_OS_WIN // Expand a symlink to a directory (#219547) const QString dirlink = m_tempDir->path() + "/dirlink"; createTestSymlink(dirlink, "subdir"); // dirlink -> subdir QVERIFY(QFileInfo(dirlink).isSymLink()); // If this test fails, your first move should be to enable all debug output and see if KDirWatch says inotify failed QTest::newRow("dirlink") << int(NoFlag) << "dirlink/subsubdir" << (QStringList() << QStringLiteral("dirlink") << QStringLiteral("dirlink/subsubdir")); #endif // Do a cold-cache test too, but nowadays it doesn't change anything anymore, // apart from testing different code paths inside KDirLister. QTest::newRow("subdir/subsubdir/testfile with reload") << int(NewDir) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); QTest::newRow("hold dest dir") // #193364 << int(NewDir | ListFinalDir) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); // Put subdir in cache too (#175035) QTest::newRow("hold subdir and dest dir") << int(NewDir | CacheSubdir | ListFinalDir | Recreate) << subsubdirfile << (QStringList() << QStringLiteral("subdir") << subsubdir << subsubdirfile); // Make sure the last test has the Recreate option set, for the subsequent test methods. } void KDirModelTest::testExpandToUrl() { QFETCH(int, flags); QFETCH(QString, expandToPath); // relative QFETCH(QStringList, expectedExpandSignals); if (flags & NewDir) { recreateTestData(); // WARNING! m_dirIndex, m_fileIndex, m_secondFileIndex etc. are not valid anymore after this point! } const QString path = m_tempDir->path() + '/'; if (flags & CacheSubdir) { // This way, the listDir for subdir will find items in cache, and will schedule a CachedItemsJob m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir")); QSignalSpy completedSpy(m_dirModel->dirLister(), SIGNAL(completed())); QVERIFY(completedSpy.wait(2000)); } if (flags & ListFinalDir) { // This way, the last listDir will find items in cache, and will schedule a CachedItemsJob m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")); QSignalSpy completedSpy(m_dirModel->dirLister(), SIGNAL(completed())); QVERIFY(completedSpy.wait(2000)); } if (!m_dirModelForExpand || (flags & NewDir)) { delete m_dirModelForExpand; m_dirModelForExpand = new KDirModel; connect(m_dirModelForExpand, &KDirModel::expand, this, &KDirModelTest::slotExpand); connect(m_dirModelForExpand, &QAbstractItemModel::rowsInserted, this, &KDirModelTest::slotRowsInserted); KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister(); dirListerForExpand->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // async } m_rowsInsertedEmitted = false; m_expectedExpandSignals = expectedExpandSignals; m_nextExpectedExpandSignals = 0; QSignalSpy spyExpand(m_dirModelForExpand, SIGNAL(expand(QModelIndex))); m_urlToExpandTo = QUrl::fromLocalFile(path + expandToPath); // If KDirModel doesn't know this URL yet, then we want to see rowsInserted signals // being emitted, so that the slots can get the index to that url then. m_expectRowsInserted = !expandToPath.isEmpty() && !m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid(); QVERIFY(QFileInfo::exists(m_urlToExpandTo.toLocalFile())); m_dirModelForExpand->expandToUrl(m_urlToExpandTo); if (expectedExpandSignals.isEmpty()) { QTest::qWait(20); // to make sure we process queued connection calls, otherwise spyExpand.count() is always 0 even if there's a bug... QCOMPARE(spyExpand.count(), 0); } else { if (spyExpand.count() < expectedExpandSignals.count()) { enterLoop(); QCOMPARE(spyExpand.count(), expectedExpandSignals.count()); } if (m_expectRowsInserted) { QVERIFY(m_rowsInsertedEmitted); } } // Now it should exist if (!expandToPath.isEmpty() && expandToPath != QLatin1String(".")) { qDebug() << "Do I know" << m_urlToExpandTo << "?"; QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid()); } if (flags & ListFinalDir) { testUpdateParentAfterExpand(); } if (flags & Recreate) { // Clean up, for the next tests recreateTestData(); fillModel(false); } } void KDirModelTest::slotExpand(const QModelIndex &index) { QVERIFY(index.isValid()); const QString path = m_tempDir->path() + '/'; KFileItem item = m_dirModelForExpand->itemForIndex(index); QVERIFY(!item.isNull()); qDebug() << item.url().toLocalFile(); QCOMPARE(item.url().toLocalFile(), QString(path + m_expectedExpandSignals[m_nextExpectedExpandSignals++])); // if rowsInserted wasn't emitted yet, then any proxy model would be unable to do anything with index at this point if (item.url() == m_urlToExpandTo) { QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid()); if (m_expectRowsInserted) { QVERIFY(m_rowsInsertedEmitted); } } if (m_nextExpectedExpandSignals == m_expectedExpandSignals.count()) { m_eventLoop.exitLoop(); // done } } void KDirModelTest::slotRowsInserted(const QModelIndex &, int, int) { m_rowsInsertedEmitted = true; } // This code is called by testExpandToUrl void KDirModelTest::testUpdateParentAfterExpand() // #193364 { const QString path = m_tempDir->path() + '/'; const QString file = path + "subdir/aNewFile"; qDebug() << "Creating" << file; QVERIFY(!QFile::exists(file)); createTestFile(file); QSignalSpy spyRowsInserted(m_dirModelForExpand, SIGNAL(rowsInserted(QModelIndex,int,int))); QVERIFY(spyRowsInserted.wait(1000)); } void KDirModelTest::testFilter() { QVERIFY(m_dirIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex); QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), SIGNAL(itemsFilteredByMime(KFileItemList))); QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), SIGNAL(itemsDeleted(KFileItemList))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*")); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), 4); // 3 toplevel* files, one subdir QCOMPARE(m_dirModel->rowCount(m_dirIndex), 1); // the files get filtered out, the subdir remains // In the subdir, we can get rowsRemoved signals like (1,2) or (0,0)+(2,2), // depending on the order of the files in the model. // So QCOMPARE(spyRowsRemoved.count(), 3) is fragile, we rather need // to sum up the removed rows per parent directory. QMap rowsRemovedPerDir; for (int i = 0; i < spyRowsRemoved.count(); ++i) { const QVariantList args = spyRowsRemoved[i]; const QModelIndex parentIdx = args[0].value(); QString dirName; if (parentIdx.isValid()) { const KFileItem item = m_dirModel->itemForIndex(parentIdx); dirName = item.name(); } else { dirName = QStringLiteral("root"); } rowsRemovedPerDir[dirName] += args[2].toInt() - args[1].toInt() + 1; //qDebug() << parentIdx << args[1].toInt() << args[2].toInt(); } QCOMPARE(rowsRemovedPerDir.count(), 3); // once for every dir QCOMPARE(rowsRemovedPerDir.value("root"), 1); // one from toplevel ('special chars') QCOMPARE(rowsRemovedPerDir.value("subdir"), 2); // two from subdir QCOMPARE(rowsRemovedPerDir.value("subsubdir"), 1); // one from subsubdir QCOMPARE(spyItemsDeleted.count(), 3); // once for every dir QCOMPARE(spyItemsDeleted[0][0].value().count(), 1); // one from toplevel ('special chars') QCOMPARE(spyItemsDeleted[1][0].value().count(), 2); // two from subdir QCOMPARE(spyItemsDeleted[2][0].value().count(), 1); // one from subsubdir QCOMPARE(spyItemsFilteredByMime.count(), 0); spyItemsDeleted.clear(); spyItemsFilteredByMime.clear(); // Reset the filter qDebug() << "reset to no filter"; m_dirModel->dirLister()->setNameFilter(QString()); m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); QCOMPARE(spyItemsDeleted.count(), 0); QCOMPARE(spyItemsFilteredByMime.count(), 0); // The order of things changed because of filtering. // Fill again, so that m_fileIndex etc. are correct again. fillModel(true); } void KDirModelTest::testMimeFilter() { QVERIFY(m_dirIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex); QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), SIGNAL(itemsFilteredByMime(KFileItemList))); QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), SIGNAL(itemsDeleted(KFileItemList))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); m_dirModel->dirLister()->setMimeFilter(QStringList() << QStringLiteral("application/pdf")); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), 1); // 1 pdf files, no subdir anymore QVERIFY(spyRowsRemoved.count() >= 1); // depends on contiguity... QVERIFY(spyItemsDeleted.count() >= 1); // once for every dir // Maybe it would make sense to have those items in itemsFilteredByMime, // but well, for the only existing use of that signal (mime filter plugin), // it's not really necessary, the plugin has seen those files before anyway. // The signal is mostly useful for the case of listing a dir with a mime filter set. //QCOMPARE(spyItemsFilteredByMime.count(), 1); //QCOMPARE(spyItemsFilteredByMime[0][0].value().count(), 4); spyItemsDeleted.clear(); spyItemsFilteredByMime.clear(); // Reset the filter qDebug() << "reset to no filter"; m_dirModel->dirLister()->setMimeFilter(QStringList()); m_dirModel->dirLister()->emitChanges(); QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); QCOMPARE(spyItemsDeleted.count(), 0); QCOMPARE(spyItemsFilteredByMime.count(), 0); // The order of things changed because of filtering. // Fill again, so that m_fileIndex etc. are correct again. fillModel(true); } void KDirModelTest::testShowHiddenFiles() // #174788 { KDirLister *dirLister = m_dirModel->dirLister(); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); QSignalSpy spyNewItems(dirLister, SIGNAL(newItems(KFileItemList))); QSignalSpy spyRowsInserted(m_dirModel, SIGNAL(rowsInserted(QModelIndex,int,int))); dirLister->setShowingDotFiles(true); dirLister->emitChanges(); const int numberOfDotFiles = 2; QCOMPARE(spyNewItems.count(), 1); QCOMPARE(spyNewItems[0][0].value().count(), numberOfDotFiles); QCOMPARE(spyRowsInserted.count(), 1); QCOMPARE(spyRowsRemoved.count(), 0); spyNewItems.clear(); spyRowsInserted.clear(); dirLister->setShowingDotFiles(false); dirLister->emitChanges(); QCOMPARE(spyNewItems.count(), 0); QCOMPARE(spyRowsInserted.count(), 0); QCOMPARE(spyRowsRemoved.count(), 1); } void KDirModelTest::testMultipleSlashes() { const QString path = m_tempDir->path() + '/'; QModelIndex index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//testfile")); QVERIFY(index.isValid()); index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//subsubdir//")); QVERIFY(index.isValid()); index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir///subsubdir////testfile")); QVERIFY(index.isValid()); } void KDirModelTest::testUrlWithRef() // #171117 { const QString path = m_tempDir->path() + '/'; KDirLister *dirLister = m_dirModel->dirLister(); QUrl url = QUrl::fromLocalFile(path); url.setFragment(QStringLiteral("ref")); QVERIFY(url.url().endsWith(QLatin1String("#ref"))); dirLister->openUrl(url, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); QCOMPARE(dirLister->url().toString(), url.toString(QUrl::StripTrailingSlash)); collectKnownIndexes(); disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } //void KDirModelTest::testFontUrlWithHost() // #160057 --> moved to kio_fonts (kfontinst/kio/autotests) void KDirModelTest::testRemoteUrlWithHost() // #178416 { if (!KProtocolInfo::isKnownProtocol(QStringLiteral("remote"))) { QSKIP("kio_remote not installed"); } QUrl url(QStringLiteral("remote://foo")); KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(url, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); QCOMPARE(dirLister->url().toString(), QString("remote:")); } void KDirModelTest::testZipFile() // # 171721 { const QString path = QFileInfo(QFINDTESTDATA("wronglocalsizes.zip")).absolutePath(); KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); enterLoop(); disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); QUrl zipUrl(QUrl::fromLocalFile(path)); zipUrl.setPath(zipUrl.path() + "/wronglocalsizes.zip"); // just a zip file lying here for other reasons QVERIFY(QFile::exists(zipUrl.toLocalFile())); zipUrl.setScheme(QStringLiteral("zip")); QModelIndex index = m_dirModel->indexForUrl(zipUrl); QVERIFY(!index.isValid()); // protocol mismatch, can't find it! zipUrl.setScheme(QStringLiteral("file")); index = m_dirModel->indexForUrl(zipUrl); QVERIFY(index.isValid()); } void KDirModelTest::testSmb() { const QUrl smbUrl(QStringLiteral("smb:/")); // TODO: feed a KDirModel without using a KDirLister. // Calling the slots directly. // This requires that KDirModel does not ask the KDirLister for its rootItem anymore, // but that KDirLister emits the root item whenever it changes. if (!KProtocolInfo::isKnownProtocol(QStringLiteral("smb"))) { QSKIP("kio_smb not installed"); } KDirLister *dirLister = m_dirModel->dirLister(); dirLister->openUrl(smbUrl, KDirLister::NoFlags); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); connect(dirLister, SIGNAL(canceled()), this, SLOT(slotListingCompleted())); QSignalSpy spyCanceled(dirLister, SIGNAL(canceled())); enterLoop(); // wait for completed signal if (!spyCanceled.isEmpty()) { QSKIP("smb:/ returns an error, probably no network available"); } QModelIndex index = m_dirModel->index(0, 0); if (index.isValid()) { QVERIFY(m_dirModel->canFetchMore(index)); m_dirModel->fetchMore(index); enterLoop(); // wait for completed signal disconnect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); } } class MyDirLister : public KDirLister { public: void emitItemsDeleted(const KFileItemList &items) { emit itemsDeleted(items); } }; void KDirModelTest::testBug196695() { KFileItem rootItem(QUrl::fromLocalFile(m_tempDir->path()), QString(), KFileItem::Unknown); KFileItem childItem(QUrl::fromLocalFile(QString(m_tempDir->path() + "/toplevelfile_1")), QString(), KFileItem::Unknown); KFileItemList list; // Important: the root item must not be first in the list to trigger bug 196695 list << childItem << rootItem; MyDirLister *dirLister = static_cast(m_dirModel->dirLister()); dirLister->emitItemsDeleted(list); fillModel(true); } void KDirModelTest::testMimeData() { QModelIndex index0 = m_dirModel->index(0, 0); QVERIFY(index0.isValid()); QModelIndex index1 = m_dirModel->index(1, 0); QVERIFY(index1.isValid()); QList indexes; indexes << index0 << index1; QMimeData *mimeData = m_dirModel->mimeData(indexes); QVERIFY(mimeData); QVERIFY(mimeData->hasUrls()); const QList urls = mimeData->urls(); QCOMPARE(urls.count(), indexes.count()); delete mimeData; } void KDirModelTest::testDotHiddenFile_data() { QTest::addColumn("fileContents"); QTest::addColumn("expectedListing"); QStringList allItems; allItems << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << SPECIALCHARS << QStringLiteral("subdir"); QTest::newRow("empty_file") << QStringList() << allItems; QTest::newRow("simple_name") << (QStringList() << QStringLiteral("toplevelfile_1")) << QStringList(allItems.mid(1)); QStringList allButSpecialChars = allItems; allButSpecialChars.removeAt(3); QTest::newRow("special_chars") << (QStringList() << SPECIALCHARS) << allButSpecialChars; QStringList allButSubdir = allItems; allButSubdir.removeAt(4); QTest::newRow("subdir") << (QStringList() << QStringLiteral("subdir")) << allButSubdir; QTest::newRow("many_lines") << (QStringList() << QStringLiteral("subdir") << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_3") << QStringLiteral("toplevelfile_2")) << (QStringList() << SPECIALCHARS); } void KDirModelTest::testDotHiddenFile() { QFETCH(QStringList, fileContents); QFETCH(QStringList, expectedListing); const QString path = m_tempDir->path() + '/'; const QString dotHiddenFile = path + ".hidden"; QTest::qWait(1000); // mtime-based cache, so we need to wait for 1 second QFile dh(dotHiddenFile); QVERIFY(dh.open(QIODevice::WriteOnly)); dh.write(fileContents.join('\n').toUtf8()); dh.close(); // Do it twice: once to read from the file and once to use the cache for (int i = 0; i < 2; ++i) { fillModel(true, false); QStringList files; for (int row = 0; row < m_dirModel->rowCount(); ++row) { files.append(m_dirModel->index(row, KDirModel::Name).data().toString()); } files.sort(); expectedListing.sort(); QCOMPARE(files, expectedListing); } dh.remove(); } +void KDirModelTest::testShowRoot() +{ + KDirModel dirModel; + const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath()); + const QUrl fsRootUrl = QUrl(QStringLiteral("file:///")); + + // openUrl("/", ShowRoot) should create a "/" item + dirModel.openUrl(fsRootUrl, KDirModel::ShowRoot); + QTRY_COMPARE(dirModel.rowCount(), 1); + const QModelIndex rootIndex = dirModel.index(0, 0); + QVERIFY(rootIndex.isValid()); + QCOMPARE(rootIndex.data().toString(), QStringLiteral("/")); + QVERIFY(!dirModel.parent(rootIndex).isValid()); + QCOMPARE(dirModel.itemForIndex(rootIndex).url(), QUrl(QStringLiteral("file:///"))); + QCOMPARE(dirModel.itemForIndex(rootIndex).name(), QStringLiteral("/")); + + // expandToUrl should work + dirModel.expandToUrl(homeUrl); + QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid()); + + // test itemForIndex and indexForUrl + QCOMPARE(dirModel.itemForIndex(QModelIndex()).url(), QUrl()); + QVERIFY(!dirModel.indexForUrl(QUrl()).isValid()); + const QUrl slashUrl = QUrl::fromLocalFile(QStringLiteral("/")); + QCOMPARE(dirModel.indexForUrl(slashUrl), rootIndex); + + // switching to another URL should also show a root node + QSignalSpy spyRowsRemoved(&dirModel, &QAbstractItemModel::rowsRemoved); + const QUrl tempUrl = QUrl::fromLocalFile(QDir::tempPath()); + dirModel.openUrl(tempUrl, KDirModel::ShowRoot); + QTRY_COMPARE(dirModel.rowCount(), 1); + QCOMPARE(spyRowsRemoved.count(), 1); + const QModelIndex newRootIndex = dirModel.index(0, 0); + QVERIFY(newRootIndex.isValid()); + QCOMPARE(newRootIndex.data().toString(), QFileInfo(QDir::tempPath()).fileName()); + QVERIFY(!dirModel.parent(newRootIndex).isValid()); + QVERIFY(!dirModel.indexForUrl(slashUrl).isValid()); + QCOMPARE(dirModel.itemForIndex(newRootIndex).url(), tempUrl); +} + +void KDirModelTest::testShowRootWithTrailingSlash() +{ + // GIVEN + KDirModel dirModel; + const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath() + QLatin1Char('/')); + + // WHEN + dirModel.openUrl(homeUrl, KDirModel::ShowRoot); + QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid()); +} + +void KDirModelTest::testShowRootAndExpandToUrl() +{ + // call expandToUrl without waiting for initial listing of root node + KDirModel dirModel; + dirModel.openUrl(QUrl(QStringLiteral("file:///")), KDirModel::ShowRoot); + const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath()); + dirModel.expandToUrl(homeUrl); + QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid()); +} + void KDirModelTest::testDeleteFile() { fillModel(true); QVERIFY(m_fileIndex.isValid()); const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QString file = path + "toplevelfile_1"; const QUrl url = QUrl::fromLocalFile(file); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row()); QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row()); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); // Recreate the file, for consistency in the next tests // So the second part of this test is a "testCreateFile" createTestFile(file); // Tricky problem - KDirLister::openUrl will emit items from cache // and then schedule an update; so just calling fillModel would // not wait enough, it would abort due to not finding toplevelfile_1 // in the items from cache. This progressive-emitting behavior is fine // for GUIs but not for unit tests ;-) fillModel(true, false); fillModel(false); } void KDirModelTest::testDeleteFileWhileListing() // doesn't really test that yet, the kdirwatch deleted signal comes too late { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QString file = path + "toplevelfile_1"; const QUrl url = QUrl::fromLocalFile(file); KDirLister *dirLister = m_dirModel->dirLister(); QSignalSpy spyCompleted(dirLister, SIGNAL(completed())); connect(dirLister, SIGNAL(completed()), this, SLOT(slotListingCompleted())); dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); if (!spyCompleted.isEmpty()) { QSKIP("listing completed too early"); } QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); if (spyCompleted.isEmpty()) { enterLoop(); } QVERIFY(spyRowsRemoved.wait(1000)); const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row()); QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row()); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); qDebug() << "Test done, recreating file"; // Recreate the file, for consistency in the next tests // So the second part of this test is a "testCreateFile" createTestFile(file); fillModel(true, false); // see testDeleteFile fillModel(false); } void KDirModelTest::testOverwriteFileWithDir() // #151851 c4 { fillModel(false); const QString path = m_tempDir->path() + '/'; const QString dir = path + "subdir"; const QString file = path + "toplevelfile_1"; const int oldTopLevelRowCount = m_dirModel->rowCount(); bool removalWithinTopLevel = false; bool dataChangedAtFirstLevel = false; auto rrc = connect(m_dirModel, &KDirModel::rowsRemoved, this, [&removalWithinTopLevel](const QModelIndex &index) { if (!index.isValid()) { // yes, that's what we have been waiting for removalWithinTopLevel = true; } }); auto dcc = connect(m_dirModel, &KDirModel::dataChanged, this, [&dataChangedAtFirstLevel](const QModelIndex &index) { if (index.isValid() && !index.parent().isValid()) { // a change of a node whose parent is root, yay, that's it dataChangedAtFirstLevel = true; } }); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::Job *job = KIO::move(QUrl::fromLocalFile(dir), QUrl::fromLocalFile(file), KIO::HideProgressInfo); job->setUiDelegate(nullptr); PredefinedAnswerJobUiDelegate extension; extension.m_renameResult = KIO::Result_Overwrite; job->setUiDelegateExtension(&extension); QVERIFY(job->exec()); QCOMPARE(extension.m_askFileRenameCalled, 1); // Wait for a removal within the top level (that's for the old file going away), and also // for a dataChanged which notifies us that a file has become a directory int retries = 0; while ((!removalWithinTopLevel || !dataChangedAtFirstLevel) && retries < 100) { QTest::qWait(10); ++retries; } QVERIFY(removalWithinTopLevel); QVERIFY(dataChangedAtFirstLevel); m_dirModel->disconnect(rrc); m_dirModel->disconnect(dcc); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(dir)).isValid()); QModelIndex newIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(newIndex.isValid()); KFileItem newItem = m_dirModel->itemForIndex(newIndex); QVERIFY(newItem.isDir()); // yes, the file is a dir now ;-) qDebug() << "========= Test done, recreating test data ========="; recreateTestData(); fillModel(false); } void KDirModelTest::testDeleteFiles() { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString file = m_tempDir->path() + "/toplevelfile_"; QList urls; urls << QUrl::fromLocalFile(file + '1') << QUrl::fromLocalFile(file + '2') << QUrl::fromLocalFile(file + '3'); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); KIO::DeleteJob *job = KIO::del(urls, KIO::HideProgressInfo); QVERIFY(job->exec()); int numRowsRemoved = 0; while (numRowsRemoved < 3) { QTest::qWait(20); numRowsRemoved = 0; for (int sigNum = 0; sigNum < spyRowsRemoved.count(); ++sigNum) { numRowsRemoved += spyRowsRemoved[sigNum][2].toInt() - spyRowsRemoved[sigNum][1].toInt() + 1; } qDebug() << "numRowsRemoved=" << numRowsRemoved; } const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 3); // three less than before qDebug() << "Recreating test data"; recreateTestData(); qDebug() << "Re-filling model"; fillModel(false); } // A renaming that looks more like a deletion to the model void KDirModelTest::testRenameFileToHidden() // #174721 { const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2"); const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/.toplevelfile_2"); QSignalSpy spyDataChanged(m_dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex))); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); QSignalSpy spyRowsInserted(m_dirModel, SIGNAL(rowsInserted(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. QCOMPARE(spyDataChanged.count(), 0); QCOMPARE(spyRowsRemoved.count(), 1); QCOMPARE(spyRowsInserted.count(), 0); COMPARE_INDEXES(spyRowsRemoved[0][0].value(), QModelIndex()); // parent is invalid const int row = spyRowsRemoved[0][1].toInt(); QCOMPARE(row, m_secondFileIndex.row()); // only compare row disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); spyRowsRemoved.clear(); // Put things back to normal, should make the file reappear connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop); job = KIO::rename(newUrl, url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister enterLoop(); QCOMPARE(spyDataChanged.count(), 0); QCOMPARE(spyRowsRemoved.count(), 0); QCOMPARE(spyRowsInserted.count(), 1); int newRow = spyRowsInserted[0][1].toInt(); m_secondFileIndex = m_dirModel->index(newRow, 0); QVERIFY(m_secondFileIndex.isValid()); QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString()); } void KDirModelTest::testDeleteDirectory() { const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path + "subdir/subsubdir"); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QSignalSpy spyDirWatchDeleted(KDirWatch::self(), SIGNAL(deleted(QString))); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. QCOMPARE(spyRowsRemoved.count(), 1); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex deletedDirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")); QVERIFY(!deletedDirIndex.isValid()); QModelIndex dirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")); QVERIFY(dirIndex.isValid()); // TODO!!! Bug in KDirWatch? ### // QCOMPARE(spyDirWatchDeleted.count(), 1); } void KDirModelTest::testDeleteCurrentDirectory() { const int oldTopLevelRowCount = m_dirModel->rowCount(); const QString path = m_tempDir->path() + '/'; const QUrl url = QUrl::fromLocalFile(path); QSignalSpy spyRowsRemoved(m_dirModel, SIGNAL(rowsRemoved(QModelIndex,int,int))); connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); KDirWatch::self()->statistics(); KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); QVERIFY(job->exec()); // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved enterLoop(); // If we come here, then rowsRemoved() was emitted - all good. const int topLevelRowCount = m_dirModel->rowCount(); QCOMPARE(topLevelRowCount, 0); // empty // We can get rowsRemoved for subdirs first, since kdirwatch notices that. QVERIFY(spyRowsRemoved.count() >= 1); // Look for the signal(s) that had QModelIndex() as parent. int i; int numDeleted = 0; for (i = 0; i < spyRowsRemoved.count(); ++i) { const int from = spyRowsRemoved[i][1].toInt(); const int to = spyRowsRemoved[i][2].toInt(); qDebug() << spyRowsRemoved[i][0].value() << from << to; if (!spyRowsRemoved[i][0].value().isValid()) { numDeleted += (to - from) + 1; } } QCOMPARE(numDeleted, oldTopLevelRowCount); disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop); QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1")); QVERIFY(!fileIndex.isValid()); } void KDirModelTest::testQUrlHash() { const int count = 3000; // Prepare an array of QUrls so that url constructing isn't part of the timing QVector urls; urls.resize(count); for (int i = 0; i < count; ++i) { urls[i] = QUrl("http://www.kde.org/path/" + QString::number(i)); } QHash qurlHash; QHash kurlHash; QElapsedTimer dt; dt.start(); for (int i = 0; i < count; ++i) { qurlHash.insert(urls[i], i); } //qDebug() << "inserting" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs"; dt.start(); for (int i = 0; i < count; ++i) { kurlHash.insert(urls[i], i); } //qDebug() << "inserting" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs"; // Nice results: for count=30000 I got 4515 (before) and 103 (after) dt.start(); for (int i = 0; i < count; ++i) { QCOMPARE(qurlHash.value(urls[i]), i); } //qDebug() << "looking up" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs"; dt.start(); for (int i = 0; i < count; ++i) { QCOMPARE(kurlHash.value(urls[i]), i); } //qDebug() << "looking up" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs"; // Nice results: for count=30000 I got 4296 (before) and 63 (after) } diff --git a/autotests/kdirmodeltest.h b/autotests/kdirmodeltest.h index 3735c170..e88d1110 100644 --- a/autotests/kdirmodeltest.h +++ b/autotests/kdirmodeltest.h @@ -1,116 +1,119 @@ /* This file is part of the KDE project Copyright (C) 2006 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDIRMODELTEST_H #define KDIRMODELTEST_H #include #include #include #include #include // If you disable this, you need to change all exitLoop into quit in connect() statements... #define USE_QTESTEVENTLOOP class KDirModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void cleanup(); void testRowCount(); void testIndex(); void testNames(); void testItemForIndex(); void testIndexForItem(); void testData(); void testReload(); void testModifyFile(); void testRenameFile(); void testMoveDirectory(); void testRenameDirectory(); void testRenameDirectoryInCache(); void testChmodDirectory(); void testExpandToUrl_data(); void testExpandToUrl(); void testFilter(); void testMimeFilter(); void testShowHiddenFiles(); void testMultipleSlashes(); void testUrlWithRef(); void testRemoteUrlWithHost(); void testZipFile(); void testSmb(); void testBug196695(); void testMimeData(); void testDotHiddenFile_data(); void testDotHiddenFile(); + void testShowRoot(); + void testShowRootWithTrailingSlash(); + void testShowRootAndExpandToUrl(); // These tests must be done last void testDeleteFile(); void testDeleteFileWhileListing(); void testOverwriteFileWithDir(); void testDeleteFiles(); void testRenameFileToHidden(); void testDeleteDirectory(); void testDeleteCurrentDirectory(); // Somewhat unrelated void testQUrlHash(); protected Q_SLOTS: // 'more private than private slots' - i.e. not seen by qtestlib void slotListingCompleted(); void slotExpand(const QModelIndex &index); void slotRowsInserted(const QModelIndex &index, int, int); private: void recreateTestData(); void enterLoop(); void fillModel(bool reload, bool expectAllIndexes = true); void collectKnownIndexes(); void testMoveDirectory(const QString &srcdir); void testUpdateParentAfterExpand(); private: #ifdef USE_QTESTEVENTLOOP QTestEventLoop m_eventLoop; #else QEventLoop m_eventLoop; #endif QTemporaryDir *m_tempDir; KDirModel *m_dirModel; QModelIndex m_fileIndex; QModelIndex m_specialFileIndex; QModelIndex m_secondFileIndex; QModelIndex m_dirIndex; QModelIndex m_fileInDirIndex; QModelIndex m_fileInSubdirIndex; QStringList m_topLevelFileNames; // files only // for slotExpand QStringList m_expectedExpandSignals; int m_nextExpectedExpandSignals; // index into m_expectedExpandSignals KDirModel *m_dirModelForExpand; QUrl m_urlToExpandTo; bool m_rowsInsertedEmitted; bool m_expectRowsInserted; }; #endif diff --git a/src/widgets/kdirmodel.cpp b/src/widgets/kdirmodel.cpp index 2c39e3fd..3963e62c 100644 --- a/src/widgets/kdirmodel.cpp +++ b/src/widgets/kdirmodel.cpp @@ -1,1316 +1,1374 @@ /* This file is part of the KDE project - Copyright (C) 2006 David Faure + Copyright (C) 2006-2019 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdirmodel.h" #include "kdirlister.h" #include "kfileitem.h" #include #include #include +#include #include #include "joburlcache_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif static QLoggingCategory category("kf5.kio.kdirmodel", QtInfoMsg); class KDirModelNode; class KDirModelDirNode; static QUrl cleanupUrl(const QUrl &url) { QUrl u = url; u.setPath(QDir::cleanPath(u.path())); // remove double slashes in the path, simplify "foo/." to "foo/", etc. u = u.adjusted(QUrl::StripTrailingSlash); // KDirLister does this too, so we remove the slash before comparing with the root node url. u.setQuery(QString()); u.setFragment(QString()); return u; } // We create our own tree behind the scenes to have fast lookup from an item to its parent, // and also to get the children of an item fast. class KDirModelNode { public: KDirModelNode(KDirModelDirNode *parent, const KFileItem &item) : m_item(item), m_parent(parent), m_preview() { } virtual ~KDirModelNode() { // Required, code will delete ptrs to this or a subclass. } // m_item is KFileItem() for the root item const KFileItem &item() const { return m_item; } void setItem(const KFileItem &item) { m_item = item; } KDirModelDirNode *parent() const { return m_parent; } // linear search int rowNumber() const; // O(n) QIcon preview() const { return m_preview; } void setPreview(const QPixmap &pix) { m_preview = QIcon(); m_preview.addPixmap(pix); } void setPreview(const QIcon &icn) { m_preview = icn; } private: KFileItem m_item; KDirModelDirNode *const m_parent; QIcon m_preview; }; // Specialization for directory nodes class KDirModelDirNode : public KDirModelNode { public: KDirModelDirNode(KDirModelDirNode *parent, const KFileItem &item) : KDirModelNode(parent, item), m_childNodes(), m_childCount(KDirModel::ChildCountUnknown), m_populated(false) {} ~KDirModelDirNode() override { qDeleteAll(m_childNodes); } QList m_childNodes; // owns the nodes // If we listed the directory, the child count is known. Otherwise it can be set via setChildCount. int childCount() const { return m_childNodes.isEmpty() ? m_childCount : m_childNodes.count(); } void setChildCount(int count) { m_childCount = count; } bool isPopulated() const { return m_populated; } void setPopulated(bool populated) { m_populated = populated; } bool isSlow() const { return item().isSlow(); } // For removing all child urls from the global hash. void collectAllChildUrls(QList &urls) const { urls.reserve(urls.size() + m_childNodes.size()); for (KDirModelNode *node : m_childNodes) { const KFileItem &item = node->item(); urls.append(cleanupUrl(item.url())); if (item.isDir()) { static_cast(node)->collectAllChildUrls(urls); } } } private: int m_childCount: 31; bool m_populated: 1; }; int KDirModelNode::rowNumber() const { if (!m_parent) { return 0; } return m_parent->m_childNodes.indexOf(const_cast(this)); } //// class KDirModelPrivate { public: explicit KDirModelPrivate(KDirModel *model) : q(model), m_dirLister(nullptr), m_rootNode(new KDirModelDirNode(nullptr, KFileItem())), m_dropsAllowed(KDirModel::NoDrops), m_jobTransfersVisible(false) { } ~KDirModelPrivate() { delete m_rootNode; } void _k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &); void _k_slotCompleted(const QUrl &directoryUrl); void _k_slotDeleteItems(const KFileItemList &); void _k_slotRefreshItems(const QList > &); void _k_slotClear(); void _k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl); void _k_slotJobUrlsChanged(const QStringList &urlList); void clear() { delete m_rootNode; m_rootNode = new KDirModelDirNode(nullptr, KFileItem()); + m_showNodeForListedUrl = false; } // Emit expand for each parent and then return the // last known parent if there is no node for this url KDirModelNode *expandAllParentsUntil(const QUrl &url) const; // Return the node for a given url, using the hash. KDirModelNode *nodeForUrl(const QUrl &url) const; KDirModelNode *nodeForIndex(const QModelIndex &index) const; QModelIndex indexForNode(KDirModelNode *node, int rowNumber = -1 /*unknown*/) const; + + static QUrl rootParentOf(const QUrl &url) { + // is what we listed, and which is visible at the root of the tree + // Here we want the (invisible) parent of that url + QUrl parent(url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash)); + if (url.path() == QLatin1String("/")) { + parent.setPath(QString()); + } + return parent; + } bool isDir(KDirModelNode *node) const { return (node == m_rootNode) || node->item().isDir(); } QUrl urlForNode(KDirModelNode *node) const { /** * Queries and fragments are removed from the URL, so that the URL of * child items really starts with the URL of the parent. * * For instance ksvn+http://url?rev=100 is the parent for ksvn+http://url/file?rev=100 * so we have to remove the query in both to be able to compare the URLs */ - QUrl url(node == m_rootNode ? m_dirLister->url() : node->item().url()); + QUrl url; + if (node == m_rootNode && !m_showNodeForListedUrl) { + url = m_dirLister->url(); + } else { + url = node->item().url(); + } if (url.hasQuery() || url.hasFragment()) { // avoid detach if not necessary. url.setQuery(QString()); url.setFragment(QString()); // kill ref (#171117) } return url; } void removeFromNodeHash(KDirModelNode *node, const QUrl &url); void clearAllPreviews(KDirModelDirNode *node); #ifndef NDEBUG void dump(); #endif + Q_DISABLE_COPY(KDirModelPrivate) KDirModel * const q; KDirLister *m_dirLister; KDirModelDirNode *m_rootNode; KDirModel::DropsAllowed m_dropsAllowed; bool m_jobTransfersVisible; + bool m_showNodeForListedUrl = false; // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient), // value = final url[s] being fetched QMap > m_urlsBeingFetched; QHash m_nodeHash; // global node hash: url -> node QStringList m_allCurrentDestUrls; //list of all dest urls that have jobs on them (e.g. copy, download) }; KDirModelNode *KDirModelPrivate::nodeForUrl(const QUrl &_url) const // O(1), well, O(length of url as a string) { QUrl url = cleanupUrl(_url); if (url == urlForNode(m_rootNode)) { return m_rootNode; } return m_nodeHash.value(url); } void KDirModelPrivate::removeFromNodeHash(KDirModelNode *node, const QUrl &url) { if (node->item().isDir()) { QList urls; static_cast(node)->collectAllChildUrls(urls); for (const QUrl &u : qAsConst(urls)) { m_nodeHash.remove(u); } } m_nodeHash.remove(cleanupUrl(url)); } KDirModelNode *KDirModelPrivate::expandAllParentsUntil(const QUrl &_url) const // O(depth) { QUrl url = cleanupUrl(_url); //qDebug() << url; QUrl nodeUrl = urlForNode(m_rootNode); + KDirModelDirNode *dirNode = m_rootNode; + if (m_showNodeForListedUrl && !m_rootNode->m_childNodes.isEmpty()) { + dirNode = static_cast(m_rootNode->m_childNodes.at(0)); // ### will be incorrect if we list drives on Windows + nodeUrl = dirNode->item().url(); + qCDebug(category) << "listed URL is visible, adjusted starting point to" << nodeUrl; + } if (url == nodeUrl) { - return m_rootNode; + return dirNode; } // Protocol mismatch? Don't even start comparing paths then. #171721 if (url.scheme() != nodeUrl.scheme()) { + qCWarning(category) << "protocol mismatch:" << url.scheme() << "vs" << nodeUrl.scheme(); return nullptr; } const QString pathStr = url.path(); // no trailing slash - KDirModelDirNode *dirNode = m_rootNode; if (!pathStr.startsWith(nodeUrl.path())) { + qCDebug(category) << pathStr << "does not start with" << nodeUrl.path(); return nullptr; } for (;;) { QString nodePath = nodeUrl.path(); if (!nodePath.endsWith(QLatin1Char('/'))) { nodePath += QLatin1Char('/'); } if (!pathStr.startsWith(nodePath)) { qCWarning(category) << "The kioslave for" << url.scheme() << "violates the hierarchy structure:" << "I arrived at node" << nodePath << ", but" << pathStr << "does not start with that path."; return nullptr; } // E.g. pathStr is /a/b/c and nodePath is /a/. We want to find the node with url /a/b const int nextSlash = pathStr.indexOf(QLatin1Char('/'), nodePath.length()); const QString newPath = pathStr.left(nextSlash); // works even if nextSlash==-1 nodeUrl.setPath(newPath); nodeUrl = nodeUrl.adjusted(QUrl::StripTrailingSlash); // #172508 KDirModelNode *node = nodeForUrl(nodeUrl); if (!node) { qCDebug(category) << nodeUrl << "not found, needs to be listed"; // return last parent found: return dirNode; } emit q->expand(indexForNode(node)); //qDebug() << " nodeUrl=" << nodeUrl; if (nodeUrl == url) { qCDebug(category) << "Found node" << node << "for" << url; return node; } qCDebug(category) << "going into" << node->item().url(); Q_ASSERT(isDir(node)); dirNode = static_cast(node); } // NOTREACHED //return 0; } #ifndef NDEBUG void KDirModelPrivate::dump() { qCDebug(category) << "Dumping contents of KDirModel" << q << "dirLister url:" << m_dirLister->url(); QHashIterator it(m_nodeHash); while (it.hasNext()) { it.next(); qCDebug(category) << it.key() << it.value(); } } #endif // node -> index. If rowNumber is set (or node is root): O(1). Otherwise: O(n). QModelIndex KDirModelPrivate::indexForNode(KDirModelNode *node, int rowNumber) const { if (node == m_rootNode) { return QModelIndex(); } Q_ASSERT(node->parent()); return q->createIndex(rowNumber == -1 ? node->rowNumber() : rowNumber, 0, node); } // index -> node. O(1) KDirModelNode *KDirModelPrivate::nodeForIndex(const QModelIndex &index) const { return index.isValid() ? static_cast(index.internalPointer()) : m_rootNode; } /* * This model wraps the data held by KDirLister. * * The internal pointer of the QModelIndex for a given file is the node for that file in our own tree. * E.g. index(2,0) returns a QModelIndex with row=2 internalPointer= * * Invalid parent index means root of the tree, m_rootNode */ static QString debugIndex(const QModelIndex &index) { QString str; if (!index.isValid()) { str = QStringLiteral("[invalid index, i.e. root]"); } else { KDirModelNode *node = static_cast(index.internalPointer()); str = QLatin1String("[index for ") + node->item().url().toString(); if (index.column() > 0) { str += QLatin1String(", column ") + QString::number(index.column()); } str += QLatin1Char(']'); } return str; } KDirModel::KDirModel(QObject *parent) : QAbstractItemModel(parent), d(new KDirModelPrivate(this)) { setDirLister(new KDirLister(this)); } KDirModel::~KDirModel() { delete d; } void KDirModel::setDirLister(KDirLister *dirLister) { if (d->m_dirLister) { d->clear(); delete d->m_dirLister; } d->m_dirLister = dirLister; d->m_dirLister->setParent(this); connect(d->m_dirLister, &KCoreDirLister::itemsAdded, this, [this](const QUrl &dirUrl, const KFileItemList &items){d->_k_slotNewItems(dirUrl, items);} ); connect(d->m_dirLister, static_cast(&KCoreDirLister::completed), this, [this](const QUrl &dirUrl){d->_k_slotCompleted(dirUrl);} ); connect(d->m_dirLister, &KCoreDirLister::itemsDeleted, this, [this](const KFileItemList &items){d->_k_slotDeleteItems(items);} ); connect(d->m_dirLister, &KCoreDirLister::refreshItems, this, [this](const QList > &items){d->_k_slotRefreshItems(items);} ); connect(d->m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, [this](){d->_k_slotClear();} ); connect(d->m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, [this](const QUrl &oldUrl, const QUrl &newUrl){d->_k_slotRedirection(oldUrl, newUrl);} ); } +void KDirModel::openUrl(const QUrl &inputUrl, OpenUrlFlags flags) +{ + Q_ASSERT(d->m_dirLister); + const QUrl url = cleanupUrl(inputUrl); + if (flags & ShowRoot) { + d->_k_slotClear(); + d->m_showNodeForListedUrl = true; + // Store the parent URL into the invisible root node + const QUrl parentUrl = d->rootParentOf(url); + d->m_rootNode->setItem(KFileItem(parentUrl)); + // Stat the requested url, to create the visible node + KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); + connect(statJob, &KJob::result, this, [statJob, parentUrl, url, this]() { + if (!statJob->error()) { + const KIO::UDSEntry entry = statJob->statResult(); + KFileItem visibleRootItem(entry, url); + visibleRootItem.setName(url.path() == QLatin1String("/") ? QStringLiteral("/") : url.fileName()); + d->_k_slotNewItems(parentUrl, QList{visibleRootItem}); + Q_ASSERT(d->m_rootNode->m_childNodes.count() == 1); + expandToUrl(url); + } else { + qWarning() << statJob->errorString(); + } + }); + } else { + d->m_dirLister->openUrl(url, (flags & Reload) ? KDirLister::Reload : KDirLister::NoFlags); + } +} + Qt::DropActions KDirModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction; } KDirLister *KDirModel::dirLister() const { return d->m_dirLister; } void KDirModelPrivate::_k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &items) { //qDebug() << "directoryUrl=" << directoryUrl; KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth) // If the directory containing the items wasn't found, then we have a big problem. - // Are you calling KDirLister::openUrl(url,true,false)? Please use expandToUrl() instead. + // Are you calling KDirLister::openUrl(url,Keep)? Please use expandToUrl() instead. if (!result) { qCWarning(category) << "Items emitted in directory" << directoryUrl << "but that directory isn't in KDirModel!" << "Root directory:" << urlForNode(m_rootNode); for (const KFileItem &item : items) { qDebug() << "Item:" << item.url(); } #ifndef NDEBUG dump(); #endif Q_ASSERT(result); } Q_ASSERT(isDir(result)); KDirModelDirNode *dirNode = static_cast(result); const QModelIndex index = indexForNode(dirNode); // O(n) const int newItemsCount = items.count(); const int newRowCount = dirNode->m_childNodes.count() + newItemsCount; qCDebug(category) << items.count() << "in" << directoryUrl << "index=" << debugIndex(index) << "newRowCount=" << newRowCount; q->beginInsertRows(index, newRowCount - newItemsCount, newRowCount - 1); // parent, first, last const QList urlsBeingFetched = m_urlsBeingFetched.value(dirNode); if (!urlsBeingFetched.isEmpty()) { qCDebug(category) << "urlsBeingFetched for dir" << dirNode << directoryUrl << ":" << urlsBeingFetched; } QList emitExpandFor; dirNode->m_childNodes.reserve(newRowCount); KFileItemList::const_iterator it = items.begin(); KFileItemList::const_iterator end = items.end(); for (; it != end; ++it) { const bool isDir = it->isDir(); KDirModelNode *node = isDir ? new KDirModelDirNode(dirNode, *it) : new KDirModelNode(dirNode, *it); #ifndef NDEBUG // Test code for possible duplication of items in the childnodes list, // not sure if/how it ever happened. //if (dirNode->m_childNodes.count() && // dirNode->m_childNodes.last()->item().name() == (*it).name()) { // qCWarning(category) << "Already having" << (*it).name() << "in" << directoryUrl // << "url=" << dirNode->m_childNodes.last()->item().url(); // abort(); //} #endif dirNode->m_childNodes.append(node); const QUrl url = it->url(); m_nodeHash.insert(cleanupUrl(url), node); if (!urlsBeingFetched.isEmpty()) { const QUrl &dirUrl = url; for (const QUrl &urlFetched : qAsConst(urlsBeingFetched)) { if (dirUrl.matches(urlFetched, QUrl::StripTrailingSlash) || dirUrl.isParentOf(urlFetched)) { //qDebug() << "Listing found" << dirUrl.url() << "which is a parent of fetched url" << urlFetched; const QModelIndex parentIndex = indexForNode(node, dirNode->m_childNodes.count() - 1); Q_ASSERT(parentIndex.isValid()); emitExpandFor.append(parentIndex); if (isDir && dirUrl != urlFetched) { q->fetchMore(parentIndex); m_urlsBeingFetched[node].append(urlFetched); } } } } } q->endInsertRows(); // Emit expand signal after rowsInserted signal has been emitted, // so that any proxy model will have updated its mapping already for (const QModelIndex &idx : qAsConst(emitExpandFor)) { emit q->expand(idx); } } void KDirModelPrivate::_k_slotCompleted(const QUrl &directoryUrl) { KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth) Q_ASSERT(isDir(result)); KDirModelDirNode *dirNode = static_cast(result); m_urlsBeingFetched.remove(dirNode); } void KDirModelPrivate::_k_slotDeleteItems(const KFileItemList &items) { qCDebug(category) << items.count() << "items"; // I assume all items are from the same directory. // From KDirLister's code, this should be the case, except maybe emitChanges? const KFileItem item = items.first(); Q_ASSERT(!item.isNull()); QUrl url = item.url(); KDirModelNode *node = nodeForUrl(url); // O(depth) if (!node) { qCWarning(category) << "No node found for item that was just removed:" << url; return; } KDirModelDirNode *dirNode = node->parent(); if (!dirNode) { return; } QModelIndex parentIndex = indexForNode(dirNode); // O(n) // Short path for deleting a single item if (items.count() == 1) { const int r = node->rowNumber(); q->beginRemoveRows(parentIndex, r, r); removeFromNodeHash(node, url); delete dirNode->m_childNodes.takeAt(r); q->endRemoveRows(); return; } // We need to make lists of consecutive row numbers, for the beginRemoveRows call. // Let's use a bit array where each bit represents a given child node. const int childCount = dirNode->m_childNodes.count(); QBitArray rowNumbers(childCount, false); for (const KFileItem &item : items) { if (!node) { // don't lookup the first item twice url = item.url(); node = nodeForUrl(url); if (!node) { qCWarning(category) << "No node found for item that was just removed:" << url; continue; } if (!node->parent()) { // The root node has been deleted, but it was not first in the list 'items'. // see https://bugs.kde.org/show_bug.cgi?id=196695 return; } } rowNumbers.setBit(node->rowNumber(), 1); // O(n) removeFromNodeHash(node, url); node = nullptr; } int start = -1; int end = -1; bool lastVal = false; // Start from the end, otherwise all the row numbers are offset while we go for (int i = childCount - 1; i >= 0; --i) { const bool val = rowNumbers.testBit(i); if (!lastVal && val) { end = i; //qDebug() << "end=" << end; } if ((lastVal && !val) || (i == 0 && val)) { start = val ? i : i + 1; //qDebug() << "beginRemoveRows" << start << end; q->beginRemoveRows(parentIndex, start, end); for (int r = end; r >= start; --r) { // reverse because takeAt changes indexes ;) //qDebug() << "Removing from m_childNodes at" << r; delete dirNode->m_childNodes.takeAt(r); } q->endRemoveRows(); } lastVal = val; } } void KDirModelPrivate::_k_slotRefreshItems(const QList > &items) { QModelIndex topLeft, bottomRight; // Solution 1: we could emit dataChanged for one row (if items.size()==1) or all rows // Solution 2: more fine-grained, actually figure out the beginning and end rows. for (QList >::const_iterator fit = items.begin(), fend = items.end(); fit != fend; ++fit) { Q_ASSERT(!fit->first.isNull()); Q_ASSERT(!fit->second.isNull()); const QUrl oldUrl = fit->first.url(); const QUrl newUrl = fit->second.url(); KDirModelNode *node = nodeForUrl(oldUrl); // O(n); maybe we could look up to the parent only once //qDebug() << "in model for" << m_dirLister->url() << ":" << oldUrl << "->" << newUrl << "node=" << node; if (!node) { // not found [can happen when renaming a dir, redirection was emitted already] continue; } if (node != m_rootNode) { // we never set an item in the rootnode, we use m_dirLister->rootItem instead. bool hasNewNode = false; // A file became directory (well, it was overwritten) if (fit->first.isDir() != fit->second.isDir()) { //qDebug() << "DIR/FILE STATUS CHANGE"; const int r = node->rowNumber(); removeFromNodeHash(node, oldUrl); KDirModelDirNode *dirNode = node->parent(); delete dirNode->m_childNodes.takeAt(r); // i.e. "delete node" node = fit->second.isDir() ? new KDirModelDirNode(dirNode, fit->second) : new KDirModelNode(dirNode, fit->second); dirNode->m_childNodes.insert(r, node); // same position! hasNewNode = true; } else { node->setItem(fit->second); } if (oldUrl != newUrl || hasNewNode) { // What if a renamed dir had children? -> kdirlister takes care of emitting for each item //qDebug() << "Renaming" << oldUrl << "to" << newUrl << "in node hash"; m_nodeHash.remove(cleanupUrl(oldUrl)); m_nodeHash.insert(cleanupUrl(newUrl), node); } // Mimetype changed -> forget cached icon (e.g. from "cut", #164185 comment #13) if (fit->first.determineMimeType().name() != fit->second.determineMimeType().name()) { node->setPreview(QIcon()); } const QModelIndex index = indexForNode(node); if (!topLeft.isValid() || index.row() < topLeft.row()) { topLeft = index; } if (!bottomRight.isValid() || index.row() > bottomRight.row()) { bottomRight = index; } } } //qDebug() << "dataChanged(" << debugIndex(topLeft) << " - " << debugIndex(bottomRight); bottomRight = bottomRight.sibling(bottomRight.row(), q->columnCount(QModelIndex()) - 1); emit q->dataChanged(topLeft, bottomRight); } // Called when a kioslave redirects (e.g. smb:/Workgroup -> smb://workgroup) // and when renaming a directory. void KDirModelPrivate::_k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl) { KDirModelNode *node = nodeForUrl(oldUrl); if (!node) { return; } m_nodeHash.remove(cleanupUrl(oldUrl)); m_nodeHash.insert(cleanupUrl(newUrl), node); // Ensure the node's URL is updated. In case of a listjob redirection // we won't get a refreshItem, and in case of renaming a directory // we'll get it too late (so the hash won't find the old url anymore). KFileItem item = node->item(); if (!item.isNull()) { // null if root item, #180156 item.setUrl(newUrl); node->setItem(item); } // The items inside the renamed directory have been handled before, // KDirLister took care of emitting refreshItem for each of them. } void KDirModelPrivate::_k_slotClear() { const int numRows = m_rootNode->m_childNodes.count(); if (numRows > 0) { q->beginRemoveRows(QModelIndex(), 0, numRows - 1); q->endRemoveRows(); } m_nodeHash.clear(); //emit layoutAboutToBeChanged(); clear(); //emit layoutChanged(); } void KDirModelPrivate::_k_slotJobUrlsChanged(const QStringList &urlList) { QStringList dirtyUrls; std::set_symmetric_difference(urlList.begin(), urlList.end(), m_allCurrentDestUrls.constBegin(), m_allCurrentDestUrls.constEnd(), std::back_inserter(dirtyUrls)); m_allCurrentDestUrls = urlList; for (const QString &dirtyUrl : qAsConst(dirtyUrls)) { if (KDirModelNode *node = nodeForUrl(QUrl(dirtyUrl))) { const QModelIndex idx = indexForNode(node); emit q->dataChanged(idx, idx, {KDirModel::HasJobRole}); } } } void KDirModelPrivate::clearAllPreviews(KDirModelDirNode *dirNode) { const int numRows = dirNode->m_childNodes.count(); if (numRows > 0) { KDirModelNode *lastNode = nullptr; for (KDirModelNode *node : qAsConst(dirNode->m_childNodes)) { node->setPreview(QIcon()); //node->setPreview(QIcon::fromTheme(node->item().iconName())); if (isDir(node)) { // recurse into child dirs clearAllPreviews(static_cast(node)); } lastNode = node; } emit q->dataChanged(indexForNode(dirNode->m_childNodes.at(0), 0), // O(1) indexForNode(lastNode, numRows - 1)); // O(1) } } void KDirModel::clearAllPreviews() { d->clearAllPreviews(d->m_rootNode); } void KDirModel::itemChanged(const QModelIndex &index) { // This method is really a itemMimeTypeChanged(), it's mostly called by KFilePreviewGenerator. // When the mimetype is determined, clear the old "preview" (could be // mimetype dependent like when cutting files, #164185) KDirModelNode *node = d->nodeForIndex(index); if (node) { node->setPreview(QIcon()); } qCDebug(category) << "dataChanged(" << debugIndex(index) << ")"; emit dataChanged(index, index); } int KDirModel::columnCount(const QModelIndex &) const { return ColumnCount; } QVariant KDirModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { KDirModelNode *node = static_cast(index.internalPointer()); const KFileItem &item(node->item()); switch (role) { case Qt::DisplayRole: switch (index.column()) { case Name: return item.text(); case Size: return KIO::convertSize(item.size()); // size formatted as QString case ModifiedTime: { QDateTime dt = item.time(KFileItem::ModificationTime); return dt.toString(Qt::SystemLocaleShortDate); } case Permissions: return item.permissionsString(); case Owner: return item.user(); case Group: return item.group(); case Type: return item.mimeComment(); } break; case Qt::EditRole: switch (index.column()) { case Name: return item.text(); } break; case Qt::DecorationRole: if (index.column() == Name) { if (!node->preview().isNull()) { //qDebug() << item->url() << " preview found"; return node->preview(); } Q_ASSERT(!item.isNull()); //qDebug() << item->url() << " overlays=" << item->overlays(); return KDE::icon(item.iconName(), item.overlays()); } break; case Qt::TextAlignmentRole: if (index.column() == Size) { // use a right alignment for L2R and R2L languages const Qt::Alignment alignment = Qt::AlignRight | Qt::AlignVCenter; return int(alignment); } break; case Qt::ToolTipRole: return item.text(); case FileItemRole: return QVariant::fromValue(item); case ChildCountRole: if (!item.isDir()) { return ChildCountUnknown; } else { KDirModelDirNode *dirNode = static_cast(node); int count = dirNode->childCount(); if (count == ChildCountUnknown && item.isReadable() && !dirNode->isSlow()) { const QString path = item.localPath(); if (!path.isEmpty()) { // slow // QDir dir(path); // count = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count(); #ifdef Q_OS_WIN QString s = path + QLatin1String("\\*.*"); s.replace(QLatin1Char('/'), QLatin1Char('\\')); count = 0; WIN32_FIND_DATA findData; HANDLE hFile = FindFirstFile((LPWSTR)s.utf16(), &findData); if (hFile != INVALID_HANDLE_VALUE) { do { if (!(findData.cFileName[0] == '.' && findData.cFileName[1] == '\0') && !(findData.cFileName[0] == '.' && findData.cFileName[1] == '.' && findData.cFileName[2] == '\0')) { ++count; } } while (FindNextFile(hFile, &findData) != 0); FindClose(hFile); } #else DIR *dir = QT_OPENDIR(QFile::encodeName(path).constData()); if (dir) { count = 0; QT_DIRENT *dirEntry = nullptr; while ((dirEntry = QT_READDIR(dir))) { if (dirEntry->d_name[0] == '.') { if (dirEntry->d_name[1] == '\0') { // skip "." continue; } if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { // skip ".." continue; } } ++count; } QT_CLOSEDIR(dir); } #endif //qDebug() << "child count for " << path << ":" << count; dirNode->setChildCount(count); } } return count; } case HasJobRole: if (d->m_jobTransfersVisible && d->m_allCurrentDestUrls.isEmpty() == false) { KDirModelNode *node = d->nodeForIndex(index); const QString url = node->item().url().toString(); //return whether or not there are job dest urls visible in the view, so the delegate knows which ones to paint. return QVariant(d->m_allCurrentDestUrls.contains(url)); } } } return QVariant(); } void KDirModel::sort(int column, Qt::SortOrder order) { // Not implemented - we should probably use QSortFilterProxyModel instead. QAbstractItemModel::sort(column, order); } bool KDirModel::setData(const QModelIndex &index, const QVariant &value, int role) { switch (role) { case Qt::EditRole: if (index.column() == Name && value.type() == QVariant::String) { Q_ASSERT(index.isValid()); KDirModelNode *node = static_cast(index.internalPointer()); const KFileItem &item = node->item(); const QString newName = value.toString(); if (newName.isEmpty() || newName == item.text() || (newName == QLatin1Char('.')) || (newName == QLatin1String(".."))) { return true; } QUrl newUrl = item.url().adjusted(QUrl::RemoveFilename); newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName)); KIO::Job *job = KIO::rename(item.url(), newUrl, item.url().isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags); job->uiDelegate()->setAutoErrorHandlingEnabled(true); // undo handling KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, QList() << item.url(), newUrl, job); return true; } break; case Qt::DecorationRole: if (index.column() == Name) { Q_ASSERT(index.isValid()); // Set new pixmap - e.g. preview KDirModelNode *node = static_cast(index.internalPointer()); //qDebug() << "setting icon for " << node->item()->url(); Q_ASSERT(node); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); node->setPreview(icon); } else if (value.type() == QVariant::Pixmap) { node->setPreview(qvariant_cast(value)); } emit dataChanged(index, index); return true; } break; default: break; } return false; } int KDirModel::rowCount(const QModelIndex &parent) const { KDirModelNode *node = d->nodeForIndex(parent); if (!node || !d->isDir(node)) { // #176555 return 0; } KDirModelDirNode *parentNode = static_cast(node); Q_ASSERT(parentNode); const int count = parentNode->m_childNodes.count(); #if 0 QStringList filenames; for (int i = 0; i < count; ++i) { filenames << d->urlForNode(parentNode->m_childNodes.at(i)).fileName(); } //qDebug() << "rowCount for " << d->urlForNode(parentNode) << ": " << count << filenames; #endif return count; } QModelIndex KDirModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDirModelNode *childNode = static_cast(index.internalPointer()); Q_ASSERT(childNode); KDirModelNode *parentNode = childNode->parent(); Q_ASSERT(parentNode); return d->indexForNode(parentNode); // O(n) } // Reimplemented to avoid the default implementation which calls parent // (O(n) for finding the parent's row number for nothing). This implementation is O(1). QModelIndex KDirModel::sibling(int row, int column, const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } KDirModelNode *oldChildNode = static_cast(index.internalPointer()); Q_ASSERT(oldChildNode); KDirModelNode *parentNode = oldChildNode->parent(); Q_ASSERT(parentNode); Q_ASSERT(d->isDir(parentNode)); KDirModelNode *childNode = static_cast(parentNode)->m_childNodes.value(row); // O(1) if (childNode) { return createIndex(row, column, childNode); } return QModelIndex(); } void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) { emit needSequenceIcon(index, sequenceIndex); } void KDirModel::setJobTransfersVisible(bool value) { if (value) { d->m_jobTransfersVisible = true; connect(&JobUrlCache::instance(), SIGNAL(jobUrlsChanged(QStringList)), this, SLOT(_k_slotJobUrlsChanged(QStringList)), Qt::UniqueConnection); JobUrlCache::instance().requestJobUrlsChanged(); } else { disconnect(this, SLOT(_k_slotJobUrlsChanged(QStringList))); } } bool KDirModel::jobTransfersVisible() const { return d->m_jobTransfersVisible; } QList KDirModel::simplifiedUrlList(const QList &urls) { if (urls.isEmpty()) { return urls; } QList ret(urls); std::sort(ret.begin(), ret.end()); QList::iterator it = ret.begin(); QUrl url = *it; ++it; while (it != ret.end()) { if (url.isParentOf(*it) || url == *it) { it = ret.erase(it); } else { url = *it; ++it; } } return ret; } QStringList KDirModel::mimeTypes() const { return KUrlMimeData::mimeDataTypes(); } QMimeData *KDirModel::mimeData(const QModelIndexList &indexes) const { QList urls, mostLocalUrls; urls.reserve(indexes.size()); mostLocalUrls.reserve(indexes.size()); bool canUseMostLocalUrls = true; for (const QModelIndex &index : indexes) { const KFileItem &item = d->nodeForIndex(index)->item(); urls << item.url(); bool isLocal; mostLocalUrls << item.mostLocalUrl(isLocal); if (!isLocal) { canUseMostLocalUrls = false; } } QMimeData *data = new QMimeData(); const bool different = canUseMostLocalUrls && (mostLocalUrls != urls); urls = simplifiedUrlList(urls); if (different) { mostLocalUrls = simplifiedUrlList(mostLocalUrls); KUrlMimeData::setUrls(urls, mostLocalUrls, data); } else { data->setUrls(urls); } return data; } // Public API; not much point in calling it internally KFileItem KDirModel::itemForIndex(const QModelIndex &index) const { if (!index.isValid()) { + if (d->m_showNodeForListedUrl) { + return {}; + } return d->m_dirLister->rootItem(); } else { return static_cast(index.internalPointer())->item(); } } QModelIndex KDirModel::indexForItem(const KFileItem *item) const { // Note that we can only use the URL here, not the pointer. // KFileItems can be copied. return indexForUrl(item->url()); // O(n) } QModelIndex KDirModel::indexForItem(const KFileItem &item) const { // Note that we can only use the URL here, not the pointer. // KFileItems can be copied. return indexForUrl(item.url()); // O(n) } // url -> index. O(n) QModelIndex KDirModel::indexForUrl(const QUrl &url) const { KDirModelNode *node = d->nodeForUrl(url); // O(depth) if (!node) { //qDebug() << url << "not found"; return QModelIndex(); } return d->indexForNode(node); // O(n) } QModelIndex KDirModel::index(int row, int column, const QModelIndex &parent) const { KDirModelNode *parentNode = d->nodeForIndex(parent); // O(1) Q_ASSERT(parentNode); if (d->isDir(parentNode)) { KDirModelNode *childNode = static_cast(parentNode)->m_childNodes.value(row); // O(1) if (childNode) { return createIndex(row, column, childNode); } } return QModelIndex(); } QVariant KDirModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case Qt::DisplayRole: switch (section) { case Name: return i18nc("@title:column", "Name"); case Size: return i18nc("@title:column", "Size"); case ModifiedTime: return i18nc("@title:column", "Date"); case Permissions: return i18nc("@title:column", "Permissions"); case Owner: return i18nc("@title:column", "Owner"); case Group: return i18nc("@title:column", "Group"); case Type: return i18nc("@title:column", "Type"); } } } return QVariant(); } bool KDirModel::hasChildren(const QModelIndex &parent) const { if (!parent.isValid()) { return true; } const KDirModelNode *parentNode = static_cast(parent.internalPointer()); const KFileItem &parentItem = parentNode->item(); Q_ASSERT(!parentItem.isNull()); if (!parentItem.isDir()) { return false; } if (static_cast(parentNode)->isPopulated()) { return !static_cast(parentNode)->m_childNodes.isEmpty(); } if (parentItem.isLocalFile()) { QDirIterator it(parentItem.localPath(), QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); return it.hasNext(); } // Remote and not listed yet, we can't know; let the user click on it so we'll find out return true; } Qt::ItemFlags KDirModel::flags(const QModelIndex &index) const { Qt::ItemFlags f = Qt::ItemIsEnabled; if (index.column() == Name) { f |= Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; } // Allow dropping onto this item? if (d->m_dropsAllowed != NoDrops) { if (!index.isValid()) { if (d->m_dropsAllowed & DropOnDirectory) { f |= Qt::ItemIsDropEnabled; } } else { KFileItem item = itemForIndex(index); if (item.isNull()) { qCWarning(category) << "Invalid item returned for index"; } else if (item.isDir()) { if (d->m_dropsAllowed & DropOnDirectory) { f |= Qt::ItemIsDropEnabled; } } else { // regular file item if (d->m_dropsAllowed & DropOnAnyFile) { f |= Qt::ItemIsDropEnabled; } else if (d->m_dropsAllowed & DropOnLocalExecutable) { if (!item.localPath().isEmpty()) { // Desktop file? if (item.determineMimeType().inherits(QStringLiteral("application/x-desktop"))) { f |= Qt::ItemIsDropEnabled; } // Executable, shell script ... ? else if (QFileInfo(item.localPath()).isExecutable()) { f |= Qt::ItemIsDropEnabled; } } } } } } return f; } bool KDirModel::canFetchMore(const QModelIndex &parent) const { if (!parent.isValid()) { return false; } // We now have a bool KDirModelNode::m_populated, // to avoid calling fetchMore more than once on empty dirs. // But this wastes memory, and how often does someone open and re-open an empty dir in a treeview? // Maybe we can ask KDirLister "have you listed already"? (to discuss with M. Brade) KDirModelNode *node = static_cast(parent.internalPointer()); const KFileItem &item = node->item(); return item.isDir() && !static_cast(node)->isPopulated() && static_cast(node)->m_childNodes.isEmpty(); } void KDirModel::fetchMore(const QModelIndex &parent) { if (!parent.isValid()) { return; } KDirModelNode *parentNode = static_cast(parent.internalPointer()); KFileItem parentItem = parentNode->item(); Q_ASSERT(!parentItem.isNull()); if (!parentItem.isDir()) { return; } KDirModelDirNode *dirNode = static_cast(parentNode); if (dirNode->isPopulated()) { return; } dirNode->setPopulated(true); const QUrl parentUrl = parentItem.url(); d->m_dirLister->openUrl(parentUrl, KDirLister::Keep); } bool KDirModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { // Not sure we want to implement any drop handling at this level, // but for sure the default QAbstractItemModel implementation makes no sense for a dir model. Q_UNUSED(data); Q_UNUSED(action); Q_UNUSED(row); Q_UNUSED(column); Q_UNUSED(parent); return false; } void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed) { d->m_dropsAllowed = dropsAllowed; } void KDirModel::expandToUrl(const QUrl &url) { // emit expand for each parent and return last parent KDirModelNode *result = d->expandAllParentsUntil(url); // O(depth) if (!result) { // doesn't seem related to our base url? qCDebug(category) << url << "does not seem related to our base URL, aborting"; return; } if (!result->item().isNull() && result->item().url() == url) { // We have it already, nothing to do qCDebug(category) << "we have it already:" << url; return; } d->m_urlsBeingFetched[result].append(url); if (result == d->m_rootNode) { qCDebug(category) << "Remembering to emit expand after listing the root url"; // the root is fetched by default, so it must be currently being fetched return; } qCDebug(category) << "Remembering to emit expand after listing" << result->item().url(); // start a new fetch to look for the next level down the URL const QModelIndex parentIndex = d->indexForNode(result); // O(n) Q_ASSERT(parentIndex.isValid()); fetchMore(parentIndex); } bool KDirModel::insertRows(int, int, const QModelIndex &) { return false; } bool KDirModel::insertColumns(int, int, const QModelIndex &) { return false; } bool KDirModel::removeRows(int, int, const QModelIndex &) { return false; } bool KDirModel::removeColumns(int, int, const QModelIndex &) { return false; } #include "moc_kdirmodel.cpp" diff --git a/src/widgets/kdirmodel.h b/src/widgets/kdirmodel.h index 72849f15..c56e1842 100644 --- a/src/widgets/kdirmodel.h +++ b/src/widgets/kdirmodel.h @@ -1,282 +1,307 @@ /* This file is part of the KDE project Copyright (C) 2006 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDIRMODEL_H #define KDIRMODEL_H #include #include "kiowidgets_export.h" #include class KDirLister; class KDirModelPrivate; class JobUrlCache; /** * @class KDirModel kdirmodel.h * * @short A model for a KIO-based directory tree. * * KDirModel implements the QAbstractItemModel interface (for use with Qt's model/view widgets) * around the directory listing for one directory or a tree of directories. * * Note that there are some cases when using QPersistentModelIndexes from this model will not give * expected results. QPersistentIndexes will remain valid and updated if its siblings are added or * removed. However, if the QPersistentIndex or one of its ancestors is moved, the QPersistentIndex will become * invalid. For example, if a file or directory is renamed after storing a QPersistentModelIndex for it, * the index (along with any stored children) will become invalid even though it is still in the model. The reason * for this is that moves of files and directories are treated as separate insert and remove actions. * * @see KDirSortFilterProxyModel * * @author David Faure * Based on work by Hamish Rodda and Pascal Letourneau */ class KIOWIDGETS_EXPORT KDirModel : public QAbstractItemModel { Q_OBJECT public: /** * @param parent parent qobject */ explicit KDirModel(QObject *parent = nullptr); ~KDirModel(); + /** + * Flags for the openUrl() method + * @since 5.69 + */ + enum OpenUrlFlag { + NoFlags = 0x0, ///< No additional flags specified. + Reload = 0x1, ///< Indicates whether to use the cache or to reread + ///< the directory from the disk. + ///< Use only when opening a dir not yet listed by our dirLister() + ///< without using the cache. Otherwise use dirLister()->updateDirectory(). + ShowRoot = 0x2, ///< Display a root node for the URL being opened. + }; + Q_DECLARE_FLAGS(OpenUrlFlags, OpenUrlFlag) + + /** + * Display the contents of @p url in the model. + * Apart from the support for the ShowRoot flag, this is equivalent to dirLister()->openUrl(url, flags) + * @param url the URL of the directory whose contents should be listed. + * Unless ShowRoot is set, the item for this directory will NOT be shown, the model starts at its children. + * @param flags see OpenUrlFlag + * @since 5.69 + */ + void openUrl(const QUrl &url, OpenUrlFlags flags = NoFlags); + /** * Set the directory lister to use by this model, instead of the default KDirLister created internally. * The model takes ownership. */ void setDirLister(KDirLister *dirLister); /** * Return the directory lister used by this model. */ KDirLister *dirLister() const; /** * Return the fileitem for a given index. This is O(1), i.e. fast. */ KFileItem itemForIndex(const QModelIndex &index) const; #if KIOWIDGETS_ENABLE_DEPRECATED_SINCE(4, 0) /** * Return the index for a given kfileitem. This can be slow. * @deprecated Since 4.0, use the method that takes a KFileItem by value */ KIOWIDGETS_DEPRECATED_VERSION(4, 0, "Use KDirModel::indexForItem(const KFileItem &)") QModelIndex indexForItem(const KFileItem *) const; #endif /** * Return the index for a given kfileitem. This can be slow. */ QModelIndex indexForItem(const KFileItem &) const; /** * Return the index for a given url. This can be slow. */ QModelIndex indexForUrl(const QUrl &url) const; /** * @short Lists subdirectories using fetchMore() as needed until the given @p url exists in the model. * * When the model is used by a treeview, call KDirLister::openUrl with the base url of the tree, * then the treeview will take care of calling fetchMore() when the user opens directories. * However if you want the tree to show a given URL (i.e. open the tree recursively until that URL), * call expandToUrl(). * Note that this is asynchronous; the necessary listing of subdirectories will take time so * the model will not immediately have this url available. * The model emits the signal expand() when an index has become available; this can be connected * to the treeview in order to let it open that index. * @param url the url of a subdirectory of the directory model (or a file in a subdirectory) */ void expandToUrl(const QUrl &url); /** * Notify the model that the item at this index has changed. * For instance because KMimeTypeResolver called determineMimeType on it. * This makes the model emit its dataChanged signal at this index, so that views repaint. * Note that for most things (renaming, changing size etc.), KDirLister's signals tell the model already. */ void itemChanged(const QModelIndex &index); /** * Forget all previews (optimization for turning previews off). * The items will again have their default appearance (not controlled by the model). * @since 5.28 */ void clearAllPreviews(); /** * Useful "default" columns. Views can use a proxy to have more control over this. */ enum ModelColumns { Name = 0, Size, ModifiedTime, Permissions, Owner, Group, Type, ColumnCount }; /// Possible return value for data(ChildCountRole), meaning the item isn't a directory, /// or we haven't calculated its child count yet enum { ChildCountUnknown = -1 }; enum AdditionalRoles { // Note: use printf "0x%08X\n" $(($RANDOM*$RANDOM)) // to define additional roles. FileItemRole = 0x07A263FF, ///< returns the KFileItem for a given index ChildCountRole = 0x2C4D0A40, ///< returns the number of items in a directory, or ChildCountUnknown HasJobRole = 0x01E555A5 ///< returns whether or not there is a job on an item (file/directory) }; enum DropsAllowedFlag { NoDrops = 0, DropOnDirectory = 1, ///< allow drops on any directory DropOnAnyFile = 2, ///< allow drops on any file DropOnLocalExecutable = 4 ///< allow drops on local executables, shell scripts and desktop files. Can be used with DropOnDirectory. }; Q_DECLARE_FLAGS(DropsAllowed, DropsAllowedFlag) /// Set whether dropping onto items should be allowed, and for which kind of item /// Drops are disabled by default. void setDropsAllowed(DropsAllowed dropsAllowed); /// Reimplemented from QAbstractItemModel. Returns true for empty directories. bool canFetchMore(const QModelIndex &parent) const override; /// Reimplemented from QAbstractItemModel. Returns ColumnCount. int columnCount(const QModelIndex &parent = QModelIndex()) const override; /// Reimplemented from QAbstractItemModel. QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; /// Reimplemented from QAbstractItemModel. Not implemented yet. bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; /// Reimplemented from QAbstractItemModel. Lists the subdirectory. void fetchMore(const QModelIndex &parent) override; /// Reimplemented from QAbstractItemModel. Qt::ItemFlags flags(const QModelIndex &index) const override; /// Reimplemented from QAbstractItemModel. Returns true for directories. bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; /// Reimplemented from QAbstractItemModel. Returns the column titles. QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; /// Reimplemented from QAbstractItemModel. O(1) QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; /// Reimplemented from QAbstractItemModel. QMimeData *mimeData(const QModelIndexList &indexes) const override; /// Reimplemented from QAbstractItemModel. QStringList mimeTypes() const override; /// Reimplemented from QAbstractItemModel. QModelIndex parent(const QModelIndex &index) const override; /// Reimplemented from QAbstractItemModel. QModelIndex sibling(int row, int column, const QModelIndex &index) const override; /// Reimplemented from QAbstractItemModel. int rowCount(const QModelIndex &parent = QModelIndex()) const override; /// Reimplemented from QAbstractItemModel. /// Call this to set a new icon, e.g. a preview bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; /// Reimplemented from QAbstractItemModel. Not implemented. @see KDirSortFilterProxyModel void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; /** * Remove urls from the list if an ancestor is present on the list. This can * be used to delete only the ancestor url and skip a potential error of a non-existent url. * * For example, for a list of "/home/foo/a", "/home/foo/a/a.txt", "/home/foo/a/a/a.txt", "/home/foo/a/b/b.txt", * "home/foo/b/b.txt", this method will return the list "/home/foo/a", "/home/foo/b/b.txt". * * @return the list @p urls without parented urls inside. * @since 4.2 */ static QList simplifiedUrlList(const QList &urls); /** * This emits the needSequenceIcon signal, requesting another sequence icon * * If there is a KFilePreviewGenerator attached to this model, that generator will care * about creating another preview. * * @param index Index of the item that should get another icon * @param sequenceIndex Index in the sequence. If it is zero, the standard icon will be assigned. * For higher indices, arbitrary different meaningful icons will be generated. * @since 4.3 */ void requestSequenceIcon(const QModelIndex &index, int sequenceIndex); /** * Enable/Disable the displaying of an animated overlay that is shown for any destination * urls (in the view). When enabled, the animations (if any) will be drawn automatically. * * Only the files/folders that are visible and have jobs associated with them * will display the animation. * You would likely not want this enabled if you perform some kind of custom painting * that takes up a whole item, and will just make this(and what you paint) look funky. * * Default is disabled. * * Note: KFileItemDelegate needs to have it's method called with the same * value, when you make the call to this method. * * @since 4.5 */ void setJobTransfersVisible(bool value); /** * Returns whether or not displaying job transfers has been enabled. * @since 4.5 */ bool jobTransfersVisible() const; Qt::DropActions supportedDropActions() const override; Q_SIGNALS: /** * Emitted for each subdirectory that is a parent of a url passed to expandToUrl * This allows to asynchronously open a tree view down to a given directory. * Also emitted for the final file, if expandToUrl is called with a file * (for instance so that it can be selected). */ void expand(const QModelIndex &index); /** * Emitted when another icon sequence index is requested * @param index Index of the item that should get another icon * @param sequenceIndex Index in the sequence. If it is zero, the standard icon should be assigned. * For higher indices, arbitrary different meaningful icons should be generated. * This is usually slowly counted up while the user hovers the icon. * If no meaningful alternative icons can be generated, this should be ignored. * @since 4.3 */ void needSequenceIcon(const QModelIndex &index, int sequenceIndex); private: // Make those private, they shouldn't be called by applications bool insertRows(int, int, const QModelIndex & = QModelIndex()) override; bool insertColumns(int, int, const QModelIndex & = QModelIndex()) override; bool removeRows(int, int, const QModelIndex & = QModelIndex()) override; bool removeColumns(int, int, const QModelIndex & = QModelIndex()) override; private: friend class KDirModelPrivate; KDirModelPrivate *const d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KDirModel::DropsAllowed) +Q_DECLARE_OPERATORS_FOR_FLAGS(KDirModel::OpenUrlFlags) #endif /* KDIRMODEL_H */ diff --git a/tests/kdirmodeltest_gui.cpp b/tests/kdirmodeltest_gui.cpp index ea542378..fb7c1160 100644 --- a/tests/kdirmodeltest_gui.cpp +++ b/tests/kdirmodeltest_gui.cpp @@ -1,108 +1,108 @@ /* * Copyright (C) 2006 David Faure * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include // Test controller for making the view open up while expandToUrl lists subdirs class TreeController : public QObject { Q_OBJECT public: explicit TreeController(QTreeView *view, KDirModel *model) : QObject(view), m_treeView(view), m_model(model) { connect(model, &KDirModel::expand, this, &TreeController::slotExpand); } private Q_SLOTS: void slotExpand(const QModelIndex &index) { KFileItem item = m_model->itemForIndex(index); qDebug() << "slotListingCompleted" << item.url(); m_treeView->setExpanded(index, true); // The scrollTo call doesn't seem to work. // We probably need to delay this until everything's listed and layouted... m_treeView->scrollTo(index); } private: QTreeView *m_treeView; KDirModel *m_model; }; int main(int argc, char **argv) { //options.add("+[directory ...]", qi18n("Directory(ies) to model")); QApplication a(argc, argv); KDirModel *dirmodel = new KDirModel(nullptr); dirmodel->dirLister()->setDelayedMimeTypes(true); #if 1 QTreeView *treeView = new QTreeView(nullptr); treeView->setModel(dirmodel); treeView->setUniformRowHeights(true); // makes visualRect() much faster treeView->resize(500, 500); treeView->show(); treeView->setItemDelegate(new KFileItemDelegate(treeView)); #endif #if 0 QListView *listView = new QListView(0); listView->setModel(dirmodel); listView->setUniformItemSizes(true); // true in list mode, not in icon mode. listView->show(); listView->setItemDelegate(new KFileItemDelegate(listView)); #endif #if 1 QListView *iconView = new QListView(nullptr); iconView->setModel(dirmodel); iconView->setSelectionMode(QListView::ExtendedSelection); iconView->setViewMode(QListView::IconMode); iconView->show(); iconView->setItemDelegate(new KFileItemDelegate(iconView)); #endif if (argc <= 1) { - dirmodel->dirLister()->openUrl(QUrl::fromLocalFile(QStringLiteral("/"))); + dirmodel->openUrl(QUrl(QStringLiteral("file:///")), KDirModel::ShowRoot); - const QUrl url = QUrl::fromLocalFile(QStringLiteral("/usr/share/applications/kde")); + const QUrl url = QUrl::fromLocalFile(QStringLiteral("/usr/share/applications")); dirmodel->expandToUrl(url); new TreeController(treeView, dirmodel); } const int count = QCoreApplication::arguments().count() - 1; for (int i = 0; i < count; i++) { QUrl u = QUrl::fromUserInput(QCoreApplication::arguments().at(i + 1)); qDebug() << "Adding: " << u; dirmodel->dirLister()->openUrl(u, KDirLister::Keep); } return a.exec(); } #include "kdirmodeltest_gui.moc"