File indexing completed on 2024-11-10 04:40:35
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 }