File indexing completed on 2024-09-22 04:41:02

0001 /*
0002     SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "entitytreemodel_p.h"
0008 
0009 #include "agentmanagerinterface.h"
0010 #include "akranges.h"
0011 #include "monitor_p.h" // For friend ref/deref
0012 #include "servermanager.h"
0013 
0014 #include <KLocalizedString>
0015 
0016 #include "agentmanager.h"
0017 #include "agenttype.h"
0018 #include "changerecorder.h"
0019 #include "collectioncopyjob.h"
0020 #include "collectionfetchscope.h"
0021 #include "collectionmovejob.h"
0022 #include "collectionstatistics.h"
0023 #include "entityhiddenattribute.h"
0024 #include "itemcopyjob.h"
0025 #include "itemfetchjob.h"
0026 #include "itemmovejob.h"
0027 #include "linkjob.h"
0028 #include "monitor.h"
0029 #include "private/protocol_p.h"
0030 #include "session.h"
0031 
0032 #include "akonadicore_debug.h"
0033 
0034 #include <QElapsedTimer>
0035 #include <QIcon>
0036 #include <QMessageBox>
0037 #include <unordered_map>
0038 
0039 // clazy:excludeall=old-style-connect
0040 
0041 QHash<KJob *, QElapsedTimer> jobTimeTracker;
0042 
0043 Q_LOGGING_CATEGORY(DebugETM, "org.kde.pim.akonadi.ETM", QtInfoMsg)
0044 
0045 using namespace Akonadi;
0046 using namespace AkRanges;
0047 
0048 static CollectionFetchJob::Type getFetchType(EntityTreeModel::CollectionFetchStrategy strategy)
0049 {
0050     switch (strategy) {
0051     case EntityTreeModel::FetchFirstLevelChildCollections:
0052         return CollectionFetchJob::FirstLevel;
0053     case EntityTreeModel::InvisibleCollectionFetch:
0054     case EntityTreeModel::FetchCollectionsRecursive:
0055     default:
0056         break;
0057     }
0058     return CollectionFetchJob::Recursive;
0059 }
0060 
0061 EntityTreeModelPrivate::EntityTreeModelPrivate(EntityTreeModel *parent)
0062     : q_ptr(parent)
0063 {
0064     // using collection as a parameter of a queued call in runItemFetchJob()
0065     qRegisterMetaType<Collection>();
0066 
0067     Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self();
0068     QObject::connect(agentManager, SIGNAL(instanceRemoved(Akonadi::AgentInstance)), q_ptr, SLOT(agentInstanceRemoved(Akonadi::AgentInstance)));
0069 }
0070 
0071 EntityTreeModelPrivate::~EntityTreeModelPrivate()
0072 {
0073     if (m_needDeleteRootNode) {
0074         delete m_rootNode;
0075     }
0076     m_rootNode = nullptr;
0077 }
0078 
0079 void EntityTreeModelPrivate::init(Monitor *monitor)
0080 {
0081     Q_Q(EntityTreeModel);
0082     Q_ASSERT(!m_monitor);
0083     m_monitor = monitor;
0084     // The default is to FetchCollectionsRecursive, so we tell the monitor to fetch collections
0085     // That way update signals from the monitor will contain the full collection.
0086     // This may be updated if the CollectionFetchStrategy is changed.
0087     m_monitor->fetchCollection(true);
0088     m_session = m_monitor->session();
0089 
0090     m_rootCollectionDisplayName = QStringLiteral("[*]");
0091 
0092     if (auto cr = qobject_cast<Akonadi::ChangeRecorder *>(m_monitor)) {
0093         cr->setChangeRecordingEnabled(false);
0094     }
0095 
0096     m_includeStatistics = true;
0097     m_monitor->fetchCollectionStatistics(true);
0098     m_monitor->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
0099 
0100     q->connect(monitor, SIGNAL(mimeTypeMonitored(QString, bool)), SLOT(monitoredMimeTypeChanged(QString, bool)));
0101     q->connect(monitor, SIGNAL(collectionMonitored(Akonadi::Collection, bool)), SLOT(monitoredCollectionsChanged(Akonadi::Collection, bool)));
0102     q->connect(monitor, SIGNAL(itemMonitored(Akonadi::Item, bool)), SLOT(monitoredItemsChanged(Akonadi::Item, bool)));
0103     q->connect(monitor, SIGNAL(resourceMonitored(QByteArray, bool)), SLOT(monitoredResourcesChanged(QByteArray, bool)));
0104 
0105     // monitor collection changes
0106     q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), SLOT(monitoredCollectionChanged(Akonadi::Collection)));
0107     q->connect(monitor,
0108                SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection)),
0109                SLOT(monitoredCollectionAdded(Akonadi::Collection, Akonadi::Collection)));
0110     q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), SLOT(monitoredCollectionRemoved(Akonadi::Collection)));
0111     q->connect(monitor,
0112                SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)),
0113                SLOT(monitoredCollectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)));
0114 
0115     // Monitor item changes.
0116     q->connect(monitor, SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemAdded(Akonadi::Item, Akonadi::Collection)));
0117     q->connect(monitor, SIGNAL(itemChanged(Akonadi::Item, QSet<QByteArray>)), SLOT(monitoredItemChanged(Akonadi::Item, QSet<QByteArray>)));
0118     q->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), SLOT(monitoredItemRemoved(Akonadi::Item)));
0119     q->connect(monitor,
0120                SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)),
0121                SLOT(monitoredItemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)));
0122 
0123     q->connect(monitor, SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemLinked(Akonadi::Item, Akonadi::Collection)));
0124     q->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemUnlinked(Akonadi::Item, Akonadi::Collection)));
0125 
0126     q->connect(monitor,
0127                SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics)),
0128                SLOT(monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics)));
0129 
0130     Akonadi::ServerManager *serverManager = Akonadi::ServerManager::self();
0131     q->connect(serverManager, SIGNAL(started()), SLOT(serverStarted()));
0132 
0133     fillModel();
0134 }
0135 
0136 void EntityTreeModelPrivate::prependNode(Node *node)
0137 {
0138     m_childEntities[node->parent].prepend(node);
0139 }
0140 
0141 void EntityTreeModelPrivate::appendNode(Node *node)
0142 {
0143     m_childEntities[node->parent].append(node);
0144 }
0145 
0146 void EntityTreeModelPrivate::serverStarted()
0147 {
0148     // Don't emit about to be reset. Too late for that
0149     endResetModel();
0150 }
0151 
0152 void EntityTreeModelPrivate::changeFetchState(const Collection &parent)
0153 {
0154     Q_Q(EntityTreeModel);
0155     const QModelIndex collectionIndex = indexForCollection(parent);
0156     if (!collectionIndex.isValid()) {
0157         // Because we are called delayed, it is possible that @p parent has been deleted.
0158         return;
0159     }
0160     Q_EMIT q->dataChanged(collectionIndex, collectionIndex);
0161 }
0162 
0163 void EntityTreeModelPrivate::agentInstanceRemoved(const Akonadi::AgentInstance &instance)
0164 {
0165     Q_Q(EntityTreeModel);
0166     if (!instance.type().capabilities().contains(QLatin1StringView("Resource"))) {
0167         return;
0168     }
0169 
0170     if (m_rootCollection.isValid()) {
0171         if (m_rootCollection != Collection::root()) {
0172             if (m_rootCollection.resource() == instance.identifier()) {
0173                 q->clearAndReset();
0174             }
0175             return;
0176         }
0177         const auto children = m_childEntities[Collection::root().id()];
0178         for (const Node *node : children) {
0179             Q_ASSERT(node->type == Node::Collection);
0180 
0181             const Collection collection = m_collections[node->id];
0182             if (collection.resource() == instance.identifier()) {
0183                 monitoredCollectionRemoved(collection);
0184             }
0185         }
0186     }
0187 }
0188 
0189 static const char s_fetchCollectionId[] = "FetchCollectionId";
0190 
0191 void EntityTreeModelPrivate::fetchItems(const Collection &parent)
0192 {
0193     Q_Q(const EntityTreeModel);
0194     Q_ASSERT(parent.isValid());
0195     Q_ASSERT(m_collections.contains(parent.id()));
0196     // TODO: Use a more specific fetch scope to get only the envelope for mails etc.
0197     auto itemFetchJob = new Akonadi::ItemFetchJob(parent, m_session);
0198     itemFetchJob->setFetchScope(m_monitor->itemFetchScope());
0199     itemFetchJob->fetchScope().setAncestorRetrieval(ItemFetchScope::All);
0200     itemFetchJob->fetchScope().setIgnoreRetrievalErrors(true);
0201     itemFetchJob->setDeliveryOption(ItemFetchJob::EmitItemsInBatches);
0202 
0203     itemFetchJob->setProperty(s_fetchCollectionId, QVariant(parent.id()));
0204 
0205     if (m_showRootCollection || parent != m_rootCollection) {
0206         m_pendingCollectionRetrieveJobs.insert(parent.id());
0207 
0208         // If collections are not in the model, there will be no valid index for them.
0209         if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)) {
0210             // We need to invoke this delayed because we would otherwise be emitting a sequence like
0211             // - beginInsertRows
0212             // - dataChanged
0213             // - endInsertRows
0214             // which would confuse proxies.
0215             QMetaObject::invokeMethod(const_cast<EntityTreeModel *>(q), "changeFetchState", Qt::QueuedConnection, Q_ARG(Akonadi::Collection, parent));
0216         }
0217     }
0218 
0219     q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this, parentId = parent.id()](const Item::List &items) {
0220         itemsFetched(parentId, items);
0221     });
0222     q->connect(itemFetchJob, &ItemFetchJob::result, q, [this, parentId = parent.id()](KJob *job) {
0223         itemFetchJobDone(parentId, job);
0224     });
0225     qCDebug(DebugETM) << "collection:" << parent.name();
0226     jobTimeTracker[itemFetchJob].start();
0227 }
0228 
0229 void EntityTreeModelPrivate::fetchCollections(Akonadi::CollectionFetchJob *job)
0230 {
0231     Q_Q(EntityTreeModel);
0232 
0233     job->fetchScope().setListFilter(m_listFilter);
0234     job->fetchScope().setContentMimeTypes(m_monitor->mimeTypesMonitored());
0235     m_pendingCollectionFetchJobs.insert(static_cast<KJob *>(job));
0236 
0237     if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
0238         // This is invisible fetch, so no model signals are emitted
0239         q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const Collection::List &collections) {
0240             for (const auto &collection : collections) {
0241                 if (isHidden(collection)) {
0242                     continue;
0243                 }
0244                 m_collections.insert(collection.id(), collection);
0245                 prependNode(new Node{Node::Collection, collection.id(), -1});
0246                 fetchItems(collection);
0247             }
0248         });
0249     } else {
0250         job->fetchScope().setIncludeStatistics(m_includeStatistics);
0251         job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
0252         q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsFetched(Akonadi::Collection::List)));
0253     }
0254     q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
0255 
0256     jobTimeTracker[job].start();
0257 }
0258 
0259 void EntityTreeModelPrivate::fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type)
0260 {
0261     fetchCollections(new CollectionFetchJob(collections, type, m_session));
0262 }
0263 
0264 void EntityTreeModelPrivate::fetchCollections(const Collection &collection, CollectionFetchJob::Type type)
0265 {
0266     Q_ASSERT(collection.isValid());
0267     auto job = new CollectionFetchJob(collection, type, m_session);
0268     fetchCollections(job);
0269 }
0270 
0271 namespace Akonadi
0272 {
0273 template<typename T>
0274 inline bool EntityTreeModelPrivate::isHiddenImpl(const T &entity, Node::Type type) const
0275 {
0276     if (m_showSystemEntities) {
0277         return false;
0278     }
0279 
0280     if (type == Node::Collection && entity.id() == m_rootCollection.id()) {
0281         return false;
0282     }
0283 
0284     // entity.hasAttribute<EntityHiddenAttribute>() does not compile w/ GCC for
0285     // some reason
0286     if (entity.hasAttribute(EntityHiddenAttribute().type())) {
0287         return true;
0288     }
0289 
0290     const Collection parent = entity.parentCollection();
0291     if (parent.isValid()) {
0292         return isHiddenImpl(parent, Node::Collection);
0293     }
0294 
0295     return false;
0296 }
0297 
0298 } // namespace Akonadi
0299 
0300 bool EntityTreeModelPrivate::isHidden(const Akonadi::Collection &collection) const
0301 {
0302     return isHiddenImpl(collection, Node::Collection);
0303 }
0304 
0305 bool EntityTreeModelPrivate::isHidden(const Akonadi::Item &item) const
0306 {
0307     return isHiddenImpl(item, Node::Item);
0308 }
0309 
0310 static QSet<Collection::Id> getChildren(Collection::Id parent, const std::unordered_map<Collection::Id, Collection::Id> &childParentMap)
0311 {
0312     QSet<Collection::Id> children;
0313     for (const auto &[childId, parentId] : childParentMap) {
0314         if (parentId == parent) {
0315             children.insert(childId);
0316             children.unite(getChildren(childId, childParentMap));
0317         }
0318     }
0319     return children;
0320 }
0321 
0322 void EntityTreeModelPrivate::collectionsFetched(const Akonadi::Collection::List &collections)
0323 {
0324     Q_Q(EntityTreeModel);
0325     QElapsedTimer t;
0326     t.start();
0327 
0328     QVectorIterator<Akonadi::Collection> it(collections);
0329 
0330     QHash<Collection::Id, Collection> collectionsToInsert;
0331 
0332     while (it.hasNext()) {
0333         const Collection collection = it.next();
0334         const Collection::Id collectionId = collection.id();
0335         if (isHidden(collection)) {
0336             continue;
0337         }
0338 
0339         auto collectionIt = m_collections.find(collectionId);
0340         if (collectionIt != m_collections.end()) {
0341             // This is probably the result of a parent of a previous collection already being in the model.
0342             // Replace the dummy collection with the real one and move on.
0343 
0344             // This could also be the result of a monitor signal having already inserted the collection
0345             // into this model. There's no way to tell, so we just emit dataChanged.
0346             *collectionIt = collection;
0347 
0348             const QModelIndex collectionIndex = indexForCollection(collection);
0349             dataChanged(collectionIndex, collectionIndex);
0350             Q_EMIT q->collectionFetched(collectionId);
0351             continue;
0352         }
0353 
0354         // If we're monitoring collections somewhere in the tree we need to retrieve their ancestors now
0355         if (collection.parentCollection() != m_rootCollection && m_monitor->collectionsMonitored().contains(collection)) {
0356             retrieveAncestors(collection, false);
0357         }
0358 
0359         collectionsToInsert.insert(collectionId, collection);
0360     }
0361 
0362     // 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
0363     std::unordered_map<Collection::Id, QSet<Collection::Id>> subTreesToInsert;
0364     {
0365         // Build a child-parent map that allows us to build the subtrees afterwards
0366         std::unordered_map<Collection::Id, Collection::Id> childParentMap;
0367         const auto initialCollectionsToInsert(collectionsToInsert);
0368         for (const auto &col : initialCollectionsToInsert) {
0369             childParentMap.insert({col.id(), col.parentCollection().id()});
0370 
0371             // Complete the subtree up to the last known parent
0372             Collection parent = col.parentCollection();
0373             while (parent.isValid() && parent != m_rootCollection && !m_collections.contains(parent.id())) {
0374                 childParentMap.insert({parent.id(), parent.parentCollection().id()});
0375 
0376                 if (!collectionsToInsert.contains(parent.id())) {
0377                     collectionsToInsert.insert(parent.id(), parent);
0378                 }
0379                 parent = parent.parentCollection();
0380             }
0381         }
0382 
0383         QSet<Collection::Id> parents;
0384 
0385         // Find toplevel parents of the subtrees
0386         for (const auto &[childId, parentId] : childParentMap) {
0387             // The child has a parent without parent (it's a toplevel node that is not yet in m_collections)
0388             if (childParentMap.find(parentId) == childParentMap.cend()) {
0389                 Q_ASSERT(!m_collections.contains(childId));
0390                 parents.insert(childId);
0391             }
0392         }
0393 
0394         // Find children of each subtree
0395         for (const auto parentId : parents) {
0396             QSet<Collection::Id> children;
0397             // We add the parent itself as well so it can be inserted below as part of the same loop
0398             children << parentId;
0399             children += getChildren(parentId, childParentMap);
0400             subTreesToInsert.insert_or_assign(parentId, std::move(children));
0401         }
0402     }
0403 
0404     const int row = 0;
0405 
0406     for (const auto &[topCollectionId, subtree] : subTreesToInsert) {
0407         qCDebug(DebugETM) << "Subtree: " << topCollectionId << subtree;
0408 
0409         Q_ASSERT(!m_collections.contains(topCollectionId));
0410         Collection topCollection = collectionsToInsert.value(topCollectionId);
0411         Q_ASSERT(topCollection.isValid());
0412 
0413         // The toplevels parent must already be part of the model
0414         Q_ASSERT(m_collections.contains(topCollection.parentCollection().id()));
0415         const QModelIndex parentIndex = indexForCollection(topCollection.parentCollection());
0416 
0417         q->beginInsertRows(parentIndex, row, row);
0418         Q_ASSERT(!subtree.empty());
0419 
0420         for (const auto collectionId : subtree) {
0421             const Collection collection = collectionsToInsert.take(collectionId);
0422             Q_ASSERT(collection.isValid());
0423 
0424             m_collections.insert(collectionId, collection);
0425 
0426             Q_ASSERT(collection.parentCollection().isValid());
0427             prependNode(new Node{Node::Collection, collectionId, collection.parentCollection().id()});
0428         }
0429         q->endInsertRows();
0430 
0431         if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
0432             for (const auto collectionId : subtree) {
0433                 const auto col = m_collections.value(collectionId);
0434                 if (!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedCollection(col)) {
0435                     fetchItems(col);
0436                 } else {
0437                     // Consider collections that don't contain relevant mimetypes to be populated
0438                     m_populatedCols.insert(collectionId);
0439                     Q_EMIT q_ptr->collectionPopulated(collectionId);
0440                     const auto idx = indexForCollection(Collection(collectionId));
0441                     Q_ASSERT(idx.isValid());
0442                     dataChanged(idx, idx);
0443                 }
0444             }
0445         }
0446     }
0447 }
0448 
0449 // Used by entitytreemodeltest
0450 void EntityTreeModelPrivate::itemsFetched(const Akonadi::Item::List &items)
0451 {
0452     Q_Q(EntityTreeModel);
0453     const auto collectionId = q->sender()->property(s_fetchCollectionId).value<Collection::Id>();
0454     itemsFetched(collectionId, items);
0455 }
0456 
0457 void EntityTreeModelPrivate::itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items)
0458 {
0459     Q_Q(EntityTreeModel);
0460 
0461     if (!m_collections.contains(collectionId)) {
0462         qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items";
0463         return;
0464     }
0465 
0466     const Collection collection = m_collections.value(collectionId);
0467 
0468     Q_ASSERT(collection.isValid());
0469 
0470     // if there are any items at all, remove from set of collections known to be empty
0471     if (!items.isEmpty()) {
0472         m_collectionsWithoutItems.remove(collectionId);
0473     }
0474 
0475     Item::List itemsToInsert;
0476     for (const auto &item : items) {
0477         if (isHidden(item)) {
0478             continue;
0479         }
0480 
0481         if ((!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedItem(item))) {
0482             // When listing virtual collections we might get results for items which are already in
0483             // the model if their concrete collection has already been listed.
0484             // In that case the collectionId should be different though.
0485 
0486             // As an additional complication, new items might be both part of fetch job results and
0487             // part of monitor notifications. We only insert items which are not already in the model
0488             // considering their (possibly virtual) parent.
0489             bool isNewItem = true;
0490             auto itemIt = m_items.find(item.id());
0491             if (itemIt != m_items.end()) {
0492                 const Akonadi::Collection::List parents = getParentCollections(item);
0493                 for (const Akonadi::Collection &parent : parents) {
0494                     if (parent.id() == collectionId) {
0495                         qCWarning(AKONADICORE_LOG) << "Fetched an item which is already in the model, id=" << item.id() << "collection id=" << collectionId;
0496                         // Update it in case the revision changed;
0497                         itemIt->value.apply(item);
0498                         isNewItem = false;
0499                         break;
0500                     }
0501                 }
0502             }
0503 
0504             if (isNewItem) {
0505                 itemsToInsert << item;
0506             }
0507         }
0508     }
0509 
0510     if (!itemsToInsert.isEmpty()) {
0511         const Collection::Id colId = m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch ? m_rootCollection.id()
0512             : m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections                              ? m_rootCollection.id()
0513                                                                                                             : collectionId;
0514         const int startRow = m_childEntities.value(colId).size();
0515 
0516         Q_ASSERT(m_collections.contains(colId));
0517 
0518         const QModelIndex parentIndex = indexForCollection(m_collections.value(colId));
0519         q->beginInsertRows(parentIndex, startRow, startRow + itemsToInsert.size() - 1);
0520         for (const Item &item : std::as_const(itemsToInsert)) {
0521             const Item::Id itemId = item.id();
0522             m_items.ref(itemId, item);
0523 
0524             m_childEntities[colId].append(new Node{Node::Item, itemId, collectionId});
0525         }
0526         q->endInsertRows();
0527     }
0528 }
0529 
0530 void EntityTreeModelPrivate::monitoredMimeTypeChanged(const QString &mimeType, bool monitored)
0531 {
0532     beginResetModel();
0533     if (monitored) {
0534         m_mimeChecker.addWantedMimeType(mimeType);
0535     } else {
0536         m_mimeChecker.removeWantedMimeType(mimeType);
0537     }
0538     endResetModel();
0539 }
0540 
0541 void EntityTreeModelPrivate::monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored)
0542 {
0543     if (monitored) {
0544         const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy);
0545         fetchCollections(collection, CollectionFetchJob::Base);
0546         fetchCollections(collection, fetchType);
0547     } else {
0548         // If a collection is dereferenced and no longer explicitly monitored it might still match other filters
0549         if (!shouldBePartOfModel(collection)) {
0550             monitoredCollectionRemoved(collection);
0551         }
0552     }
0553 }
0554 
0555 void EntityTreeModelPrivate::monitoredItemsChanged(const Akonadi::Item &item, bool monitored)
0556 {
0557     Q_UNUSED(item)
0558     Q_UNUSED(monitored)
0559     beginResetModel();
0560     endResetModel();
0561 }
0562 
0563 void EntityTreeModelPrivate::monitoredResourcesChanged(const QByteArray &resource, bool monitored)
0564 {
0565     Q_UNUSED(resource)
0566     Q_UNUSED(monitored)
0567     beginResetModel();
0568     endResetModel();
0569 }
0570 
0571 bool EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection)
0572 {
0573     Q_Q(EntityTreeModel);
0574 
0575     Collection parentCollection = collection.parentCollection();
0576 
0577     Q_ASSERT(parentCollection.isValid());
0578     Q_ASSERT(parentCollection != Collection::root());
0579 
0580     Collection::List ancestors;
0581 
0582     while (parentCollection != Collection::root() && !m_collections.contains(parentCollection.id())) {
0583         // Put a temporary node in the tree later.
0584         ancestors.prepend(parentCollection);
0585 
0586         parentCollection = parentCollection.parentCollection();
0587         // If we got here through Collection added notification, the parent chain may be incomplete
0588         // and if the model is still populating or the collection belongs to a yet-unknown subtree
0589         // this will break here
0590         if (!parentCollection.isValid()) {
0591             break;
0592         }
0593     }
0594     // if m_rootCollection is Collection::root(), we always have common ancestor and do the retrieval
0595     // if we traversed up to Collection::root() but are looking at a subtree only (m_rootCollection != Collection::root())
0596     // we have no common ancestor, and we don't have to retrieve anything
0597     if (parentCollection == Collection::root() && m_rootCollection != Collection::root()) {
0598         return true;
0599     }
0600 
0601     if (ancestors.isEmpty() && !insertBaseCollection) {
0602         // Nothing to do, avoid emitting insert signals
0603         return true;
0604     }
0605 
0606     CollectionFetchJob *job = nullptr;
0607     // We were unable to reach the top of the tree due to an incomplete ancestor chain, we will have
0608     // to retrieve it from the server.
0609     if (!parentCollection.isValid()) {
0610         if (insertBaseCollection) {
0611             job = new CollectionFetchJob(collection, CollectionFetchJob::Recursive, m_session);
0612         } else {
0613             job = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::Recursive, m_session);
0614         }
0615     } else if (!ancestors.isEmpty()) {
0616         // Fetch the real ancestors
0617         job = new CollectionFetchJob(ancestors, CollectionFetchJob::Base, m_session);
0618     }
0619 
0620     if (job) {
0621         job->fetchScope().setListFilter(m_listFilter);
0622         job->fetchScope().setIncludeStatistics(m_includeStatistics);
0623         q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(ancestorsFetched(Akonadi::Collection::List)));
0624         q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
0625     }
0626 
0627     if (!parentCollection.isValid()) {
0628         // We can't proceed to insert the fake collections to complete the tree because
0629         // we do not have the complete ancestor chain. However, once the fetch job is
0630         // finished the tree will be populated accordingly.
0631         return false;
0632     }
0633 
0634     //  Q_ASSERT( parentCollection != m_rootCollection );
0635     const QModelIndex parent = indexForCollection(parentCollection);
0636 
0637     // Still prepending all collections for now.
0638     int row = 0;
0639 
0640     // Although we insert several Collections here, we only need to notify though the model
0641     // about the top-level one. The rest will be found automatically by the view.
0642     q->beginInsertRows(parent, row, row);
0643 
0644     for (const auto &ancestor : std::as_const(ancestors)) {
0645         Q_ASSERT(ancestor.parentCollection().isValid());
0646         m_collections.insert(ancestor.id(), ancestor);
0647 
0648         prependNode(new Node{Node::Collection, ancestor.id(), ancestor.parentCollection().id()});
0649     }
0650 
0651     if (insertBaseCollection) {
0652         m_collections.insert(collection.id(), collection);
0653         // Can't just use parentCollection because that doesn't necessarily refer to collection.
0654         prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()});
0655     }
0656 
0657     q->endInsertRows();
0658 
0659     return true;
0660 }
0661 
0662 void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List &collectionList)
0663 {
0664     for (const Collection &collection : collectionList) {
0665         m_collections[collection.id()] = collection;
0666 
0667         const QModelIndex index = indexForCollection(collection);
0668         Q_ASSERT(index.isValid());
0669         dataChanged(index, index);
0670     }
0671 }
0672 
0673 void EntityTreeModelPrivate::insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
0674 {
0675     Q_ASSERT(collection.isValid());
0676     Q_ASSERT(parent.isValid());
0677 
0678     Q_Q(EntityTreeModel);
0679 
0680     const int row = 0;
0681     const QModelIndex parentIndex = indexForCollection(parent);
0682     q->beginInsertRows(parentIndex, row, row);
0683     m_collections.insert(collection.id(), collection);
0684     prependNode(new Node{Node::Collection, collection.id(), parent.id()});
0685     q->endInsertRows();
0686 }
0687 
0688 bool EntityTreeModelPrivate::hasChildCollection(const Collection &collection) const
0689 {
0690     const auto &children = m_childEntities[collection.id()];
0691     for (const Node *node : children) {
0692         if (node->type == Node::Collection) {
0693             const Collection subcol = m_collections[node->id];
0694             if (shouldBePartOfModel(subcol)) {
0695                 return true;
0696             }
0697         }
0698     }
0699     return false;
0700 }
0701 
0702 bool EntityTreeModelPrivate::isAncestorMonitored(const Collection &collection) const
0703 {
0704     Akonadi::Collection parent = collection.parentCollection();
0705     while (parent.isValid()) {
0706         if (m_monitor->collectionsMonitored().contains(parent)) {
0707             return true;
0708         }
0709         parent = parent.parentCollection();
0710     }
0711     return false;
0712 }
0713 
0714 bool EntityTreeModelPrivate::shouldBePartOfModel(const Collection &collection) const
0715 {
0716     if (isHidden(collection)) {
0717         return false;
0718     }
0719 
0720     // We want a parent collection if it has at least one child that matches the
0721     // wanted mimetype
0722     if (hasChildCollection(collection)) {
0723         return true;
0724     }
0725 
0726     // Explicitly monitored collection
0727     if (m_monitor->collectionsMonitored().contains(collection)) {
0728         return true;
0729     }
0730 
0731     // We're explicitly monitoring collections, but didn't match the filter
0732     if (!m_mimeChecker.hasWantedMimeTypes() && !m_monitor->collectionsMonitored().isEmpty()) {
0733         // The collection should be included if one of the parents is monitored
0734         return isAncestorMonitored(collection);
0735     }
0736 
0737     // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we
0738     // only get the ones we're interested in from the job, we have to filter on collections received through signals too.
0739     if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedCollection(collection)) {
0740         return false;
0741     }
0742 
0743     if (m_listFilter == CollectionFetchScope::Enabled) {
0744         if (!collection.enabled()) {
0745             return false;
0746         }
0747     } else if (m_listFilter == CollectionFetchScope::Display) {
0748         if (!collection.shouldList(Collection::ListDisplay)) {
0749             return false;
0750         }
0751     } else if (m_listFilter == CollectionFetchScope::Sync) {
0752         if (!collection.shouldList(Collection::ListSync)) {
0753             return false;
0754         }
0755     } else if (m_listFilter == CollectionFetchScope::Index) {
0756         if (!collection.shouldList(Collection::ListIndex)) {
0757             return false;
0758         }
0759     }
0760 
0761     return true;
0762 }
0763 
0764 void EntityTreeModelPrivate::removeChildEntities(Collection::Id collectionId)
0765 {
0766     const QList<Node *> childList = m_childEntities.value(collectionId);
0767     for (const Node *node : childList) {
0768         if (node->type == Node::Item) {
0769             m_items.unref(node->id);
0770         } else {
0771             removeChildEntities(node->id);
0772             m_collections.remove(node->id);
0773             m_populatedCols.remove(node->id);
0774         }
0775     }
0776 
0777     qDeleteAll(m_childEntities.take(collectionId));
0778 }
0779 
0780 QStringList EntityTreeModelPrivate::childCollectionNames(const Collection &collection) const
0781 {
0782     return m_childEntities[collection.id()] | Views::filter([](const Node *node) {
0783                return node->type == Node::Collection;
0784            })
0785         | Views::transform([this](const Node *node) {
0786                return m_collections.value(node->id).name();
0787            })
0788         | Actions::toQList;
0789 }
0790 
0791 void EntityTreeModelPrivate::monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
0792 {
0793     // If the resource is removed while populating the model with it, we might still
0794     // get some monitor signals. These stale/out-of-order signals can't be completely eliminated
0795     // in the akonadi server due to implementation details, so we also handle such signals in the model silently
0796     // in all the monitored slots.
0797     // Stephen Kelly, 28, July 2009
0798 
0799     // If a fetch job is started and a collection is added to akonadi after the fetch job is started, the
0800     // new collection will be added to the fetch job results. It will also be notified through the monitor.
0801     // We return early here in that case.
0802     if (m_collections.contains(collection.id())) {
0803         return;
0804     }
0805 
0806     // If the resource is explicitly monitored all other checks are skipped. topLevelCollectionsFetched still checks the hidden attribute.
0807     if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && collection.parentCollection() == Collection::root()) {
0808         topLevelCollectionsFetched({collection});
0809         return;
0810     }
0811 
0812     if (!shouldBePartOfModel(collection)) {
0813         return;
0814     }
0815 
0816     if (!m_collections.contains(parent.id())) {
0817         // The collection we're interested in is contained in a collection we're not interested in.
0818         // We download the ancestors of the collection we're interested in to complete the tree.
0819         if (collection != Collection::root()) {
0820             if (!retrieveAncestors(collection)) {
0821                 return;
0822             }
0823         }
0824     } else {
0825         insertCollection(collection, parent);
0826     }
0827 
0828     if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
0829         fetchItems(collection);
0830     }
0831 }
0832 
0833 void EntityTreeModelPrivate::monitoredCollectionRemoved(const Akonadi::Collection &collection)
0834 {
0835     // 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)
0836     if ((collection == m_rootCollection) || m_monitor->collectionsMonitored().contains(collection)) {
0837         beginResetModel();
0838         endResetModel();
0839         return;
0840     }
0841 
0842     Collection::Id parentId = collection.parentCollection().id();
0843     if (parentId < 0) {
0844         parentId = -1;
0845     }
0846 
0847     if (!m_collections.contains(parentId)) {
0848         return;
0849     }
0850 
0851     // This may be a signal for a collection we've already removed by removing its ancestor.
0852     // Or the collection may have been hidden.
0853     if (!m_collections.contains(collection.id())) {
0854         return;
0855     }
0856 
0857     Q_Q(EntityTreeModel);
0858 
0859     Q_ASSERT(m_childEntities.contains(parentId));
0860 
0861     const int row = indexOf<Node::Collection>(m_childEntities.value(parentId), collection.id());
0862     Q_ASSERT(row >= 0);
0863 
0864     Q_ASSERT(m_collections.contains(parentId));
0865     const Collection parentCollection = m_collections.value(parentId);
0866 
0867     m_populatedCols.remove(collection.id());
0868 
0869     const QModelIndex parentIndex = indexForCollection(parentCollection);
0870 
0871     q->beginRemoveRows(parentIndex, row, row);
0872     // Delete all descendant collections and items.
0873     removeChildEntities(collection.id());
0874     // Remove deleted collection from its parent.
0875     delete m_childEntities[parentId].takeAt(row);
0876     // Remove deleted collection itself.
0877     m_collections.remove(collection.id());
0878     q->endRemoveRows();
0879 
0880     // After removing a collection, check whether it's parent should be removed too
0881     if (!shouldBePartOfModel(parentCollection)) {
0882         monitoredCollectionRemoved(parentCollection);
0883     }
0884 }
0885 
0886 void EntityTreeModelPrivate::monitoredCollectionMoved(const Akonadi::Collection &collection,
0887                                                       const Akonadi::Collection &sourceCollection,
0888                                                       const Akonadi::Collection &destCollection)
0889 {
0890     if (isHidden(collection)) {
0891         return;
0892     }
0893 
0894     if (isHidden(sourceCollection)) {
0895         if (isHidden(destCollection)) {
0896             return;
0897         }
0898 
0899         monitoredCollectionAdded(collection, destCollection);
0900         return;
0901     } else if (isHidden(destCollection)) {
0902         monitoredCollectionRemoved(collection);
0903         return;
0904     }
0905 
0906     if (!m_collections.contains(collection.id())) {
0907         return;
0908     }
0909 
0910     if (m_monitor->collectionsMonitored().contains(collection)) {
0911         // if we don't reset here, we would have to make sure that destination collection is actually available,
0912         // and remove the sources parents if they were only included as parents of the moved collection
0913         beginResetModel();
0914         endResetModel();
0915         return;
0916     }
0917 
0918     Q_Q(EntityTreeModel);
0919 
0920     const QModelIndex srcParentIndex = indexForCollection(sourceCollection);
0921     const QModelIndex destParentIndex = indexForCollection(destCollection);
0922 
0923     Q_ASSERT(collection.parentCollection().isValid());
0924     Q_ASSERT(destCollection.isValid());
0925     Q_ASSERT(collection.parentCollection() == destCollection);
0926 
0927     const int srcRow = indexOf<Node::Collection>(m_childEntities.value(sourceCollection.id()), collection.id());
0928     const int destRow = 0; // Prepend collections
0929 
0930     if (!q->beginMoveRows(srcParentIndex, srcRow, srcRow, destParentIndex, destRow)) {
0931         qCWarning(AKONADICORE_LOG) << "Cannot move collection" << collection.id() << " from collection" << sourceCollection.id() << "to" << destCollection.id();
0932         return;
0933     }
0934 
0935     Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow);
0936     // collection has the correct parentCollection etc. We need to set it on the
0937     // internal data structure to not corrupt things.
0938     m_collections.insert(collection.id(), collection);
0939     node->parent = destCollection.id();
0940     m_childEntities[destCollection.id()].prepend(node);
0941     q->endMoveRows();
0942 }
0943 
0944 void EntityTreeModelPrivate::monitoredCollectionChanged(const Akonadi::Collection &collection)
0945 {
0946     if (!m_collections.contains(collection.id())) {
0947         // This can happen if
0948         // * we get a change notification after removing the collection.
0949         // * a collection of a non-monitored mimetype is changed elsewhere. Monitor does not
0950         //    filter by content mimetype of Collections so we get notifications for all of them.
0951 
0952         // We might match the filter now, retry adding the collection
0953         monitoredCollectionAdded(collection, collection.parentCollection());
0954         return;
0955     }
0956 
0957     if (!shouldBePartOfModel(collection)) {
0958         monitoredCollectionRemoved(collection);
0959         return;
0960     }
0961 
0962     m_collections[collection.id()] = collection;
0963 
0964     if (!m_showRootCollection && collection == m_rootCollection) {
0965         // If the root of the model is not Collection::root it might be modified.
0966         // But it doesn't exist in the accessible model structure, so we need to early return
0967         return;
0968     }
0969 
0970     const QModelIndex index = indexForCollection(collection);
0971     Q_ASSERT(index.isValid());
0972     dataChanged(index, index);
0973 }
0974 
0975 void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics)
0976 {
0977     if (!m_collections.contains(id)) {
0978         return;
0979     }
0980 
0981     m_collections[id].setStatistics(statistics);
0982 
0983     // if the item count becomes 0, add to set of collections we know to be empty
0984     // otherwise remove if in there
0985     if (statistics.count() == 0) {
0986         m_collectionsWithoutItems.insert(id);
0987     } else {
0988         m_collectionsWithoutItems.remove(id);
0989     }
0990 
0991     if (!m_showRootCollection && id == m_rootCollection.id()) {
0992         // If the root of the model is not Collection::root it might be modified.
0993         // But it doesn't exist in the accessible model structure, so we need to early return
0994         return;
0995     }
0996 
0997     const QModelIndex index = indexForCollection(m_collections[id]);
0998     dataChanged(index, index);
0999 }
1000 
1001 void EntityTreeModelPrivate::monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
1002 {
1003     Q_Q(EntityTreeModel);
1004 
1005     if (isHidden(item)) {
1006         return;
1007     }
1008 
1009     if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) {
1010         qCWarning(AKONADICORE_LOG) << "Got a stale 'added' notification for an item whose collection was already removed." << item.id() << item.remoteId();
1011         return;
1012     }
1013 
1014     if (m_items.contains(item.id())) {
1015         return;
1016     }
1017 
1018     Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true);
1019 
1020     if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) {
1021         return;
1022     }
1023 
1024     // Adding items to not yet populated collections would block fetchMore, resulting in only new items showing up in the collection
1025     // This is only a problem with lazy population, otherwise fetchMore is not used at all
1026     if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collection.id())) {
1027         return;
1028     }
1029 
1030     int row;
1031     QModelIndex parentIndex;
1032     if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) {
1033         row = m_childEntities.value(collection.id()).size();
1034         parentIndex = indexForCollection(m_collections.value(collection.id()));
1035     } else {
1036         row = q->rowCount();
1037     }
1038     q->beginInsertRows(parentIndex, row, row);
1039     m_items.ref(item.id(), item);
1040     Node *node = new Node{Node::Item, item.id(), collection.id()};
1041     if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) {
1042         m_childEntities[collection.id()].append(node);
1043     } else {
1044         m_childEntities[m_rootCollection.id()].append(node);
1045     }
1046     q->endInsertRows();
1047 }
1048 
1049 void EntityTreeModelPrivate::monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &parentCollection)
1050 {
1051     Q_Q(EntityTreeModel);
1052 
1053     if (isHidden(item)) {
1054         return;
1055     }
1056 
1057     if ((m_itemPopulation == EntityTreeModel::LazyPopulation)
1058         && !m_populatedCols.contains(parentCollection.isValid() ? parentCollection.id() : item.parentCollection().id())) {
1059         return;
1060     }
1061 
1062     const Collection::List parents = getParentCollections(item);
1063     if (parents.isEmpty()) {
1064         return;
1065     }
1066 
1067     if (!m_items.contains(item.id())) {
1068         qCWarning(AKONADICORE_LOG) << "Got a stale 'removed' notification for an item which was already removed." << item.id() << item.remoteId();
1069         return;
1070     }
1071 
1072     for (const auto &collection : parents) {
1073         Q_ASSERT(m_collections.contains(collection.id()));
1074         Q_ASSERT(m_childEntities.contains(collection.id()));
1075 
1076         const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1077         Q_ASSERT(row >= 0);
1078 
1079         const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id()));
1080 
1081         q->beginRemoveRows(parentIndex, row, row);
1082         m_items.unref(item.id());
1083         delete m_childEntities[collection.id()].takeAt(row);
1084         q->endRemoveRows();
1085     }
1086 }
1087 
1088 void EntityTreeModelPrivate::monitoredItemChanged(const Akonadi::Item &item, const QSet<QByteArray> & /*unused*/)
1089 {
1090     if (isHidden(item)) {
1091         return;
1092     }
1093 
1094     if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) {
1095         return;
1096     }
1097 
1098     auto itemIt = m_items.find(item.id());
1099     if (itemIt == m_items.end()) {
1100         qCWarning(AKONADICORE_LOG) << "Got a stale 'changed' notification for an item which was already removed." << item.id() << item.remoteId();
1101         return;
1102     }
1103 
1104     itemIt->value.apply(item);
1105     // Notifications about itemChange are always dispatched for real collection
1106     // and also all virtual collections the item belongs to. In order to preserve
1107     // the original storage collection when we need to have special handling for
1108     // notifications for virtual collections
1109     if (item.parentCollection().isVirtual()) {
1110         const Collection originalParent = itemIt->value.parentCollection();
1111         itemIt->value.setParentCollection(originalParent);
1112     }
1113 
1114     const QModelIndexList indexes = indexesForItem(item);
1115     for (const QModelIndex &index : indexes) {
1116         if (index.isValid()) {
1117             dataChanged(index, index);
1118         } else {
1119             qCWarning(AKONADICORE_LOG) << "item has invalid index:" << item.id() << item.remoteId();
1120         }
1121     }
1122 }
1123 
1124 void EntityTreeModelPrivate::monitoredItemMoved(const Akonadi::Item &item,
1125                                                 const Akonadi::Collection &sourceCollection,
1126                                                 const Akonadi::Collection &destCollection)
1127 {
1128     if (isHidden(item)) {
1129         return;
1130     }
1131 
1132     if (isHidden(sourceCollection)) {
1133         if (isHidden(destCollection)) {
1134             return;
1135         }
1136 
1137         monitoredItemAdded(item, destCollection);
1138         return;
1139     } else if (isHidden(destCollection)) {
1140         monitoredItemRemoved(item, sourceCollection);
1141         return;
1142     } else {
1143         monitoredItemRemoved(item, sourceCollection);
1144         monitoredItemAdded(item, destCollection);
1145         return;
1146     }
1147     // "Temporarily" commented out as it's likely the best course to
1148     // avoid the dreaded "reset storm" (or layoutChanged storm). The
1149     // whole itemMoved idea is great but not practical until all the
1150     // other proxy models play nicely with it, right now they just
1151     // transform moved signals in layout changed, which explodes into
1152     // a reset of the source model inside of the message list (ouch!)
1153 #if 0
1154     if (!m_items.contains(item.id())) {
1155         qCWarning(AKONADICORE_LOG) << "Got a stale 'moved' notification for an item which was already removed." << item.id() << item.remoteId();
1156         return;
1157     }
1158 
1159     Q_ASSERT(m_collections.contains(sourceCollection.id()));
1160     Q_ASSERT(m_collections.contains(destCollection.id()));
1161 
1162     const QModelIndex srcIndex = indexForCollection(sourceCollection);
1163     const QModelIndex destIndex = indexForCollection(destCollection);
1164 
1165     // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes?
1166 
1167     const Item::Id itemId = item.id();
1168 
1169     const int srcRow = indexOf<Node::Item>(m_childEntities.value(sourceCollection.id()), itemId);
1170     const int destRow = q->rowCount(destIndex);
1171 
1172     Q_ASSERT(srcRow >= 0);
1173     Q_ASSERT(destRow >= 0);
1174     if (!q->beginMoveRows(srcIndex, srcRow, srcRow, destIndex, destRow)) {
1175         qCWarning(AKONADICORE_LOG) << "Invalid move";
1176         return;
1177     }
1178 
1179     Q_ASSERT(m_childEntities.contains(sourceCollection.id()));
1180     Q_ASSERT(m_childEntities[sourceCollection.id()].size() > srcRow);
1181 
1182     Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow);
1183     m_items.insert(item.id(), item);
1184     node->parent = destCollection.id();
1185     m_childEntities[destCollection.id()].append(node);
1186     q->endMoveRows();
1187 #endif
1188 }
1189 
1190 void EntityTreeModelPrivate::monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection)
1191 {
1192     Q_Q(EntityTreeModel);
1193 
1194     if (isHidden(item)) {
1195         return;
1196     }
1197 
1198     const Collection::Id collectionId = collection.id();
1199     const Item::Id itemId = item.id();
1200 
1201     if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) {
1202         qCWarning(AKONADICORE_LOG) << "Got a stale 'linked' notification for an item whose collection was already removed." << item.id() << item.remoteId();
1203         return;
1204     }
1205 
1206     Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collectionId) : true);
1207 
1208     if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) {
1209         return;
1210     }
1211 
1212     // Adding items to not yet populated collections would block fetchMore, resullting in only new items showing up in the collection
1213     // This is only a problem with lazy population, otherwise fetchMore is not used at all
1214     if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collectionId)) {
1215         return;
1216     }
1217 
1218     QList<Node *> &collectionEntities = m_childEntities[collectionId];
1219 
1220     const int existingPosition = indexOf<Node::Item>(collectionEntities, itemId);
1221 
1222     if (existingPosition > 0) {
1223         qCWarning(AKONADICORE_LOG) << "Item with id " << itemId << " already in virtual collection with id " << collectionId;
1224         return;
1225     }
1226 
1227     const int row = collectionEntities.size();
1228 
1229     const QModelIndex parentIndex = indexForCollection(m_collections.value(collectionId));
1230 
1231     q->beginInsertRows(parentIndex, row, row);
1232     m_items.ref(itemId, item);
1233     collectionEntities.append(new Node{Node::Item, itemId, collectionId});
1234     q->endInsertRows();
1235 }
1236 
1237 void EntityTreeModelPrivate::monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection)
1238 {
1239     Q_Q(EntityTreeModel);
1240 
1241     if (isHidden(item)) {
1242         return;
1243     }
1244 
1245     if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) {
1246         return;
1247     }
1248 
1249     if (!m_items.contains(item.id())) {
1250         qCWarning(AKONADICORE_LOG) << "Got a stale 'unlinked' notification for an item which was already removed." << item.id() << item.remoteId();
1251         return;
1252     }
1253 
1254     Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true);
1255     const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1256     if (row < 0 || row >= m_childEntities[collection.id()].size()) {
1257         qCWarning(AKONADICORE_LOG) << "couldn't find index of unlinked item " << item.id() << collection.id() << row;
1258         Q_ASSERT(false);
1259         return;
1260     }
1261 
1262     const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id()));
1263 
1264     q->beginRemoveRows(parentIndex, row, row);
1265     delete m_childEntities[collection.id()].takeAt(row);
1266     m_items.unref(item.id());
1267     q->endRemoveRows();
1268 }
1269 
1270 void EntityTreeModelPrivate::collectionFetchJobDone(KJob *job)
1271 {
1272     m_pendingCollectionFetchJobs.remove(job);
1273     auto cJob = static_cast<CollectionFetchJob *>(job);
1274     if (job->error()) {
1275         qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << cJob->collections();
1276         return;
1277     }
1278 
1279     if (!m_collectionTreeFetched && m_pendingCollectionFetchJobs.isEmpty()) {
1280         m_collectionTreeFetched = true;
1281         Q_EMIT q_ptr->collectionTreeFetched(m_collections | Views::values | Actions::toQVector);
1282     }
1283 
1284     qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec";
1285     qCDebug(DebugETM) << "was collection fetch job: collections:" << cJob->collections().size();
1286     if (!cJob->collections().isEmpty()) {
1287         qCDebug(DebugETM) << "first fetched collection:" << cJob->collections().at(0).name();
1288     }
1289 }
1290 
1291 void EntityTreeModelPrivate::itemFetchJobDone(Collection::Id collectionId, KJob *job)
1292 {
1293     m_pendingCollectionRetrieveJobs.remove(collectionId);
1294 
1295     if (job->error()) {
1296         qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << collectionId;
1297         return;
1298     }
1299     if (!m_collections.contains(collectionId)) {
1300         qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items";
1301         return;
1302     }
1303     auto iJob = static_cast<ItemFetchJob *>(job);
1304     qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec";
1305     qCDebug(DebugETM) << "was item fetch job: items:" << iJob->count();
1306 
1307     if (iJob->count() == 0) {
1308         m_collectionsWithoutItems.insert(collectionId);
1309     } else {
1310         m_collectionsWithoutItems.remove(collectionId);
1311     }
1312 
1313     m_populatedCols.insert(collectionId);
1314     Q_EMIT q_ptr->collectionPopulated(collectionId);
1315 
1316     // If collections are not in the model, there will be no valid index for them.
1317     if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)
1318         && (m_showRootCollection || collectionId != m_rootCollection.id())) {
1319         const QModelIndex index = indexForCollection(Collection(collectionId));
1320         Q_ASSERT(index.isValid());
1321         // To notify about the changed fetch and population state
1322         dataChanged(index, index);
1323     }
1324 }
1325 
1326 void EntityTreeModelPrivate::pasteJobDone(KJob *job)
1327 {
1328     if (job->error()) {
1329         QString errorMsg;
1330         if (qobject_cast<ItemCopyJob *>(job)) {
1331             errorMsg = i18nc("@info", "Could not copy item: <message>%1</message>", job->errorString());
1332         } else if (qobject_cast<CollectionCopyJob *>(job)) {
1333             errorMsg = i18nc("@info", "Could not copy collection: <message>%1</message>", job->errorString());
1334         } else if (qobject_cast<ItemMoveJob *>(job)) {
1335             errorMsg = i18nc("@info", "Could not move item: <message>%1</message>", job->errorString());
1336         } else if (qobject_cast<CollectionMoveJob *>(job)) {
1337             errorMsg = i18nc("@info", "Could not move collection: <message>%1</message>", job->errorString());
1338         } else if (qobject_cast<LinkJob *>(job)) {
1339             errorMsg = i18nc("@info", "Could not link entity: <message>%1</message>", job->errorString());
1340         }
1341         QMessageBox::critical(nullptr, i18nc("@title:window", "Error"), errorMsg);
1342     }
1343 }
1344 
1345 void EntityTreeModelPrivate::updateJobDone(KJob *job)
1346 {
1347     if (job->error()) {
1348         // TODO: handle job errors
1349         qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString();
1350     }
1351 }
1352 
1353 void EntityTreeModelPrivate::rootFetchJobDone(KJob *job)
1354 {
1355     if (job->error()) {
1356         qCWarning(AKONADICORE_LOG) << job->errorString();
1357         return;
1358     }
1359     auto collectionJob = qobject_cast<CollectionFetchJob *>(job);
1360     const Collection::List list = collectionJob->collections();
1361 
1362     Q_ASSERT(list.size() == 1);
1363     m_rootCollection = list.first();
1364     startFirstListJob();
1365 }
1366 
1367 void EntityTreeModelPrivate::startFirstListJob()
1368 {
1369     Q_Q(EntityTreeModel);
1370 
1371     if (!m_collections.isEmpty()) {
1372         return;
1373     }
1374 
1375     // Even if the root collection is the invalid collection, we still need to start
1376     // the first list job with Collection::root.
1377     auto node = new Node{Node::Collection, m_rootCollection.id(), -1};
1378     if (m_showRootCollection) {
1379         // Notify the outside that we're putting collection::root into the model.
1380         q->beginInsertRows(QModelIndex(), 0, 0);
1381         m_collections.insert(m_rootCollection.id(), m_rootCollection);
1382         delete m_rootNode;
1383         appendNode(node);
1384         q->endInsertRows();
1385     } else {
1386         // Otherwise store it silently because it's not part of the usable model.
1387         delete m_rootNode;
1388         m_rootNode = node;
1389         m_needDeleteRootNode = true;
1390         m_collections.insert(m_rootCollection.id(), m_rootCollection);
1391     }
1392 
1393     const bool noMimetypes = !m_mimeChecker.hasWantedMimeTypes();
1394     const bool noResources = m_monitor->resourcesMonitored().isEmpty();
1395     const bool multipleCollections = m_monitor->collectionsMonitored().size() > 1;
1396     const bool generalPopulation = !noMimetypes || noResources;
1397 
1398     const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy);
1399 
1400     // Collections can only be monitored if no resources and no mimetypes are monitored
1401     if (multipleCollections && noMimetypes && noResources) {
1402         fetchCollections(m_monitor->collectionsMonitored(), CollectionFetchJob::Base);
1403         fetchCollections(m_monitor->collectionsMonitored(), fetchType);
1404         return;
1405     }
1406 
1407     qCDebug(DebugETM) << "GEN" << generalPopulation << noMimetypes << noResources;
1408     if (generalPopulation) {
1409         fetchCollections(m_rootCollection, fetchType);
1410     }
1411 
1412     // If the root collection is not collection::root, then it could have items, and they will need to be
1413     // retrieved now.
1414     // Only fetch items NOT if there is NoItemPopulation, or if there is Lazypopulation and the root is visible
1415     // (if the root is not visible the lazy population can not be triggered)
1416     if ((m_itemPopulation != EntityTreeModel::NoItemPopulation) && !((m_itemPopulation == EntityTreeModel::LazyPopulation) && m_showRootCollection)) {
1417         if (m_rootCollection != Collection::root()) {
1418             fetchItems(m_rootCollection);
1419         }
1420     }
1421 
1422     // Resources which are explicitly monitored won't have appeared yet if their mimetype didn't match.
1423     // We fetch the top level collections and examine them for whether to add them.
1424     // This fetches virtual collections into the tree.
1425     if (!m_monitor->resourcesMonitored().isEmpty()) {
1426         fetchTopLevelCollections();
1427     }
1428 }
1429 
1430 void EntityTreeModelPrivate::fetchTopLevelCollections() const
1431 {
1432     Q_Q(const EntityTreeModel);
1433     auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, m_session);
1434     q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(topLevelCollectionsFetched(Akonadi::Collection::List)));
1435     q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *)));
1436     qCDebug(DebugETM) << "EntityTreeModelPrivate::fetchTopLevelCollections";
1437     jobTimeTracker[job].start();
1438 }
1439 
1440 void EntityTreeModelPrivate::topLevelCollectionsFetched(const Akonadi::Collection::List &list)
1441 {
1442     Q_Q(EntityTreeModel);
1443     for (const Collection &collection : list) {
1444         // These collections have been explicitly shown in the Monitor,
1445         // but hidden trumps that for now. This may change in the future if we figure out a use for it.
1446         if (isHidden(collection)) {
1447             continue;
1448         }
1449 
1450         if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && !m_collections.contains(collection.id())) {
1451             const QModelIndex parentIndex = indexForCollection(collection.parentCollection());
1452             // Prepending new collections.
1453             const int row = 0;
1454             q->beginInsertRows(parentIndex, row, row);
1455 
1456             m_collections.insert(collection.id(), collection);
1457             Q_ASSERT(collection.parentCollection() == Collection::root());
1458             prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()});
1459             q->endInsertRows();
1460 
1461             if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) {
1462                 fetchItems(collection);
1463             }
1464 
1465             Q_ASSERT(collection.isValid());
1466             fetchCollections(collection, CollectionFetchJob::Recursive);
1467         }
1468     }
1469 }
1470 
1471 Akonadi::Collection::List EntityTreeModelPrivate::getParentCollections(const Item &item) const
1472 {
1473     Collection::List list;
1474     for (auto it = m_childEntities.constKeyValueBegin(), end = m_childEntities.constKeyValueEnd(); it != end; ++it) {
1475         const auto &[parentId, childNodes] = *it;
1476         int nodeIndex = indexOf<Node::Item>(childNodes, item.id());
1477         if (nodeIndex != -1 && childNodes.at(nodeIndex)->type == Node::Item) {
1478             list.push_back(m_collections.value(parentId));
1479         }
1480     }
1481 
1482     return list;
1483 }
1484 
1485 void EntityTreeModelPrivate::ref(Collection::Id id)
1486 {
1487     m_monitor->d_ptr->ref(id);
1488 }
1489 
1490 bool EntityTreeModelPrivate::shouldPurge(Collection::Id id) const
1491 {
1492     // reference counted collections should never be purged
1493     // they first have to be deref'ed until they reach 0.
1494     // if the collection is buffered, keep it, otherwise we can safely purge this item
1495     return !m_monitor->d_ptr->isMonitored(id);
1496 }
1497 
1498 bool EntityTreeModelPrivate::isMonitored(Collection::Id id) const
1499 {
1500     return m_monitor->d_ptr->isMonitored(id);
1501 }
1502 
1503 bool EntityTreeModelPrivate::isBuffered(Collection::Id id) const
1504 {
1505     return m_monitor->d_ptr->m_buffer.isBuffered(id);
1506 }
1507 
1508 void EntityTreeModelPrivate::deref(Collection::Id id)
1509 {
1510     const Collection::Id bumpedId = m_monitor->d_ptr->deref(id);
1511 
1512     if (bumpedId < 0) {
1513         return;
1514     }
1515 
1516     // The collection has already been removed, don't purge
1517     if (!m_collections.contains(bumpedId)) {
1518         return;
1519     }
1520 
1521     if (shouldPurge(bumpedId)) {
1522         purgeItems(bumpedId);
1523     }
1524 }
1525 
1526 QList<Node *>::iterator EntityTreeModelPrivate::skipCollections(QList<Node *>::iterator it, const QList<Node *>::iterator &end, int *pos)
1527 {
1528     for (; it != end; ++it) {
1529         if ((*it)->type == Node::Item) {
1530             break;
1531         }
1532 
1533         ++(*pos);
1534     }
1535 
1536     return it;
1537 }
1538 
1539 QList<Node *>::iterator
1540 EntityTreeModelPrivate::removeItems(QList<Node *>::iterator it, const QList<Node *>::iterator &end, int *pos, const Collection &collection)
1541 {
1542     Q_Q(EntityTreeModel);
1543 
1544     QList<Node *>::iterator startIt = it;
1545 
1546     // figure out how many items we will delete
1547     int start = *pos;
1548     for (; it != end; ++it) {
1549         if ((*it)->type != Node::Item) {
1550             break;
1551         }
1552 
1553         ++(*pos);
1554     }
1555     it = startIt;
1556 
1557     const QModelIndex parentIndex = indexForCollection(collection);
1558 
1559     q->beginRemoveRows(parentIndex, start, (*pos) - 1);
1560     const int toDelete = (*pos) - start;
1561     Q_ASSERT(toDelete > 0);
1562 
1563     QList<Node *> &es = m_childEntities[collection.id()];
1564     // NOTE: .erase will invalidate all iterators besides "it"!
1565     for (int i = 0; i < toDelete; ++i) {
1566         Q_ASSERT(es.count(*it) == 1);
1567         // don't keep implicitly shared data alive
1568         Q_ASSERT(m_items.contains((*it)->id));
1569         m_items.unref((*it)->id);
1570         // delete actual node
1571         delete *it;
1572         it = es.erase(it);
1573     }
1574     q->endRemoveRows();
1575 
1576     return it;
1577 }
1578 
1579 void EntityTreeModelPrivate::purgeItems(Collection::Id id)
1580 {
1581     QList<Node *> &childEntities = m_childEntities[id];
1582 
1583     const Collection collection = m_collections.value(id);
1584     Q_ASSERT(collection.isValid());
1585 
1586     QList<Node *>::iterator begin = childEntities.begin();
1587     QList<Node *>::iterator end = childEntities.end();
1588 
1589     int pos = 0;
1590     while ((begin = skipCollections(begin, end, &pos)) != end) {
1591         begin = removeItems(begin, end, &pos, collection);
1592         end = childEntities.end();
1593     }
1594     m_populatedCols.remove(id);
1595     // if an empty collection is purged and we leave it in here, itemAdded will be ignored for the collection
1596     // and the collection is never populated by fetchMore (but maybe by statistics changed?)
1597     m_collectionsWithoutItems.remove(id);
1598 }
1599 
1600 void EntityTreeModelPrivate::dataChanged(const QModelIndex &top, const QModelIndex &bottom)
1601 {
1602     Q_Q(EntityTreeModel);
1603 
1604     QModelIndex rightIndex;
1605 
1606     const Node *node = static_cast<Node *>(bottom.internalPointer());
1607     if (!node) {
1608         return;
1609     }
1610 
1611     if (node->type == Node::Collection) {
1612         rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::CollectionTreeHeaders) - 1);
1613     }
1614     if (node->type == Node::Item) {
1615         rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::ItemListHeaders) - 1);
1616     }
1617 
1618     Q_EMIT q->dataChanged(top, rightIndex);
1619 }
1620 
1621 QModelIndex EntityTreeModelPrivate::indexForCollection(const Collection &collection) const
1622 {
1623     Q_Q(const EntityTreeModel);
1624 
1625     if (!collection.isValid()) {
1626         return QModelIndex();
1627     }
1628 
1629     if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
1630         return QModelIndex();
1631     }
1632 
1633     // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob,
1634     // we ensure that we use -1 for the invalid Collection.
1635     Collection::Id parentId = -1;
1636 
1637     if ((collection == m_rootCollection)) {
1638         if (m_showRootCollection) {
1639             return q->createIndex(0, 0, static_cast<void *>(m_rootNode));
1640         }
1641         return QModelIndex();
1642     }
1643 
1644     if (collection == Collection::root()) {
1645         parentId = -1;
1646     } else if (collection.parentCollection().isValid()) {
1647         parentId = collection.parentCollection().id();
1648     } else {
1649         for (const auto &children : m_childEntities) {
1650             const int row = indexOf<Node::Collection>(children, collection.id());
1651             if (row < 0) {
1652                 continue;
1653             }
1654 
1655             Node *node = children.at(row);
1656             return q->createIndex(row, 0, static_cast<void *>(node));
1657         }
1658         return QModelIndex();
1659     }
1660 
1661     const int row = indexOf<Node::Collection>(m_childEntities.value(parentId), collection.id());
1662 
1663     if (row < 0) {
1664         return QModelIndex();
1665     }
1666 
1667     Node *node = m_childEntities.value(parentId).at(row);
1668 
1669     return q->createIndex(row, 0, static_cast<void *>(node));
1670 }
1671 
1672 QModelIndexList EntityTreeModelPrivate::indexesForItem(const Item &item) const
1673 {
1674     Q_Q(const EntityTreeModel);
1675     QModelIndexList indexes;
1676 
1677     if (m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections) {
1678         Q_ASSERT(m_childEntities.contains(m_rootCollection.id()));
1679         QList<Node *> nodeList = m_childEntities.value(m_rootCollection.id());
1680         const int row = indexOf<Node::Item>(nodeList, item.id());
1681         Q_ASSERT(row >= 0);
1682         Q_ASSERT(row < nodeList.size());
1683         Node *node = nodeList.at(row);
1684 
1685         indexes << q->createIndex(row, 0, static_cast<void *>(node));
1686         return indexes;
1687     }
1688 
1689     const Collection::List collections = getParentCollections(item);
1690 
1691     indexes.reserve(collections.size());
1692     for (const Collection &collection : collections) {
1693         const int row = indexOf<Node::Item>(m_childEntities.value(collection.id()), item.id());
1694         Q_ASSERT(row >= 0);
1695         Q_ASSERT(m_childEntities.contains(collection.id()));
1696         QList<Node *> nodeList = m_childEntities.value(collection.id());
1697         Q_ASSERT(row < nodeList.size());
1698         Node *node = nodeList.at(row);
1699 
1700         indexes << q->createIndex(row, 0, static_cast<void *>(node));
1701     }
1702 
1703     return indexes;
1704 }
1705 
1706 void EntityTreeModelPrivate::beginResetModel()
1707 {
1708     Q_Q(EntityTreeModel);
1709     q->beginResetModel();
1710 }
1711 
1712 void EntityTreeModelPrivate::endResetModel()
1713 {
1714     Q_Q(EntityTreeModel);
1715     auto subjobs = m_session->findChildren<Akonadi::Job *>();
1716     for (auto job : subjobs) {
1717         job->disconnect(q);
1718     }
1719     m_collections.clear();
1720     m_collectionsWithoutItems.clear();
1721     m_populatedCols.clear();
1722     m_items.clear();
1723     m_pendingCollectionFetchJobs.clear();
1724     m_pendingCollectionRetrieveJobs.clear();
1725     m_collectionTreeFetched = false;
1726 
1727     for (const QList<Node *> &list : std::as_const(m_childEntities)) {
1728         qDeleteAll(list);
1729     }
1730     m_childEntities.clear();
1731     if (m_needDeleteRootNode) {
1732         m_needDeleteRootNode = false;
1733         delete m_rootNode;
1734     }
1735     m_rootNode = nullptr;
1736 
1737     q->endResetModel();
1738     fillModel();
1739 }
1740 
1741 void EntityTreeModelPrivate::monitoredItemsRetrieved(KJob *job)
1742 {
1743     if (job->error()) {
1744         qCWarning(AKONADICORE_LOG) << job->errorString();
1745         return;
1746     }
1747 
1748     Q_Q(EntityTreeModel);
1749 
1750     auto fetchJob = qobject_cast<ItemFetchJob *>(job);
1751     Q_ASSERT(fetchJob);
1752     Item::List list = fetchJob->items();
1753 
1754     q->beginResetModel();
1755     for (const Item &item : list) {
1756         m_childEntities[-1].append(new Node{Node::Item, item.id(), m_rootCollection.id()});
1757         m_items.ref(item.id(), item);
1758     }
1759     q->endResetModel();
1760 }
1761 
1762 void EntityTreeModelPrivate::fillModel()
1763 {
1764     Q_Q(EntityTreeModel);
1765 
1766     m_mimeChecker.setWantedMimeTypes(m_monitor->mimeTypesMonitored());
1767 
1768     const Collection::List collections = m_monitor->collectionsMonitored();
1769 
1770     if (collections.isEmpty() && m_monitor->numMimeTypesMonitored() == 0 && m_monitor->numResourcesMonitored() == 0 && m_monitor->numItemsMonitored() != 0) {
1771         m_rootCollection = Collection(-1);
1772         m_collectionTreeFetched = true;
1773         Q_EMIT q_ptr->collectionTreeFetched(collections); // there are no collections to fetch
1774 
1775         const auto items = m_monitor->itemsMonitoredEx() | Views::transform([](const auto id) {
1776                                return Item{id};
1777                            })
1778             | Actions::toQVector;
1779         auto itemFetch = new ItemFetchJob(items, m_session);
1780         itemFetch->setFetchScope(m_monitor->itemFetchScope());
1781         itemFetch->fetchScope().setIgnoreRetrievalErrors(true);
1782         q->connect(itemFetch, SIGNAL(finished(KJob *)), q, SLOT(monitoredItemsRetrieved(KJob *)));
1783         return;
1784     }
1785     // In case there is only a single collection monitored, we can use this
1786     // collection as root of the node tree, in all other cases
1787     // Collection::root() is used
1788     if (collections.size() == 1) {
1789         m_rootCollection = collections.first();
1790     } else {
1791         m_rootCollection = Collection::root();
1792     }
1793 
1794     if (m_rootCollection == Collection::root()) {
1795         QTimer::singleShot(0, q, SLOT(startFirstListJob()));
1796     } else {
1797         Q_ASSERT(m_rootCollection.isValid());
1798         auto rootFetchJob = new CollectionFetchJob(m_rootCollection, CollectionFetchJob::Base, m_session);
1799         q->connect(rootFetchJob, SIGNAL(result(KJob *)), SLOT(rootFetchJobDone(KJob *)));
1800         qCDebug(DebugETM) << "";
1801         jobTimeTracker[rootFetchJob].start();
1802     }
1803 }
1804 
1805 bool EntityTreeModelPrivate::canFetchMore(const QModelIndex &parent) const
1806 {
1807     const Item item = parent.data(EntityTreeModel::ItemRole).value<Item>();
1808 
1809     if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) {
1810         return false;
1811     }
1812 
1813     if (item.isValid()) {
1814         // items can't have more rows.
1815         // TODO: Should I use this for fetching more of an item, ie more payload parts?
1816         return false;
1817     } else {
1818         // but collections can...
1819         const Collection::Id colId = parent.data(EntityTreeModel::CollectionIdRole).toULongLong();
1820 
1821         // But the root collection can't...
1822         if (Collection::root().id() == colId) {
1823             return false;
1824         }
1825 
1826         // Collections which contain no items at all can't contain more
1827         if (m_collectionsWithoutItems.contains(colId)) {
1828             return false;
1829         }
1830 
1831         // Don't start the same job multiple times.
1832         if (m_pendingCollectionRetrieveJobs.contains(colId)) {
1833             return false;
1834         }
1835 
1836         // Can't fetch more if the collection's items have already been fetched
1837         if (m_populatedCols.contains(colId)) {
1838             return false;
1839         }
1840 
1841         // Only try to fetch more from a collection if we don't already have items in it.
1842         // Otherwise we'd spend all the time listing items in collections.
1843         return m_childEntities.value(colId) | Actions::none(Node::isItem);
1844     }
1845 }
1846 
1847 QIcon EntityTreeModelPrivate::iconForName(const QString &name) const
1848 {
1849     if (m_iconThemeName != QIcon::themeName()) {
1850         m_iconThemeName = QIcon::themeName();
1851         m_iconCache.clear();
1852     }
1853 
1854     QIcon &icon = m_iconCache[name];
1855     if (icon.isNull()) {
1856         icon = QIcon::fromTheme(name);
1857     }
1858     return icon;
1859 }