1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2006-2019 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kdirmodel.h"
9#include "kdirlister.h"
10#include "kfileitem.h"
11
12#include "joburlcache_p.h"
13#include <KIconUtils>
14#include <KJobUiDelegate>
15#include <KLocalizedString>
16#include <KUrlMimeData>
17#include <kio/fileundomanager.h>
18#include <kio/simplejob.h>
19#include <kio/statjob.h>
20
21#include <QBitArray>
22#include <QDebug>
23#include <QDir>
24#include <QDirIterator>
25#include <QFile>
26#include <QFileInfo>
27#include <QIcon>
28#include <QLocale>
29#include <QLoggingCategory>
30#include <QMimeData>
31#include <qplatformdefs.h>
32
33#include <algorithm>
34
35#ifdef Q_OS_WIN
36#include <qt_windows.h>
37#endif
38
39Q_LOGGING_CATEGORY(category, "kf.kio.widgets.kdirmodel", QtInfoMsg)
40
41class KDirModelNode;
42class KDirModelDirNode;
43
44static QUrl cleanupUrl(const QUrl &url)
45{
46 QUrl u = url;
47 u.setPath(path: QDir::cleanPath(path: u.path())); // remove double slashes in the path, simplify "foo/." to "foo/", etc.
48 u = u.adjusted(options: QUrl::StripTrailingSlash); // KDirLister does this too, so we remove the slash before comparing with the root node url.
49 if (u.scheme().startsWith(QStringLiteral("ksvn")) || u.scheme().startsWith(QStringLiteral("svn"))) {
50 u.setQuery(query: QString());
51 u.setFragment(fragment: QString());
52 }
53 return u;
54}
55
56// We create our own tree behind the scenes to have fast lookup from an item to its parent,
57// and also to get the children of an item fast.
58class KDirModelNode
59{
60public:
61 KDirModelNode(KDirModelDirNode *parent, const KFileItem &item)
62 : m_item(item)
63 , m_parent(parent)
64 {
65 }
66
67 virtual ~KDirModelNode() = default; // Required, code will delete ptrs to this or a subclass.
68
69 // m_item is KFileItem() for the root item
70 const KFileItem &item() const
71 {
72 return m_item;
73 }
74
75 virtual void setItem(const KFileItem &item)
76 {
77 m_item = item;
78 }
79
80 KDirModelDirNode *parent() const
81 {
82 return m_parent;
83 }
84
85 // linear search
86 int rowNumber() const; // O(n)
87
88 QIcon preview() const
89 {
90 return m_preview;
91 }
92
93 void setPreview(const QPixmap &pix)
94 {
95 m_preview = QIcon();
96 m_preview.addPixmap(pixmap: pix);
97 }
98
99 void setPreview(const QIcon &icn)
100 {
101 m_preview = icn;
102 }
103
104private:
105 KFileItem m_item;
106 KDirModelDirNode *const m_parent;
107 QIcon m_preview;
108};
109
110// Specialization for directory nodes
111class KDirModelDirNode : public KDirModelNode
112{
113public:
114 KDirModelDirNode(KDirModelDirNode *parent, const KFileItem &item)
115 : KDirModelNode(parent, item)
116 , m_childCount(KDirModel::ChildCountUnknown)
117 , m_populated(false)
118 , m_fsType(FsTypeUnknown)
119 {
120 // If the parent node is on the network, all children are too. Opposite is not always true.
121 if (parent && parent->isOnNetwork()) {
122 m_fsType = NetworkFs;
123 }
124 }
125 ~KDirModelDirNode() override
126 {
127 qDeleteAll(c: m_childNodes);
128 }
129 QList<KDirModelNode *> m_childNodes; // owns the nodes
130
131 void setItem(const KFileItem &item) override
132 {
133 KDirModelNode::setItem(item);
134 if (item.isNull() || !item.url().isValid()) {
135 m_fsType = LocalFs;
136 } else {
137 m_fsType = FsTypeUnknown;
138 }
139 }
140
141 // If we listed the directory, the child count is known. Otherwise it can be set via setChildCount.
142 int childCount() const
143 {
144 return m_childNodes.isEmpty() ? m_childCount : m_childNodes.count();
145 }
146
147 void setChildCount(int count)
148 {
149 m_childCount = count;
150 }
151
152 bool isPopulated() const
153 {
154 return m_populated;
155 }
156
157 void setPopulated(bool populated)
158 {
159 m_populated = populated;
160 }
161
162 bool isOnNetwork() const
163 {
164 if (!item().isNull() && m_fsType == FsTypeUnknown) {
165 m_fsType = item().isSlow() ? NetworkFs : LocalFs;
166 }
167 return m_fsType == NetworkFs;
168 }
169
170 // For removing all child urls from the global hash.
171 QList<QUrl> collectAllChildUrls() const
172 {
173 QList<QUrl> urls;
174 urls.reserve(asize: urls.size() + m_childNodes.size());
175 for (KDirModelNode *node : m_childNodes) {
176 const KFileItem &item = node->item();
177 urls.append(t: cleanupUrl(url: item.url()));
178 if (item.isDir()) {
179 urls += static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
180 }
181 }
182 return urls;
183 }
184
185private:
186 int m_childCount : 31;
187 bool m_populated : 1;
188 // Network file system? (nfs/smb/ssh)
189 mutable enum { FsTypeUnknown, LocalFs, NetworkFs } m_fsType : 3;
190};
191
192int KDirModelNode::rowNumber() const
193{
194 if (!m_parent) {
195 return 0;
196 }
197 return m_parent->m_childNodes.indexOf(t: const_cast<KDirModelNode *>(this));
198}
199
200////
201
202class KDirModelPrivate
203{
204public:
205 explicit KDirModelPrivate(KDirModel *qq)
206 : q(qq)
207 , m_rootNode(new KDirModelDirNode(nullptr, KFileItem()))
208 {
209 }
210 ~KDirModelPrivate()
211 {
212 delete m_rootNode;
213 }
214
215 void _k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &);
216 void _k_slotCompleted(const QUrl &directoryUrl);
217 void _k_slotDeleteItems(const KFileItemList &);
218 void _k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &);
219 void _k_slotClear();
220 void _k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl);
221 void _k_slotJobUrlsChanged(const QStringList &urlList);
222
223 void clear()
224 {
225 delete m_rootNode;
226 m_rootNode = new KDirModelDirNode(nullptr, KFileItem());
227 m_showNodeForListedUrl = false;
228 m_rootNode->setItem(KFileItem(m_dirLister->url()));
229 }
230
231 // Emit expand for each parent and then return the
232 // last known parent if there is no node for this url
233 KDirModelNode *expandAllParentsUntil(const QUrl &url) const;
234
235 // Return the node for a given url, using the hash.
236 KDirModelNode *nodeForUrl(const QUrl &url) const;
237 KDirModelNode *nodeForIndex(const QModelIndex &index) const;
238 QModelIndex indexForNode(KDirModelNode *node, int rowNumber = -1 /*unknown*/) const;
239
240 static QUrl rootParentOf(const QUrl &url)
241 {
242 // <url> is what we listed, and which is visible at the root of the tree
243 // Here we want the (invisible) parent of that url
244 QUrl parent(url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
245 if (url.path() == QLatin1String("/")) {
246 parent.setPath(path: QString());
247 }
248 return parent;
249 }
250
251 bool isDir(KDirModelNode *node) const
252 {
253 return (node == m_rootNode) || node->item().isDir();
254 }
255
256 QUrl urlForNode(KDirModelNode *node) const
257 {
258 /**
259 * Queries and fragments are removed from the URL, so that the URL of
260 * child items really starts with the URL of the parent.
261 *
262 * For instance ksvn+http://url?rev=100 is the parent for ksvn+http://url/file?rev=100
263 * so we have to remove the query in both to be able to compare the URLs
264 */
265 QUrl url;
266 if (node == m_rootNode && !m_showNodeForListedUrl) {
267 url = m_dirLister->url();
268 } else {
269 url = node->item().url();
270 }
271 if (url.scheme().startsWith(QStringLiteral("ksvn")) || url.scheme().startsWith(QStringLiteral("svn"))) {
272 if (url.hasQuery() || url.hasFragment()) { // avoid detach if not necessary.
273 url.setQuery(query: QString());
274 url.setFragment(fragment: QString()); // kill ref (#171117)
275 }
276 }
277 return url;
278 }
279
280 void removeFromNodeHash(KDirModelNode *node, const QUrl &url);
281 void clearAllPreviews(KDirModelDirNode *node);
282#ifndef NDEBUG
283 void dump();
284#endif
285 Q_DISABLE_COPY(KDirModelPrivate)
286
287 KDirModel *const q;
288 KDirLister *m_dirLister = nullptr;
289 KDirModelDirNode *m_rootNode = nullptr;
290 KDirModel::DropsAllowed m_dropsAllowed = KDirModel::NoDrops;
291 bool m_jobTransfersVisible = false;
292 bool m_showNodeForListedUrl = false;
293 // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient),
294 // value = final url[s] being fetched
295 QMap<KDirModelNode *, QList<QUrl>> m_urlsBeingFetched;
296 QHash<QUrl, KDirModelNode *> m_nodeHash; // global node hash: url -> node
297 QStringList m_allCurrentDestUrls; // list of all dest urls that have jobs on them (e.g. copy, download)
298};
299
300KDirModelNode *KDirModelPrivate::nodeForUrl(const QUrl &_url) const // O(1), well, O(length of url as a string)
301{
302 QUrl url = cleanupUrl(url: _url);
303 if (url == urlForNode(node: m_rootNode)) {
304 return m_rootNode;
305 }
306 return m_nodeHash.value(key: url);
307}
308
309void KDirModelPrivate::removeFromNodeHash(KDirModelNode *node, const QUrl &url)
310{
311 if (node->item().isDir()) {
312 const QList<QUrl> urls = static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
313 for (const QUrl &u : urls) {
314 m_nodeHash.remove(key: u);
315 }
316 }
317 m_nodeHash.remove(key: cleanupUrl(url));
318}
319
320KDirModelNode *KDirModelPrivate::expandAllParentsUntil(const QUrl &_url) const // O(depth)
321{
322 QUrl url = cleanupUrl(url: _url);
323
324 // qDebug() << url;
325 QUrl nodeUrl = urlForNode(node: m_rootNode);
326 KDirModelDirNode *dirNode = m_rootNode;
327 if (m_showNodeForListedUrl && !m_rootNode->m_childNodes.isEmpty()) {
328 dirNode = static_cast<KDirModelDirNode *>(m_rootNode->m_childNodes.at(i: 0)); // ### will be incorrect if we list drives on Windows
329 nodeUrl = dirNode->item().url();
330 qCDebug(category) << "listed URL is visible, adjusted starting point to" << nodeUrl;
331 }
332 if (url == nodeUrl) {
333 return dirNode;
334 }
335
336 // Protocol mismatch? Don't even start comparing paths then. #171721
337 if (url.scheme() != nodeUrl.scheme()) {
338 qCWarning(category) << "protocol mismatch:" << url.scheme() << "vs" << nodeUrl.scheme();
339 return nullptr;
340 }
341
342 const QString pathStr = url.path(); // no trailing slash
343
344 if (!pathStr.startsWith(s: nodeUrl.path())) {
345 qCDebug(category) << pathStr << "does not start with" << nodeUrl.path();
346 return nullptr;
347 }
348
349 for (;;) {
350 QString nodePath = nodeUrl.path();
351 if (!nodePath.endsWith(c: QLatin1Char('/'))) {
352 nodePath += QLatin1Char('/');
353 }
354 if (!pathStr.startsWith(s: nodePath)) {
355 qCWarning(category) << "The KIO worker for" << url.scheme() << "violates the hierarchy structure:"
356 << "I arrived at node" << nodePath << ", but" << pathStr << "does not start with that path.";
357 return nullptr;
358 }
359
360 // E.g. pathStr is /a/b/c and nodePath is /a/. We want to find the node with url /a/b
361 const int nextSlash = pathStr.indexOf(c: QLatin1Char('/'), from: nodePath.length());
362 const QString newPath = pathStr.left(n: nextSlash); // works even if nextSlash==-1
363 nodeUrl.setPath(path: newPath);
364 nodeUrl = nodeUrl.adjusted(options: QUrl::StripTrailingSlash); // #172508
365 KDirModelNode *node = nodeForUrl(url: nodeUrl);
366 if (!node) {
367 qCDebug(category) << nodeUrl << "not found, needs to be listed";
368 // return last parent found:
369 return dirNode;
370 }
371
372 Q_EMIT q->expand(index: indexForNode(node));
373
374 // qDebug() << " nodeUrl=" << nodeUrl;
375 if (nodeUrl == url) {
376 qCDebug(category) << "Found node" << node << "for" << url;
377 return node;
378 }
379 qCDebug(category) << "going into" << node->item().url();
380 Q_ASSERT(isDir(node));
381 dirNode = static_cast<KDirModelDirNode *>(node);
382 }
383 // NOTREACHED
384 // return 0;
385}
386
387#ifndef NDEBUG
388void KDirModelPrivate::dump()
389{
390 qCDebug(category) << "Dumping contents of KDirModel" << q << "dirLister url:" << m_dirLister->url();
391 QHashIterator<QUrl, KDirModelNode *> it(m_nodeHash);
392 while (it.hasNext()) {
393 it.next();
394 qCDebug(category) << it.key() << it.value();
395 }
396}
397#endif
398
399// node -> index. If rowNumber is set (or node is root): O(1). Otherwise: O(n).
400QModelIndex KDirModelPrivate::indexForNode(KDirModelNode *node, int rowNumber) const
401{
402 if (node == m_rootNode) {
403 return QModelIndex();
404 }
405
406 Q_ASSERT(node->parent());
407 return q->createIndex(arow: rowNumber == -1 ? node->rowNumber() : rowNumber, acolumn: 0, adata: node);
408}
409
410// index -> node. O(1)
411KDirModelNode *KDirModelPrivate::nodeForIndex(const QModelIndex &index) const
412{
413 return index.isValid() ? static_cast<KDirModelNode *>(index.internalPointer()) : m_rootNode;
414}
415
416/*
417 * This model wraps the data held by KDirLister.
418 *
419 * The internal pointer of the QModelIndex for a given file is the node for that file in our own tree.
420 * E.g. index(2,0) returns a QModelIndex with row=2 internalPointer=<KDirModelNode for the 3rd child of the root>
421 *
422 * Invalid parent index means root of the tree, m_rootNode
423 */
424
425static QString debugIndex(const QModelIndex &index)
426{
427 QString str;
428 if (!index.isValid()) {
429 str = QStringLiteral("[invalid index, i.e. root]");
430 } else {
431 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
432 str = QLatin1String("[index for ") + node->item().url().toString();
433 if (index.column() > 0) {
434 str += QLatin1String(", column ") + QString::number(index.column());
435 }
436 str += QLatin1Char(']');
437 }
438 return str;
439}
440
441KDirModel::KDirModel(QObject *parent)
442 : QAbstractItemModel(parent)
443 , d(new KDirModelPrivate(this))
444{
445 setDirLister(new KDirLister(this));
446}
447
448KDirModel::~KDirModel() = default;
449
450void KDirModel::setDirLister(KDirLister *dirLister)
451{
452 if (d->m_dirLister) {
453 d->clear();
454 delete d->m_dirLister;
455 }
456 d->m_dirLister = dirLister;
457 d->m_dirLister->setParent(this);
458 connect(sender: d->m_dirLister, signal: &KCoreDirLister::itemsAdded, context: this, slot: [this](const QUrl &dirUrl, const KFileItemList &items) {
459 d->_k_slotNewItems(directoryUrl: dirUrl, items);
460 });
461 connect(sender: d->m_dirLister, signal: &KCoreDirLister::listingDirCompleted, context: this, slot: [this](const QUrl &dirUrl) {
462 d->_k_slotCompleted(directoryUrl: dirUrl);
463 });
464 connect(sender: d->m_dirLister, signal: &KCoreDirLister::itemsDeleted, context: this, slot: [this](const KFileItemList &items) {
465 d->_k_slotDeleteItems(items);
466 });
467 connect(sender: d->m_dirLister, signal: &KCoreDirLister::refreshItems, context: this, slot: [this](const QList<QPair<KFileItem, KFileItem>> &items) {
468 d->_k_slotRefreshItems(items);
469 });
470 connect(sender: d->m_dirLister, signal: qOverload<>(&KCoreDirLister::clear), context: this, slot: [this]() {
471 d->_k_slotClear();
472 });
473 connect(sender: d->m_dirLister, signal: &KCoreDirLister::redirection, context: this, slot: [this](const QUrl &oldUrl, const QUrl &newUrl) {
474 d->_k_slotRedirection(oldUrl, newUrl);
475 });
476}
477
478void KDirModel::openUrl(const QUrl &inputUrl, OpenUrlFlags flags)
479{
480 Q_ASSERT(d->m_dirLister);
481 const QUrl url = cleanupUrl(url: inputUrl);
482 if (flags & ShowRoot) {
483 d->_k_slotClear();
484 d->m_showNodeForListedUrl = true;
485 // Store the parent URL into the invisible root node
486 const QUrl parentUrl = d->rootParentOf(url);
487 d->m_rootNode->setItem(KFileItem(parentUrl));
488 // Stat the requested url, to create the visible node
489 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
490 connect(sender: statJob, signal: &KJob::result, context: this, slot: [statJob, parentUrl, url, this]() {
491 if (!statJob->error()) {
492 const KIO::UDSEntry entry = statJob->statResult();
493 KFileItem visibleRootItem(entry, url);
494 visibleRootItem.setName(url.path() == QLatin1String("/") ? QStringLiteral("/") : url.fileName());
495 d->_k_slotNewItems(directoryUrl: parentUrl, QList<KFileItem>{visibleRootItem});
496 Q_ASSERT(d->m_rootNode->m_childNodes.count() == 1);
497 expandToUrl(url);
498 } else {
499 qWarning() << statJob->errorString();
500 }
501 });
502 } else {
503 d->m_dirLister->openUrl(dirUrl: url, flags: (flags & Reload) ? KDirLister::Reload : KDirLister::NoFlags);
504 }
505}
506
507Qt::DropActions KDirModel::supportedDropActions() const
508{
509 return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction;
510}
511
512KDirLister *KDirModel::dirLister() const
513{
514 return d->m_dirLister;
515}
516
517void KDirModelPrivate::_k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &items)
518{
519 // qDebug() << "directoryUrl=" << directoryUrl;
520
521 KDirModelNode *result = nodeForUrl(url: directoryUrl); // O(depth)
522 // If the directory containing the items wasn't found, then we have a big problem.
523 // Are you calling KDirLister::openUrl(url,Keep)? Please use expandToUrl() instead.
524 if (!result) {
525 qCWarning(category) << "Items emitted in directory" << directoryUrl << "but that directory isn't in KDirModel!"
526 << "Root directory:" << urlForNode(node: m_rootNode);
527 for (const KFileItem &item : items) {
528 qDebug() << "Item:" << item.url();
529 }
530#ifndef NDEBUG
531 dump();
532#endif
533 Q_ASSERT(result);
534 return;
535 }
536 Q_ASSERT(isDir(result));
537 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
538
539 const QModelIndex index = indexForNode(node: dirNode); // O(n)
540 const int newItemsCount = items.count();
541 const int newRowCount = dirNode->m_childNodes.count() + newItemsCount;
542
543 qCDebug(category) << items.count() << "in" << directoryUrl << "index=" << debugIndex(index) << "newRowCount=" << newRowCount;
544
545 q->beginInsertRows(parent: index, first: newRowCount - newItemsCount, last: newRowCount - 1); // parent, first, last
546
547 const QList<QUrl> urlsBeingFetched = m_urlsBeingFetched.value(key: dirNode);
548 if (!urlsBeingFetched.isEmpty()) {
549 qCDebug(category) << "urlsBeingFetched for dir" << dirNode << directoryUrl << ":" << urlsBeingFetched;
550 }
551
552 QList<QModelIndex> emitExpandFor;
553
554 dirNode->m_childNodes.reserve(asize: newRowCount);
555 for (const auto &item : items) {
556 const bool isDir = item.isDir();
557 KDirModelNode *node = isDir ? new KDirModelDirNode(dirNode, item) : new KDirModelNode(dirNode, item);
558#ifndef NDEBUG
559 // Test code for possible duplication of items in the childnodes list,
560 // not sure if/how it ever happened.
561 // if (dirNode->m_childNodes.count() &&
562 // dirNode->m_childNodes.last()->item().name() == item.name()) {
563 // qCWarning(category) << "Already having" << item.name() << "in" << directoryUrl
564 // << "url=" << dirNode->m_childNodes.last()->item().url();
565 // abort();
566 //}
567#endif
568 dirNode->m_childNodes.append(t: node);
569 const QUrl url = item.url();
570 m_nodeHash.insert(key: cleanupUrl(url), value: node);
571
572 if (!urlsBeingFetched.isEmpty()) {
573 const QUrl &dirUrl = url;
574 for (const QUrl &urlFetched : std::as_const(t: urlsBeingFetched)) {
575 if (dirUrl.matches(url: urlFetched, options: QUrl::StripTrailingSlash) || dirUrl.isParentOf(url: urlFetched)) {
576 // qDebug() << "Listing found" << dirUrl.url() << "which is a parent of fetched url" << urlFetched;
577 const QModelIndex parentIndex = indexForNode(node, rowNumber: dirNode->m_childNodes.count() - 1);
578 Q_ASSERT(parentIndex.isValid());
579 emitExpandFor.append(t: parentIndex);
580 if (isDir && dirUrl != urlFetched) {
581 q->fetchMore(parent: parentIndex);
582 m_urlsBeingFetched[node].append(t: urlFetched);
583 }
584 }
585 }
586 }
587 }
588
589 q->endInsertRows();
590
591 // Emit expand signal after rowsInserted signal has been emitted,
592 // so that any proxy model will have updated its mapping already
593 for (const QModelIndex &idx : std::as_const(t&: emitExpandFor)) {
594 Q_EMIT q->expand(index: idx);
595 }
596}
597
598void KDirModelPrivate::_k_slotCompleted(const QUrl &directoryUrl)
599{
600 KDirModelNode *result = nodeForUrl(url: directoryUrl); // O(depth)
601 Q_ASSERT(isDir(result));
602 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
603 m_urlsBeingFetched.remove(key: dirNode);
604}
605
606void KDirModelPrivate::_k_slotDeleteItems(const KFileItemList &items)
607{
608 qCDebug(category) << items.count() << "items";
609
610 // I assume all items are from the same directory.
611 // From KDirLister's code, this should be the case, except maybe emitChanges?
612 const KFileItem item = items.first();
613 Q_ASSERT(!item.isNull());
614 QUrl url = item.url();
615 KDirModelNode *node = nodeForUrl(url: url); // O(depth)
616 if (!node) {
617 qCWarning(category) << "No node found for item that was just removed:" << url;
618 return;
619 }
620
621 KDirModelDirNode *dirNode = node->parent();
622 if (!dirNode) {
623 return;
624 }
625
626 QModelIndex parentIndex = indexForNode(node: dirNode); // O(n)
627
628 // Short path for deleting a single item
629 if (items.count() == 1) {
630 const int r = node->rowNumber();
631 q->beginRemoveRows(parent: parentIndex, first: r, last: r);
632 removeFromNodeHash(node, url);
633 delete dirNode->m_childNodes.takeAt(i: r);
634 q->endRemoveRows();
635 return;
636 }
637
638 // We need to make lists of consecutive row numbers, for the beginRemoveRows call.
639 // Let's use a bit array where each bit represents a given child node.
640 const int childCount = dirNode->m_childNodes.count();
641 QBitArray rowNumbers(childCount, false);
642 for (const KFileItem &item : items) {
643 if (!node) { // don't lookup the first item twice
644 url = item.url();
645 node = nodeForUrl(url: url);
646 if (!node) {
647 qCWarning(category) << "No node found for item that was just removed:" << url;
648 continue;
649 }
650 if (!node->parent()) {
651 // The root node has been deleted, but it was not first in the list 'items'.
652 // see https://bugs.kde.org/show_bug.cgi?id=196695
653 return;
654 }
655 }
656 rowNumbers.setBit(i: node->rowNumber(), val: 1); // O(n)
657 removeFromNodeHash(node, url);
658 node = nullptr;
659 }
660
661 int start = -1;
662 int end = -1;
663 bool lastVal = false;
664 // Start from the end, otherwise all the row numbers are offset while we go
665 for (int i = childCount - 1; i >= 0; --i) {
666 const bool val = rowNumbers.testBit(i);
667 if (!lastVal && val) {
668 end = i;
669 // qDebug() << "end=" << end;
670 }
671 if ((lastVal && !val) || (i == 0 && val)) {
672 start = val ? i : i + 1;
673 // qDebug() << "beginRemoveRows" << start << end;
674 q->beginRemoveRows(parent: parentIndex, first: start, last: end);
675 for (int r = end; r >= start; --r) { // reverse because takeAt changes indexes ;)
676 // qDebug() << "Removing from m_childNodes at" << r;
677 delete dirNode->m_childNodes.takeAt(i: r);
678 }
679 q->endRemoveRows();
680 }
681 lastVal = val;
682 }
683}
684
685void KDirModelPrivate::_k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items)
686{
687 QModelIndex topLeft;
688 QModelIndex bottomRight;
689
690 // Solution 1: we could emit dataChanged for one row (if items.size()==1) or all rows
691 // Solution 2: more fine-grained, actually figure out the beginning and end rows.
692 for (const auto &[oldItem, newItem] : items) {
693 Q_ASSERT(!oldItem.isNull());
694 Q_ASSERT(!newItem.isNull());
695 const QUrl oldUrl = oldItem.url();
696 const QUrl newUrl = newItem.url();
697 KDirModelNode *node = nodeForUrl(url: oldUrl); // O(n); maybe we could look up to the parent only once
698 // qDebug() << "in model for" << m_dirLister->url() << ":" << oldUrl << "->" << newUrl << "node=" << node;
699 if (!node) { // not found [can happen when renaming a dir, redirection was emitted already]
700 continue;
701 }
702 if (node != m_rootNode) { // we never set an item in the rootnode, we use m_dirLister->rootItem instead.
703 bool hasNewNode = false;
704 // A file became directory (well, it was overwritten)
705 if (oldItem.isDir() != newItem.isDir()) {
706 // qDebug() << "DIR/FILE STATUS CHANGE";
707 const int r = node->rowNumber();
708 removeFromNodeHash(node, url: oldUrl);
709 KDirModelDirNode *dirNode = node->parent();
710 delete dirNode->m_childNodes.takeAt(i: r); // i.e. "delete node"
711 node = newItem.isDir() ? new KDirModelDirNode(dirNode, newItem) : new KDirModelNode(dirNode, newItem);
712 dirNode->m_childNodes.insert(i: r, t: node); // same position!
713 hasNewNode = true;
714 } else {
715 node->setItem(newItem);
716 }
717
718 if (oldUrl != newUrl || hasNewNode) {
719 // What if a renamed dir had children? -> kdirlister takes care of emitting for each item
720 // qDebug() << "Renaming" << oldUrl << "to" << newUrl << "in node hash";
721 m_nodeHash.remove(key: cleanupUrl(url: oldUrl));
722 m_nodeHash.insert(key: cleanupUrl(url: newUrl), value: node);
723 }
724 // MIME type changed -> forget cached icon (e.g. from "cut", #164185 comment #13)
725 if (oldItem.determineMimeType().name() != newItem.determineMimeType().name()) {
726 node->setPreview(QIcon());
727 }
728
729 const QModelIndex index = indexForNode(node);
730 if (!topLeft.isValid() || index.row() < topLeft.row()) {
731 topLeft = index;
732 }
733 if (!bottomRight.isValid() || index.row() > bottomRight.row()) {
734 bottomRight = index;
735 }
736 }
737 }
738 // qDebug() << "dataChanged(" << debugIndex(topLeft) << " - " << debugIndex(bottomRight);
739 bottomRight = bottomRight.sibling(arow: bottomRight.row(), acolumn: q->columnCount(parent: QModelIndex()) - 1);
740 Q_EMIT q->dataChanged(topLeft, bottomRight);
741}
742
743// Called when a KIO worker redirects (e.g. smb:/Workgroup -> smb://workgroup)
744// and when renaming a directory.
745void KDirModelPrivate::_k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl)
746{
747 KDirModelNode *node = nodeForUrl(url: oldUrl);
748 if (!node) {
749 return;
750 }
751 m_nodeHash.remove(key: cleanupUrl(url: oldUrl));
752 m_nodeHash.insert(key: cleanupUrl(url: newUrl), value: node);
753
754 // Ensure the node's URL is updated. In case of a listjob redirection
755 // we won't get a refreshItem, and in case of renaming a directory
756 // we'll get it too late (so the hash won't find the old url anymore).
757 KFileItem item = node->item();
758 if (!item.isNull()) { // null if root item, #180156
759 item.setUrl(newUrl);
760 node->setItem(item);
761 }
762
763 // The items inside the renamed directory have been handled before,
764 // KDirLister took care of emitting refreshItem for each of them.
765}
766
767void KDirModelPrivate::_k_slotClear()
768{
769 const int numRows = m_rootNode->m_childNodes.count();
770 if (numRows > 0) {
771 q->beginRemoveRows(parent: QModelIndex(), first: 0, last: numRows - 1);
772 }
773 m_nodeHash.clear();
774 clear();
775 if (numRows > 0) {
776 q->endRemoveRows();
777 }
778}
779
780void KDirModelPrivate::_k_slotJobUrlsChanged(const QStringList &urlList)
781{
782 QStringList dirtyUrls;
783
784 std::set_symmetric_difference(first1: urlList.begin(),
785 last1: urlList.end(),
786 first2: m_allCurrentDestUrls.constBegin(),
787 last2: m_allCurrentDestUrls.constEnd(),
788 result: std::back_inserter(x&: dirtyUrls));
789
790 m_allCurrentDestUrls = urlList;
791
792 for (const QString &dirtyUrl : std::as_const(t&: dirtyUrls)) {
793 if (KDirModelNode *node = nodeForUrl(url: QUrl(dirtyUrl))) {
794 const QModelIndex idx = indexForNode(node);
795 Q_EMIT q->dataChanged(topLeft: idx, bottomRight: idx, roles: {KDirModel::HasJobRole});
796 }
797 }
798}
799
800void KDirModelPrivate::clearAllPreviews(KDirModelDirNode *dirNode)
801{
802 const int numRows = dirNode->m_childNodes.count();
803 if (numRows > 0) {
804 KDirModelNode *lastNode = nullptr;
805 for (KDirModelNode *node : std::as_const(t&: dirNode->m_childNodes)) {
806 node->setPreview(QIcon());
807 // node->setPreview(QIcon::fromTheme(node->item().iconName()));
808 if (isDir(node)) {
809 // recurse into child dirs
810 clearAllPreviews(dirNode: static_cast<KDirModelDirNode *>(node));
811 }
812 lastNode = node;
813 }
814 Q_EMIT q->dataChanged(topLeft: indexForNode(node: dirNode->m_childNodes.at(i: 0), rowNumber: 0), // O(1)
815 bottomRight: indexForNode(node: lastNode, rowNumber: numRows - 1)); // O(1)
816 }
817}
818
819void KDirModel::clearAllPreviews()
820{
821 d->clearAllPreviews(dirNode: d->m_rootNode);
822}
823
824void KDirModel::itemChanged(const QModelIndex &index)
825{
826 // This method is really a itemMimeTypeChanged(), it's mostly called by KFilePreviewGenerator.
827 // When the MIME type is determined, clear the old "preview" (could be
828 // MIME type dependent like when cutting files, #164185)
829 KDirModelNode *node = d->nodeForIndex(index);
830 if (node) {
831 node->setPreview(QIcon());
832 }
833
834 qCDebug(category) << "dataChanged(" << debugIndex(index) << ")";
835 Q_EMIT dataChanged(topLeft: index, bottomRight: index);
836}
837
838int KDirModel::columnCount(const QModelIndex &) const
839{
840 return ColumnCount;
841}
842
843QVariant KDirModel::data(const QModelIndex &index, int role) const
844{
845 if (index.isValid()) {
846 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
847 const KFileItem &item(node->item());
848 switch (role) {
849 case Qt::DisplayRole:
850 switch (index.column()) {
851 case Name:
852 return item.text();
853 case Size:
854 return KIO::convertSize(size: item.size()); // size formatted as QString
855 case ModifiedTime: {
856 QDateTime dt = item.time(which: KFileItem::ModificationTime);
857 return QLocale().toString(dateTime: dt, format: QLocale::ShortFormat);
858 }
859 case Permissions:
860 return item.permissionsString();
861 case Owner:
862 return item.user();
863 case Group:
864 return item.group();
865 case Type:
866 return item.mimeComment();
867 }
868 break;
869 case Qt::EditRole:
870 switch (index.column()) {
871 case Name:
872 return item.text();
873 }
874 break;
875 case Qt::DecorationRole:
876 if (index.column() == Name) {
877 if (!node->preview().isNull()) {
878 // qDebug() << item->url() << " preview found";
879 return node->preview();
880 }
881 Q_ASSERT(!item.isNull());
882 // qDebug() << item->url() << " overlays=" << item->overlays();
883 static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown"));
884
885 const QString iconName(item.iconName());
886 QIcon icon;
887
888 if (QDir::isAbsolutePath(path: iconName)) {
889 icon = QIcon(iconName);
890 }
891 if (icon.isNull()
892 || (!(iconName.endsWith(s: QLatin1String(".svg")) || iconName.endsWith(s: QLatin1String(".svgz"))) && icon.availableSizes().isEmpty())) {
893 icon = QIcon::fromTheme(name: iconName, fallback: fallbackIcon);
894 }
895
896 const auto parentNode = node->parent();
897 if (parentNode->isOnNetwork()) {
898 return icon;
899 } else {
900 return KIconUtils::addOverlays(icon, overlays: item.overlays());
901 }
902 }
903 break;
904 case Qt::TextAlignmentRole:
905 if (index.column() == Size) {
906 // use a right alignment for L2R and R2L languages
907 const Qt::Alignment alignment = Qt::AlignRight | Qt::AlignVCenter;
908 return int(alignment);
909 }
910 break;
911 case Qt::ToolTipRole:
912 return item.text();
913 case FileItemRole:
914 return QVariant::fromValue(value: item);
915 case ChildCountRole:
916 if (!item.isDir()) {
917 return ChildCountUnknown;
918 } else {
919 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(node);
920 int count = dirNode->childCount();
921 if (count == ChildCountUnknown && !dirNode->isOnNetwork() && item.isReadable()) {
922 const QString path = item.localPath();
923 if (!path.isEmpty()) {
924// slow
925// QDir dir(path);
926// count = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count();
927#ifdef Q_OS_WIN
928 QString s = path + QLatin1String("\\*.*");
929 s.replace(QLatin1Char('/'), QLatin1Char('\\'));
930 count = 0;
931 WIN32_FIND_DATA findData;
932 HANDLE hFile = FindFirstFile((LPWSTR)s.utf16(), &findData);
933 if (hFile != INVALID_HANDLE_VALUE) {
934 do {
935 if (!(findData.cFileName[0] == '.' && findData.cFileName[1] == '\0')
936 && !(findData.cFileName[0] == '.' && findData.cFileName[1] == '.' && findData.cFileName[2] == '\0')) {
937 ++count;
938 }
939 } while (FindNextFile(hFile, &findData) != 0);
940 FindClose(hFile);
941 }
942#else
943 DIR *dir = QT_OPENDIR(name: QFile::encodeName(fileName: path).constData());
944 if (dir) {
945 count = 0;
946 QT_DIRENT *dirEntry = nullptr;
947 while ((dirEntry = QT_READDIR(dirp: dir))) {
948 if (dirEntry->d_name[0] == '.') {
949 if (dirEntry->d_name[1] == '\0') { // skip "."
950 continue;
951 }
952 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { // skip ".."
953 continue;
954 }
955 }
956 ++count;
957 }
958 QT_CLOSEDIR(dirp: dir);
959 }
960#endif
961 // qDebug() << "child count for " << path << ":" << count;
962 dirNode->setChildCount(count);
963 }
964 }
965 return count;
966 }
967 case HasJobRole:
968 if (d->m_jobTransfersVisible && d->m_allCurrentDestUrls.isEmpty() == false) {
969 KDirModelNode *node = d->nodeForIndex(index);
970 const QString url = node->item().url().toString();
971 // return whether or not there are job dest urls visible in the view, so the delegate knows which ones to paint.
972 return QVariant(d->m_allCurrentDestUrls.contains(str: url));
973 }
974 }
975 }
976 return QVariant();
977}
978
979void KDirModel::sort(int column, Qt::SortOrder order)
980{
981 // Not implemented - we should probably use QSortFilterProxyModel instead.
982 QAbstractItemModel::sort(column, order);
983}
984
985bool KDirModel::setData(const QModelIndex &index, const QVariant &value, int role)
986{
987 switch (role) {
988 case Qt::EditRole:
989 if (index.column() == Name && value.typeId() == QMetaType::QString) {
990 Q_ASSERT(index.isValid());
991 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
992 const KFileItem &item = node->item();
993 const QString newName = value.toString();
994 if (newName.isEmpty() || newName == item.text() || (newName == QLatin1Char('.')) || (newName == QLatin1String(".."))) {
995 return true;
996 }
997 QUrl newUrl = item.url().adjusted(options: QUrl::RemoveFilename);
998 newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: newName));
999 KIO::Job *job = KIO::rename(src: item.url(), dest: newUrl, flags: item.url().isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
1000 job->uiDelegate()->setAutoErrorHandlingEnabled(true);
1001 // undo handling
1002 KIO::FileUndoManager::self()->recordJob(op: KIO::FileUndoManager::Rename, src: QList<QUrl>() << item.url(), dst: 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>(v: value));
1015 node->setPreview(icon);
1016 } else if (value.typeId() == QMetaType::QPixmap) {
1017 node->setPreview(qvariant_cast<QPixmap>(v: value));
1018 }
1019 Q_EMIT dataChanged(topLeft: index, bottomRight: index);
1020 return true;
1021 }
1022 break;
1023 default:
1024 break;
1025 }
1026 return false;
1027}
1028
1029int KDirModel::rowCount(const QModelIndex &parent) const
1030{
1031 if (parent.column() > 0) { // for QAbstractItemModelTester
1032 return 0;
1033 }
1034 KDirModelNode *node = d->nodeForIndex(index: 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
1052QModelIndex 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(node: 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).
1066QModelIndex 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(i: row); // O(1)
1077 if (childNode) {
1078 return createIndex(arow: row, acolumn: column, adata: childNode);
1079 }
1080 return QModelIndex();
1081}
1082
1083void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex)
1084{
1085 Q_EMIT needSequenceIcon(index, sequenceIndex);
1086}
1087
1088void 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(sender: &JobUrlCache::instance(), signal: &JobUrlCache::jobUrlsChanged, context: this, slot: [this](const QStringList &urlList) {
1097 d->_k_slotJobUrlsChanged(urlList);
1098 });
1099
1100 JobUrlCache::instance().requestJobUrlsChanged();
1101 } else {
1102 disconnect(sender: &JobUrlCache::instance(), signal: &JobUrlCache::jobUrlsChanged, receiver: this, zero: nullptr);
1103 }
1104}
1105
1106bool KDirModel::jobTransfersVisible() const
1107{
1108 return d->m_jobTransfersVisible;
1109}
1110
1111QList<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(first: ret.begin(), last: ret.end());
1119
1120 QUrl url;
1121
1122 auto filterFunc = [&url](const QUrl &u) {
1123 if (url == u || url.isParentOf(url: 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(first: beginIt, last: ret.end(), pred: filterFunc);
1135 ret.erase(abegin: it, aend: ret.end());
1136
1137 return ret;
1138}
1139
1140QStringList KDirModel::mimeTypes() const
1141{
1142 return KUrlMimeData::mimeDataTypes();
1143}
1144
1145QMimeData *KDirModel::mimeData(const QModelIndexList &indexes) const
1146{
1147 QList<QUrl> urls;
1148 QList<QUrl> mostLocalUrls;
1149 urls.reserve(asize: indexes.size());
1150 mostLocalUrls.reserve(asize: indexes.size());
1151 bool canUseMostLocalUrls = true;
1152 for (const QModelIndex &index : indexes) {
1153 const KFileItem &item = d->nodeForIndex(index)->item();
1154 urls.append(t: item.url());
1155 const auto [url, isLocal] = item.isMostLocalUrl();
1156 mostLocalUrls.append(t: 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(urls: mostLocalUrls);
1166 KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData: data);
1167 } else {
1168 data->setUrls(urls);
1169 }
1170
1171 return data;
1172}
1173
1174// Public API; not much point in calling it internally
1175KFileItem 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
1187QModelIndex KDirModel::indexForItem(const KFileItem &item) const
1188{
1189 return indexForUrl(url: item.url()); // O(n)
1190}
1191
1192// url -> index. O(n)
1193QModelIndex KDirModel::indexForUrl(const QUrl &url) const
1194{
1195 KDirModelNode *node = d->nodeForUrl(url: url); // O(depth)
1196 if (!node) {
1197 // qDebug() << url << "not found";
1198 return QModelIndex();
1199 }
1200 return d->indexForNode(node); // O(n)
1201}
1202
1203QModelIndex KDirModel::index(int row, int column, const QModelIndex &parent) const
1204{
1205 KDirModelNode *parentNode = d->nodeForIndex(index: parent); // O(1)
1206 Q_ASSERT(parentNode);
1207 if (d->isDir(node: parentNode)) {
1208 KDirModelNode *childNode = static_cast<KDirModelDirNode *>(parentNode)->m_childNodes.value(i: row); // O(1)
1209 if (childNode) {
1210 return createIndex(arow: row, acolumn: column, adata: childNode);
1211 }
1212 }
1213 return QModelIndex();
1214}
1215
1216QVariant 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
1242bool 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
1277Qt::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
1323bool 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
1339void 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(dirUrl: parentUrl, flags: KDirLister::Keep);
1360}
1361
1362bool 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
1374void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed)
1375{
1376 d->m_dropsAllowed = dropsAllowed;
1377}
1378
1379void KDirModel::expandToUrl(const QUrl &url)
1380{
1381 // emit expand for each parent and return last parent
1382 KDirModelNode *result = d->expandAllParentsUntil(url: 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(t: 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(node: result); // O(n)
1406 Q_ASSERT(parentIndex.isValid());
1407 fetchMore(parent: parentIndex);
1408}
1409
1410bool KDirModel::insertRows(int, int, const QModelIndex &)
1411{
1412 return false;
1413}
1414
1415bool KDirModel::insertColumns(int, int, const QModelIndex &)
1416{
1417 return false;
1418}
1419
1420bool KDirModel::removeRows(int, int, const QModelIndex &)
1421{
1422 return false;
1423}
1424
1425bool KDirModel::removeColumns(int, int, const QModelIndex &)
1426{
1427 return false;
1428}
1429
1430QHash<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"
1442

source code of kio/src/widgets/kdirmodel.cpp