diff --git a/autotests/kdirmodeltest.h b/autotests/kdirmodeltest.h --- a/autotests/kdirmodeltest.h +++ b/autotests/kdirmodeltest.h @@ -62,6 +62,9 @@ void testMimeData(); void testDotHiddenFile_data(); void testDotHiddenFile(); + void testShowRoot(); + void testShowRootWithTrailingSlash(); + 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,67 @@ 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); 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.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. @@ -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 @@ -203,6 +204,7 @@ { 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 @@ -212,6 +214,16 @@ 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(); @@ -225,7 +237,12 @@ * 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) @@ -237,12 +254,14 @@ #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; @@ -277,19 +296,26 @@ //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; } @@ -421,6 +447,35 @@ [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; @@ -438,7 +493,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!" @@ -1069,6 +1124,9 @@ 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(); diff --git a/tests/kdirmodeltest_gui.cpp b/tests/kdirmodeltest_gui.cpp --- a/tests/kdirmodeltest_gui.cpp +++ b/tests/kdirmodeltest_gui.cpp @@ -88,9 +88,9 @@ #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); }