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
104 bool previewHandlesSequences()
105 {
106 return m_previewHandlesSequences;
107 }
108
109 void setPreviewHandlesSequences(bool handlesSequences)
110 {
111 m_previewHandlesSequences = handlesSequences;
112 }
113
114private:
115 KFileItem m_item;
116 KDirModelDirNode *const m_parent;
117 QIcon m_preview;
118 bool m_previewHandlesSequences = true; // First sequence is always allowed
119};
120
121// Specialization for directory nodes
122class KDirModelDirNode : public KDirModelNode
123{
124public:
125 KDirModelDirNode(KDirModelDirNode *parent, const KFileItem &item)
126 : KDirModelNode(parent, item)
127 , m_childCount(KDirModel::ChildCountUnknown)
128 , m_populated(false)
129 , m_fsType(FsTypeUnknown)
130 {
131 // If the parent node is on the network, all children are too. Opposite is not always true.
132 if (parent && parent->isOnNetwork()) {
133 m_fsType = NetworkFs;
134 }
135 }
136 ~KDirModelDirNode() override
137 {
138 qDeleteAll(c: m_childNodes);
139 }
140 QList<KDirModelNode *> m_childNodes; // owns the nodes
141
142 void setItem(const KFileItem &item) override
143 {
144 KDirModelNode::setItem(item);
145 if (item.isNull() || !item.url().isValid()) {
146 m_fsType = LocalFs;
147 } else {
148 m_fsType = FsTypeUnknown;
149 }
150 }
151
152 // If we listed the directory, the child count is known. Otherwise it can be set via setChildCount.
153 int childCount() const
154 {
155 return m_childNodes.isEmpty() ? m_childCount : m_childNodes.count();
156 }
157
158 void setChildCount(int count)
159 {
160 m_childCount = count;
161 }
162
163 bool isPopulated() const
164 {
165 return m_populated;
166 }
167
168 void setPopulated(bool populated)
169 {
170 m_populated = populated;
171 }
172
173 bool isOnNetwork() const
174 {
175 if (!item().isNull() && m_fsType == FsTypeUnknown) {
176 m_fsType = item().isSlow() ? NetworkFs : LocalFs;
177 }
178 return m_fsType == NetworkFs;
179 }
180
181 // For removing all child urls from the global hash.
182 QList<QUrl> collectAllChildUrls() const
183 {
184 QList<QUrl> urls;
185 urls.reserve(asize: urls.size() + m_childNodes.size());
186 for (KDirModelNode *node : m_childNodes) {
187 const KFileItem &item = node->item();
188 urls.append(t: cleanupUrl(url: item.url()));
189 if (item.isDir()) {
190 urls += static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
191 }
192 }
193 return urls;
194 }
195
196private:
197 int m_childCount : 31;
198 bool m_populated : 1;
199 // Network file system? (nfs/smb/ssh)
200 mutable enum {
201 FsTypeUnknown,
202 LocalFs,
203 NetworkFs
204 } m_fsType : 3;
205};
206
207int KDirModelNode::rowNumber() const
208{
209 if (!m_parent) {
210 return 0;
211 }
212 return m_parent->m_childNodes.indexOf(t: const_cast<KDirModelNode *>(this));
213}
214
215////
216
217class KDirModelPrivate
218{
219public:
220 explicit KDirModelPrivate(KDirModel *qq)
221 : q(qq)
222 , m_rootNode(new KDirModelDirNode(nullptr, KFileItem()))
223 {
224 }
225 ~KDirModelPrivate()
226 {
227 delete m_rootNode;
228 }
229
230 void _k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &);
231 void _k_slotCompleted(const QUrl &directoryUrl);
232 void _k_slotDeleteItems(const KFileItemList &);
233 void _k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &);
234 void _k_slotClear();
235 void _k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl);
236 void _k_slotJobUrlsChanged(const QStringList &urlList);
237
238 void clear()
239 {
240 delete m_rootNode;
241 m_rootNode = new KDirModelDirNode(nullptr, KFileItem());
242 m_showNodeForListedUrl = false;
243 m_rootNode->setItem(KFileItem(m_dirLister->url()));
244 }
245
246 // Emit expand for each parent and then return the
247 // last known parent if there is no node for this url
248 KDirModelNode *expandAllParentsUntil(const QUrl &url) const;
249
250 // Return the node for a given url, using the hash.
251 KDirModelNode *nodeForUrl(const QUrl &url) const;
252 KDirModelNode *nodeForIndex(const QModelIndex &index) const;
253 QModelIndex indexForNode(KDirModelNode *node, int rowNumber = -1 /*unknown*/) const;
254
255 static QUrl rootParentOf(const QUrl &url)
256 {
257 // <url> is what we listed, and which is visible at the root of the tree
258 // Here we want the (invisible) parent of that url
259 QUrl parent(url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
260 if (url.path() == QLatin1String("/")) {
261 parent.setPath(path: QString());
262 }
263 return parent;
264 }
265
266 bool isDir(KDirModelNode *node) const
267 {
268 return (node == m_rootNode) || node->item().isDir();
269 }
270
271 QUrl urlForNode(KDirModelNode *node) const
272 {
273 /*
274 * Queries and fragments are removed from the URL, so that the URL of
275 * child items really starts with the URL of the parent.
276 *
277 * For instance ksvn+http://url?rev=100 is the parent for ksvn+http://url/file?rev=100
278 * so we have to remove the query in both to be able to compare the URLs
279 */
280 QUrl url;
281 if (node == m_rootNode && !m_showNodeForListedUrl) {
282 url = m_dirLister->url();
283 } else {
284 url = node->item().url();
285 }
286 if (url.scheme().startsWith(QStringLiteral("ksvn")) || url.scheme().startsWith(QStringLiteral("svn"))) {
287 if (url.hasQuery() || url.hasFragment()) { // avoid detach if not necessary.
288 url.setQuery(query: QString());
289 url.setFragment(fragment: QString()); // kill ref (#171117)
290 }
291 }
292 return url;
293 }
294
295 void removeFromNodeHash(KDirModelNode *node, const QUrl &url);
296 void clearAllPreviews(KDirModelDirNode *node);
297#ifndef NDEBUG
298 void dump();
299#endif
300 Q_DISABLE_COPY(KDirModelPrivate)
301
302 KDirModel *const q;
303 KDirLister *m_dirLister = nullptr;
304 KDirModelDirNode *m_rootNode = nullptr;
305 KDirModel::DropsAllowed m_dropsAllowed = KDirModel::NoDrops;
306 bool m_jobTransfersVisible = false;
307 bool m_showNodeForListedUrl = false;
308 // key = current known parent node (always a KDirModelDirNode but KDirModelNode is more convenient),
309 // value = final url[s] being fetched
310 QMap<KDirModelNode *, QList<QUrl>> m_urlsBeingFetched;
311 QHash<QUrl, KDirModelNode *> m_nodeHash; // global node hash: url -> node
312 QStringList m_allCurrentDestUrls; // list of all dest urls that have jobs on them (e.g. copy, download)
313};
314
315KDirModelNode *KDirModelPrivate::nodeForUrl(const QUrl &_url) const // O(1), well, O(length of url as a string)
316{
317 QUrl url = cleanupUrl(url: _url);
318 if (url == urlForNode(node: m_rootNode)) {
319 return m_rootNode;
320 }
321 return m_nodeHash.value(key: url);
322}
323
324void KDirModelPrivate::removeFromNodeHash(KDirModelNode *node, const QUrl &url)
325{
326 if (node->item().isDir()) {
327 const QList<QUrl> urls = static_cast<KDirModelDirNode *>(node)->collectAllChildUrls();
328 for (const QUrl &u : urls) {
329 m_nodeHash.remove(key: u);
330 }
331 }
332 m_nodeHash.remove(key: cleanupUrl(url));
333}
334
335KDirModelNode *KDirModelPrivate::expandAllParentsUntil(const QUrl &_url) const // O(depth)
336{
337 QUrl url = cleanupUrl(url: _url);
338
339 // qDebug() << url;
340 QUrl nodeUrl = urlForNode(node: m_rootNode);
341 KDirModelDirNode *dirNode = m_rootNode;
342 if (m_showNodeForListedUrl && !m_rootNode->m_childNodes.isEmpty()) {
343 dirNode = static_cast<KDirModelDirNode *>(m_rootNode->m_childNodes.at(i: 0)); // ### will be incorrect if we list drives on Windows
344 nodeUrl = dirNode->item().url();
345 qCDebug(category) << "listed URL is visible, adjusted starting point to" << nodeUrl;
346 }
347 if (url == nodeUrl) {
348 return dirNode;
349 }
350
351 // Protocol mismatch? Don't even start comparing paths then. #171721
352 if (url.scheme() != nodeUrl.scheme()) {
353 qCWarning(category) << "protocol mismatch:" << url.scheme() << "vs" << nodeUrl.scheme();
354 return nullptr;
355 }
356
357 const QString pathStr = url.path(); // no trailing slash
358
359 if (!pathStr.startsWith(s: nodeUrl.path())) {
360 qCDebug(category) << pathStr << "does not start with" << nodeUrl.path();
361 return nullptr;
362 }
363
364 for (;;) {
365 QString nodePath = nodeUrl.path();
366 if (!nodePath.endsWith(c: QLatin1Char('/'))) {
367 nodePath += QLatin1Char('/');
368 }
369 if (!pathStr.startsWith(s: nodePath)) {
370 qCWarning(category) << "The KIO worker for" << url.scheme() << "violates the hierarchy structure:"
371 << "I arrived at node" << nodePath << ", but" << pathStr << "does not start with that path.";
372 return nullptr;
373 }
374
375 // E.g. pathStr is /a/b/c and nodePath is /a/. We want to find the node with url /a/b
376 const int nextSlash = pathStr.indexOf(ch: QLatin1Char('/'), from: nodePath.length());
377 const QString newPath = pathStr.left(n: nextSlash); // works even if nextSlash==-1
378 nodeUrl.setPath(path: newPath);
379 nodeUrl = nodeUrl.adjusted(options: QUrl::StripTrailingSlash); // #172508
380 KDirModelNode *node = nodeForUrl(url: nodeUrl);
381 if (!node) {
382 qCDebug(category) << nodeUrl << "not found, needs to be listed";
383 // return last parent found:
384 return dirNode;
385 }
386
387 Q_EMIT q->expand(index: indexForNode(node));
388
389 // qDebug() << " nodeUrl=" << nodeUrl;
390 if (nodeUrl == url) {
391 qCDebug(category) << "Found node" << node << "for" << url;
392 return node;
393 }
394 qCDebug(category) << "going into" << node->item().url();
395 Q_ASSERT(isDir(node));
396 dirNode = static_cast<KDirModelDirNode *>(node);
397 }
398 // NOTREACHED
399 // return 0;
400}
401
402#ifndef NDEBUG
403void KDirModelPrivate::dump()
404{
405 qCDebug(category) << "Dumping contents of KDirModel" << q << "dirLister url:" << m_dirLister->url();
406 QHashIterator<QUrl, KDirModelNode *> it(m_nodeHash);
407 while (it.hasNext()) {
408 it.next();
409 qCDebug(category) << it.key() << it.value();
410 }
411}
412#endif
413
414// node -> index. If rowNumber is set (or node is root): O(1). Otherwise: O(n).
415QModelIndex KDirModelPrivate::indexForNode(KDirModelNode *node, int rowNumber) const
416{
417 if (node == m_rootNode) {
418 return QModelIndex();
419 }
420
421 Q_ASSERT(node->parent());
422 return q->createIndex(arow: rowNumber == -1 ? node->rowNumber() : rowNumber, acolumn: 0, adata: node);
423}
424
425// index -> node. O(1)
426KDirModelNode *KDirModelPrivate::nodeForIndex(const QModelIndex &index) const
427{
428 return index.isValid() ? static_cast<KDirModelNode *>(index.internalPointer()) : m_rootNode;
429}
430
431/*
432 * This model wraps the data held by KDirLister.
433 *
434 * The internal pointer of the QModelIndex for a given file is the node for that file in our own tree.
435 * E.g. index(2,0) returns a QModelIndex with row=2 internalPointer=<KDirModelNode for the 3rd child of the root>
436 *
437 * Invalid parent index means root of the tree, m_rootNode
438 */
439
440static QString debugIndex(const QModelIndex &index)
441{
442 QString str;
443 if (!index.isValid()) {
444 str = QStringLiteral("[invalid index, i.e. root]");
445 } else {
446 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
447 str = QLatin1String("[index for ") + node->item().url().toString();
448 if (index.column() > 0) {
449 str += QLatin1String(", column ") + QString::number(index.column());
450 }
451 str += QLatin1Char(']');
452 }
453 return str;
454}
455
456KDirModel::KDirModel(QObject *parent)
457 : QAbstractItemModel(parent)
458 , d(new KDirModelPrivate(this))
459{
460 setDirLister(new KDirLister(this));
461}
462
463KDirModel::~KDirModel() = default;
464
465void KDirModel::setDirLister(KDirLister *dirLister)
466{
467 if (d->m_dirLister) {
468 d->clear();
469 delete d->m_dirLister;
470 }
471 d->m_dirLister = dirLister;
472 d->m_dirLister->setParent(this);
473 connect(sender: d->m_dirLister, signal: &KCoreDirLister::itemsAdded, context: this, slot: [this](const QUrl &dirUrl, const KFileItemList &items) {
474 d->_k_slotNewItems(directoryUrl: dirUrl, items);
475 });
476 connect(sender: d->m_dirLister, signal: &KCoreDirLister::listingDirCompleted, context: this, slot: [this](const QUrl &dirUrl) {
477 d->_k_slotCompleted(directoryUrl: dirUrl);
478 });
479 connect(sender: d->m_dirLister, signal: &KCoreDirLister::itemsDeleted, context: this, slot: [this](const KFileItemList &items) {
480 d->_k_slotDeleteItems(items);
481 });
482 connect(sender: d->m_dirLister, signal: &KCoreDirLister::refreshItems, context: this, slot: [this](const QList<QPair<KFileItem, KFileItem>> &items) {
483 d->_k_slotRefreshItems(items);
484 });
485 connect(sender: d->m_dirLister, signal: qOverload<>(&KCoreDirLister::clear), context: this, slot: [this]() {
486 d->_k_slotClear();
487 });
488 connect(sender: d->m_dirLister, signal: &KCoreDirLister::redirection, context: this, slot: [this](const QUrl &oldUrl, const QUrl &newUrl) {
489 d->_k_slotRedirection(oldUrl, newUrl);
490 });
491}
492
493void KDirModel::openUrl(const QUrl &inputUrl, OpenUrlFlags flags)
494{
495 Q_ASSERT(d->m_dirLister);
496 const QUrl url = cleanupUrl(url: inputUrl);
497 if (flags & ShowRoot) {
498 d->_k_slotClear();
499 d->m_showNodeForListedUrl = true;
500 // Store the parent URL into the invisible root node
501 const QUrl parentUrl = d->rootParentOf(url);
502 d->m_rootNode->setItem(KFileItem(parentUrl));
503 // Stat the requested url, to create the visible node
504 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
505 connect(sender: statJob, signal: &KJob::result, context: this, slot: [statJob, parentUrl, url, this]() {
506 if (!statJob->error()) {
507 const KIO::UDSEntry entry = statJob->statResult();
508 KFileItem visibleRootItem(entry, url);
509 visibleRootItem.setName(url.path() == QLatin1String("/") ? QStringLiteral("/") : url.fileName());
510 d->_k_slotNewItems(directoryUrl: parentUrl, QList<KFileItem>{visibleRootItem});
511 Q_ASSERT(d->m_rootNode->m_childNodes.count() == 1);
512 expandToUrl(url);
513 } else {
514 qWarning() << statJob->errorString();
515 }
516 });
517 } else {
518 d->m_dirLister->openUrl(dirUrl: url, flags: (flags & Reload) ? KDirLister::Reload : KDirLister::NoFlags);
519 }
520}
521
522Qt::DropActions KDirModel::supportedDropActions() const
523{
524 return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction;
525}
526
527KDirLister *KDirModel::dirLister() const
528{
529 return d->m_dirLister;
530}
531
532void KDirModelPrivate::_k_slotNewItems(const QUrl &directoryUrl, const KFileItemList &items)
533{
534 // qDebug() << "directoryUrl=" << directoryUrl;
535
536 KDirModelNode *result = nodeForUrl(url: directoryUrl); // O(depth)
537 // If the directory containing the items wasn't found, then we have a big problem.
538 // Are you calling KDirLister::openUrl(url,Keep)? Please use expandToUrl() instead.
539 if (!result) {
540 qCWarning(category) << "Items emitted in directory" << directoryUrl << "but that directory isn't in KDirModel!"
541 << "Root directory:" << urlForNode(node: m_rootNode);
542 for (const KFileItem &item : items) {
543 qDebug() << "Item:" << item.url();
544 }
545#ifndef NDEBUG
546 dump();
547#endif
548 Q_ASSERT(result);
549 return;
550 }
551 Q_ASSERT(isDir(result));
552 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
553
554 const QModelIndex index = indexForNode(node: dirNode); // O(n)
555 const int newItemsCount = items.count();
556 const int newRowCount = dirNode->m_childNodes.count() + newItemsCount;
557
558 qCDebug(category) << items.count() << "in" << directoryUrl << "index=" << debugIndex(index) << "newRowCount=" << newRowCount;
559
560 q->beginInsertRows(parent: index, first: newRowCount - newItemsCount, last: newRowCount - 1); // parent, first, last
561
562 const QList<QUrl> urlsBeingFetched = m_urlsBeingFetched.value(key: dirNode);
563 if (!urlsBeingFetched.isEmpty()) {
564 qCDebug(category) << "urlsBeingFetched for dir" << dirNode << directoryUrl << ":" << urlsBeingFetched;
565 }
566
567 QList<QModelIndex> emitExpandFor;
568
569 dirNode->m_childNodes.reserve(asize: newRowCount);
570 for (const auto &item : items) {
571 const bool isDir = item.isDir();
572 KDirModelNode *node = isDir ? new KDirModelDirNode(dirNode, item) : new KDirModelNode(dirNode, item);
573#ifndef NDEBUG
574 // Test code for possible duplication of items in the childnodes list,
575 // not sure if/how it ever happened.
576 // if (dirNode->m_childNodes.count() &&
577 // dirNode->m_childNodes.last()->item().name() == item.name()) {
578 // qCWarning(category) << "Already having" << item.name() << "in" << directoryUrl
579 // << "url=" << dirNode->m_childNodes.last()->item().url();
580 // abort();
581 //}
582#endif
583 dirNode->m_childNodes.append(t: node);
584 const QUrl url = item.url();
585 m_nodeHash.insert(key: cleanupUrl(url), value: node);
586
587 if (!urlsBeingFetched.isEmpty()) {
588 const QUrl &dirUrl = url;
589 for (const QUrl &urlFetched : std::as_const(t: urlsBeingFetched)) {
590 if (dirUrl.matches(url: urlFetched, options: QUrl::StripTrailingSlash) || dirUrl.isParentOf(url: urlFetched)) {
591 // qDebug() << "Listing found" << dirUrl.url() << "which is a parent of fetched url" << urlFetched;
592 const QModelIndex parentIndex = indexForNode(node, rowNumber: dirNode->m_childNodes.count() - 1);
593 Q_ASSERT(parentIndex.isValid());
594 emitExpandFor.append(t: parentIndex);
595 if (isDir && dirUrl != urlFetched) {
596 q->fetchMore(parent: parentIndex);
597 m_urlsBeingFetched[node].append(t: urlFetched);
598 }
599 }
600 }
601 }
602 }
603
604 q->endInsertRows();
605
606 // Emit expand signal after rowsInserted signal has been emitted,
607 // so that any proxy model will have updated its mapping already
608 for (const QModelIndex &idx : std::as_const(t&: emitExpandFor)) {
609 Q_EMIT q->expand(index: idx);
610 }
611}
612
613void KDirModelPrivate::_k_slotCompleted(const QUrl &directoryUrl)
614{
615 KDirModelNode *result = nodeForUrl(url: directoryUrl); // O(depth)
616 Q_ASSERT(isDir(result));
617 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(result);
618 m_urlsBeingFetched.remove(key: dirNode);
619}
620
621void KDirModelPrivate::_k_slotDeleteItems(const KFileItemList &items)
622{
623 qCDebug(category) << items.count() << "items";
624
625 // I assume all items are from the same directory.
626 // From KDirLister's code, this should be the case, except maybe emitChanges?
627
628 // We need to find first item with existing node
629 // because the first deleted item could be a hidden file not belonging to any node.
630 auto findFirstNodeAndUrl = [this](const KFileItemList &items) -> QPair<KDirModelNode *, QUrl> {
631 for (const KFileItem &item : items) {
632 Q_ASSERT(!item.isNull());
633 const QUrl url = item.url();
634 KDirModelNode *node = nodeForUrl(url: url); // O(depth)
635 if (node) {
636 return {node, url};
637 } else {
638 qCWarning(category) << "No node found for item that was just removed:" << url;
639 }
640 }
641 return {nullptr, QUrl()};
642 };
643
644 auto [node, url] = findFirstNodeAndUrl(items);
645 if (!node) {
646 return;
647 }
648
649 KDirModelDirNode *dirNode = node->parent();
650 if (!dirNode) {
651 return;
652 }
653
654 QModelIndex parentIndex = indexForNode(node: dirNode); // O(n)
655
656 // Short path for deleting a single item
657 if (items.count() == 1) {
658 const int r = node->rowNumber();
659 q->beginRemoveRows(parent: parentIndex, first: r, last: r);
660 removeFromNodeHash(node, url);
661 delete dirNode->m_childNodes.takeAt(i: r);
662 q->endRemoveRows();
663 return;
664 }
665
666 // We need to make lists of consecutive row numbers, for the beginRemoveRows call.
667 // Let's use a bit array where each bit represents a given child node.
668 const int childCount = dirNode->m_childNodes.count();
669 QBitArray rowNumbers(childCount, false);
670 for (const KFileItem &item : items) {
671 url = item.url();
672 node = nodeForUrl(url: url);
673 if (!node) {
674 qCWarning(category) << "No node found for item that was just removed:" << url;
675 continue;
676 }
677 if (!node->parent()) {
678 // The root node has been deleted, but it was not first in the list 'items'.
679 // see https://bugs.kde.org/show_bug.cgi?id=196695
680 return;
681 }
682 rowNumbers.setBit(i: node->rowNumber(), val: 1); // O(n)
683 removeFromNodeHash(node, url);
684 }
685
686 int start = -1;
687 int end = -1;
688 bool lastVal = false;
689 // Start from the end, otherwise all the row numbers are offset while we go
690 for (int i = childCount - 1; i >= 0; --i) {
691 const bool val = rowNumbers.testBit(i);
692 if (!lastVal && val) {
693 end = i;
694 // qDebug() << "end=" << end;
695 }
696 if ((lastVal && !val) || (i == 0 && val)) {
697 start = val ? i : i + 1;
698 // qDebug() << "beginRemoveRows" << start << end;
699 q->beginRemoveRows(parent: parentIndex, first: start, last: end);
700 for (int r = end; r >= start; --r) { // reverse because takeAt changes indexes ;)
701 // qDebug() << "Removing from m_childNodes at" << r;
702 delete dirNode->m_childNodes.takeAt(i: r);
703 }
704 q->endRemoveRows();
705 }
706 lastVal = val;
707 }
708}
709
710void KDirModelPrivate::_k_slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items)
711{
712 QModelIndex topLeft;
713 QModelIndex bottomRight;
714
715 // Solution 1: we could emit dataChanged for one row (if items.size()==1) or all rows
716 // Solution 2: more fine-grained, actually figure out the beginning and end rows.
717 for (const auto &[oldItem, newItem] : items) {
718 Q_ASSERT(!oldItem.isNull());
719 Q_ASSERT(!newItem.isNull());
720 const QUrl oldUrl = oldItem.url();
721 const QUrl newUrl = newItem.url();
722 KDirModelNode *node = nodeForUrl(url: oldUrl); // O(n); maybe we could look up to the parent only once
723 // qDebug() << "in model for" << m_dirLister->url() << ":" << oldUrl << "->" << newUrl << "node=" << node;
724 if (!node) { // not found [can happen when renaming a dir, redirection was emitted already]
725 continue;
726 }
727 if (node != m_rootNode) { // we never set an item in the rootnode, we use m_dirLister->rootItem instead.
728 bool hasNewNode = false;
729 // A file became directory (well, it was overwritten)
730 if (oldItem.isDir() != newItem.isDir()) {
731 // qDebug() << "DIR/FILE STATUS CHANGE";
732 const int r = node->rowNumber();
733 removeFromNodeHash(node, url: oldUrl);
734 KDirModelDirNode *dirNode = node->parent();
735 delete dirNode->m_childNodes.takeAt(i: r); // i.e. "delete node"
736 node = newItem.isDir() ? new KDirModelDirNode(dirNode, newItem) : new KDirModelNode(dirNode, newItem);
737 dirNode->m_childNodes.insert(i: r, t: node); // same position!
738 hasNewNode = true;
739 } else {
740 node->setItem(newItem);
741 }
742
743 if (oldUrl != newUrl || hasNewNode) {
744 // What if a renamed dir had children? -> kdirlister takes care of emitting for each item
745 // qDebug() << "Renaming" << oldUrl << "to" << newUrl << "in node hash";
746 m_nodeHash.remove(key: cleanupUrl(url: oldUrl));
747 m_nodeHash.insert(key: cleanupUrl(url: newUrl), value: node);
748 }
749 // MIME type changed -> forget cached icon (e.g. from "cut", #164185 comment #13)
750 if (oldItem.determineMimeType().name() != newItem.determineMimeType().name()) {
751 node->setPreview(QIcon());
752 }
753
754 const QModelIndex index = indexForNode(node);
755 if (!topLeft.isValid() || index.row() < topLeft.row()) {
756 topLeft = index;
757 }
758 if (!bottomRight.isValid() || index.row() > bottomRight.row()) {
759 bottomRight = index;
760 }
761 }
762 }
763 // qDebug() << "dataChanged(" << debugIndex(topLeft) << " - " << debugIndex(bottomRight);
764 bottomRight = bottomRight.sibling(arow: bottomRight.row(), acolumn: q->columnCount(parent: QModelIndex()) - 1);
765 Q_EMIT q->dataChanged(topLeft, bottomRight);
766}
767
768// Called when a KIO worker redirects (e.g. smb:/Workgroup -> smb://workgroup)
769// and when renaming a directory.
770void KDirModelPrivate::_k_slotRedirection(const QUrl &oldUrl, const QUrl &newUrl)
771{
772 KDirModelNode *node = nodeForUrl(url: oldUrl);
773 if (!node) {
774 return;
775 }
776 m_nodeHash.remove(key: cleanupUrl(url: oldUrl));
777 m_nodeHash.insert(key: cleanupUrl(url: newUrl), value: node);
778
779 // Ensure the node's URL is updated. In case of a listjob redirection
780 // we won't get a refreshItem, and in case of renaming a directory
781 // we'll get it too late (so the hash won't find the old url anymore).
782 KFileItem item = node->item();
783 if (!item.isNull()) { // null if root item, #180156
784 item.setUrl(newUrl);
785 node->setItem(item);
786 }
787
788 // The items inside the renamed directory have been handled before,
789 // KDirLister took care of emitting refreshItem for each of them.
790}
791
792void KDirModelPrivate::_k_slotClear()
793{
794 const int numRows = m_rootNode->m_childNodes.count();
795 if (numRows > 0) {
796 q->beginRemoveRows(parent: QModelIndex(), first: 0, last: numRows - 1);
797 }
798 m_nodeHash.clear();
799 clear();
800 if (numRows > 0) {
801 q->endRemoveRows();
802 }
803}
804
805void KDirModelPrivate::_k_slotJobUrlsChanged(const QStringList &urlList)
806{
807 QStringList dirtyUrls;
808
809 std::set_symmetric_difference(first1: urlList.begin(),
810 last1: urlList.end(),
811 first2: m_allCurrentDestUrls.constBegin(),
812 last2: m_allCurrentDestUrls.constEnd(),
813 result: std::back_inserter(x&: dirtyUrls));
814
815 m_allCurrentDestUrls = urlList;
816
817 for (const QString &dirtyUrl : std::as_const(t&: dirtyUrls)) {
818 if (KDirModelNode *node = nodeForUrl(url: QUrl(dirtyUrl))) {
819 const QModelIndex idx = indexForNode(node);
820 Q_EMIT q->dataChanged(topLeft: idx, bottomRight: idx, roles: {KDirModel::HasJobRole});
821 }
822 }
823}
824
825void KDirModelPrivate::clearAllPreviews(KDirModelDirNode *dirNode)
826{
827 const int numRows = dirNode->m_childNodes.count();
828 if (numRows > 0) {
829 KDirModelNode *lastNode = nullptr;
830 for (KDirModelNode *node : std::as_const(t&: dirNode->m_childNodes)) {
831 node->setPreview(QIcon());
832 // node->setPreview(QIcon::fromTheme(node->item().iconName()));
833 if (isDir(node)) {
834 // recurse into child dirs
835 clearAllPreviews(dirNode: static_cast<KDirModelDirNode *>(node));
836 }
837 lastNode = node;
838 }
839 Q_EMIT q->dataChanged(topLeft: indexForNode(node: dirNode->m_childNodes.at(i: 0), rowNumber: 0), // O(1)
840 bottomRight: indexForNode(node: lastNode, rowNumber: numRows - 1)); // O(1)
841 }
842}
843
844void KDirModel::clearAllPreviews()
845{
846 d->clearAllPreviews(dirNode: d->m_rootNode);
847}
848
849void KDirModel::itemChanged(const QModelIndex &index)
850{
851 // This method is really a itemMimeTypeChanged(), it's mostly called by KFilePreviewGenerator.
852 // When the MIME type is determined, clear the old "preview" (could be
853 // MIME type dependent like when cutting files, #164185)
854 KDirModelNode *node = d->nodeForIndex(index);
855 if (node) {
856 node->setPreview(QIcon());
857 }
858
859 qCDebug(category) << "dataChanged(" << debugIndex(index) << ")";
860 Q_EMIT dataChanged(topLeft: index, bottomRight: index);
861}
862
863int KDirModel::columnCount(const QModelIndex &) const
864{
865 return ColumnCount;
866}
867
868QVariant KDirModel::data(const QModelIndex &index, int role) const
869{
870 if (index.isValid()) {
871 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
872 const KFileItem &item(node->item());
873 switch (role) {
874 case Qt::DisplayRole:
875 switch (index.column()) {
876 case Name:
877 return item.text();
878 case Size:
879 return KIO::convertSize(size: item.size()); // size formatted as QString
880 case ModifiedTime: {
881 QDateTime dt = item.time(which: KFileItem::ModificationTime);
882 return QLocale().toString(dateTime: dt, format: QLocale::ShortFormat);
883 }
884 case Permissions:
885 return item.permissionsString();
886 case Owner:
887 return item.user();
888 case Group:
889 return item.group();
890 case Type:
891 return item.mimeComment();
892 }
893 break;
894 case Qt::EditRole:
895 switch (index.column()) {
896 case Name:
897 return item.text();
898 }
899 break;
900 case Qt::DecorationRole:
901 if (index.column() == Name) {
902 if (!node->preview().isNull()) {
903 // qDebug() << item->url() << " preview found";
904 return node->preview();
905 }
906 Q_ASSERT(!item.isNull());
907 // qDebug() << item->url() << " overlays=" << item->overlays();
908 static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown"));
909
910 const QString iconName(item.iconName());
911 QIcon icon;
912
913 if (QDir::isAbsolutePath(path: iconName)) {
914 icon = QIcon(iconName);
915 }
916 if (icon.isNull()
917 || (!(iconName.endsWith(s: QLatin1String(".svg")) || iconName.endsWith(s: QLatin1String(".svgz"))) && icon.availableSizes().isEmpty())) {
918 icon = QIcon::fromTheme(name: iconName, fallback: fallbackIcon);
919 }
920
921 const auto parentNode = node->parent();
922 if (parentNode->isOnNetwork()) {
923 return icon;
924 } else {
925 return KIconUtils::addOverlays(icon, overlays: item.overlays());
926 }
927 }
928 break;
929 case HandleSequencesRole:
930 if (index.column() == Name) {
931 return node->previewHandlesSequences();
932 }
933 break;
934 case Qt::TextAlignmentRole:
935 if (index.column() == Size) {
936 // use a right alignment for L2R and R2L languages
937 const Qt::Alignment alignment = Qt::AlignRight | Qt::AlignVCenter;
938 return int(alignment);
939 }
940 break;
941 case Qt::ToolTipRole:
942 return item.text();
943 case FileItemRole:
944 return QVariant::fromValue(value: item);
945 case ChildCountRole:
946 if (!item.isDir()) {
947 return ChildCountUnknown;
948 } else {
949 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(node);
950 int count = dirNode->childCount();
951 if (count == ChildCountUnknown && !dirNode->isOnNetwork() && item.isReadable()) {
952 const QString path = item.localPath();
953 if (!path.isEmpty()) {
954// slow
955// QDir dir(path);
956// count = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count();
957#ifdef Q_OS_WIN
958 QString s = path + QLatin1String("\\*.*");
959 s.replace(QLatin1Char('/'), QLatin1Char('\\'));
960 count = 0;
961 WIN32_FIND_DATA findData;
962 HANDLE hFile = FindFirstFile((LPWSTR)s.utf16(), &findData);
963 if (hFile != INVALID_HANDLE_VALUE) {
964 do {
965 if (!(findData.cFileName[0] == '.' && findData.cFileName[1] == '\0')
966 && !(findData.cFileName[0] == '.' && findData.cFileName[1] == '.' && findData.cFileName[2] == '\0')) {
967 ++count;
968 }
969 } while (FindNextFile(hFile, &findData) != 0);
970 FindClose(hFile);
971 }
972#else
973 DIR *dir = QT_OPENDIR(name: QFile::encodeName(fileName: path).constData());
974 if (dir) {
975 count = 0;
976 QT_DIRENT *dirEntry = nullptr;
977 while ((dirEntry = QT_READDIR(dirp: dir))) {
978 if (dirEntry->d_name[0] == '.') {
979 if (dirEntry->d_name[1] == '\0') { // skip "."
980 continue;
981 }
982 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { // skip ".."
983 continue;
984 }
985 }
986 ++count;
987 }
988 QT_CLOSEDIR(dirp: dir);
989 }
990#endif
991 // qDebug() << "child count for " << path << ":" << count;
992 dirNode->setChildCount(count);
993 }
994 }
995 return count;
996 }
997 case HasJobRole:
998 if (d->m_jobTransfersVisible && d->m_allCurrentDestUrls.isEmpty() == false) {
999 KDirModelNode *node = d->nodeForIndex(index);
1000 const QString url = node->item().url().toString();
1001 // return whether or not there are job dest urls visible in the view, so the delegate knows which ones to paint.
1002 return QVariant(d->m_allCurrentDestUrls.contains(str: url));
1003 }
1004 }
1005 }
1006 return QVariant();
1007}
1008
1009void KDirModel::sort(int column, Qt::SortOrder order)
1010{
1011 // Not implemented - we should probably use QSortFilterProxyModel instead.
1012 QAbstractItemModel::sort(column, order);
1013}
1014
1015bool KDirModel::setData(const QModelIndex &index, const QVariant &value, int role)
1016{
1017 switch (role) {
1018 case Qt::EditRole:
1019 if (index.column() == Name && value.typeId() == QMetaType::QString) {
1020 Q_ASSERT(index.isValid());
1021 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
1022 const KFileItem &item = node->item();
1023 const QString newName = value.toString();
1024 if (newName.isEmpty() || newName == item.text() || (newName == QLatin1Char('.')) || (newName == QLatin1String(".."))) {
1025 return true;
1026 }
1027 QUrl newUrl = item.url().adjusted(options: QUrl::RemoveFilename);
1028 newUrl.setPath(path: newUrl.path() + KIO::encodeFileName(str: newName));
1029 KIO::Job *job = KIO::rename(src: item.url(), dest: newUrl, flags: item.url().isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
1030 job->uiDelegate()->setAutoErrorHandlingEnabled(true);
1031 // undo handling
1032 KIO::FileUndoManager::self()->recordJob(op: KIO::FileUndoManager::Rename, src: QList<QUrl>() << item.url(), dst: newUrl, job);
1033 return true;
1034 }
1035 break;
1036 case Qt::DecorationRole:
1037 if (index.column() == Name) {
1038 Q_ASSERT(index.isValid());
1039 // Set new pixmap - e.g. preview
1040 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
1041 // qDebug() << "setting icon for " << node->item()->url();
1042 Q_ASSERT(node);
1043 if (value.typeId() == QMetaType::QIcon) {
1044 const QIcon icon(qvariant_cast<QIcon>(v: value));
1045 node->setPreview(icon);
1046 } else if (value.typeId() == QMetaType::QPixmap) {
1047 node->setPreview(qvariant_cast<QPixmap>(v: value));
1048 }
1049 Q_EMIT dataChanged(topLeft: index, bottomRight: index);
1050 return true;
1051 }
1052 break;
1053 case HandleSequencesRole:
1054 if (index.column() == Name) {
1055 KDirModelNode *node = static_cast<KDirModelNode *>(index.internalPointer());
1056 Q_ASSERT(node);
1057 node->setPreviewHandlesSequences(value.toBool());
1058 return true;
1059 }
1060 break;
1061 default:
1062 break;
1063 }
1064 return false;
1065}
1066
1067int KDirModel::rowCount(const QModelIndex &parent) const
1068{
1069 if (parent.column() > 0) { // for QAbstractItemModelTester
1070 return 0;
1071 }
1072 KDirModelNode *node = d->nodeForIndex(index: parent);
1073 if (!node || !d->isDir(node)) { // #176555
1074 return 0;
1075 }
1076
1077 KDirModelDirNode *parentNode = static_cast<KDirModelDirNode *>(node);
1078 Q_ASSERT(parentNode);
1079 const int count = parentNode->m_childNodes.count();
1080#if 0
1081 QStringList filenames;
1082 for (int i = 0; i < count; ++i) {
1083 filenames << d->urlForNode(parentNode->m_childNodes.at(i)).fileName();
1084 }
1085 qDebug() << "rowCount for " << d->urlForNode(parentNode) << ": " << count << filenames;
1086#endif
1087 return count;
1088}
1089
1090QModelIndex KDirModel::parent(const QModelIndex &index) const
1091{
1092 if (!index.isValid()) {
1093 return QModelIndex();
1094 }
1095 KDirModelNode *childNode = static_cast<KDirModelNode *>(index.internalPointer());
1096 Q_ASSERT(childNode);
1097 KDirModelNode *parentNode = childNode->parent();
1098 Q_ASSERT(parentNode);
1099 return d->indexForNode(node: parentNode); // O(n)
1100}
1101
1102// Reimplemented to avoid the default implementation which calls parent
1103// (O(n) for finding the parent's row number for nothing). This implementation is O(1).
1104QModelIndex KDirModel::sibling(int row, int column, const QModelIndex &index) const
1105{
1106 if (!index.isValid()) {
1107 return QModelIndex();
1108 }
1109 KDirModelNode *oldChildNode = static_cast<KDirModelNode *>(index.internalPointer());
1110 Q_ASSERT(oldChildNode);
1111 KDirModelNode *parentNode = oldChildNode->parent();
1112 Q_ASSERT(parentNode);
1113 Q_ASSERT(d->isDir(parentNode));
1114 KDirModelNode *childNode = static_cast<KDirModelDirNode *>(parentNode)->m_childNodes.value(i: row); // O(1)
1115 if (childNode) {
1116 return createIndex(arow: row, acolumn: column, adata: childNode);
1117 }
1118 return QModelIndex();
1119}
1120
1121void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex)
1122{
1123 Q_EMIT needSequenceIcon(index, sequenceIndex);
1124}
1125
1126void KDirModel::setJobTransfersVisible(bool show)
1127{
1128 if (d->m_jobTransfersVisible == show) {
1129 return;
1130 }
1131
1132 d->m_jobTransfersVisible = show;
1133 if (show) {
1134 connect(sender: &JobUrlCache::instance(), signal: &JobUrlCache::jobUrlsChanged, context: this, slot: [this](const QStringList &urlList) {
1135 d->_k_slotJobUrlsChanged(urlList);
1136 });
1137
1138 JobUrlCache::instance().requestJobUrlsChanged();
1139 } else {
1140 disconnect(sender: &JobUrlCache::instance(), signal: &JobUrlCache::jobUrlsChanged, receiver: this, zero: nullptr);
1141 }
1142}
1143
1144bool KDirModel::jobTransfersVisible() const
1145{
1146 return d->m_jobTransfersVisible;
1147}
1148
1149QList<QUrl> KDirModel::simplifiedUrlList(const QList<QUrl> &urls)
1150{
1151 if (urls.isEmpty()) {
1152 return urls;
1153 }
1154
1155 QList<QUrl> ret(urls);
1156 std::sort(first: ret.begin(), last: ret.end());
1157
1158 QUrl url;
1159
1160 auto filterFunc = [&url](const QUrl &u) {
1161 if (url == u || url.isParentOf(url: u)) {
1162 return true;
1163 } else {
1164 url = u;
1165 return false;
1166 }
1167 };
1168
1169 auto beginIt = ret.begin();
1170 url = *beginIt;
1171 ++beginIt;
1172 auto it = std::remove_if(first: beginIt, last: ret.end(), pred: filterFunc);
1173 ret.erase(abegin: it, aend: ret.end());
1174
1175 return ret;
1176}
1177
1178QStringList KDirModel::mimeTypes() const
1179{
1180 return KUrlMimeData::mimeDataTypes();
1181}
1182
1183QMimeData *KDirModel::mimeData(const QModelIndexList &indexes) const
1184{
1185 QList<QUrl> urls;
1186 QList<QUrl> mostLocalUrls;
1187 urls.reserve(asize: indexes.size());
1188 mostLocalUrls.reserve(asize: indexes.size());
1189 bool canUseMostLocalUrls = true;
1190 for (const QModelIndex &index : indexes) {
1191 const KFileItem &item = d->nodeForIndex(index)->item();
1192 urls.append(t: item.url());
1193 const auto [url, isLocal] = item.isMostLocalUrl();
1194 mostLocalUrls.append(t: url);
1195 if (!isLocal) {
1196 canUseMostLocalUrls = false;
1197 }
1198 }
1199 QMimeData *data = new QMimeData();
1200 const bool different = canUseMostLocalUrls && (mostLocalUrls != urls);
1201 urls = simplifiedUrlList(urls);
1202 if (different) {
1203 mostLocalUrls = simplifiedUrlList(urls: mostLocalUrls);
1204 KUrlMimeData::setUrls(urls, mostLocalUrls, mimeData: data);
1205 } else {
1206 data->setUrls(urls);
1207 }
1208
1209 return data;
1210}
1211
1212// Public API; not much point in calling it internally
1213KFileItem KDirModel::itemForIndex(const QModelIndex &index) const
1214{
1215 if (!index.isValid()) {
1216 if (d->m_showNodeForListedUrl) {
1217 return {};
1218 }
1219 return d->m_dirLister->rootItem();
1220 } else {
1221 return static_cast<KDirModelNode *>(index.internalPointer())->item();
1222 }
1223}
1224
1225QModelIndex KDirModel::indexForItem(const KFileItem &item) const
1226{
1227 return indexForUrl(url: item.url()); // O(n)
1228}
1229
1230// url -> index. O(n)
1231QModelIndex KDirModel::indexForUrl(const QUrl &url) const
1232{
1233 KDirModelNode *node = d->nodeForUrl(url: url); // O(depth)
1234 if (!node) {
1235 // qDebug() << url << "not found";
1236 return QModelIndex();
1237 }
1238 return d->indexForNode(node); // O(n)
1239}
1240
1241QModelIndex KDirModel::index(int row, int column, const QModelIndex &parent) const
1242{
1243 KDirModelNode *parentNode = d->nodeForIndex(index: parent); // O(1)
1244 Q_ASSERT(parentNode);
1245 if (d->isDir(node: parentNode)) {
1246 KDirModelNode *childNode = static_cast<KDirModelDirNode *>(parentNode)->m_childNodes.value(i: row); // O(1)
1247 if (childNode) {
1248 return createIndex(arow: row, acolumn: column, adata: childNode);
1249 }
1250 }
1251 return QModelIndex();
1252}
1253
1254QVariant KDirModel::headerData(int section, Qt::Orientation orientation, int role) const
1255{
1256 if (orientation == Qt::Horizontal) {
1257 switch (role) {
1258 case Qt::DisplayRole:
1259 switch (section) {
1260 case Name:
1261 return i18nc("@title:column", "Name");
1262 case Size:
1263 return i18nc("@title:column", "Size");
1264 case ModifiedTime:
1265 return i18nc("@title:column", "Date");
1266 case Permissions:
1267 return i18nc("@title:column", "Permissions");
1268 case Owner:
1269 return i18nc("@title:column", "Owner");
1270 case Group:
1271 return i18nc("@title:column", "Group");
1272 case Type:
1273 return i18nc("@title:column", "Type");
1274 }
1275 }
1276 }
1277 return QVariant();
1278}
1279
1280bool KDirModel::hasChildren(const QModelIndex &parent) const
1281{
1282 if (!parent.isValid()) {
1283 return true;
1284 }
1285
1286 const KDirModelNode *parentNode = static_cast<KDirModelNode *>(parent.internalPointer());
1287 const KFileItem &parentItem = parentNode->item();
1288 Q_ASSERT(!parentItem.isNull());
1289 if (!parentItem.isDir()) {
1290 return false;
1291 }
1292 if (static_cast<const KDirModelDirNode *>(parentNode)->isPopulated()) {
1293 return !static_cast<const KDirModelDirNode *>(parentNode)->m_childNodes.isEmpty();
1294 }
1295 if (parentItem.isLocalFile() && !static_cast<const KDirModelDirNode *>(parentNode)->isOnNetwork()) {
1296 QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
1297
1298 if (d->m_dirLister->dirOnlyMode()) {
1299 filters |= QDir::NoSymLinks;
1300 } else {
1301 filters |= QDir::Files | QDir::System;
1302 }
1303
1304 if (d->m_dirLister->showHiddenFiles()) {
1305 filters |= QDir::Hidden;
1306 }
1307
1308 QDirIterator it(parentItem.localPath(), filters, QDirIterator::Subdirectories);
1309 return it.hasNext();
1310 }
1311 // Remote and not listed yet, we can't know; let the user click on it so we'll find out
1312 return true;
1313}
1314
1315Qt::ItemFlags KDirModel::flags(const QModelIndex &index) const
1316{
1317 Qt::ItemFlags f;
1318 if (index.isValid()) {
1319 f |= Qt::ItemIsEnabled;
1320 if (index.column() == Name) {
1321 f |= Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
1322 }
1323 }
1324
1325 // Allow dropping onto this item?
1326 if (d->m_dropsAllowed != NoDrops) {
1327 if (!index.isValid()) {
1328 if (d->m_dropsAllowed & DropOnDirectory) {
1329 f |= Qt::ItemIsDropEnabled;
1330 }
1331 } else {
1332 KFileItem item = itemForIndex(index);
1333 if (item.isNull()) {
1334 qCWarning(category) << "Invalid item returned for index";
1335 } else if (item.isDir()) {
1336 if (d->m_dropsAllowed & DropOnDirectory) {
1337 f |= Qt::ItemIsDropEnabled;
1338 }
1339 } else { // regular file item
1340 if (d->m_dropsAllowed & DropOnAnyFile) {
1341 f |= Qt::ItemIsDropEnabled;
1342 } else if (d->m_dropsAllowed & DropOnLocalExecutable) {
1343 if (!item.localPath().isEmpty()) {
1344 // Desktop file?
1345 if (item.determineMimeType().inherits(QStringLiteral("application/x-desktop"))) {
1346 f |= Qt::ItemIsDropEnabled;
1347 }
1348 // Executable, shell script ... ?
1349 else if (QFileInfo(item.localPath()).isExecutable()) {
1350 f |= Qt::ItemIsDropEnabled;
1351 }
1352 }
1353 }
1354 }
1355 }
1356 }
1357
1358 return f;
1359}
1360
1361bool KDirModel::canFetchMore(const QModelIndex &parent) const
1362{
1363 if (!parent.isValid()) {
1364 return false;
1365 }
1366
1367 // We now have a bool KDirModelNode::m_populated,
1368 // to avoid calling fetchMore more than once on empty dirs.
1369 // But this wastes memory, and how often does someone open and re-open an empty dir in a treeview?
1370 // Maybe we can ask KDirLister "have you listed <url> already"? (to discuss with M. Brade)
1371
1372 KDirModelNode *node = static_cast<KDirModelNode *>(parent.internalPointer());
1373 const KFileItem &item = node->item();
1374 return item.isDir() && !static_cast<KDirModelDirNode *>(node)->isPopulated() && static_cast<KDirModelDirNode *>(node)->m_childNodes.isEmpty();
1375}
1376
1377void KDirModel::fetchMore(const QModelIndex &parent)
1378{
1379 if (!parent.isValid()) {
1380 return;
1381 }
1382
1383 KDirModelNode *parentNode = static_cast<KDirModelNode *>(parent.internalPointer());
1384
1385 KFileItem parentItem = parentNode->item();
1386 Q_ASSERT(!parentItem.isNull());
1387 if (!parentItem.isDir()) {
1388 return;
1389 }
1390 KDirModelDirNode *dirNode = static_cast<KDirModelDirNode *>(parentNode);
1391 if (dirNode->isPopulated()) {
1392 return;
1393 }
1394 dirNode->setPopulated(true);
1395
1396 const QUrl parentUrl = parentItem.url();
1397 d->m_dirLister->openUrl(dirUrl: parentUrl, flags: KDirLister::Keep);
1398}
1399
1400bool KDirModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
1401{
1402 // Not sure we want to implement any drop handling at this level,
1403 // but for sure the default QAbstractItemModel implementation makes no sense for a dir model.
1404 Q_UNUSED(data);
1405 Q_UNUSED(action);
1406 Q_UNUSED(row);
1407 Q_UNUSED(column);
1408 Q_UNUSED(parent);
1409 return false;
1410}
1411
1412void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed)
1413{
1414 d->m_dropsAllowed = dropsAllowed;
1415}
1416
1417void KDirModel::expandToUrl(const QUrl &url)
1418{
1419 // emit expand for each parent and return last parent
1420 KDirModelNode *result = d->expandAllParentsUntil(url: url); // O(depth)
1421
1422 if (!result) { // doesn't seem related to our base url?
1423 qCDebug(category) << url << "does not seem related to our base URL, aborting";
1424 return;
1425 }
1426 if (!result->item().isNull() && result->item().url() == url) {
1427 // We have it already, nothing to do
1428 qCDebug(category) << "we have it already:" << url;
1429 return;
1430 }
1431
1432 d->m_urlsBeingFetched[result].append(t: url);
1433
1434 if (result == d->m_rootNode) {
1435 qCDebug(category) << "Remembering to emit expand after listing the root url";
1436 // the root is fetched by default, so it must be currently being fetched
1437 return;
1438 }
1439
1440 qCDebug(category) << "Remembering to emit expand after listing" << result->item().url();
1441
1442 // start a new fetch to look for the next level down the URL
1443 const QModelIndex parentIndex = d->indexForNode(node: result); // O(n)
1444 Q_ASSERT(parentIndex.isValid());
1445 fetchMore(parent: parentIndex);
1446}
1447
1448bool KDirModel::insertRows(int, int, const QModelIndex &)
1449{
1450 return false;
1451}
1452
1453bool KDirModel::insertColumns(int, int, const QModelIndex &)
1454{
1455 return false;
1456}
1457
1458bool KDirModel::removeRows(int, int, const QModelIndex &)
1459{
1460 return false;
1461}
1462
1463bool KDirModel::removeColumns(int, int, const QModelIndex &)
1464{
1465 return false;
1466}
1467
1468QHash<int, QByteArray> KDirModel::roleNames() const
1469{
1470 auto super = QAbstractItemModel::roleNames();
1471
1472 super[AdditionalRoles::FileItemRole] = "fileItem";
1473 super[AdditionalRoles::ChildCountRole] = "childCount";
1474 super[AdditionalRoles::HasJobRole] = "hasJob";
1475
1476 return super;
1477}
1478
1479#include "moc_kdirmodel.cpp"
1480

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