1 /*
2 SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "entitytreemodel_p.h"
8
9 #include "agentmanagerinterface.h"
10 #include "akranges.h"
11 #include "monitor_p.h" // For friend ref/deref
12 #include "servermanager.h"
13
14 #include <KLocalizedString>
15
16 #include "agentmanager.h"
17 #include "agenttype.h"
18 #include "changerecorder.h"
19 #include "collectioncopyjob.h"
20 #include "collectionfetchscope.h"
21 #include "collectionmovejob.h"
22 #include "collectionstatistics.h"
23 #include "collectionstatisticsjob.h"
24 #include "entityhiddenattribute.h"
25 #include "itemcopyjob.h"
26 #include "itemfetchjob.h"
27 #include "itemmodifyjob.h"
28 #include "itemmovejob.h"
29 #include "linkjob.h"
30 #include "monitor.h"
31 #include "private/protocol_p.h"
32 #include "session.h"
33
34 #include "akonadicore_debug.h"
35
36 #include <QElapsedTimer>
37 #include <QIcon>
38 #include <QMessageBox>
39 #include <unordered_map>
40
41 // clazy:excludeall=old-style-connect
42
43 QHash<KJob *, QElapsedTimer> jobTimeTracker;
44
45 Q_LOGGING_CATEGORY(DebugETM, "org.kde.pim.akonadi.ETM", QtInfoMsg)
46
47 using namespace Akonadi;
48 using namespace AkRanges;
49
getFetchType(EntityTreeModel::CollectionFetchStrategy strategy)50 static CollectionFetchJob::Type getFetchType(EntityTreeModel::CollectionFetchStrategy strategy)
51 {
52 switch (strategy) {
53 case EntityTreeModel::FetchFirstLevelChildCollections:
54 return CollectionFetchJob::FirstLevel;
55 case EntityTreeModel::InvisibleCollectionFetch:
56 case EntityTreeModel::FetchCollectionsRecursive:
57 default:
58 break;
59 }
60 return CollectionFetchJob::Recursive;
61 }
62
EntityTreeModelPrivate(EntityTreeModel * parent)63 EntityTreeModelPrivate::EntityTreeModelPrivate(EntityTreeModel *parent)
64 : q_ptr(parent)
65 {
66 // using collection as a parameter of a queued call in runItemFetchJob()
67 qRegisterMetaType<Collection>();
68
69 Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self();
70 QObject::connect(agentManager, SIGNAL(instanceRemoved(Akonadi::AgentInstance)), q_ptr, SLOT(agentInstanceRemoved(Akonadi::AgentInstance)));
71 }
72
~EntityTreeModelPrivate()73 EntityTreeModelPrivate::~EntityTreeModelPrivate()
74 {
75 if (m_needDeleteRootNode) {
76 delete m_rootNode;
77 }
78 m_rootNode = nullptr;
79 }
80
init(Monitor * monitor)81 void EntityTreeModelPrivate::init(Monitor *monitor)
82 {
83 Q_Q(EntityTreeModel);
84 Q_ASSERT(!m_monitor);
85 m_monitor = monitor;
86 // The default is to FetchCollectionsRecursive, so we tell the monitor to fetch collections
87 // That way update signals from the monitor will contain the full collection.
88 // This may be updated if the CollectionFetchStrategy is changed.
89 m_monitor->fetchCollection(true);
90 m_session = m_monitor->session();
91
92 m_rootCollectionDisplayName = QStringLiteral("[*]");
93
94 if (auto cr = qobject_cast<Akonadi::ChangeRecorder *>(m_monitor)) {
95 cr->setChangeRecordingEnabled(false);
96 }
97
98 m_includeStatistics = true;
99 m_monitor->fetchCollectionStatistics(true);
100 m_monitor->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
101
102 q->connect(monitor, SIGNAL(mimeTypeMonitored(QString, bool)), SLOT(monitoredMimeTypeChanged(QString, bool)));
103 q->connect(monitor, SIGNAL(collectionMonitored(Akonadi::Collection, bool)), SLOT(monitoredCollectionsChanged(Akonadi::Collection, bool)));
104 q->connect(monitor, SIGNAL(itemMonitored(Akonadi::Item, bool)), SLOT(monitoredItemsChanged(Akonadi::Item, bool)));
105 q->connect(monitor, SIGNAL(resourceMonitored(QByteArray, bool)), SLOT(monitoredResourcesChanged(QByteArray, bool)));
106
107 // monitor collection changes
108 q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), SLOT(monitoredCollectionChanged(Akonadi::Collection)));
109 q->connect(monitor,
110 SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection)),
111 SLOT(monitoredCollectionAdded(Akonadi::Collection, Akonadi::Collection)));
112 q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), SLOT(monitoredCollectionRemoved(Akonadi::Collection)));
113 q->connect(monitor,
114 SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)),
115 SLOT(monitoredCollectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)));
116
117 // Monitor item changes.
118 q->connect(monitor, SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemAdded(Akonadi::Item, Akonadi::Collection)));
119 q->connect(monitor, SIGNAL(itemChanged(Akonadi::Item, QSet<QByteArray>)), SLOT(monitoredItemChanged(Akonadi::Item, QSet<QByteArray>)));
120 q->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), SLOT(monitoredItemRemoved(Akonadi::Item)));
121 q->connect(monitor,
122 SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)),
123 SLOT(monitoredItemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)));
124
125 q->connect(monitor, SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemLinked(Akonadi::Item, Akonadi::Collection)));
126 q->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemUnlinked(Akonadi::Item, Akonadi::Collection)));
127
128 q->connect(monitor,
129 SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics)),
130 SLOT(monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics)));
131
132 Akonadi::ServerManager *serverManager = Akonadi::ServerManager::self();
133 q->connect(serverManager, SIGNAL(started()), SLOT(serverStarted()));
134
135 fillModel();
136 }
137
prependNode(Node * node)138 void EntityTreeModelPrivate::prependNode(Node *node)
139 {
140 m_childEntities[node->parent].prepend(node);
141 }
142
appendNode(Node * node)143 void EntityTreeModelPrivate::appendNode(Node *node)
144 {
145 m_childEntities[node->parent].append(node);
146 }
147
serverStarted()148 void EntityTreeModelPrivate::serverStarted()
149 {
150 // Don't emit about to be reset. Too late for that
151 endResetModel();
152 }
153
changeFetchState(const Collection & parent)154 void EntityTreeModelPrivate::changeFetchState(const Collection &parent)
155 {
156 Q_Q(EntityTreeModel);
157 const QModelIndex collectionIndex = indexForCollection(parent);
158 if (!collectionIndex.isValid()) {
159 // Because we are called delayed, it is possible that @p parent has been deleted.
160 return;
161 }
162 Q_EMIT q->dataChanged(collectionIndex, collectionIndex);
163 }
164
agentInstanceRemoved(const Akonadi::AgentInstance & instance)165 void EntityTreeModelPrivate::agentInstanceRemoved(const Akonadi::AgentInstance &instance)
166 {
167 Q_Q(EntityTreeModel);
168 if (!instance.type().capabilities().contains(QLatin1String("Resource"))) {
169 return;
170 }
171
172 if (m_rootCollection.isValid()) {
173 if (m_rootCollection != Collection::root()) {
174 if (m_rootCollection.resource() == instance.identifier()) {
175 q->clearAndReset();
176 }
177 return;
178 }
179 const auto &children = m_childEntities[Collection::root().id()];
180 for (const Node *node : children) {
181 Q_ASSERT(node->type == Node::Collection);
182
183 const Collection collection = m_collections[node->id];
184 if (collection.resource() == instance.identifier()) {
185 monitoredCollectionRemoved(collection);
186 }
187 }
188 }
189 }
190
191 static const char s_fetchCollectionId[] = "FetchCollectionId";
192
fetchItems(const Collection & parent)193 void EntityTreeModelPrivate::fetchItems(const Collection &parent)
194 {
195 Q_Q(const EntityTreeModel);
196 Q_ASSERT(parent.isValid());
197 Q_ASSERT(m_collections.contains(parent.id()));
198 // TODO: Use a more specific fetch scope to get only the envelope for mails etc.
199 auto itemFetchJob = new Akonadi::ItemFetchJob(parent, m_session);
200 itemFetchJob->setFetchScope(m_monitor->itemFetchScope());
201 itemFetchJob->fetchScope().setAncestorRetrieval(ItemFetchScope::All);
202 itemFetchJob->fetchScope().setIgnoreRetrievalErrors(true);
203 itemFetchJob->setDeliveryOption(ItemFetchJob::EmitItemsInBatches);
204
205 itemFetchJob->setProperty(s_fetchCollectionId, QVariant(parent.id()));
206
207 if (m_showRootCollection || parent != m_rootCollection) {
208 m_pendingCollectionRetrieveJobs.insert(parent.id());
209
210 // If collections are not in the model, there will be no valid index for them.
211 if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)) {
212 // We need to invoke this delayed because we would otherwise be emitting a sequence like
213 // - beginInsertRows
214 // - dataChanged
215 // - endInsertRows
216 // which would confuse proxies.
217 QMetaObject::invokeMethod(const_cast<EntityTreeModel *>(q), "changeFetchState", Qt::QueuedConnection, Q_ARG(Akonadi::Collection, parent));
218 }
219 }
220
221 q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this, parentId = parent.id()](const Item::List &items) {
222 itemsFetched(parentId, items);
223 });
224 q->connect(itemFetchJob, &ItemFetchJob::result, q, [this, parentId = parent.id()](KJob *job) {
225 itemFetchJobDone(parentId, job);
226 });
227 qCDebug(DebugETM) << "collection:" << parent.name();
228 jobTimeTracker[itemFetchJob].start();
229 }
230
fetchCollections(Akonadi::CollectionFetchJob * job)231 void EntityTreeModelPrivate::fetchCollections(Akonadi::CollectionFetchJob *job)
232 {
233 Q_Q(EntityTreeModel);
234
235 job->fetchScope().setListFilter(m_listFilter);
236 job->fetchScope().setContentMimeTypes(m_monitor->mimeTypesMonitored());
237 m_pendingCollectionFetchJobs.insert(static_cast<KJob *>(job));
238
239 if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
240 // This is invisible fetch, so no model signals are emitted
241 q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const Collection::List &collections) {
242 for (const auto &collection : collections) {
243 if (isHidden(collection)) {
244 continue;
245 }
246 m_collections.insert(collection.id(), collection);
247 prependNode(new Node{Node::Collection, collection.id(), -1});
248 fetchItems(collection);
249 }
250 });
251 } else {
252 job->fetchScope().setIncludeStatistics(m_includeStatistics);
253 job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
254 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsFetched(Akonadi::Collection::List)));
255 }
256 q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
257
258 jobTimeTracker[job].start();
259 }
260
fetchCollections(const Collection::List & collections,CollectionFetchJob::Type type)261 void EntityTreeModelPrivate::fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type)
262 {
263 fetchCollections(new CollectionFetchJob(collections, type, m_session));
264 }
265
fetchCollections(const Collection & collection,CollectionFetchJob::Type type)266 void EntityTreeModelPrivate::fetchCollections(const Collection &collection, CollectionFetchJob::Type type)
267 {
268 Q_ASSERT(collection.isValid());
269 auto job = new CollectionFetchJob(collection, type, m_session);
270 fetchCollections(job);
271 }
272
273 namespace Akonadi
274 {
isHiddenImpl(const T & entity,Node::Type type) const275 template<typename T> inline bool EntityTreeModelPrivate::isHiddenImpl(const T &entity, Node::Type type) const
276 {
277 if (m_showSystemEntities) {
278 return false;
279 }
280
281 if (type == Node::Collection && entity.id() == m_rootCollection.id()) {
282 return false;
283 }
284
285 // entity.hasAttribute<EntityHiddenAttribute>() does not compile w/ GCC for
286 // some reason
287 if (entity.hasAttribute(EntityHiddenAttribute().type())) {
288 return true;
289 }
290
291 const Collection parent = entity.parentCollection();
292 if (parent.isValid()) {
293 return isHiddenImpl(parent, Node::Collection);
294 }
295
296 return false;
297 }
298
299 } // namespace Akonadi
300
isHidden(const Akonadi::Collection & collection) const301 bool EntityTreeModelPrivate::isHidden(const Akonadi::Collection &collection) const
302 {
303 return isHiddenImpl(collection, Node::Collection);
304 }
305
isHidden(const Akonadi::Item & item) const306 bool EntityTreeModelPrivate::isHidden(const Akonadi::Item &item) const
307 {
308 return isHiddenImpl(item, Node::Item);
309 }
310
getChildren(Collection::Id parent,const std::unordered_map<Collection::Id,Collection::Id> & childParentMap)311 static QSet<Collection::Id> getChildren(Collection::Id parent, const std::unordered_map<Collection::Id, Collection::Id> &childParentMap)
312 {
313 QSet<Collection::Id> children;
314 for (const auto &[childId, parentId] : childParentMap) {
315 if (parentId == parent) {
316 children.insert(childId);
317 children.unite(getChildren(childId, childParentMap));
318 }
319 }
320 return children;
321 }
322
collectionsFetched(const Akonadi::Collection::List & collections)323 void EntityTreeModelPrivate::collectionsFetched(const Akonadi::Collection::List &collections)
324 {
325 Q_Q(EntityTreeModel);
326 QElapsedTimer t;
327 t.start();
328
329 QVectorIterator<Akonadi::Collection> it(collections);
330
331 QHash<Collection::Id, Collection> collectionsToInsert;
332
333 while (it.hasNext()) {
334 const Collection collection = it.next();
335 const Collection::Id collectionId = collection.id();
336 if (isHidden(collection)) {
337 continue;
338 }
339
340 auto collectionIt = m_collections.find(collectionId);
341 if (collectionIt != m_collections.end()) {
342 // This is probably the result of a parent of a previous collection already being in the model.
343 // Replace the dummy collection with the real one and move on.
344
345 // This could also be the result of a monitor signal having already inserted the collection
346 // into this model. There's no way to tell, so we just emit dataChanged.
347 *collectionIt = collection;
348
349 const QModelIndex collectionIndex = indexForCollection(collection);
350 dataChanged(collectionIndex, collectionIndex);
351 Q_EMIT q->collectionFetched(collectionId);
352 continue;
353 }
354
355 // If we're monitoring collections somewhere in the tree we need to retrieve their ancestors now
356 if (collection.parentCollection() != m_rootCollection && m_monitor->collectionsMonitored().contains(collection)) {
357 retrieveAncestors(collection, false);
358 }
359
360 collectionsToInsert.insert(collectionId, collection);
361 }
362
363 // Build a list of subtrees to insert, with the root of the subtree on the left, and the complete subtree including root on the right
364 std::unordered_map<Collection::Id, QSet<Collection::Id>> subTreesToInsert;
365 {
366 // Build a child-parent map that allows us to build the subtrees afterwards
367 std::unordered_map<Collection::Id, Collection::Id> childParentMap;
368 const auto initialCollectionsToInsert(collectionsToInsert);
369 for (const auto &col : initialCollectionsToInsert) {
370 childParentMap.insert({col.id(), col.parentCollection().id()});
371
372 // Complete the subtree up to the last known parent
373 Collection parent = col.parentCollection();
374 while (parent.isValid() && parent != m_rootCollection && !m_collections.contains(parent.id())) {
375 childParentMap.insert({parent.id(), parent.parentCollection().id()});
376
377 if (!collectionsToInsert.contains(parent.id())) {
378 collectionsToInsert.insert(parent.id(), parent);
379 }
380 parent = parent.parentCollection();
381 }
382 }
383
384 QSet<Collection::Id> parents;
385
386 // Find toplevel parents of the subtrees
387 for (const auto &[childId, parentId] : childParentMap) {
388 // The child has a parent without parent (it's a toplevel node that is not yet in m_collections)
389 if (childParentMap.find(parentId) == childParentMap.cend()) {
390 Q_ASSERT(!m_collections.contains(childId));
391 parents.insert(childId);
392 }
393 }
394
395 // Find children of each subtree
396 for (const auto parentId : parents) {
397 QSet<Collection::Id> children;
398 // We add the parent itself as well so it can be inserted below as part of the same loop
399 children << parentId;
400 children += getChildren(parentId, childParentMap);
401 subTreesToInsert.insert_or_assign(parentId, std::move(children));
402 }
403 }
404
405 const int row = 0;
406
407 for (const auto &[topCollectionId, subtree] : subTreesToInsert) {
408 qCDebug(DebugETM) << "Subtree: " << topCollectionId << subtree;
409
410 Q_ASSERT(!m_collections.contains(topCollectionId));
411 Collection topCollection = collectionsToInsert.value(topCollectionId);
412 Q_ASSERT(topCollection.isValid());
413
414 // The toplevels parent must already be part of the model
415 Q_ASSERT(m_collections.contains(topCollection.parentCollection().id()));
416 const QModelIndex parentIndex = indexForCollection(topCollection.parentCollection());
417
418 q->beginInsertRows(parentIndex, row, row);
419 Q_ASSERT(!subtree.empty());
420
421 for (const auto collectionId : subtree) {
422 const Collection collection = collectionsToInsert.take(collectionId);
423 Q_ASSERT(collection.isValid());
424
425 m_collections.insert(collectionId, collection);
426
427 Q_ASSERT(collection.parentCollection().isValid());
428 prependNode(new Node{Node::Collection, collectionId, collection.parentCollection().id()});
429 }
430 q->endInsertRows();
431
432 if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
433 for (const auto collectionId : subtree) {
434 const auto col = m_collections.value(collectionId);
435 if (!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedCollection(col)) {
436 fetchItems(col);
437 } else {
438 // Consider collections that don't contain relevant mimetypes to be populated
439 m_populatedCols.insert(collectionId);
440 Q_EMIT q_ptr->collectionPopulated(collectionId);
441 const auto idx = indexForCollection(Collection(collectionId));
442 Q_ASSERT(idx.isValid());
443 dataChanged(idx, idx);
444 }
445 }
446 }
447 }
448 }
449
450 // Used by entitytreemodeltest
itemsFetched(const Akonadi::Item::List & items)451 void EntityTreeModelPrivate::itemsFetched(const Akonadi::Item::List &items)
452 {
453 Q_Q(EntityTreeModel);
454 const auto collectionId = q->sender()->property(s_fetchCollectionId).value<Collection::Id>();
455 itemsFetched(collectionId, items);
456 }
457
itemsFetched(const Collection::Id collectionId,const Akonadi::Item::List & items)458 void EntityTreeModelPrivate::itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items)
459 {
460 Q_Q(EntityTreeModel);
461
462 if (!m_collections.contains(collectionId)) {
463 qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items";
464 return;
465 }
466
467 const Collection collection = m_collections.value(collectionId);
468
469 Q_ASSERT(collection.isValid());
470
471 // if there are any items at all, remove from set of collections known to be empty
472 if (!items.isEmpty()) {
473 m_collectionsWithoutItems.remove(collectionId);
474 }
475
476 Item::List itemsToInsert;
477 for (const auto &item : items) {
478 if (isHidden(item)) {
479 continue;
480 }
481
482 if ((!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedItem(item))) {
483 // When listing virtual collections we might get results for items which are already in
484 // the model if their concrete collection has already been listed.
485 // In that case the collectionId should be different though.
486
487 // As an additional complication, new items might be both part of fetch job results and
488 // part of monitor notifications. We only insert items which are not already in the model
489 // considering their (possibly virtual) parent.
490 bool isNewItem = true;
491 auto itemIt = m_items.find(item.id());
492 if (itemIt != m_items.end()) {
493 const Akonadi::Collection::List parents = getParentCollections(item);
494 for (const Akonadi::Collection &parent : parents) {
495 if (parent.id() == collectionId) {
496 qCWarning(AKONADICORE_LOG) << "Fetched an item which is already in the model, id=" << item.id() << "collection id=" << collectionId;
497 // Update it in case the revision changed;
498 itemIt->value.apply(item);
499 isNewItem = false;
500 break;
501 }
502 }
503 }
504
505 if (isNewItem) {
506 itemsToInsert << item;
507 }
508 }
509 }
510
511 if (!itemsToInsert.isEmpty()) {
512 const Collection::Id colId = m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch ? m_rootCollection.id()
513 : m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections ? m_rootCollection.id()
514 : collectionId;
515 const int startRow = m_childEntities.value(colId).size();
516
517 Q_ASSERT(m_collections.contains(colId));
518
519 const QModelIndex parentIndex = indexForCollection(m_collections.value(colId));
520 q->beginInsertRows(parentIndex, startRow, startRow + itemsToInsert.size() - 1);
521 for (const Item &item : std::as_const(itemsToInsert)) {
522 const Item::Id itemId = item.id();
523 m_items.ref(itemId, item);
524
525 m_childEntities[colId].append(new Node{Node::Item, itemId, collectionId});
526 }
527 q->endInsertRows();
528 }
529 }
530
monitoredMimeTypeChanged(const QString & mimeType,bool monitored)531 void EntityTreeModelPrivate::monitoredMimeTypeChanged(const QString &mimeType, bool monitored)
532 {
533 beginResetModel();
534 if (monitored) {
535 m_mimeChecker.addWantedMimeType(mimeType);
536 } else {
537 m_mimeChecker.removeWantedMimeType(mimeType);
538 }
539 endResetModel();
540 }
541
monitoredCollectionsChanged(const Akonadi::Collection & collection,bool monitored)542 void EntityTreeModelPrivate::monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored)
543 {
544 if (monitored) {
545 const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy);
546 fetchCollections(collection, CollectionFetchJob::Base);
547 fetchCollections(collection, fetchType);
548 } else {
549 // If a collection is dereferenced and no longer explicitly monitored it might still match other filters
550 if (!shouldBePartOfModel(collection)) {
551 monitoredCollectionRemoved(collection);
552 }
553 }
554 }
555
monitoredItemsChanged(const Akonadi::Item & item,bool monitored)556 void EntityTreeModelPrivate::monitoredItemsChanged(const Akonadi::Item &item, bool monitored)
557 {
558 Q_UNUSED(item)
559 Q_UNUSED(monitored)
560 beginResetModel();
561 endResetModel();
562 }
563
monitoredResourcesChanged(const QByteArray & resource,bool monitored)564 void EntityTreeModelPrivate::monitoredResourcesChanged(const QByteArray &resource, bool monitored)
565 {
566 Q_UNUSED(resource)
567 Q_UNUSED(monitored)
568 beginResetModel();
569 endResetModel();
570 }
571
retrieveAncestors(const Akonadi::Collection & collection,bool insertBaseCollection)572 bool EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection)
573 {
574 Q_Q(EntityTreeModel);
575
576 Collection parentCollection = collection.parentCollection();
577
578 Q_ASSERT(parentCollection.isValid());
579 Q_ASSERT(parentCollection != Collection::root());
580
581 Collection::List ancestors;
582
583 while (parentCollection != Collection::root() && !m_collections.contains(parentCollection.id())) {
584 // Put a temporary node in the tree later.
585 ancestors.prepend(parentCollection);
586
587 parentCollection = parentCollection.parentCollection();
588 // If we got here through Collection added notification, the parent chain may be incomplete
589 // and if the model is still populating or the collection belongs to a yet-unknown subtree
590 // this will break here
591 if (!parentCollection.isValid()) {
592 break;
593 }
594 }
595 // if m_rootCollection is Collection::root(), we always have common ancestor and do the retrieval
596 // if we traversed up to Collection::root() but are looking at a subtree only (m_rootCollection != Collection::root())
597 // we have no common ancestor, and we don't have to retrieve anything
598 if (parentCollection == Collection::root() && m_rootCollection != Collection::root()) {
599 return true;
600 }
601
602 if (ancestors.isEmpty() && !insertBaseCollection) {
603 // Nothing to do, avoid emitting insert signals
604 return true;
605 }
606
607 CollectionFetchJob *job = nullptr;
608 // We were unable to reach the top of the tree due to an incomplete ancestor chain, we will have
609 // to retrieve it from the server.
610 if (!parentCollection.isValid()) {
611 if (insertBaseCollection) {
612 job = new CollectionFetchJob(collection, CollectionFetchJob::Recursive, m_session);
613 } else {
614 job = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::Recursive, m_session);
615 }
616 } else if (!ancestors.isEmpty()) {
617 // Fetch the real ancestors
618 job = new CollectionFetchJob(ancestors, CollectionFetchJob::Base, m_session);
619 }
620
621 if (job) {
622 job->fetchScope().setListFilter(m_listFilter);
623 job->fetchScope().setIncludeStatistics(m_includeStatistics);
624 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(ancestorsFetched(Akonadi::Collection::List)));
625 q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
626 }
627
628 if (!parentCollection.isValid()) {
629 // We can't proceed to insert the fake collections to complete the tree because
630 // we do not have the complete ancestor chain. However, once the fetch job is
631 // finished the tree will be populated accordingly.
632 return false;
633 }
634
635 // Q_ASSERT( parentCollection != m_rootCollection );
636 const QModelIndex parent = indexForCollection(parentCollection);
637
638 // Still prepending all collections for now.
639 int row = 0;
640
641 // Although we insert several Collections here, we only need to notify though the model
642 // about the top-level one. The rest will be found automatically by the view.
643 q->beginInsertRows(parent, row, row);
644
645 for (const auto &ancestor : std::as_const(ancestors)) {
646 Q_ASSERT(ancestor.parentCollection().isValid());
647 m_collections.insert(ancestor.id(), ancestor);
648
649 prependNode(new Node{Node::Collection, ancestor.id(), ancestor.parentCollection().id()});
650 }
651
652 if (insertBaseCollection) {
653 m_collections.insert(collection.id(), collection);
654 // Can't just use parentCollection because that doesn't necessarily refer to collection.
655 prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()});
656 }
657
658 q->endInsertRows();
659
660 return true;
661 }
662
ancestorsFetched(const Akonadi::Collection::List & collectionList)663 void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List &collectionList)
664 {
665 for (const Collection &collection : collectionList) {
666 m_collections[collection.id()] = collection;
667
668 const QModelIndex index = indexForCollection(collection);
669 Q_ASSERT(index.isValid());
670 dataChanged(index, index);
671 }
672 }
673
insertCollection(const Akonadi::Collection & collection,const Akonadi::Collection & parent)674 void EntityTreeModelPrivate::insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
675 {
676 Q_ASSERT(collection.isValid());
677 Q_ASSERT(parent.isValid());
678
679 Q_Q(EntityTreeModel);
680
681 const int row = 0;
682 const QModelIndex parentIndex = indexForCollection(parent);
683 q->beginInsertRows(parentIndex, row, row);
684 m_collections.insert(collection.id(), collection);
685 prependNode(new Node{Node::Collection, collection.id(), parent.id()});
686 q->endInsertRows();
687 }
688
hasChildCollection(const Collection & collection) const689 bool EntityTreeModelPrivate::hasChildCollection(const Collection &collection) const
690 {
691 const auto &children = m_childEntities[collection.id()];
692 for (const Node *node : children) {
693 if (node->type == Node::Collection) {
694 const Collection subcol = m_collections[node->id];
695 if (shouldBePartOfModel(subcol)) {
696 return true;
697 }
698 }
699 }
700 return false;
701 }
702
isAncestorMonitored(const Collection & collection) const703 bool EntityTreeModelPrivate::isAncestorMonitored(const Collection &collection) const
704 {
705 Akonadi::Collection parent = collection.parentCollection();
706 while (parent.isValid()) {
707 if (m_monitor->collectionsMonitored().contains(parent)) {
708 return true;
709 }
710 parent = parent.parentCollection();
711 }
712 return false;
713 }
714
shouldBePartOfModel(const Collection & collection) const715 bool EntityTreeModelPrivate::shouldBePartOfModel(const Collection &collection) const
716 {
717 if (isHidden(collection)) {
718 return false;
719 }
720
721 // We want a parent collection if it has at least one child that matches the
722 // wanted mimetype
723 if (hasChildCollection(collection)) {
724 return true;
725 }
726
727 // Explicitly monitored collection
728 if (m_monitor->collectionsMonitored().contains(collection)) {
729 return true;
730 }
731
732 // We're explicitly monitoring collections, but didn't match the filter
733 if (!m_mimeChecker.hasWantedMimeTypes() && !m_monitor->collectionsMonitored().isEmpty()) {
734 // The collection should be included if one of the parents is monitored
735 return isAncestorMonitored(collection);
736 }
737
738 // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we
739 // only get the ones we're interested in from the job, we have to filter on collections received through signals too.
740 if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedCollection(collection)) {
741 return false;
742 }
743
744 if (m_listFilter == CollectionFetchScope::Enabled) {
745 if (!collection.enabled()) {
746 return false;
747 }
748 } else if (m_listFilter == CollectionFetchScope::Display) {
749 if (!collection.shouldList(Collection::ListDisplay)) {
750 return false;
751 }
752 } else if (m_listFilter == CollectionFetchScope::Sync) {
753 if (!collection.shouldList(Collection::ListSync)) {
754 return false;
755 }
756 } else if (m_listFilter == CollectionFetchScope::Index) {
757 if (!collection.shouldList(Collection::ListIndex)) {
758 return false;
759 }
760 }
761
762 return true;
763 }
764
removeChildEntities(Collection::Id collectionId)765 void EntityTreeModelPrivate::removeChildEntities(Collection::Id collectionId)
766 {
767 const QList<Node *> childList = m_childEntities.value(collectionId);
768 for (const Node *node : childList) {
769 if (node->type == Node::Item) {
770 m_items.unref(node->id);
771 } else {
772 removeChildEntities(node->id);
773 m_collections.remove(node->id);
774 m_populatedCols.remove(node->id);
775 }
776 }
777
778 qDeleteAll(m_childEntities.take(collectionId));
779 }
780
childCollectionNames(const Collection & collection) const781 QStringList EntityTreeModelPrivate::childCollectionNames(const Collection &collection) const
782 {
783 return m_childEntities[collection.id()] | Views::filter([](const Node *node) {
784 return node->type == Node::Collection;
785 })
786 | Views::transform([this](const Node *node) {
787 return m_collections.value(node->id).name();
788 })
789 | Actions::toQList;
790 }
791
monitoredCollectionAdded(const Akonadi::Collection & collection,const Akonadi::Collection & parent)792 void EntityTreeModelPrivate::monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
793 {
794 // If the resource is removed while populating the model with it, we might still
795 // get some monitor signals. These stale/out-of-order signals can't be completely eliminated
796 // in the akonadi server due to implementation details, so we also handle such signals in the model silently
797 // in all the monitored slots.
798 // Stephen Kelly, 28, July 2009
799
800 // If a fetch job is started and a collection is added to akonadi after the fetch job is started, the
801 // new collection will be added to the fetch job results. It will also be notified through the monitor.
802 // We return early here in that case.
803 if (m_collections.contains(collection.id())) {
804 return;
805 }
806
807 // If the resource is explicitly monitored all other checks are skipped. topLevelCollectionsFetched still checks the hidden attribute.
808 if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && collection.parentCollection() == Collection::root()) {
809 topLevelCollectionsFetched({collection});
810 return;
811 }
812
813 if (!shouldBePartOfModel(collection)) {
814 return;
815 }
816
817 if (!m_collections.contains(parent.id())) {
818 // The collection we're interested in is contained in a collection we're not interested in.
819 // We download the ancestors of the collection we're interested in to complete the tree.
820 if (collection != Collection::root()) {
821 if (!retrieveAncestors(collection)) {
822 return;
823 }
824 }
825 } else {
826 insertCollection(collection, parent);
827 }
828
829 if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
830 fetchItems(collection);
831 }
832 }
833
monitoredCollectionRemoved(const Akonadi::Collection & collection)834 void EntityTreeModelPrivate::monitoredCollectionRemoved(const Akonadi::Collection &collection)
835 {
836 // if an explicitly monitored collection is removed, we would also have to remove collections which were included to show it (as in the move case)
837 if ((collection == m_rootCollection) || m_monitor->collectionsMonitored().contains(collection)) {
838 beginResetModel();
839 endResetModel();
840 return;
841 }
842
843 Collection::Id parentId = collection.parentCollection().id();
844 if (parentId < 0) {
845 parentId = -1;
846 }
847
848 if (!m_collections.contains(parentId)) {
849 return;
850 }
851
852 // This may be a signal for a collection we've already removed by removing its ancestor.
853 // Or the collection may have been hidden.
854 if (!m_collections.contains(collection.id())) {
855 return;
856 }
857
858 Q_Q(EntityTreeModel);
859
860 Q_ASSERT(m_childEntities.contains(parentId));
861
862 const int row = indexOf<Node::Collection>(m_childEntities.value(parentId), collection.id());
863 Q_ASSERT(row >= 0);
864
865 Q_ASSERT(m_collections.contains(parentId));
866 const Collection parentCollection = m_collections.value(parentId);
867
868 m_populatedCols.remove(collection.id());
869
870 const QModelIndex parentIndex = indexForCollection(parentCollection);
871
872 q->beginRemoveRows(parentIndex, row, row);
873 // Delete all descendant collections and items.
874 removeChildEntities(collection.id());
875 // Remove deleted collection from its parent.
876 delete m_childEntities[parentId].takeAt(row);
877 // Remove deleted collection itself.
878 m_collections.remove(collection.id());
879 q->endRemoveRows();
880
881 // After removing a collection, check whether it's parent should be removed too
882 if (!shouldBePartOfModel(parentCollection)) {
883 monitoredCollectionRemoved(parentCollection);
884 }
885 }
886
monitoredCollectionMoved(const Akonadi::Collection & collection,const Akonadi::Collection & sourceCollection,const Akonadi::Collection & destCollection)887 void EntityTreeModelPrivate::monitoredCollectionMoved(const Akonadi::Collection &collection,
888 const Akonadi::Collection &sourceCollection,
889 const Akonadi::Collection &destCollection)
890 {
891 if (isHidden(collection)) {
892 return;
893 }
894
895 if (isHidden(sourceCollection)) {
896 if (isHidden(destCollection)) {
897 return;
898 }
899
900 monitoredCollectionAdded(collection, destCollection);
901 return;
902 } else if (isHidden(destCollection)) {
903 monitoredCollectionRemoved(collection);
904 return;
905 }
906
907 if (!m_collections.contains(collection.id())) {
908 return;
909 }
910
911 if (m_monitor->collectionsMonitored().contains(collection)) {
912 // if we don't reset here, we would have to make sure that destination collection is actually available,
913 // and remove the sources parents if they were only included as parents of the moved collection
914 beginResetModel();
915 endResetModel();
916 return;
917 }
918
919 Q_Q(EntityTreeModel);
920
921 const QModelIndex srcParentIndex = indexForCollection(sourceCollection);
922 const QModelIndex destParentIndex = indexForCollection(destCollection);
923
924 Q_ASSERT(collection.parentCollection().isValid());
925 Q_ASSERT(destCollection.isValid());
926 Q_ASSERT(collection.parentCollection() == destCollection);
927
928 const int srcRow = indexOf<Node::Collection>(m_childEntities.value(sourceCollection.id()), collection.id());
929 const int destRow = 0; // Prepend collections
930
931 if (!q->beginMoveRows(srcParentIndex, srcRow, srcRow, destParentIndex, destRow)) {
932 qCWarning(AKONADICORE_LOG) << "Cannot move collection" << collection.id() << " from collection" << sourceCollection.id() << "to" << destCollection.id();
933 return;
934 }
935
936 Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow);
937 // collection has the correct parentCollection etc. We need to set it on the
938 // internal data structure to not corrupt things.
939 m_collections.insert(collection.id(), collection);
940 node->parent = destCollection.id();
941 m_childEntities[destCollection.id()].prepend(node);
942 q->endMoveRows();
943 }
944
monitoredCollectionChanged(const Akonadi::Collection & collection)945 void EntityTreeModelPrivate::monitoredCollectionChanged(const Akonadi::Collection &collection)
946 {
947 if (!m_collections.contains(collection.id())) {
948 // This can happen if
949 // * we get a change notification after removing the collection.
950 // * a collection of a non-monitored mimetype is changed elsewhere. Monitor does not
951 // filter by content mimetype of Collections so we get notifications for all of them.
952
953 // We might match the filter now, retry adding the collection
954 monitoredCollectionAdded(collection, collection.parentCollection());
955 return;
956 }
957
958 if (!shouldBePartOfModel(collection)) {
959 monitoredCollectionRemoved(collection);
960 return;
961 }
962
963 m_collections[collection.id()] = collection;
964
965 if (!m_showRootCollection && collection == m_rootCollection) {
966 // If the root of the model is not Collection::root it might be modified.
967 // But it doesn't exist in the accessible model structure, so we need to early return
968 return;
969 }
970
971 const QModelIndex index = indexForCollection(collection);
972 Q_ASSERT(index.isValid());
973 dataChanged(index, index);
974 }
975
monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id,const Akonadi::CollectionStatistics & statistics)976 void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics)
977 {
978 if (!m_collections.contains(id)) {
979 return;
980 }
981
982 m_collections[id].setStatistics(statistics);
983
984 // if the item count becomes 0, add to set of collections we know to be empty
985 // otherwise remove if in there
986 if (statistics.count() == 0) {
987 m_collectionsWithoutItems.insert(id);
988 } else {
989 m_collectionsWithoutItems.remove(id);
990 }
991
992 if (!m_showRootCollection && id == m_rootCollection.id()) {
993 // If the root of the model is not Collection::root it might be modified.
994 // But it doesn't exist in the accessible model structure, so we need to early return
995 return;
996 }
997
998 const QModelIndex index = indexForCollection(m_collections[id]);
999 dataChanged(index, index);
1000 }
1001
monitoredItemAdded(const Akonadi::Item & item,const Akonadi::Collection & collection)1002 void EntityTreeModelPrivate::monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
1003 {
1004 Q_Q(EntityTreeModel);
1005
1006 if (isHidden(item)) {
1007 return;
1008 }
1009
1010 if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) {
1011 qCWarning(AKONADICORE_LOG) << "Got a stale 'added' notification for an item whose collection was already removed." << item.id() << item.remoteId();
1012 return;
1013 }
1014
1015 if (m_items.contains(item.id())) {
1016 return;
1017 }
1018
1019 Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true);
1020
1021 if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) {
1022 return;
1023 }
1024
1025 // Adding items to not yet populated collections would block fetchMore, resulting in only new items showing up in the collection
1026 // This is only a problem with lazy population, otherwise fetchMore is not used at all
1027 if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collection.id())) {
1028 return;
1029 }
1030
1031 int row;
1032 QModelIndex parentIndex;
1033 if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) {
1034 row = m_childEntities.value(collection.id()).size();
1035 parentIndex = indexForCollection(m_collections.value(collection.id()));
1036 } else {
1037 row = q->rowCount();
1038 }
1039 q->beginInsertRows(parentIndex, row, row);
1040 m_items.ref(item.id(), item);
1041 Node *node = new Node{Node::Item, item.id(), collection.id()};
1042 if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) {
1043 m_childEntities[collection.id()].append(node);
1044 } else {
1045 m_childEntities[m_rootCollection.id()].append(node);
1046 }
1047 q->endInsertRows();
1048 }
1049
monitoredItemRemoved(const Akonadi::Item & item,const Akonadi::Collection & parentCollection)1050 void EntityTreeModelPrivate::monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &parentCollection)
1051 {
1052 Q_Q(EntityTreeModel);
1053
1054 if (isHidden(item)) {
1055 return;
1056 }
1057
1058 if ((m_itemPopulation == EntityTreeModel::LazyPopulation)
1059 && !m_populatedCols.contains(parentCollection.isValid() ? parentCollection.id() : item.parentCollection().id())) {
1060 return;
1061 }
1062
1063 const Collection::List parents = getParentCollections(item);
1064 if (parents.isEmpty()) {
1065 return;
1066 }
1067
1068 if (!m_items.contains(item.id())) {
1069 qCWarning(AKONADICORE_LOG) << "Got a stale 'removed' notification for an item which was already removed." << item.id() << item.remoteId();
1070 return;
1071 }
1072
1073 for (const auto &collection : parents) {
1074 Q_ASSERT(m_collections.contains(collection.id()));
1075 Q_ASSERT(m_childEntities.contains(collection.id()));
1076
1077 const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1078 Q_ASSERT(row >= 0);
1079
1080 const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id()));
1081
1082 q->beginRemoveRows(parentIndex, row, row);
1083 m_items.unref(item.id());
1084 delete m_childEntities[collection.id()].takeAt(row);
1085 q->endRemoveRows();
1086 }
1087 }
1088
monitoredItemChanged(const Akonadi::Item & item,const QSet<QByteArray> &)1089 void EntityTreeModelPrivate::monitoredItemChanged(const Akonadi::Item &item, const QSet<QByteArray> & /*unused*/)
1090 {
1091 if (isHidden(item)) {
1092 return;
1093 }
1094
1095 if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) {
1096 return;
1097 }
1098
1099 auto itemIt = m_items.find(item.id());
1100 if (itemIt == m_items.end()) {
1101 qCWarning(AKONADICORE_LOG) << "Got a stale 'changed' notification for an item which was already removed." << item.id() << item.remoteId();
1102 return;
1103 }
1104
1105 itemIt->value.apply(item);
1106 // Notifications about itemChange are always dispatched for real collection
1107 // and also all virtual collections the item belongs to. In order to preserve
1108 // the original storage collection when we need to have special handling for
1109 // notifications for virtual collections
1110 if (item.parentCollection().isVirtual()) {
1111 const Collection originalParent = itemIt->value.parentCollection();
1112 itemIt->value.setParentCollection(originalParent);
1113 }
1114
1115 const QModelIndexList indexes = indexesForItem(item);
1116 for (const QModelIndex &index : indexes) {
1117 if (index.isValid()) {
1118 dataChanged(index, index);
1119 } else {
1120 qCWarning(AKONADICORE_LOG) << "item has invalid index:" << item.id() << item.remoteId();
1121 }
1122 }
1123 }
1124
monitoredItemMoved(const Akonadi::Item & item,const Akonadi::Collection & sourceCollection,const Akonadi::Collection & destCollection)1125 void EntityTreeModelPrivate::monitoredItemMoved(const Akonadi::Item &item,
1126 const Akonadi::Collection &sourceCollection,
1127 const Akonadi::Collection &destCollection)
1128 {
1129 if (isHidden(item)) {
1130 return;
1131 }
1132
1133 if (isHidden(sourceCollection)) {
1134 if (isHidden(destCollection)) {
1135 return;
1136 }
1137
1138 monitoredItemAdded(item, destCollection);
1139 return;
1140 } else if (isHidden(destCollection)) {
1141 monitoredItemRemoved(item, sourceCollection);
1142 return;
1143 } else {
1144 monitoredItemRemoved(item, sourceCollection);
1145 monitoredItemAdded(item, destCollection);
1146 return;
1147 }
1148 // "Temporarily" commented out as it's likely the best course to
1149 // avoid the dreaded "reset storm" (or layoutChanged storm). The
1150 // whole itemMoved idea is great but not practical until all the
1151 // other proxy models play nicely with it, right now they just
1152 // transform moved signals in layout changed, which explodes into
1153 // a reset of the source model inside of the message list (ouch!)
1154 #if 0
1155 if (!m_items.contains(item.id())) {
1156 qCWarning(AKONADICORE_LOG) << "Got a stale 'moved' notification for an item which was already removed." << item.id() << item.remoteId();
1157 return;
1158 }
1159
1160 Q_ASSERT(m_collections.contains(sourceCollection.id()));
1161 Q_ASSERT(m_collections.contains(destCollection.id()));
1162
1163 const QModelIndex srcIndex = indexForCollection(sourceCollection);
1164 const QModelIndex destIndex = indexForCollection(destCollection);
1165
1166 // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes?
1167
1168 const Item::Id itemId = item.id();
1169
1170 const int srcRow = indexOf<Node::Item>(m_childEntities.value(sourceCollection.id()), itemId);
1171 const int destRow = q->rowCount(destIndex);
1172
1173 Q_ASSERT(srcRow >= 0);
1174 Q_ASSERT(destRow >= 0);
1175 if (!q->beginMoveRows(srcIndex, srcRow, srcRow, destIndex, destRow)) {
1176 qCWarning(AKONADICORE_LOG) << "Invalid move";
1177 return;
1178 }
1179
1180 Q_ASSERT(m_childEntities.contains(sourceCollection.id()));
1181 Q_ASSERT(m_childEntities[sourceCollection.id()].size() > srcRow);
1182
1183 Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow);
1184 m_items.insert(item.id(), item);
1185 node->parent = destCollection.id();
1186 m_childEntities[destCollection.id()].append(node);
1187 q->endMoveRows();
1188 #endif
1189 }
1190
monitoredItemLinked(const Akonadi::Item & item,const Akonadi::Collection & collection)1191 void EntityTreeModelPrivate::monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection)
1192 {
1193 Q_Q(EntityTreeModel);
1194
1195 if (isHidden(item)) {
1196 return;
1197 }
1198
1199 const Collection::Id collectionId = collection.id();
1200 const Item::Id itemId = item.id();
1201
1202 if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) {
1203 qCWarning(AKONADICORE_LOG) << "Got a stale 'linked' notification for an item whose collection was already removed." << item.id() << item.remoteId();
1204 return;
1205 }
1206
1207 Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collectionId) : true);
1208
1209 if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) {
1210 return;
1211 }
1212
1213 // Adding items to not yet populated collections would block fetchMore, resullting in only new items showing up in the collection
1214 // This is only a problem with lazy population, otherwise fetchMore is not used at all
1215 if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collectionId)) {
1216 return;
1217 }
1218
1219 QList<Node *> &collectionEntities = m_childEntities[collectionId];
1220
1221 const int existingPosition = indexOf<Node::Item>(collectionEntities, itemId);
1222
1223 if (existingPosition > 0) {
1224 qCWarning(AKONADICORE_LOG) << "Item with id " << itemId << " already in virtual collection with id " << collectionId;
1225 return;
1226 }
1227
1228 const int row = collectionEntities.size();
1229
1230 const QModelIndex parentIndex = indexForCollection(m_collections.value(collectionId));
1231
1232 q->beginInsertRows(parentIndex, row, row);
1233 m_items.ref(itemId, item);
1234 collectionEntities.append(new Node{Node::Item, itemId, collectionId});
1235 q->endInsertRows();
1236 }
1237
monitoredItemUnlinked(const Akonadi::Item & item,const Akonadi::Collection & collection)1238 void EntityTreeModelPrivate::monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection)
1239 {
1240 Q_Q(EntityTreeModel);
1241
1242 if (isHidden(item)) {
1243 return;
1244 }
1245
1246 if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) {
1247 return;
1248 }
1249
1250 if (!m_items.contains(item.id())) {
1251 qCWarning(AKONADICORE_LOG) << "Got a stale 'unlinked' notification for an item which was already removed." << item.id() << item.remoteId();
1252 return;
1253 }
1254
1255 Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true);
1256 const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1257 if (row < 0 || row >= m_childEntities[collection.id()].size()) {
1258 qCWarning(AKONADICORE_LOG) << "couldn't find index of unlinked item " << item.id() << collection.id() << row;
1259 Q_ASSERT(false);
1260 return;
1261 }
1262
1263 const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id()));
1264
1265 q->beginRemoveRows(parentIndex, row, row);
1266 delete m_childEntities[collection.id()].takeAt(row);
1267 m_items.unref(item.id());
1268 q->endRemoveRows();
1269 }
1270
collectionFetchJobDone(KJob * job)1271 void EntityTreeModelPrivate::collectionFetchJobDone(KJob *job)
1272 {
1273 m_pendingCollectionFetchJobs.remove(job);
1274 auto cJob = static_cast<CollectionFetchJob *>(job);
1275 if (job->error()) {
1276 qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << cJob->collections();
1277 return;
1278 }
1279
1280 if (!m_collectionTreeFetched && m_pendingCollectionFetchJobs.isEmpty()) {
1281 m_collectionTreeFetched = true;
1282 Q_EMIT q_ptr->collectionTreeFetched(m_collections | Views::values | Actions::toQVector);
1283 }
1284
1285 qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec";
1286 qCDebug(DebugETM) << "was collection fetch job: collections:" << cJob->collections().size();
1287 if (!cJob->collections().isEmpty()) {
1288 qCDebug(DebugETM) << "first fetched collection:" << cJob->collections().at(0).name();
1289 }
1290 }
1291
itemFetchJobDone(Collection::Id collectionId,KJob * job)1292 void EntityTreeModelPrivate::itemFetchJobDone(Collection::Id collectionId, KJob *job)
1293 {
1294 m_pendingCollectionRetrieveJobs.remove(collectionId);
1295
1296 if (job->error()) {
1297 qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << collectionId;
1298 return;
1299 }
1300 if (!m_collections.contains(collectionId)) {
1301 qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items";
1302 return;
1303 }
1304 auto iJob = static_cast<ItemFetchJob *>(job);
1305 qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec";
1306 qCDebug(DebugETM) << "was item fetch job: items:" << iJob->count();
1307
1308 if (iJob->count() == 0) {
1309 m_collectionsWithoutItems.insert(collectionId);
1310 } else {
1311 m_collectionsWithoutItems.remove(collectionId);
1312 }
1313
1314 m_populatedCols.insert(collectionId);
1315 Q_EMIT q_ptr->collectionPopulated(collectionId);
1316
1317 // If collections are not in the model, there will be no valid index for them.
1318 if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)
1319 && (m_showRootCollection || collectionId != m_rootCollection.id())) {
1320 const QModelIndex index = indexForCollection(Collection(collectionId));
1321 Q_ASSERT(index.isValid());
1322 // To notify about the changed fetch and population state
1323 dataChanged(index, index);
1324 }
1325 }
1326
pasteJobDone(KJob * job)1327 void EntityTreeModelPrivate::pasteJobDone(KJob *job)
1328 {
1329 if (job->error()) {
1330 QString errorMsg;
1331 if (qobject_cast<ItemCopyJob *>(job)) {
1332 errorMsg = i18nc("@info", "Could not copy item: <message>%1</message>", job->errorString());
1333 } else if (qobject_cast<CollectionCopyJob *>(job)) {
1334 errorMsg = i18nc("@info", "Could not copy collection: <message>%1</message>", job->errorString());
1335 } else if (qobject_cast<ItemMoveJob *>(job)) {
1336 errorMsg = i18nc("@info", "Could not move item: <message>%1</message>", job->errorString());
1337 } else if (qobject_cast<CollectionMoveJob *>(job)) {
1338 errorMsg = i18nc("@info", "Could not move collection: <message>%1</message>", job->errorString());
1339 } else if (qobject_cast<LinkJob *>(job)) {
1340 errorMsg = i18nc("@info", "Could not link entity: <message>%1</message>", job->errorString());
1341 }
1342 QMessageBox::critical(nullptr, i18nc("@title:window", "Error"), errorMsg);
1343 }
1344 }
1345
updateJobDone(KJob * job)1346 void EntityTreeModelPrivate::updateJobDone(KJob *job)
1347 {
1348 if (job->error()) {
1349 // TODO: handle job errors
1350 qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString();
1351 }
1352 }
1353
rootFetchJobDone(KJob * job)1354 void EntityTreeModelPrivate::rootFetchJobDone(KJob *job)
1355 {
1356 if (job->error()) {
1357 qCWarning(AKONADICORE_LOG) << job->errorString();
1358 return;
1359 }
1360 auto collectionJob = qobject_cast<CollectionFetchJob *>(job);
1361 const Collection::List list = collectionJob->collections();
1362
1363 Q_ASSERT(list.size() == 1);
1364 m_rootCollection = list.first();
1365 startFirstListJob();
1366 }
1367
startFirstListJob()1368 void EntityTreeModelPrivate::startFirstListJob()
1369 {
1370 Q_Q(EntityTreeModel);
1371
1372 if (!m_collections.isEmpty()) {
1373 return;
1374 }
1375
1376 // Even if the root collection is the invalid collection, we still need to start
1377 // the first list job with Collection::root.
1378 auto node = new Node{Node::Collection, m_rootCollection.id(), -1};
1379 if (m_showRootCollection) {
1380 // Notify the outside that we're putting collection::root into the model.
1381 q->beginInsertRows(QModelIndex(), 0, 0);
1382 m_collections.insert(m_rootCollection.id(), m_rootCollection);
1383 delete m_rootNode;
1384 appendNode(node);
1385 q->endInsertRows();
1386 } else {
1387 // Otherwise store it silently because it's not part of the usable model.
1388 delete m_rootNode;
1389 m_rootNode = node;
1390 m_needDeleteRootNode = true;
1391 m_collections.insert(m_rootCollection.id(), m_rootCollection);
1392 }
1393
1394 const bool noMimetypes = !m_mimeChecker.hasWantedMimeTypes();
1395 const bool noResources = m_monitor->resourcesMonitored().isEmpty();
1396 const bool multipleCollections = m_monitor->collectionsMonitored().size() > 1;
1397 const bool generalPopulation = !noMimetypes || noResources;
1398
1399 const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy);
1400
1401 // Collections can only be monitored if no resources and no mimetypes are monitored
1402 if (multipleCollections && noMimetypes && noResources) {
1403 fetchCollections(m_monitor->collectionsMonitored(), CollectionFetchJob::Base);
1404 fetchCollections(m_monitor->collectionsMonitored(), fetchType);
1405 return;
1406 }
1407
1408 qCDebug(DebugETM) << "GEN" << generalPopulation << noMimetypes << noResources;
1409 if (generalPopulation) {
1410 fetchCollections(m_rootCollection, fetchType);
1411 }
1412
1413 // If the root collection is not collection::root, then it could have items, and they will need to be
1414 // retrieved now.
1415 // Only fetch items NOT if there is NoItemPopulation, or if there is Lazypopulation and the root is visible
1416 // (if the root is not visible the lazy population can not be triggered)
1417 if ((m_itemPopulation != EntityTreeModel::NoItemPopulation) && !((m_itemPopulation == EntityTreeModel::LazyPopulation) && m_showRootCollection)) {
1418 if (m_rootCollection != Collection::root()) {
1419 fetchItems(m_rootCollection);
1420 }
1421 }
1422
1423 // Resources which are explicitly monitored won't have appeared yet if their mimetype didn't match.
1424 // We fetch the top level collections and examine them for whether to add them.
1425 // This fetches virtual collections into the tree.
1426 if (!m_monitor->resourcesMonitored().isEmpty()) {
1427 fetchTopLevelCollections();
1428 }
1429 }
1430
fetchTopLevelCollections() const1431 void EntityTreeModelPrivate::fetchTopLevelCollections() const
1432 {
1433 Q_Q(const EntityTreeModel);
1434 auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, m_session);
1435 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(topLevelCollectionsFetched(Akonadi::Collection::List)));
1436 q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
1437 qCDebug(DebugETM) << "EntityTreeModelPrivate::fetchTopLevelCollections";
1438 jobTimeTracker[job].start();
1439 }
1440
topLevelCollectionsFetched(const Akonadi::Collection::List & list)1441 void EntityTreeModelPrivate::topLevelCollectionsFetched(const Akonadi::Collection::List &list)
1442 {
1443 Q_Q(EntityTreeModel);
1444 for (const Collection &collection : list) {
1445 // These collections have been explicitly shown in the Monitor,
1446 // but hidden trumps that for now. This may change in the future if we figure out a use for it.
1447 if (isHidden(collection)) {
1448 continue;
1449 }
1450
1451 if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && !m_collections.contains(collection.id())) {
1452 const QModelIndex parentIndex = indexForCollection(collection.parentCollection());
1453 // Prepending new collections.
1454 const int row = 0;
1455 q->beginInsertRows(parentIndex, row, row);
1456
1457 m_collections.insert(collection.id(), collection);
1458 Q_ASSERT(collection.parentCollection() == Collection::root());
1459 prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()});
1460 q->endInsertRows();
1461
1462 if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
1463 fetchItems(collection);
1464 }
1465
1466 Q_ASSERT(collection.isValid());
1467 fetchCollections(collection, CollectionFetchJob::Recursive);
1468 }
1469 }
1470 }
1471
getParentCollections(const Item & item) const1472 Akonadi::Collection::List EntityTreeModelPrivate::getParentCollections(const Item &item) const
1473 {
1474 Collection::List list;
1475 for (auto it = m_childEntities.constKeyValueBegin(), end = m_childEntities.constKeyValueEnd(); it != end; ++it) {
1476 const auto &[parentId, childNodes] = *it;
1477 int nodeIndex = indexOf<Node::Item>(childNodes, item.id());
1478 if (nodeIndex != -1 && childNodes.at(nodeIndex)->type == Node::Item) {
1479 list.push_back(m_collections.value(parentId));
1480 }
1481 }
1482
1483 return list;
1484 }
1485
ref(Collection::Id id)1486 void EntityTreeModelPrivate::ref(Collection::Id id)
1487 {
1488 m_monitor->d_ptr->ref(id);
1489 }
1490
shouldPurge(Collection::Id id) const1491 bool EntityTreeModelPrivate::shouldPurge(Collection::Id id) const
1492 {
1493 // reference counted collections should never be purged
1494 // they first have to be deref'ed until they reach 0.
1495 // if the collection is buffered, keep it, otherwise we can safely purge this item
1496 return !m_monitor->d_ptr->isMonitored(id);
1497 }
1498
isMonitored(Collection::Id id) const1499 bool EntityTreeModelPrivate::isMonitored(Collection::Id id) const
1500 {
1501 return m_monitor->d_ptr->isMonitored(id);
1502 }
1503
isBuffered(Collection::Id id) const1504 bool EntityTreeModelPrivate::isBuffered(Collection::Id id) const
1505 {
1506 return m_monitor->d_ptr->m_buffer.isBuffered(id);
1507 }
1508
deref(Collection::Id id)1509 void EntityTreeModelPrivate::deref(Collection::Id id)
1510 {
1511 const Collection::Id bumpedId = m_monitor->d_ptr->deref(id);
1512
1513 if (bumpedId < 0) {
1514 return;
1515 }
1516
1517 // The collection has already been removed, don't purge
1518 if (!m_collections.contains(bumpedId)) {
1519 return;
1520 }
1521
1522 if (shouldPurge(bumpedId)) {
1523 purgeItems(bumpedId);
1524 }
1525 }
1526
skipCollections(QList<Node * >::iterator it,const QList<Node * >::iterator & end,int * pos)1527 QList<Node *>::iterator EntityTreeModelPrivate::skipCollections(QList<Node *>::iterator it, const QList<Node *>::iterator &end, int *pos)
1528 {
1529 for (; it != end; ++it) {
1530 if ((*it)->type == Node::Item) {
1531 break;
1532 }
1533
1534 ++(*pos);
1535 }
1536
1537 return it;
1538 }
1539
1540 QList<Node *>::iterator
removeItems(QList<Node * >::iterator it,const QList<Node * >::iterator & end,int * pos,const Collection & collection)1541 EntityTreeModelPrivate::removeItems(QList<Node *>::iterator it, const QList<Node *>::iterator &end, int *pos, const Collection &collection)
1542 {
1543 Q_Q(EntityTreeModel);
1544
1545 QList<Node *>::iterator startIt = it;
1546
1547 // figure out how many items we will delete
1548 int start = *pos;
1549 for (; it != end; ++it) {
1550 if ((*it)->type != Node::Item) {
1551 break;
1552 }
1553
1554 ++(*pos);
1555 }
1556 it = startIt;
1557
1558 const QModelIndex parentIndex = indexForCollection(collection);
1559
1560 q->beginRemoveRows(parentIndex, start, (*pos) - 1);
1561 const int toDelete = (*pos) - start;
1562 Q_ASSERT(toDelete > 0);
1563
1564 QList<Node *> &es = m_childEntities[collection.id()];
1565 // NOTE: .erase will invalidate all iterators besides "it"!
1566 for (int i = 0; i < toDelete; ++i) {
1567 Q_ASSERT(es.count(*it) == 1);
1568 // don't keep implicitly shared data alive
1569 Q_ASSERT(m_items.contains((*it)->id));
1570 m_items.unref((*it)->id);
1571 // delete actual node
1572 delete *it;
1573 it = es.erase(it);
1574 }
1575 q->endRemoveRows();
1576
1577 return it;
1578 }
1579
purgeItems(Collection::Id id)1580 void EntityTreeModelPrivate::purgeItems(Collection::Id id)
1581 {
1582 QList<Node *> &childEntities = m_childEntities[id];
1583
1584 const Collection collection = m_collections.value(id);
1585 Q_ASSERT(collection.isValid());
1586
1587 QList<Node *>::iterator begin = childEntities.begin();
1588 QList<Node *>::iterator end = childEntities.end();
1589
1590 int pos = 0;
1591 while ((begin = skipCollections(begin, end, &pos)) != end) {
1592 begin = removeItems(begin, end, &pos, collection);
1593 end = childEntities.end();
1594 }
1595 m_populatedCols.remove(id);
1596 // if an empty collection is purged and we leave it in here, itemAdded will be ignored for the collection
1597 // and the collection is never populated by fetchMore (but maybe by statistics changed?)
1598 m_collectionsWithoutItems.remove(id);
1599 }
1600
dataChanged(const QModelIndex & top,const QModelIndex & bottom)1601 void EntityTreeModelPrivate::dataChanged(const QModelIndex &top, const QModelIndex &bottom)
1602 {
1603 Q_Q(EntityTreeModel);
1604
1605 QModelIndex rightIndex;
1606
1607 const Node *node = static_cast<Node *>(bottom.internalPointer());
1608 if (!node) {
1609 return;
1610 }
1611
1612 if (node->type == Node::Collection) {
1613 rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::CollectionTreeHeaders) - 1);
1614 }
1615 if (node->type == Node::Item) {
1616 rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::ItemListHeaders) - 1);
1617 }
1618
1619 Q_EMIT q->dataChanged(top, rightIndex);
1620 }
1621
indexForCollection(const Collection & collection) const1622 QModelIndex EntityTreeModelPrivate::indexForCollection(const Collection &collection) const
1623 {
1624 Q_Q(const EntityTreeModel);
1625
1626 if (!collection.isValid()) {
1627 return QModelIndex();
1628 }
1629
1630 if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
1631 return QModelIndex();
1632 }
1633
1634 // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob,
1635 // we ensure that we use -1 for the invalid Collection.
1636 Collection::Id parentId = -1;
1637
1638 if ((collection == m_rootCollection)) {
1639 if (m_showRootCollection) {
1640 return q->createIndex(0, 0, static_cast<void *>(m_rootNode));
1641 }
1642 return QModelIndex();
1643 }
1644
1645 if (collection == Collection::root()) {
1646 parentId = -1;
1647 } else if (collection.parentCollection().isValid()) {
1648 parentId = collection.parentCollection().id();
1649 } else {
1650 for (const auto &children : m_childEntities) {
1651 const int row = indexOf<Node::Collection>(children, collection.id());
1652 if (row < 0) {
1653 continue;
1654 }
1655
1656 Node *node = children.at(row);
1657 return q->createIndex(row, 0, static_cast<void *>(node));
1658 }
1659 return QModelIndex();
1660 }
1661
1662 const int row = indexOf<Node::Collection>(m_childEntities.value(parentId), collection.id());
1663
1664 if (row < 0) {
1665 return QModelIndex();
1666 }
1667
1668 Node *node = m_childEntities.value(parentId).at(row);
1669
1670 return q->createIndex(row, 0, static_cast<void *>(node));
1671 }
1672
indexesForItem(const Item & item) const1673 QModelIndexList EntityTreeModelPrivate::indexesForItem(const Item &item) const
1674 {
1675 Q_Q(const EntityTreeModel);
1676 QModelIndexList indexes;
1677
1678 if (m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections) {
1679 Q_ASSERT(m_childEntities.contains(m_rootCollection.id()));
1680 QList<Node *> nodeList = m_childEntities.value(m_rootCollection.id());
1681 const int row = indexOf<Node::Item>(nodeList, item.id());
1682 Q_ASSERT(row >= 0);
1683 Q_ASSERT(row < nodeList.size());
1684 Node *node = nodeList.at(row);
1685
1686 indexes << q->createIndex(row, 0, static_cast<void *>(node));
1687 return indexes;
1688 }
1689
1690 const Collection::List collections = getParentCollections(item);
1691
1692 indexes.reserve(collections.size());
1693 for (const Collection &collection : collections) {
1694 const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1695 Q_ASSERT(row >= 0);
1696 Q_ASSERT(m_childEntities.contains(collection.id()));
1697 QList<Node *> nodeList = m_childEntities.value(collection.id());
1698 Q_ASSERT(row < nodeList.size());
1699 Node *node = nodeList.at(row);
1700
1701 indexes << q->createIndex(row, 0, static_cast<void *>(node));
1702 }
1703
1704 return indexes;
1705 }
1706
beginResetModel()1707 void EntityTreeModelPrivate::beginResetModel()
1708 {
1709 Q_Q(EntityTreeModel);
1710 q->beginResetModel();
1711 }
1712
endResetModel()1713 void EntityTreeModelPrivate::endResetModel()
1714 {
1715 Q_Q(EntityTreeModel);
1716 auto subjobs = m_session->findChildren<Akonadi::Job *>();
1717 for (auto job : subjobs) {
1718 job->disconnect(q);
1719 }
1720 m_collections.clear();
1721 m_collectionsWithoutItems.clear();
1722 m_populatedCols.clear();
1723 m_items.clear();
1724 m_pendingCollectionFetchJobs.clear();
1725 m_pendingCollectionRetrieveJobs.clear();
1726 m_collectionTreeFetched = false;
1727
1728 for (const QList<Node *> &list : std::as_const(m_childEntities)) {
1729 qDeleteAll(list);
1730 }
1731 m_childEntities.clear();
1732 if (m_needDeleteRootNode) {
1733 m_needDeleteRootNode = false;
1734 delete m_rootNode;
1735 }
1736 m_rootNode = nullptr;
1737
1738 q->endResetModel();
1739 fillModel();
1740 }
1741
monitoredItemsRetrieved(KJob * job)1742 void EntityTreeModelPrivate::monitoredItemsRetrieved(KJob *job)
1743 {
1744 if (job->error()) {
1745 qCWarning(AKONADICORE_LOG) << job->errorString();
1746 return;
1747 }
1748
1749 Q_Q(EntityTreeModel);
1750
1751 auto fetchJob = qobject_cast<ItemFetchJob *>(job);
1752 Q_ASSERT(fetchJob);
1753 Item::List list = fetchJob->items();
1754
1755 q->beginResetModel();
1756 for (const Item &item : list) {
1757 m_childEntities[-1].append(new Node{Node::Item, item.id(), m_rootCollection.id()});
1758 m_items.ref(item.id(), item);
1759 }
1760 q->endResetModel();
1761 }
1762
fillModel()1763 void EntityTreeModelPrivate::fillModel()
1764 {
1765 Q_Q(EntityTreeModel);
1766
1767 m_mimeChecker.setWantedMimeTypes(m_monitor->mimeTypesMonitored());
1768
1769 const Collection::List collections = m_monitor->collectionsMonitored();
1770
1771 if (collections.isEmpty() && m_monitor->numMimeTypesMonitored() == 0 && m_monitor->numResourcesMonitored() == 0 && m_monitor->numItemsMonitored() != 0) {
1772 m_rootCollection = Collection(-1);
1773 m_collectionTreeFetched = true;
1774 Q_EMIT q_ptr->collectionTreeFetched(collections); // there are no collections to fetch
1775
1776 const auto items = m_monitor->itemsMonitoredEx() | Views::transform([](const auto id) {
1777 return Item{id};
1778 })
1779 | Actions::toQVector;
1780 auto itemFetch = new ItemFetchJob(items, m_session);
1781 itemFetch->setFetchScope(m_monitor->itemFetchScope());
1782 itemFetch->fetchScope().setIgnoreRetrievalErrors(true);
1783 q->connect(itemFetch, SIGNAL(finished(KJob *)), q, SLOT(monitoredItemsRetrieved(KJob *)));
1784 return;
1785 }
1786 // In case there is only a single collection monitored, we can use this
1787 // collection as root of the node tree, in all other cases
1788 // Collection::root() is used
1789 if (collections.size() == 1) {
1790 m_rootCollection = collections.first();
1791 } else {
1792 m_rootCollection = Collection::root();
1793 }
1794
1795 if (m_rootCollection == Collection::root()) {
1796 QTimer::singleShot(0, q, SLOT(startFirstListJob()));
1797 } else {
1798 Q_ASSERT(m_rootCollection.isValid());
1799 auto rootFetchJob = new CollectionFetchJob(m_rootCollection, CollectionFetchJob::Base, m_session);
1800 q->connect(rootFetchJob, SIGNAL(result(KJob *)), SLOT(rootFetchJobDone(KJob *)));
1801 qCDebug(DebugETM) << "";
1802 jobTimeTracker[rootFetchJob].start();
1803 }
1804 }
1805
canFetchMore(const QModelIndex & parent) const1806 bool EntityTreeModelPrivate::canFetchMore(const QModelIndex &parent) const
1807 {
1808 const Item item = parent.data(EntityTreeModel::ItemRole).value<Item>();
1809
1810 if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
1811 return false;
1812 }
1813
1814 if (item.isValid()) {
1815 // items can't have more rows.
1816 // TODO: Should I use this for fetching more of an item, ie more payload parts?
1817 return false;
1818 } else {
1819 // but collections can...
1820 const Collection::Id colId = parent.data(EntityTreeModel::CollectionIdRole).toULongLong();
1821
1822 // But the root collection can't...
1823 if (Collection::root().id() == colId) {
1824 return false;
1825 }
1826
1827 // Collections which contain no items at all can't contain more
1828 if (m_collectionsWithoutItems.contains(colId)) {
1829 return false;
1830 }
1831
1832 // Don't start the same job multiple times.
1833 if (m_pendingCollectionRetrieveJobs.contains(colId)) {
1834 return false;
1835 }
1836
1837 // Can't fetch more if the collection's items have already been fetched
1838 if (m_populatedCols.contains(colId)) {
1839 return false;
1840 }
1841
1842 // Only try to fetch more from a collection if we don't already have items in it.
1843 // Otherwise we'd spend all the time listing items in collections.
1844 return m_childEntities.value(colId) | Actions::none(Node::isItem);
1845 }
1846 }
1847
iconForName(const QString & name) const1848 QIcon EntityTreeModelPrivate::iconForName(const QString &name) const
1849 {
1850 if (m_iconThemeName != QIcon::themeName()) {
1851 m_iconThemeName = QIcon::themeName();
1852 m_iconCache.clear();
1853 }
1854
1855 QIcon &icon = m_iconCache[name];
1856 if (icon.isNull()) {
1857 icon = QIcon::fromTheme(name);
1858 }
1859 return icon;
1860 }
1861