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