File indexing completed on 2024-04-14 03:53:32

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2006-2019 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kdirmodel.h"
0009 #include "kdirlister.h"
0010 #include "kfileitem.h"
0011 
0012 #include "joburlcache_p.h"
0013 #include <KIconUtils>
0014 #include <KJobUiDelegate>
0015 #include <KLocalizedString>
0016 #include <KUrlMimeData>
0017 #include <kio/fileundomanager.h>
0018 #include <kio/simplejob.h>
0019 #include <kio/statjob.h>
0020 
0021 #include <QBitArray>
0022 #include <QDebug>
0023 #include <QDir>
0024 #include <QDirIterator>
0025 #include <QFile>
0026 #include <QFileInfo>
0027 #include <QIcon>
0028 #include <QLocale>
0029 #include <QLoggingCategory>
0030 #include <QMimeData>
0031 #include <qplatformdefs.h>
0032 
0033 #include <algorithm>
0034 
0035 #ifdef Q_OS_WIN
0036 #include <qt_windows.h>
0037 #endif
0038 
0039 Q_LOGGING_CATEGORY(category, "kf.kio.widgets.kdirmodel", QtInfoMsg)
0040 
0041 class KDirModelNode;
0042 class KDirModelDirNode;
0043 
0044 static QUrl cleanupUrl(const QUrl &url)
0045 {
0046     QUrl u = url;
0047     u.setPath(QDir::cleanPath(u.path())); // remove double slashes in the path, simplify "foo/." to "foo/", etc.
0048     u = u.adjusted(QUrl::StripTrailingSlash); // KDirLister does this too, so we remove the slash before comparing with the root node url.
0049     if (u.scheme().startsWith(QStringLiteral("ksvn")) || u.scheme().startsWith(QStringLiteral("svn"))) {
0050         u.setQuery(QString());
0051         u.setFragment(QString());
0052     }
0053     return u;
0054 }
0055 
0056 // We create our own tree behind the scenes to have fast lookup from an item to its parent,
0057 // and also to get the children of an item fast.
0058 class KDirModelNode
0059 {
0060 public:
0061     KDirModelNode(KDirModelDirNode *parent, const KFileItem &item)
0062         : m_item(item)
0063         , m_parent(parent)
0064     {
0065     }
0066 
0067     virtual ~KDirModelNode() = default; // Required, code will delete ptrs to this or a subclass.
0068 
0069     // m_item is KFileItem() for the root item
0070     const KFileItem &item() const
0071     {
0072         return m_item;
0073     }
0074 
0075     virtual void setItem(const KFileItem &item)
0076     {
0077         m_item = item;
0078     }
0079 
0080     KDirModelDirNode *parent() const
0081     {
0082         return m_parent;
0083     }
0084 
0085     // linear search
0086     int rowNumber() const; // O(n)
0087 
0088     QIcon preview() const
0089     {
0090         return m_preview;
0091     }
0092 
0093     void setPreview(const QPixmap &pix)
0094     {
0095         m_preview = QIcon();
0096         m_preview.addPixmap(pix);
0097     }
0098 
0099     void setPreview(const QIcon &icn)
0100     {
0101         m_preview = icn;
0102     }
0103 
0104 private:
0105     KFileItem m_item;
0106     KDirModelDirNode *const m_parent;
0107     QIcon m_preview;
0108 };
0109 
0110 // Specialization for directory nodes
0111 class KDirModelDirNode : public KDirModelNode
0112 {
0113 public:
0114     KDirModelDirNode(KDirModelDirNode *parent, const KFileItem &item)
0115         : KDirModelNode(parent, item)
0116         , m_childCount(KDirModel::ChildCountUnknown)
0117         , m_populated(false)
0118         , m_fsType(FsTypeUnknown)
0119     {
0120         // If the parent node is on the network, all children are too. Opposite is not always true.
0121         if (parent && parent->isOnNetwork()) {
0122             m_fsType = NetworkFs;
0123         }
0124     }
0125     ~KDirModelDirNode() override
0126     {
0127         qDeleteAll(m_childNodes);
0128     }
0129     QList<KDirModelNode *> m_childNodes; // owns the nodes
0130 
0131     void setItem(const KFileItem &item) override
0132     {
0133         KDirModelNode::setItem(item);
0134         if (item.isNull() || !item.url().isValid()) {
0135             m_fsType = LocalFs;
0136         } else {
0137             m_fsType = FsTypeUnknown;
0138         }
0139     }
0140 
0141     // If we listed the directory, the child count is known. Otherwise it can be set via setChildCount.
0142     int childCount() const
0143     {
0144         return m_childNodes.isEmpty() ? m_childCount : m_childNodes.count();
0145     }
0146 
0147     void setChildCount(int count)
0148     {
0149         m_childCount = count;
0150     }
0151 
0152     bool isPopulated() const
0153     {
0154         return m_populated;
0155     }
0156 
0157     void setPopulated(bool populated)
0158     {
0159         m_populated = populated;
0160     }
0161 
0162     bool isOnNetwork() const
0163     {
0164         if (!item().isNull() && m_fsType == FsTypeUnknown) {
0165             m_fsType = item().isSlow() ? NetworkFs : LocalFs;
0166         }
0167         return m_fsType == NetworkFs;
0168     }
0169 
0170     // For removing all child urls from the global hash.
0171     QList<QUrl> collectAllChildUrls() const
0172     {
0173         QList<QUrl> urls;
0174         urls.reserve(urls.size() + m_childNodes.size());
0175         for (KDirModelNode *node : m_childNodes) {
0176             const KFileItem &item = node->item();
0177             urls.append(cleanupUrl(item.url()));
0178             if (item.isDir()) {
0179                 urls += static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
0180             }
0181         }
0182         return urls;
0183     }
0184 
0185 private:
0186     int m_childCount : 31;
0187     bool m_populated : 1;
0188     // Network file system? (nfs/smb/ssh)
0189     mutable enum { FsTypeUnknown, LocalFs, NetworkFs } m_fsType : 3;
0190 };
0191 
0192 int KDirModelNode::rowNumber() const
0193 {
0194     if (!m_parent) {
0195         return 0;
0196     }
0197     return m_parent->m_childNodes.indexOf(const_cast<KDirModelNode *>(this));
0198 }
0199 
0200 ////
0201 
0202 class KDirModelPrivate
0203 {
0204 public:
0205     explicit KDirModelPrivate(KDirModel *qq)
0206         : q(qq)
0207         , m_rootNode(new KDirModelDirNode(nullptr, KFileItem()))
0208     {
0209     }
0210     ~KDirModelPrivate()
0211     {
0212         delete m_rootNode;
0213     }
0214 
0215     void _k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &);
0216     void _k_slotCompleted(const QUrl &directoryUrl);
0217     void _k_slotDeleteItems(const KFileItemList &);
0218     void _k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &);
0219     void _k_slotClear();
0220     void _k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl);
0221     void _k_slotJobUrlsChanged(const QStringList &urlList);
0222 
0223     void clear()
0224     {
0225         delete m_rootNode;
0226         m_rootNode = new KDirModelDirNode(nullptr, KFileItem());
0227         m_showNodeForListedUrl = false;
0228         m_rootNode->setItem(KFileItem(m_dirLister->url()));
0229     }
0230 
0231     // Emit expand for each parent and then return the
0232     // last known parent if there is no node for this url
0233     KDirModelNode *expandAllParentsUntil(const QUrl &url) const;
0234 
0235     // Return the node for a given url, using the hash.
0236     KDirModelNode *nodeForUrl(const QUrl &url) const;
0237     KDirModelNode *nodeForIndex(const QModelIndex &index) const;
0238     QModelIndex indexForNode(KDirModelNode *node, int rowNumber = -1 /*unknown*/) const;
0239 
0240     static QUrl rootParentOf(const QUrl &url)
0241     {
0242         // <url> is what we listed, and which is visible at the root of the tree
0243         // Here we want the (invisible) parent of that url
0244         QUrl parent(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
0245         if (url.path() == QLatin1String("/")) {
0246             parent.setPath(QString());
0247         }
0248         return parent;
0249     }
0250 
0251     bool isDir(KDirModelNode *node) const
0252     {
0253         return (node == m_rootNode) || node->item().isDir();
0254     }
0255 
0256     QUrl urlForNode(KDirModelNode *node) const
0257     {
0258         /**
0259          * Queries and fragments are removed from the URL, so that the URL of
0260          * child items really starts with the URL of the parent.
0261          *
0262          * For instance ksvn+http://url?rev=100 is the parent for ksvn+http://url/file?rev=100
0263          * so we have to remove the query in both to be able to compare the URLs
0264          */
0265         QUrl url;
0266         if (node == m_rootNode && !m_showNodeForListedUrl) {
0267             url = m_dirLister->url();
0268         } else {
0269             url = node->item().url();
0270         }
0271         if (url.scheme().startsWith(QStringLiteral("ksvn")) || url.scheme().startsWith(QStringLiteral("svn"))) {
0272             if (url.hasQuery() || url.hasFragment()) { // avoid detach if not necessary.
0273                 url.setQuery(QString());
0274                 url.setFragment(QString()); // kill ref (#171117)
0275             }
0276         }
0277         return url;
0278     }
0279 
0280     void removeFromNodeHash(KDirModelNode *node, const QUrl &url);
0281     void clearAllPreviews(KDirModelDirNode *node);
0282 #ifndef NDEBUG
0283     void dump();
0284 #endif
0285     Q_DISABLE_COPY(KDirModelPrivate)
0286 
0287     KDirModel *const q;
0288     KDirLister *m_dirLister = nullptr;
0289     KDirModelDirNode *m_rootNode = nullptr;
0290     KDirModel::DropsAllowed m_dropsAllowed = KDirModel::NoDrops;
0291     bool m_jobTransfersVisible = false;
0292     bool m_showNodeForListedUrl = false;
0293     // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient),
0294     // value = final url[s] being fetched
0295     QMap<KDirModelNode *, QList<QUrl>> m_urlsBeingFetched;
0296     QHash<QUrl, KDirModelNode *> m_nodeHash; // global node hash: url -> node
0297     QStringList m_allCurrentDestUrls; // list of all dest urls that have jobs on them (e.g. copy, download)
0298 };
0299 
0300 KDirModelNode *KDirModelPrivate::nodeForUrl(const QUrl &_url) const // O(1), well, O(length of url as a string)
0301 {
0302     QUrl url = cleanupUrl(_url);
0303     if (url == urlForNode(m_rootNode)) {
0304         return m_rootNode;
0305     }
0306     return m_nodeHash.value(url);
0307 }
0308 
0309 void KDirModelPrivate::removeFromNodeHash(KDirModelNode *node, const QUrl &url)
0310 {
0311     if (node->item().isDir()) {
0312         const QList<QUrl> urls = static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
0313         for (const QUrl &u : urls) {
0314             m_nodeHash.remove(u);
0315         }
0316     }
0317     m_nodeHash.remove(cleanupUrl(url));
0318 }
0319 
0320 KDirModelNode *KDirModelPrivate::expandAllParentsUntil(const QUrl &_url) const // O(depth)
0321 {
0322     QUrl url = cleanupUrl(_url);
0323 
0324     // qDebug() << url;
0325     QUrl nodeUrl = urlForNode(m_rootNode);
0326     KDirModelDirNode *dirNode = m_rootNode;
0327     if (m_showNodeForListedUrl && !m_rootNode->m_childNodes.isEmpty()) {
0328         dirNode = static_cast<KDirModelDirNode *>(m_rootNode->m_childNodes.at(0)); // ### will be incorrect if we list drives on Windows
0329         nodeUrl = dirNode->item().url();
0330         qCDebug(category) << "listed URL is visible, adjusted starting point to" << nodeUrl;
0331     }
0332     if (url == nodeUrl) {
0333         return dirNode;
0334     }
0335 
0336     // Protocol mismatch? Don't even start comparing paths then. #171721
0337     if (url.scheme() != nodeUrl.scheme()) {
0338         qCWarning(category) << "protocol mismatch:" << url.scheme() << "vs" << nodeUrl.scheme();
0339         return nullptr;
0340     }
0341 
0342     const QString pathStr = url.path(); // no trailing slash
0343 
0344     if (!pathStr.startsWith(nodeUrl.path())) {
0345         qCDebug(category) << pathStr << "does not start with" << nodeUrl.path();
0346         return nullptr;
0347     }
0348 
0349     for (;;) {
0350         QString nodePath = nodeUrl.path();
0351         if (!nodePath.endsWith(QLatin1Char('/'))) {
0352             nodePath += QLatin1Char('/');
0353         }
0354         if (!pathStr.startsWith(nodePath)) {
0355             qCWarning(category) << "The KIO worker for" << url.scheme() << "violates the hierarchy structure:"
0356                                 << "I arrived at node" << nodePath << ", but" << pathStr << "does not start with that path.";
0357             return nullptr;
0358         }
0359 
0360         // E.g. pathStr is /a/b/c and nodePath is /a/. We want to find the node with url /a/b
0361         const int nextSlash = pathStr.indexOf(QLatin1Char('/'), nodePath.length());
0362         const QString newPath = pathStr.left(nextSlash); // works even if nextSlash==-1
0363         nodeUrl.setPath(newPath);
0364         nodeUrl = nodeUrl.adjusted(QUrl::StripTrailingSlash); // #172508
0365         KDirModelNode *node = nodeForUrl(nodeUrl);
0366         if (!node) {
0367             qCDebug(category) << nodeUrl << "not found, needs to be listed";
0368             // return last parent found:
0369             return dirNode;
0370         }
0371 
0372         Q_EMIT q->expand(indexForNode(node));
0373 
0374         // qDebug() << " nodeUrl=" << nodeUrl;
0375         if (nodeUrl == url) {
0376             qCDebug(category) << "Found node" << node << "for" << url;
0377             return node;
0378         }
0379         qCDebug(category) << "going into" << node->item().url();
0380         Q_ASSERT(isDir(node));
0381         dirNode = static_cast<KDirModelDirNode *>(node);
0382     }
0383     // NOTREACHED
0384     // return 0;
0385 }
0386 
0387 #ifndef NDEBUG
0388 void KDirModelPrivate::dump()
0389 {
0390     qCDebug(category) << "Dumping contents of KDirModel" << q << "dirLister url:" << m_dirLister->url();
0391     QHashIterator<QUrl, KDirModelNode *> it(m_nodeHash);
0392     while (it.hasNext()) {
0393         it.next();
0394         qCDebug(category) << it.key() << it.value();
0395     }
0396 }
0397 #endif
0398 
0399 // node -> index. If rowNumber is set (or node is root): O(1). Otherwise: O(n).
0400 QModelIndex KDirModelPrivate::indexForNode(KDirModelNode *node, int rowNumber) const
0401 {
0402     if (node == m_rootNode) {
0403         return QModelIndex();
0404     }
0405 
0406     Q_ASSERT(node->parent());
0407     return q->createIndex(rowNumber == -1 ? node->rowNumber() : rowNumber, 0, node);
0408 }
0409 
0410 // index -> node. O(1)
0411 KDirModelNode *KDirModelPrivate::nodeForIndex(const QModelIndex &index) const
0412 {
0413     return index.isValid() ? static_cast<KDirModelNode *>(index.internalPointer()) : m_rootNode;
0414 }
0415 
0416 /*
0417  * This model wraps the data held by KDirLister.
0418  *
0419  * The internal pointer of the QModelIndex for a given file is the node for that file in our own tree.
0420  * E.g. index(2,0) returns a QModelIndex with row=2 internalPointer=<KDirModelNode for the 3rd child of the root>
0421  *
0422  * Invalid parent index means root of the tree, m_rootNode
0423  */
0424 
0425 static QString debugIndex(const QModelIndex &index)
0426 {
0427     QString str;
0428     if (!index.isValid()) {
0429         str = QStringLiteral("[invalid index, i.e. root]");
0430     } else {
0431         KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
0432         str = QLatin1String("[index for ") + node->item().url().toString();
0433         if (index.column() > 0) {
0434             str += QLatin1String(", column ") + QString::number(index.column());
0435         }
0436         str += QLatin1Char(']');
0437     }
0438     return str;
0439 }
0440 
0441 KDirModel::KDirModel(QObject *parent)
0442     : QAbstractItemModel(parent)
0443     , d(new KDirModelPrivate(this))
0444 {
0445     setDirLister(new KDirLister(this));
0446 }
0447 
0448 KDirModel::~KDirModel() = default;
0449 
0450 void KDirModel::setDirLister(KDirLister *dirLister)
0451 {
0452     if (d->m_dirLister) {
0453         d->clear();
0454         delete d->m_dirLister;
0455     }
0456     d->m_dirLister = dirLister;
0457     d->m_dirLister->setParent(this);
0458     connect(d->m_dirLister, &KCoreDirLister::itemsAdded, this, [this](const QUrl &dirUrl, const KFileItemList &items) {
0459         d->_k_slotNewItems(dirUrl, items);
0460     });
0461     connect(d->m_dirLister, &KCoreDirLister::listingDirCompleted, this, [this](const QUrl &dirUrl) {
0462         d->_k_slotCompleted(dirUrl);
0463     });
0464     connect(d->m_dirLister, &KCoreDirLister::itemsDeleted, this, [this](const KFileItemList &items) {
0465         d->_k_slotDeleteItems(items);
0466     });
0467     connect(d->m_dirLister, &KCoreDirLister::refreshItems, this, [this](const QList<QPair<KFileItem, KFileItem>> &items) {
0468         d->_k_slotRefreshItems(items);
0469     });
0470     connect(d->m_dirLister, qOverload<>(&KCoreDirLister::clear), this, [this]() {
0471         d->_k_slotClear();
0472     });
0473     connect(d->m_dirLister, &KCoreDirLister::redirection, this, [this](const QUrl &oldUrl, const QUrl &newUrl) {
0474         d->_k_slotRedirection(oldUrl, newUrl);
0475     });
0476 }
0477 
0478 void KDirModel::openUrl(const QUrl &inputUrl, OpenUrlFlags flags)
0479 {
0480     Q_ASSERT(d->m_dirLister);
0481     const QUrl url = cleanupUrl(inputUrl);
0482     if (flags & ShowRoot) {
0483         d->_k_slotClear();
0484         d->m_showNodeForListedUrl = true;
0485         // Store the parent URL into the invisible root node
0486         const QUrl parentUrl = d->rootParentOf(url);
0487         d->m_rootNode->setItem(KFileItem(parentUrl));
0488         // Stat the requested url, to create the visible node
0489         KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
0490         connect(statJob, &KJob::result, this, [statJob, parentUrl, url, this]() {
0491             if (!statJob->error()) {
0492                 const KIO::UDSEntry entry = statJob->statResult();
0493                 KFileItem visibleRootItem(entry, url);
0494                 visibleRootItem.setName(url.path() == QLatin1String("/") ? QStringLiteral("/") : url.fileName());
0495                 d->_k_slotNewItems(parentUrl, QList<KFileItem>{visibleRootItem});
0496                 Q_ASSERT(d->m_rootNode->m_childNodes.count() == 1);
0497                 expandToUrl(url);
0498             } else {
0499                 qWarning() << statJob->errorString();
0500             }
0501         });
0502     } else {
0503         d->m_dirLister->openUrl(url, (flags & Reload) ? KDirLister::Reload : KDirLister::NoFlags);
0504     }
0505 }
0506 
0507 Qt::DropActions KDirModel::supportedDropActions() const
0508 {
0509     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction;
0510 }
0511 
0512 KDirLister *KDirModel::dirLister() const
0513 {
0514     return d->m_dirLister;
0515 }
0516 
0517 void KDirModelPrivate::_k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &items)
0518 {
0519     // qDebug() << "directoryUrl=" << directoryUrl;
0520 
0521     KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth)
0522     // If the directory containing the items wasn't found, then we have a big problem.
0523     // Are you calling KDirLister::openUrl(url,Keep)? Please use expandToUrl() instead.
0524     if (!result) {
0525         qCWarning(category) << "Items emitted in directory" << directoryUrl << "but that directory isn't in KDirModel!"
0526                             << "Root directory:" << urlForNode(m_rootNode);
0527         for (const KFileItem &item : items) {
0528             qDebug() << "Item:" << item.url();
0529         }
0530 #ifndef NDEBUG
0531         dump();
0532 #endif
0533         Q_ASSERT(result);
0534         return;
0535     }
0536     Q_ASSERT(isDir(result));
0537     KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
0538 
0539     const QModelIndex index = indexForNode(dirNode); // O(n)
0540     const int newItemsCount = items.count();
0541     const int newRowCount = dirNode->m_childNodes.count() + newItemsCount;
0542 
0543     qCDebug(category) << items.count() << "in" << directoryUrl << "index=" << debugIndex(index) << "newRowCount=" << newRowCount;
0544 
0545     q->beginInsertRows(index, newRowCount - newItemsCount, newRowCount - 1); // parent, first, last
0546 
0547     const QList<QUrl> urlsBeingFetched = m_urlsBeingFetched.value(dirNode);
0548     if (!urlsBeingFetched.isEmpty()) {
0549         qCDebug(category) << "urlsBeingFetched for dir" << dirNode << directoryUrl << ":" << urlsBeingFetched;
0550     }
0551 
0552     QList<QModelIndex> emitExpandFor;
0553 
0554     dirNode->m_childNodes.reserve(newRowCount);
0555     for (const auto &item : items) {
0556         const bool isDir = item.isDir();
0557         KDirModelNode *node = isDir ? new KDirModelDirNode(dirNode, item) : new KDirModelNode(dirNode, item);
0558 #ifndef NDEBUG
0559         // Test code for possible duplication of items in the childnodes list,
0560         // not sure if/how it ever happened.
0561         // if (dirNode->m_childNodes.count() &&
0562         //    dirNode->m_childNodes.last()->item().name() == item.name()) {
0563         //    qCWarning(category) << "Already having" << item.name() << "in" << directoryUrl
0564         //             << "url=" << dirNode->m_childNodes.last()->item().url();
0565         //    abort();
0566         //}
0567 #endif
0568         dirNode->m_childNodes.append(node);
0569         const QUrl url = item.url();
0570         m_nodeHash.insert(cleanupUrl(url), node);
0571 
0572         if (!urlsBeingFetched.isEmpty()) {
0573             const QUrl &dirUrl = url;
0574             for (const QUrl &urlFetched : std::as_const(urlsBeingFetched)) {
0575                 if (dirUrl.matches(urlFetched, QUrl::StripTrailingSlash) || dirUrl.isParentOf(urlFetched)) {
0576                     // qDebug() << "Listing found" << dirUrl.url() << "which is a parent of fetched url" << urlFetched;
0577                     const QModelIndex parentIndex = indexForNode(node, dirNode->m_childNodes.count() - 1);
0578                     Q_ASSERT(parentIndex.isValid());
0579                     emitExpandFor.append(parentIndex);
0580                     if (isDir && dirUrl != urlFetched) {
0581                         q->fetchMore(parentIndex);
0582                         m_urlsBeingFetched[node].append(urlFetched);
0583                     }
0584                 }
0585             }
0586         }
0587     }
0588 
0589     q->endInsertRows();
0590 
0591     // Emit expand signal after rowsInserted signal has been emitted,
0592     // so that any proxy model will have updated its mapping already
0593     for (const QModelIndex &idx : std::as_const(emitExpandFor)) {
0594         Q_EMIT q->expand(idx);
0595     }
0596 }
0597 
0598 void KDirModelPrivate::_k_slotCompleted(const QUrl &directoryUrl)
0599 {
0600     KDirModelNode *result = nodeForUrl(directoryUrl); // O(depth)
0601     Q_ASSERT(isDir(result));
0602     KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
0603     m_urlsBeingFetched.remove(dirNode);
0604 }
0605 
0606 void KDirModelPrivate::_k_slotDeleteItems(const KFileItemList &items)
0607 {
0608     qCDebug(category) << items.count() << "items";
0609 
0610     // I assume all items are from the same directory.
0611     // From KDirLister's code, this should be the case, except maybe emitChanges?
0612     const KFileItem item = items.first();
0613     Q_ASSERT(!item.isNull());
0614     QUrl url = item.url();
0615     KDirModelNode *node = nodeForUrl(url); // O(depth)
0616     if (!node) {
0617         qCWarning(category) << "No node found for item that was just removed:" << url;
0618         return;
0619     }
0620 
0621     KDirModelDirNode *dirNode = node->parent();
0622     if (!dirNode) {
0623         return;
0624     }
0625 
0626     QModelIndex parentIndex = indexForNode(dirNode); // O(n)
0627 
0628     // Short path for deleting a single item
0629     if (items.count() == 1) {
0630         const int r = node->rowNumber();
0631         q->beginRemoveRows(parentIndex, r, r);
0632         removeFromNodeHash(node, url);
0633         delete dirNode->m_childNodes.takeAt(r);
0634         q->endRemoveRows();
0635         return;
0636     }
0637 
0638     // We need to make lists of consecutive row numbers, for the beginRemoveRows call.
0639     // Let's use a bit array where each bit represents a given child node.
0640     const int childCount = dirNode->m_childNodes.count();
0641     QBitArray rowNumbers(childCount, false);
0642     for (const KFileItem &item : items) {
0643         if (!node) { // don't lookup the first item twice
0644             url = item.url();
0645             node = nodeForUrl(url);
0646             if (!node) {
0647                 qCWarning(category) << "No node found for item that was just removed:" << url;
0648                 continue;
0649             }
0650             if (!node->parent()) {
0651                 // The root node has been deleted, but it was not first in the list 'items'.
0652                 // see https://bugs.kde.org/show_bug.cgi?id=196695
0653                 return;
0654             }
0655         }
0656         rowNumbers.setBit(node->rowNumber(), 1); // O(n)
0657         removeFromNodeHash(node, url);
0658         node = nullptr;
0659     }
0660 
0661     int start = -1;
0662     int end = -1;
0663     bool lastVal = false;
0664     // Start from the end, otherwise all the row numbers are offset while we go
0665     for (int i = childCount - 1; i >= 0; --i) {
0666         const bool val = rowNumbers.testBit(i);
0667         if (!lastVal && val) {
0668             end = i;
0669             // qDebug() << "end=" << end;
0670         }
0671         if ((lastVal && !val) || (i == 0 && val)) {
0672             start = val ? i : i + 1;
0673             // qDebug() << "beginRemoveRows" << start << end;
0674             q->beginRemoveRows(parentIndex, start, end);
0675             for (int r = end; r >= start; --r) { // reverse because takeAt changes indexes ;)
0676                 // qDebug() << "Removing from m_childNodes at" << r;
0677                 delete dirNode->m_childNodes.takeAt(r);
0678             }
0679             q->endRemoveRows();
0680         }
0681         lastVal = val;
0682     }
0683 }
0684 
0685 void KDirModelPrivate::_k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items)
0686 {
0687     QModelIndex topLeft;
0688     QModelIndex bottomRight;
0689 
0690     // Solution 1: we could emit dataChanged for one row (if items.size()==1) or all rows
0691     // Solution 2: more fine-grained, actually figure out the beginning and end rows.
0692     for (const auto &[oldItem, newItem] : items) {
0693         Q_ASSERT(!oldItem.isNull());
0694         Q_ASSERT(!newItem.isNull());
0695         const QUrl oldUrl = oldItem.url();
0696         const QUrl newUrl = newItem.url();
0697         KDirModelNode *node = nodeForUrl(oldUrl); // O(n); maybe we could look up to the parent only once
0698         // qDebug() << "in model for" << m_dirLister->url() << ":" << oldUrl << "->" << newUrl << "node=" << node;
0699         if (!node) { // not found [can happen when renaming a dir, redirection was emitted already]
0700             continue;
0701         }
0702         if (node != m_rootNode) { // we never set an item in the rootnode, we use m_dirLister->rootItem instead.
0703             bool hasNewNode = false;
0704             // A file became directory (well, it was overwritten)
0705             if (oldItem.isDir() != newItem.isDir()) {
0706                 // qDebug() << "DIR/FILE STATUS CHANGE";
0707                 const int r = node->rowNumber();
0708                 removeFromNodeHash(node, oldUrl);
0709                 KDirModelDirNode *dirNode = node->parent();
0710                 delete dirNode->m_childNodes.takeAt(r); // i.e. "delete node"
0711                 node = newItem.isDir() ? new KDirModelDirNode(dirNode, newItem) : new KDirModelNode(dirNode, newItem);
0712                 dirNode->m_childNodes.insert(r, node); // same position!
0713                 hasNewNode = true;
0714             } else {
0715                 node->setItem(newItem);
0716             }
0717 
0718             if (oldUrl != newUrl || hasNewNode) {
0719                 // What if a renamed dir had children? -> kdirlister takes care of emitting for each item
0720                 // qDebug() << "Renaming" << oldUrl << "to" << newUrl << "in node hash";
0721                 m_nodeHash.remove(cleanupUrl(oldUrl));
0722                 m_nodeHash.insert(cleanupUrl(newUrl), node);
0723             }
0724             // MIME type changed -> forget cached icon (e.g. from "cut", #164185 comment #13)
0725             if (oldItem.determineMimeType().name() != newItem.determineMimeType().name()) {
0726                 node->setPreview(QIcon());
0727             }
0728 
0729             const QModelIndex index = indexForNode(node);
0730             if (!topLeft.isValid() || index.row() < topLeft.row()) {
0731                 topLeft = index;
0732             }
0733             if (!bottomRight.isValid() || index.row() > bottomRight.row()) {
0734                 bottomRight = index;
0735             }
0736         }
0737     }
0738     // qDebug() << "dataChanged(" << debugIndex(topLeft) << " - " << debugIndex(bottomRight);
0739     bottomRight = bottomRight.sibling(bottomRight.row(), q->columnCount(QModelIndex()) - 1);
0740     Q_EMIT q->dataChanged(topLeft, bottomRight);
0741 }
0742 
0743 // Called when a KIO worker redirects (e.g. smb:/Workgroup -> smb://workgroup)
0744 // and when renaming a directory.
0745 void KDirModelPrivate::_k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl)
0746 {
0747     KDirModelNode *node = nodeForUrl(oldUrl);
0748     if (!node) {
0749         return;
0750     }
0751     m_nodeHash.remove(cleanupUrl(oldUrl));
0752     m_nodeHash.insert(cleanupUrl(newUrl), node);
0753 
0754     // Ensure the node's URL is updated. In case of a listjob redirection
0755     // we won't get a refreshItem, and in case of renaming a directory
0756     // we'll get it too late (so the hash won't find the old url anymore).
0757     KFileItem item = node->item();
0758     if (!item.isNull()) { // null if root item, #180156
0759         item.setUrl(newUrl);
0760         node->setItem(item);
0761     }
0762 
0763     // The items inside the renamed directory have been handled before,
0764     // KDirLister took care of emitting refreshItem for each of them.
0765 }
0766 
0767 void KDirModelPrivate::_k_slotClear()
0768 {
0769     const int numRows = m_rootNode->m_childNodes.count();
0770     if (numRows > 0) {
0771         q->beginRemoveRows(QModelIndex(), 0, numRows - 1);
0772     }
0773     m_nodeHash.clear();
0774     clear();
0775     if (numRows > 0) {
0776         q->endRemoveRows();
0777     }
0778 }
0779 
0780 void KDirModelPrivate::_k_slotJobUrlsChanged(const QStringList &urlList)
0781 {
0782     QStringList dirtyUrls;
0783 
0784     std::set_symmetric_difference(urlList.begin(),
0785                                   urlList.end(),
0786                                   m_allCurrentDestUrls.constBegin(),
0787                                   m_allCurrentDestUrls.constEnd(),
0788                                   std::back_inserter(dirtyUrls));
0789 
0790     m_allCurrentDestUrls = urlList;
0791 
0792     for (const QString &dirtyUrl : std::as_const(dirtyUrls)) {
0793         if (KDirModelNode *node = nodeForUrl(QUrl(dirtyUrl))) {
0794             const QModelIndex idx = indexForNode(node);
0795             Q_EMIT q->dataChanged(idx, idx, {KDirModel::HasJobRole});
0796         }
0797     }
0798 }
0799 
0800 void KDirModelPrivate::clearAllPreviews(KDirModelDirNode *dirNode)
0801 {
0802     const int numRows = dirNode->m_childNodes.count();
0803     if (numRows > 0) {
0804         KDirModelNode *lastNode = nullptr;
0805         for (KDirModelNode *node : std::as_const(dirNode->m_childNodes)) {
0806             node->setPreview(QIcon());
0807             // node->setPreview(QIcon::fromTheme(node->item().iconName()));
0808             if (isDir(node)) {
0809                 // recurse into child dirs
0810                 clearAllPreviews(static_cast<KDirModelDirNode *>(node));
0811             }
0812             lastNode = node;
0813         }
0814         Q_EMIT q->dataChanged(indexForNode(dirNode->m_childNodes.at(0), 0), // O(1)
0815                               indexForNode(lastNode, numRows - 1)); // O(1)
0816     }
0817 }
0818 
0819 void KDirModel::clearAllPreviews()
0820 {
0821     d->clearAllPreviews(d->m_rootNode);
0822 }
0823 
0824 void KDirModel::itemChanged(const QModelIndex &index)
0825 {
0826     // This method is really a itemMimeTypeChanged(), it's mostly called by KFilePreviewGenerator.
0827     // When the MIME type is determined, clear the old "preview" (could be
0828     // MIME type dependent like when cutting files, #164185)
0829     KDirModelNode *node = d->nodeForIndex(index);
0830     if (node) {
0831         node->setPreview(QIcon());
0832     }
0833 
0834     qCDebug(category) << "dataChanged(" << debugIndex(index) << ")";
0835     Q_EMIT dataChanged(index, index);
0836 }
0837 
0838 int KDirModel::columnCount(const QModelIndex &) const
0839 {
0840     return ColumnCount;
0841 }
0842 
0843 QVariant KDirModel::data(const QModelIndex &index, int role) const
0844 {
0845     if (index.isValid()) {
0846         KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
0847         const KFileItem &item(node->item());
0848         switch (role) {
0849         case Qt::DisplayRole:
0850             switch (index.column()) {
0851             case Name:
0852                 return item.text();
0853             case Size:
0854                 return KIO::convertSize(item.size()); // size formatted as QString
0855             case ModifiedTime: {
0856                 QDateTime dt = item.time(KFileItem::ModificationTime);
0857                 return QLocale().toString(dt, QLocale::ShortFormat);
0858             }
0859             case Permissions:
0860                 return item.permissionsString();
0861             case Owner:
0862                 return item.user();
0863             case Group:
0864                 return item.group();
0865             case Type:
0866                 return item.mimeComment();
0867             }
0868             break;
0869         case Qt::EditRole:
0870             switch (index.column()) {
0871             case Name:
0872                 return item.text();
0873             }
0874             break;
0875         case Qt::DecorationRole:
0876             if (index.column() == Name) {
0877                 if (!node->preview().isNull()) {
0878                     // qDebug() << item->url() << " preview found";
0879                     return node->preview();
0880                 }
0881                 Q_ASSERT(!item.isNull());
0882                 // qDebug() << item->url() << " overlays=" << item->overlays();
0883                 static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown"));
0884 
0885                 const QString iconName(item.iconName());
0886                 QIcon icon;
0887 
0888                 if (QDir::isAbsolutePath(iconName)) {
0889                     icon = QIcon(iconName);
0890                 }
0891                 if (icon.isNull()
0892                     || (!(iconName.endsWith(QLatin1String(".svg")) || iconName.endsWith(QLatin1String(".svgz"))) && icon.availableSizes().isEmpty())) {
0893                     icon = QIcon::fromTheme(iconName, fallbackIcon);
0894                 }
0895 
0896                 const auto parentNode = node->parent();
0897                 if (parentNode->isOnNetwork()) {
0898                     return icon;
0899                 } else {
0900                     return KIconUtils::addOverlays(icon, item.overlays());
0901                 }
0902             }
0903             break;
0904         case Qt::TextAlignmentRole:
0905             if (index.column() == Size) {
0906                 // use a right alignment for L2R and R2L languages
0907                 const Qt::Alignment alignment = Qt::AlignRight | Qt::AlignVCenter;
0908                 return int(alignment);
0909             }
0910             break;
0911         case Qt::ToolTipRole:
0912             return item.text();
0913         case FileItemRole:
0914             return QVariant::fromValue(item);
0915         case ChildCountRole:
0916             if (!item.isDir()) {
0917                 return ChildCountUnknown;
0918             } else {
0919                 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(node);
0920                 int count = dirNode->childCount();
0921                 if (count == ChildCountUnknown && !dirNode->isOnNetwork() && item.isReadable()) {
0922                     const QString path = item.localPath();
0923                     if (!path.isEmpty()) {
0924 //                        slow
0925 //                        QDir dir(path);
0926 //                        count = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count();
0927 #ifdef Q_OS_WIN
0928                         QString s = path + QLatin1String("\\*.*");
0929                         s.replace(QLatin1Char('/'), QLatin1Char('\\'));
0930                         count = 0;
0931                         WIN32_FIND_DATA findData;
0932                         HANDLE hFile = FindFirstFile((LPWSTR)s.utf16(), &findData);
0933                         if (hFile != INVALID_HANDLE_VALUE) {
0934                             do {
0935                                 if (!(findData.cFileName[0] == '.' && findData.cFileName[1] == '\0')
0936                                     && !(findData.cFileName[0] == '.' && findData.cFileName[1] == '.' && findData.cFileName[2] == '\0')) {
0937                                     ++count;
0938                                 }
0939                             } while (FindNextFile(hFile, &findData) != 0);
0940                             FindClose(hFile);
0941                         }
0942 #else
0943                         DIR *dir = QT_OPENDIR(QFile::encodeName(path).constData());
0944                         if (dir) {
0945                             count = 0;
0946                             QT_DIRENT *dirEntry = nullptr;
0947                             while ((dirEntry = QT_READDIR(dir))) {
0948                                 if (dirEntry->d_name[0] == '.') {
0949                                     if (dirEntry->d_name[1] == '\0') { // skip "."
0950                                         continue;
0951                                     }
0952                                     if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { // skip ".."
0953                                         continue;
0954                                     }
0955                                 }
0956                                 ++count;
0957                             }
0958                             QT_CLOSEDIR(dir);
0959                         }
0960 #endif
0961                         // qDebug() << "child count for " << path << ":" << count;
0962                         dirNode->setChildCount(count);
0963                     }
0964                 }
0965                 return count;
0966             }
0967         case HasJobRole:
0968             if (d->m_jobTransfersVisible && d->m_allCurrentDestUrls.isEmpty() == false) {
0969                 KDirModelNode *node = d->nodeForIndex(index);
0970                 const QString url = node->item().url().toString();
0971                 // return whether or not there are job dest urls visible in the view, so the delegate knows which ones to paint.
0972                 return QVariant(d->m_allCurrentDestUrls.contains(url));
0973             }
0974         }
0975     }
0976     return QVariant();
0977 }
0978 
0979 void KDirModel::sort(int column, Qt::SortOrder order)
0980 {
0981     // Not implemented - we should probably use QSortFilterProxyModel instead.
0982     QAbstractItemModel::sort(column, order);
0983 }
0984 
0985 bool KDirModel::setData(const QModelIndex &index, const QVariant &value, int role)
0986 {
0987     switch (role) {
0988     case Qt::EditRole:
0989         if (index.column() == Name && value.typeId() == QMetaType::QString) {
0990             Q_ASSERT(index.isValid());
0991             KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
0992             const KFileItem &item = node->item();
0993             const QString newName = value.toString();
0994             if (newName.isEmpty() || newName == item.text() || (newName == QLatin1Char('.')) || (newName == QLatin1String(".."))) {
0995                 return true;
0996             }
0997             QUrl newUrl = item.url().adjusted(QUrl::RemoveFilename);
0998             newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName));
0999             KIO::Job *job = KIO::rename(item.url(), newUrl, item.url().isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
1000             job->uiDelegate()->setAutoErrorHandlingEnabled(true);
1001             // undo handling
1002             KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, QList<QUrl>() << item.url(), newUrl, job);
1003             return true;
1004         }
1005         break;
1006     case Qt::DecorationRole:
1007         if (index.column() == Name) {
1008             Q_ASSERT(index.isValid());
1009             // Set new pixmap - e.g. preview
1010             KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
1011             // qDebug() << "setting icon for " << node->item()->url();
1012             Q_ASSERT(node);
1013             if (value.typeId() == QMetaType::QIcon) {
1014                 const QIcon icon(qvariant_cast<QIcon>(value));
1015                 node->setPreview(icon);
1016             } else if (value.typeId() == QMetaType::QPixmap) {
1017                 node->setPreview(qvariant_cast<QPixmap>(value));
1018             }
1019             Q_EMIT dataChanged(index, index);
1020             return true;
1021         }
1022         break;
1023     default:
1024         break;
1025     }
1026     return false;
1027 }
1028 
1029 int KDirModel::rowCount(const QModelIndex &parent) const
1030 {
1031     if (parent.column() > 0) { // for QAbstractItemModelTester
1032         return 0;
1033     }
1034     KDirModelNode *node = d->nodeForIndex(parent);
1035     if (!node || !d->isDir(node)) { // #176555
1036         return 0;
1037     }
1038 
1039     KDirModelDirNode *parentNode = static_cast<KDirModelDirNode *>(node);
1040     Q_ASSERT(parentNode);
1041     const int count = parentNode->m_childNodes.count();
1042 #if 0
1043     QStringList filenames;
1044     for (int i = 0; i < count; ++i) {
1045         filenames << d->urlForNode(parentNode->m_childNodes.at(i)).fileName();
1046     }
1047     qDebug() << "rowCount for " << d->urlForNode(parentNode) << ": " << count << filenames;
1048 #endif
1049     return count;
1050 }
1051 
1052 QModelIndex KDirModel::parent(const QModelIndex &index) const
1053 {
1054     if (!index.isValid()) {
1055         return QModelIndex();
1056     }
1057     KDirModelNode *childNode = static_cast<KDirModelNode *>(index.internalPointer());
1058     Q_ASSERT(childNode);
1059     KDirModelNode *parentNode = childNode->parent();
1060     Q_ASSERT(parentNode);
1061     return d->indexForNode(parentNode); // O(n)
1062 }
1063 
1064 // Reimplemented to avoid the default implementation which calls parent
1065 // (O(n) for finding the parent's row number for nothing). This implementation is O(1).
1066 QModelIndex KDirModel::sibling(int row, int column, const QModelIndex &index) const
1067 {
1068     if (!index.isValid()) {
1069         return QModelIndex();
1070     }
1071     KDirModelNode *oldChildNode = static_cast<KDirModelNode *>(index.internalPointer());
1072     Q_ASSERT(oldChildNode);
1073     KDirModelNode *parentNode = oldChildNode->parent();
1074     Q_ASSERT(parentNode);
1075     Q_ASSERT(d->isDir(parentNode));
1076     KDirModelNode *childNode = static_cast<KDirModelDirNode *>(parentNode)->m_childNodes.value(row); // O(1)
1077     if (childNode) {
1078         return createIndex(row, column, childNode);
1079     }
1080     return QModelIndex();
1081 }
1082 
1083 void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex)
1084 {
1085     Q_EMIT needSequenceIcon(index, sequenceIndex);
1086 }
1087 
1088 void KDirModel::setJobTransfersVisible(bool show)
1089 {
1090     if (d->m_jobTransfersVisible == show) {
1091         return;
1092     }
1093 
1094     d->m_jobTransfersVisible = show;
1095     if (show) {
1096         connect(&JobUrlCache::instance(), &JobUrlCache::jobUrlsChanged, this, [this](const QStringList &urlList) {
1097             d->_k_slotJobUrlsChanged(urlList);
1098         });
1099 
1100         JobUrlCache::instance().requestJobUrlsChanged();
1101     } else {
1102         disconnect(&JobUrlCache::instance(), &JobUrlCache::jobUrlsChanged, this, nullptr);
1103     }
1104 }
1105 
1106 bool KDirModel::jobTransfersVisible() const
1107 {
1108     return d->m_jobTransfersVisible;
1109 }
1110 
1111 QList<QUrl> KDirModel::simplifiedUrlList(const QList<QUrl> &urls)
1112 {
1113     if (urls.isEmpty()) {
1114         return urls;
1115     }
1116 
1117     QList<QUrl> ret(urls);
1118     std::sort(ret.begin(), ret.end());
1119 
1120     QUrl url;
1121 
1122     auto filterFunc = [&url](const QUrl &u) {
1123         if (url == u || url.isParentOf(u)) {
1124             return true;
1125         } else {
1126             url = u;
1127             return false;
1128         }
1129     };
1130 
1131     auto beginIt = ret.begin();
1132     url = *beginIt;
1133     ++beginIt;
1134     auto it = std::remove_if(beginIt, ret.end(), filterFunc);
1135     ret.erase(it, ret.end());
1136 
1137     return ret;
1138 }
1139 
1140 QStringList KDirModel::mimeTypes() const
1141 {
1142     return KUrlMimeData::mimeDataTypes();
1143 }
1144 
1145 QMimeData *KDirModel::mimeData(const QModelIndexList &indexes) const
1146 {
1147     QList<QUrl> urls;
1148     QList<QUrl> mostLocalUrls;
1149     urls.reserve(indexes.size());
1150     mostLocalUrls.reserve(indexes.size());
1151     bool canUseMostLocalUrls = true;
1152     for (const QModelIndex &index : indexes) {
1153         const KFileItem &item = d->nodeForIndex(index)->item();
1154         urls.append(item.url());
1155         const auto [url, isLocal] = item.isMostLocalUrl();
1156         mostLocalUrls.append(url);
1157         if (!isLocal) {
1158             canUseMostLocalUrls = false;
1159         }
1160     }
1161     QMimeData *data = new QMimeData();
1162     const bool different = canUseMostLocalUrls && (mostLocalUrls != urls);
1163     urls = simplifiedUrlList(urls);
1164     if (different) {
1165         mostLocalUrls = simplifiedUrlList(mostLocalUrls);
1166         KUrlMimeData::setUrls(urls, mostLocalUrls, data);
1167     } else {
1168         data->setUrls(urls);
1169     }
1170 
1171     return data;
1172 }
1173 
1174 // Public API; not much point in calling it internally
1175 KFileItem KDirModel::itemForIndex(const QModelIndex &index) const
1176 {
1177     if (!index.isValid()) {
1178         if (d->m_showNodeForListedUrl) {
1179             return {};
1180         }
1181         return d->m_dirLister->rootItem();
1182     } else {
1183         return static_cast<KDirModelNode *>(index.internalPointer())->item();
1184     }
1185 }
1186 
1187 QModelIndex KDirModel::indexForItem(const KFileItem &item) const
1188 {
1189     return indexForUrl(item.url()); // O(n)
1190 }
1191 
1192 // url -> index. O(n)
1193 QModelIndex KDirModel::indexForUrl(const QUrl &url) const
1194 {
1195     KDirModelNode *node = d->nodeForUrl(url); // O(depth)
1196     if (!node) {
1197         // qDebug() << url << "not found";
1198         return QModelIndex();
1199     }
1200     return d->indexForNode(node); // O(n)
1201 }
1202 
1203 QModelIndex KDirModel::index(int row, int column, const QModelIndex &parent) const
1204 {
1205     KDirModelNode *parentNode = d->nodeForIndex(parent); // O(1)
1206     Q_ASSERT(parentNode);
1207     if (d->isDir(parentNode)) {
1208         KDirModelNode *childNode = static_cast<KDirModelDirNode *>(parentNode)->m_childNodes.value(row); // O(1)
1209         if (childNode) {
1210             return createIndex(row, column, childNode);
1211         }
1212     }
1213     return QModelIndex();
1214 }
1215 
1216 QVariant KDirModel::headerData(int section, Qt::Orientation orientation, int role) const
1217 {
1218     if (orientation == Qt::Horizontal) {
1219         switch (role) {
1220         case Qt::DisplayRole:
1221             switch (section) {
1222             case Name:
1223                 return i18nc("@title:column", "Name");
1224             case Size:
1225                 return i18nc("@title:column", "Size");
1226             case ModifiedTime:
1227                 return i18nc("@title:column", "Date");
1228             case Permissions:
1229                 return i18nc("@title:column", "Permissions");
1230             case Owner:
1231                 return i18nc("@title:column", "Owner");
1232             case Group:
1233                 return i18nc("@title:column", "Group");
1234             case Type:
1235                 return i18nc("@title:column", "Type");
1236             }
1237         }
1238     }
1239     return QVariant();
1240 }
1241 
1242 bool KDirModel::hasChildren(const QModelIndex &parent) const
1243 {
1244     if (!parent.isValid()) {
1245         return true;
1246     }
1247 
1248     const KDirModelNode *parentNode = static_cast<KDirModelNode *>(parent.internalPointer());
1249     const KFileItem &parentItem = parentNode->item();
1250     Q_ASSERT(!parentItem.isNull());
1251     if (!parentItem.isDir()) {
1252         return false;
1253     }
1254     if (static_cast<const KDirModelDirNode *>(parentNode)->isPopulated()) {
1255         return !static_cast<const KDirModelDirNode *>(parentNode)->m_childNodes.isEmpty();
1256     }
1257     if (parentItem.isLocalFile() && !static_cast<const KDirModelDirNode *>(parentNode)->isOnNetwork()) {
1258         QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
1259 
1260         if (d->m_dirLister->dirOnlyMode()) {
1261             filters |= QDir::NoSymLinks;
1262         } else {
1263             filters |= QDir::Files | QDir::System;
1264         }
1265 
1266         if (d->m_dirLister->showHiddenFiles()) {
1267             filters |= QDir::Hidden;
1268         }
1269 
1270         QDirIterator it(parentItem.localPath(), filters, QDirIterator::Subdirectories);
1271         return it.hasNext();
1272     }
1273     // Remote and not listed yet, we can't know; let the user click on it so we'll find out
1274     return true;
1275 }
1276 
1277 Qt::ItemFlags KDirModel::flags(const QModelIndex &index) const
1278 {
1279     Qt::ItemFlags f;
1280     if (index.isValid()) {
1281         f |= Qt::ItemIsEnabled;
1282         if (index.column() == Name) {
1283             f |= Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
1284         }
1285     }
1286 
1287     // Allow dropping onto this item?
1288     if (d->m_dropsAllowed != NoDrops) {
1289         if (!index.isValid()) {
1290             if (d->m_dropsAllowed & DropOnDirectory) {
1291                 f |= Qt::ItemIsDropEnabled;
1292             }
1293         } else {
1294             KFileItem item = itemForIndex(index);
1295             if (item.isNull()) {
1296                 qCWarning(category) << "Invalid item returned for index";
1297             } else if (item.isDir()) {
1298                 if (d->m_dropsAllowed & DropOnDirectory) {
1299                     f |= Qt::ItemIsDropEnabled;
1300                 }
1301             } else { // regular file item
1302                 if (d->m_dropsAllowed & DropOnAnyFile) {
1303                     f |= Qt::ItemIsDropEnabled;
1304                 } else if (d->m_dropsAllowed & DropOnLocalExecutable) {
1305                     if (!item.localPath().isEmpty()) {
1306                         // Desktop file?
1307                         if (item.determineMimeType().inherits(QStringLiteral("application/x-desktop"))) {
1308                             f |= Qt::ItemIsDropEnabled;
1309                         }
1310                         // Executable, shell script ... ?
1311                         else if (QFileInfo(item.localPath()).isExecutable()) {
1312                             f |= Qt::ItemIsDropEnabled;
1313                         }
1314                     }
1315                 }
1316             }
1317         }
1318     }
1319 
1320     return f;
1321 }
1322 
1323 bool KDirModel::canFetchMore(const QModelIndex &parent) const
1324 {
1325     if (!parent.isValid()) {
1326         return false;
1327     }
1328 
1329     // We now have a bool KDirModelNode::m_populated,
1330     // to avoid calling fetchMore more than once on empty dirs.
1331     // But this wastes memory, and how often does someone open and re-open an empty dir in a treeview?
1332     // Maybe we can ask KDirLister "have you listed <url> already"? (to discuss with M. Brade)
1333 
1334     KDirModelNode *node = static_cast<KDirModelNode *>(parent.internalPointer());
1335     const KFileItem &item = node->item();
1336     return item.isDir() && !static_cast<KDirModelDirNode *>(node)->isPopulated() && static_cast<KDirModelDirNode *>(node)->m_childNodes.isEmpty();
1337 }
1338 
1339 void KDirModel::fetchMore(const QModelIndex &parent)
1340 {
1341     if (!parent.isValid()) {
1342         return;
1343     }
1344 
1345     KDirModelNode *parentNode = static_cast<KDirModelNode *>(parent.internalPointer());
1346 
1347     KFileItem parentItem = parentNode->item();
1348     Q_ASSERT(!parentItem.isNull());
1349     if (!parentItem.isDir()) {
1350         return;
1351     }
1352     KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(parentNode);
1353     if (dirNode->isPopulated()) {
1354         return;
1355     }
1356     dirNode->setPopulated(true);
1357 
1358     const QUrl parentUrl = parentItem.url();
1359     d->m_dirLister->openUrl(parentUrl, KDirLister::Keep);
1360 }
1361 
1362 bool KDirModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1363 {
1364     // Not sure we want to implement any drop handling at this level,
1365     // but for sure the default QAbstractItemModel implementation makes no sense for a dir model.
1366     Q_UNUSED(data);
1367     Q_UNUSED(action);
1368     Q_UNUSED(row);
1369     Q_UNUSED(column);
1370     Q_UNUSED(parent);
1371     return false;
1372 }
1373 
1374 void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed)
1375 {
1376     d->m_dropsAllowed = dropsAllowed;
1377 }
1378 
1379 void KDirModel::expandToUrl(const QUrl &url)
1380 {
1381     // emit expand for each parent and return last parent
1382     KDirModelNode *result = d->expandAllParentsUntil(url); // O(depth)
1383 
1384     if (!result) { // doesn't seem related to our base url?
1385         qCDebug(category) << url << "does not seem related to our base URL, aborting";
1386         return;
1387     }
1388     if (!result->item().isNull() && result->item().url() == url) {
1389         // We have it already, nothing to do
1390         qCDebug(category) << "we have it already:" << url;
1391         return;
1392     }
1393 
1394     d->m_urlsBeingFetched[result].append(url);
1395 
1396     if (result == d->m_rootNode) {
1397         qCDebug(category) << "Remembering to emit expand after listing the root url";
1398         // the root is fetched by default, so it must be currently being fetched
1399         return;
1400     }
1401 
1402     qCDebug(category) << "Remembering to emit expand after listing" << result->item().url();
1403 
1404     // start a new fetch to look for the next level down the URL
1405     const QModelIndex parentIndex = d->indexForNode(result); // O(n)
1406     Q_ASSERT(parentIndex.isValid());
1407     fetchMore(parentIndex);
1408 }
1409 
1410 bool KDirModel::insertRows(int, int, const QModelIndex &)
1411 {
1412     return false;
1413 }
1414 
1415 bool KDirModel::insertColumns(int, int, const QModelIndex &)
1416 {
1417     return false;
1418 }
1419 
1420 bool KDirModel::removeRows(int, int, const QModelIndex &)
1421 {
1422     return false;
1423 }
1424 
1425 bool KDirModel::removeColumns(int, int, const QModelIndex &)
1426 {
1427     return false;
1428 }
1429 
1430 QHash<int, QByteArray> KDirModel::roleNames() const
1431 {
1432     auto super = QAbstractItemModel::roleNames();
1433 
1434     super[AdditionalRoles::FileItemRole] = "fileItem";
1435     super[AdditionalRoles::ChildCountRole] = "childCount";
1436     super[AdditionalRoles::HasJobRole] = "hasJob";
1437 
1438     return super;
1439 }
1440 
1441 #include "moc_kdirmodel.cpp"