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 | |
39 | Q_LOGGING_CATEGORY(category, "kf.kio.widgets.kdirmodel" , QtInfoMsg) |
40 | |
41 | class KDirModelNode; |
42 | class KDirModelDirNode; |
43 | |
44 | static 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. |
58 | class KDirModelNode |
59 | { |
60 | public: |
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 | |
114 | private: |
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 |
122 | class KDirModelDirNode : public KDirModelNode |
123 | { |
124 | public: |
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 | |
196 | private: |
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 | |
207 | int 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 | |
217 | class KDirModelPrivate |
218 | { |
219 | public: |
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 | |
315 | KDirModelNode *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 | |
324 | void 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 | |
335 | KDirModelNode *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 |
403 | void 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). |
415 | QModelIndex 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) |
426 | KDirModelNode *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 | |
440 | static 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 | |
456 | KDirModel::KDirModel(QObject *parent) |
457 | : QAbstractItemModel(parent) |
458 | , d(new KDirModelPrivate(this)) |
459 | { |
460 | setDirLister(new KDirLister(this)); |
461 | } |
462 | |
463 | KDirModel::~KDirModel() = default; |
464 | |
465 | void 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 | |
493 | void 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 | |
522 | Qt::DropActions KDirModel::supportedDropActions() const |
523 | { |
524 | return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction | Qt::IgnoreAction; |
525 | } |
526 | |
527 | KDirLister *KDirModel::dirLister() const |
528 | { |
529 | return d->m_dirLister; |
530 | } |
531 | |
532 | void 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 | |
613 | void 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 | |
621 | void 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 | |
710 | void 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. |
770 | void 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 | |
792 | void 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 | |
805 | void 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 | |
825 | void 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 | |
844 | void KDirModel::clearAllPreviews() |
845 | { |
846 | d->clearAllPreviews(dirNode: d->m_rootNode); |
847 | } |
848 | |
849 | void 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 | |
863 | int KDirModel::columnCount(const QModelIndex &) const |
864 | { |
865 | return ColumnCount; |
866 | } |
867 | |
868 | QVariant 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 | |
1009 | void KDirModel::sort(int column, Qt::SortOrder order) |
1010 | { |
1011 | // Not implemented - we should probably use QSortFilterProxyModel instead. |
1012 | QAbstractItemModel::sort(column, order); |
1013 | } |
1014 | |
1015 | bool 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 | |
1067 | int 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 | |
1090 | QModelIndex 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). |
1104 | QModelIndex 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 | |
1121 | void KDirModel::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) |
1122 | { |
1123 | Q_EMIT needSequenceIcon(index, sequenceIndex); |
1124 | } |
1125 | |
1126 | void 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 | |
1144 | bool KDirModel::jobTransfersVisible() const |
1145 | { |
1146 | return d->m_jobTransfersVisible; |
1147 | } |
1148 | |
1149 | QList<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 | |
1178 | QStringList KDirModel::mimeTypes() const |
1179 | { |
1180 | return KUrlMimeData::mimeDataTypes(); |
1181 | } |
1182 | |
1183 | QMimeData *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 |
1213 | KFileItem 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 | |
1225 | QModelIndex KDirModel::indexForItem(const KFileItem &item) const |
1226 | { |
1227 | return indexForUrl(url: item.url()); // O(n) |
1228 | } |
1229 | |
1230 | // url -> index. O(n) |
1231 | QModelIndex 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 | |
1241 | QModelIndex 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 | |
1254 | QVariant KDirModel::(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 | |
1280 | bool 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 | |
1315 | Qt::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 | |
1361 | bool 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 | |
1377 | void 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 | |
1400 | bool 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 | |
1412 | void KDirModel::setDropsAllowed(DropsAllowed dropsAllowed) |
1413 | { |
1414 | d->m_dropsAllowed = dropsAllowed; |
1415 | } |
1416 | |
1417 | void 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 | |
1448 | bool KDirModel::insertRows(int, int, const QModelIndex &) |
1449 | { |
1450 | return false; |
1451 | } |
1452 | |
1453 | bool KDirModel::insertColumns(int, int, const QModelIndex &) |
1454 | { |
1455 | return false; |
1456 | } |
1457 | |
1458 | bool KDirModel::removeRows(int, int, const QModelIndex &) |
1459 | { |
1460 | return false; |
1461 | } |
1462 | |
1463 | bool KDirModel::removeColumns(int, int, const QModelIndex &) |
1464 | { |
1465 | return false; |
1466 | } |
1467 | |
1468 | QHash<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 | |