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