File indexing completed on 2023-09-24 04:09:20

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