diff --git a/autotests/kdirmodeltest.h b/autotests/kdirmodeltest.h --- a/autotests/kdirmodeltest.h +++ b/autotests/kdirmodeltest.h @@ -62,6 +62,8 @@ void testMimeData(); void testDotHiddenFile_data(); void testDotHiddenFile(); + void testShowRoot(); + void testShowRootAndExpandToUrl(); // These tests must be done last void testDeleteFile(); diff --git a/autotests/kdirmodeltest.cpp b/autotests/kdirmodeltest.cpp --- a/autotests/kdirmodeltest.cpp +++ b/autotests/kdirmodeltest.cpp @@ -131,7 +131,7 @@ 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(); @@ -1180,6 +1180,51 @@ dh.remove(); } +void KDirModelTest::testShowRoot() +{ + KDirModel dirModel; + const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath()); + + // openUrl(ShowRoot) should create a "/" item + dirModel.openUrl(homeUrl, 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).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 go back to normal mode of operation + QSignalSpy spyRowsRemoved(&dirModel, &QAbstractItemModel::rowsRemoved); + QSignalSpy spyDirListerCompleted(dirModel.dirLister(), QOverload<>::of(&KDirLister::completed)); + const QUrl tempUrl = QUrl::fromLocalFile(QDir::tempPath()); + dirModel.openUrl(tempUrl); + QVERIFY(spyDirListerCompleted.wait()); + QCOMPARE(spyRowsRemoved.count(), 1); + QVERIFY(!dirModel.indexForUrl(slashUrl).isValid()); + QCOMPARE(dirModel.itemForIndex(QModelIndex()).url(), tempUrl); +} + +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); diff --git a/src/widgets/kdirmodel.h b/src/widgets/kdirmodel.h --- a/src/widgets/kdirmodel.h +++ b/src/widgets/kdirmodel.h @@ -58,6 +58,30 @@ explicit KDirModel(QObject *parent = nullptr); ~KDirModel(); + /** + * Flags for the openUrl() method + * @since 5.65 + */ + 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 "file:///" node. + }; + 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.65 + */ + 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. @@ -278,5 +302,6 @@ }; Q_DECLARE_OPERATORS_FOR_FLAGS(KDirModel::DropsAllowed) +Q_DECLARE_OPERATORS_FOR_FLAGS(KDirModel::OpenUrlFlags) #endif /* KDIRMODEL_H */ diff --git a/src/widgets/kdirmodel.cpp b/src/widgets/kdirmodel.cpp --- a/src/widgets/kdirmodel.cpp +++ b/src/widgets/kdirmodel.cpp @@ -1,5 +1,5 @@ /* 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 @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "joburlcache_p.h" #include @@ -202,6 +203,7 @@ { delete m_rootNode; m_rootNode = new KDirModelDirNode(nullptr, KFileItem()); + m_fsRootVisible = false; } // Emit expand for each parent and then return the // last known parent if there is no node for this url @@ -224,7 +226,14 @@ * 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) { + if (m_fsRootVisible) + return QUrl(); + 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) @@ -242,6 +251,7 @@ KDirModelDirNode *m_rootNode; KDirModel::DropsAllowed m_dropsAllowed; bool m_jobTransfersVisible; + bool m_fsRootVisible = false; // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient), // value = final url[s] being fetched QMap > m_urlsBeingFetched; @@ -280,15 +290,23 @@ return m_rootNode; } + KDirModelDirNode *dirNode = m_rootNode; + if (nodeUrl.isEmpty() && m_fsRootVisible && !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(); // i.e. file:/// + qCDebug(category) << "FS root visible, adjusted starting point to" << nodeUrl; + } + // Protocol mismatch? Don't even start comparing paths then. #171721 - if (url.scheme() != nodeUrl.scheme()) { + if (url.scheme() != nodeUrl.scheme() && !(m_fsRootVisible && url.isLocalFile())) { + qCDebug(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; } @@ -420,6 +438,31 @@ [this](const QUrl &oldUrl, const QUrl &newUrl){d->_k_slotRedirection(oldUrl, newUrl);} ); } +void KDirModel::openUrl(const QUrl &url, OpenUrlFlags flags) +{ + Q_ASSERT(d->m_dirLister); + if (flags & ShowRoot) { + d->m_fsRootVisible = true; + // ### on Windows we might want to list drives? (but see expandAllParentsUntil) + const QUrl root(QStringLiteral("file:///")); + KIO::StatJob *statJob = KIO::stat(root, KIO::HideProgressInfo); + connect(statJob, &KJob::result, this, [statJob, root, url, this]() { + if (!statJob->error()) { + const KIO::UDSEntry entry = statJob->statResult(); + KFileItem rootItem(entry, root); + rootItem.setName(QStringLiteral("/")); + d->_k_slotNewItems(QUrl(), QList{rootItem}); + 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; @@ -437,7 +480,7 @@ 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!" @@ -1068,6 +1111,9 @@ KFileItem KDirModel::itemForIndex(const QModelIndex &index) const { if (!index.isValid()) { + if (d->m_fsRootVisible) { + return {}; + } return d->m_dirLister->rootItem(); } else { return static_cast(index.internalPointer())->item(); diff --git a/tests/kdirmodeltest_gui.cpp b/tests/kdirmodeltest_gui.cpp --- a/tests/kdirmodeltest_gui.cpp +++ b/tests/kdirmodeltest_gui.cpp @@ -88,7 +88,7 @@ #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")); dirmodel->expandToUrl(url);