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