1 /*
2     SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "entitytreemodel.h"
8 #include "akonadicore_debug.h"
9 #include "entitytreemodel_p.h"
10 #include "monitor_p.h"
11 
12 #include <QAbstractProxyModel>
13 #include <QHash>
14 #include <QMessageBox>
15 #include <QMimeData>
16 
17 #include <KLocalizedString>
18 #include <QUrl>
19 #include <QUrlQuery>
20 
21 #include "attributefactory.h"
22 #include "collectionmodifyjob.h"
23 #include "entitydisplayattribute.h"
24 #include "itemmodifyjob.h"
25 #include "monitor.h"
26 #include "session.h"
27 #include "transactionsequence.h"
28 
29 #include "collectionutils.h"
30 
31 #include "pastehelper_p.h"
32 
33 // clazy:excludeall=old-style-connect
34 
35 Q_DECLARE_METATYPE(QSet<QByteArray>)
36 
37 using namespace Akonadi;
38 
EntityTreeModel(Monitor * monitor,QObject * parent)39 EntityTreeModel::EntityTreeModel(Monitor *monitor, QObject *parent)
40     : QAbstractItemModel(parent)
41     , d_ptr(new EntityTreeModelPrivate(this))
42 {
43     Q_D(EntityTreeModel);
44     d->init(monitor);
45 }
46 
EntityTreeModel(Monitor * monitor,EntityTreeModelPrivate * d,QObject * parent)47 EntityTreeModel::EntityTreeModel(Monitor *monitor, EntityTreeModelPrivate *d, QObject *parent)
48     : QAbstractItemModel(parent)
49     , d_ptr(d)
50 {
51     d->init(monitor);
52 }
53 
~EntityTreeModel()54 EntityTreeModel::~EntityTreeModel()
55 {
56     Q_D(EntityTreeModel);
57 
58     for (const QList<Node *> &list : std::as_const(d->m_childEntities)) {
59         qDeleteAll(list);
60     }
61 }
62 
listFilter() const63 CollectionFetchScope::ListFilter EntityTreeModel::listFilter() const
64 {
65     Q_D(const EntityTreeModel);
66     return d->m_listFilter;
67 }
68 
setListFilter(CollectionFetchScope::ListFilter filter)69 void EntityTreeModel::setListFilter(CollectionFetchScope::ListFilter filter)
70 {
71     Q_D(EntityTreeModel);
72     d->beginResetModel();
73     d->m_listFilter = filter;
74     d->m_monitor->setAllMonitored(filter == CollectionFetchScope::NoFilter);
75     d->endResetModel();
76 }
77 
setCollectionsMonitored(const Collection::List & collections)78 void EntityTreeModel::setCollectionsMonitored(const Collection::List &collections)
79 {
80     Q_D(EntityTreeModel);
81     d->beginResetModel();
82     const Akonadi::Collection::List lstCols = d->m_monitor->collectionsMonitored();
83     for (const Akonadi::Collection &col : lstCols) {
84         d->m_monitor->setCollectionMonitored(col, false);
85     }
86     for (const Akonadi::Collection &col : collections) {
87         d->m_monitor->setCollectionMonitored(col, true);
88     }
89     d->endResetModel();
90 }
91 
setCollectionMonitored(const Collection & col,bool monitored)92 void EntityTreeModel::setCollectionMonitored(const Collection &col, bool monitored)
93 {
94     Q_D(EntityTreeModel);
95     d->m_monitor->setCollectionMonitored(col, monitored);
96 }
97 
systemEntitiesShown() const98 bool EntityTreeModel::systemEntitiesShown() const
99 {
100     Q_D(const EntityTreeModel);
101     return d->m_showSystemEntities;
102 }
103 
setShowSystemEntities(bool show)104 void EntityTreeModel::setShowSystemEntities(bool show)
105 {
106     Q_D(EntityTreeModel);
107     d->m_showSystemEntities = show;
108 }
109 
clearAndReset()110 void EntityTreeModel::clearAndReset()
111 {
112     Q_D(EntityTreeModel);
113     d->beginResetModel();
114     d->endResetModel();
115 }
116 
roleNames() const117 QHash<int, QByteArray> EntityTreeModel::roleNames() const
118 {
119     return {{Qt::DecorationRole, "decoration"},
120             {Qt::DisplayRole, "display"},
121 
122             {EntityTreeModel::ItemIdRole, "itemId"},
123             {EntityTreeModel::CollectionIdRole, "collectionId"},
124 
125             {EntityTreeModel::UnreadCountRole, "unreadCount"},
126             // TODO: expose when states for reporting of fetching payload parts of items is changed
127             // { EntityTreeModel::FetchStateRole, "fetchState" },
128             {EntityTreeModel::EntityUrlRole, "url"},
129             {EntityTreeModel::RemoteIdRole, "remoteId"},
130             {EntityTreeModel::IsPopulatedRole, "isPopulated"},
131             {EntityTreeModel::CollectionRole, "collection"}};
132 }
133 
columnCount(const QModelIndex & parent) const134 int EntityTreeModel::columnCount(const QModelIndex &parent) const
135 {
136     // TODO: Statistics?
137     if (parent.isValid() && parent.column() != 0) {
138         return 0;
139     }
140 
141     return qMax(entityColumnCount(CollectionTreeHeaders), entityColumnCount(ItemListHeaders));
142 }
143 
entityData(const Item & item,int column,int role) const144 QVariant EntityTreeModel::entityData(const Item &item, int column, int role) const
145 {
146     Q_D(const EntityTreeModel);
147 
148     if (column == 0) {
149         switch (role) {
150         case Qt::DisplayRole:
151         case Qt::EditRole:
152             if (const auto *attr = item.attribute<EntityDisplayAttribute>(); attr && !attr->displayName().isEmpty()) {
153                 return attr->displayName();
154             } else if (!item.remoteId().isEmpty()) {
155                 return item.remoteId();
156             }
157             return QString(QLatin1Char('<') + QString::number(item.id()) + QLatin1Char('>'));
158         case Qt::DecorationRole:
159             if (const auto *attr = item.attribute<EntityDisplayAttribute>(); attr && !attr->iconName().isEmpty()) {
160                 return d->iconForName(attr->iconName());
161             }
162             break;
163         default:
164             break;
165         }
166     }
167 
168     return QVariant();
169 }
170 
entityData(const Collection & collection,int column,int role) const171 QVariant EntityTreeModel::entityData(const Collection &collection, int column, int role) const
172 {
173     Q_D(const EntityTreeModel);
174 
175     if (column > 0) {
176         return QString();
177     }
178 
179     if (collection == Collection::root()) {
180         // Only display the root collection. It may not be edited.
181         if (role == Qt::DisplayRole) {
182             return d->m_rootCollectionDisplayName;
183         } else if (role == Qt::EditRole) {
184             return QVariant();
185         }
186     }
187 
188     switch (role) {
189     case Qt::DisplayRole:
190     case Qt::EditRole:
191         if (column == 0) {
192             if (const QString displayName = collection.displayName(); !displayName.isEmpty()) {
193                 return displayName;
194             } else {
195                 return i18nc("@info:status", "Loading...");
196             }
197         }
198         break;
199     case Qt::DecorationRole:
200         if (const auto *const attr = collection.attribute<EntityDisplayAttribute>(); attr && !attr->iconName().isEmpty()) {
201             return d->iconForName(attr->iconName());
202         }
203         return d->iconForName(CollectionUtils::defaultIconName(collection));
204     default:
205         break;
206     }
207 
208     return QVariant();
209 }
210 
data(const QModelIndex & index,int role) const211 QVariant EntityTreeModel::data(const QModelIndex &index, int role) const
212 {
213     Q_D(const EntityTreeModel);
214     if (role == SessionRole) {
215         return QVariant::fromValue(qobject_cast<QObject *>(d->m_session));
216     }
217 
218     // Ugly, but at least the API is clean.
219     const auto headerGroup = static_cast<HeaderGroup>((role / static_cast<int>(TerminalUserRole)));
220 
221     role %= TerminalUserRole;
222     if (!index.isValid()) {
223         if (ColumnCountRole != role) {
224             return QVariant();
225         }
226 
227         return entityColumnCount(headerGroup);
228     }
229 
230     if (ColumnCountRole == role) {
231         return entityColumnCount(headerGroup);
232     }
233 
234     const Node *node = reinterpret_cast<Node *>(index.internalPointer());
235 
236     if (ParentCollectionRole == role && d->m_collectionFetchStrategy != FetchNoCollections) {
237         const Collection parentCollection = d->m_collections.value(node->parent);
238         Q_ASSERT(parentCollection.isValid());
239 
240         return QVariant::fromValue(parentCollection);
241     }
242 
243     if (Node::Collection == node->type) {
244         const Collection collection = d->m_collections.value(node->id);
245         if (!collection.isValid()) {
246             return QVariant();
247         }
248 
249         switch (role) {
250         case MimeTypeRole:
251             return collection.mimeType();
252         case RemoteIdRole:
253             return collection.remoteId();
254         case CollectionIdRole:
255             return collection.id();
256         case ItemIdRole:
257             // QVariant().toInt() is 0, not -1, so we have to handle the ItemIdRole
258             // and CollectionIdRole (below) specially
259             return -1;
260         case CollectionRole:
261             return QVariant::fromValue(collection);
262         case EntityUrlRole:
263             return collection.url().url();
264         case UnreadCountRole:
265             return collection.statistics().unreadCount();
266         case FetchStateRole:
267             return d->m_pendingCollectionRetrieveJobs.contains(collection.id()) ? FetchingState : IdleState;
268         case IsPopulatedRole:
269             return d->m_populatedCols.contains(collection.id());
270         case OriginalCollectionNameRole:
271             return entityData(collection, index.column(), Qt::DisplayRole);
272         case PendingCutRole:
273             return d->m_pendingCutCollections.contains(node->id);
274         case Qt::BackgroundRole:
275             if (const auto *const attr = collection.attribute<EntityDisplayAttribute>(); attr && attr->backgroundColor().isValid()) {
276                 return attr->backgroundColor();
277             }
278             Q_FALLTHROUGH();
279         default:
280             return entityData(collection, index.column(), role);
281         }
282 
283     } else if (Node::Item == node->type) {
284         const Item item = d->m_items.value(node->id);
285         if (!item.isValid()) {
286             return QVariant();
287         }
288 
289         switch (role) {
290         case ParentCollectionRole:
291             return QVariant::fromValue(item.parentCollection());
292         case MimeTypeRole:
293             return item.mimeType();
294         case RemoteIdRole:
295             return item.remoteId();
296         case ItemRole:
297             return QVariant::fromValue(item);
298         case ItemIdRole:
299             return item.id();
300         case CollectionIdRole:
301             return -1;
302         case LoadedPartsRole:
303             return QVariant::fromValue(item.loadedPayloadParts());
304         case AvailablePartsRole:
305             return QVariant::fromValue(item.availablePayloadParts());
306         case EntityUrlRole:
307             return item.url(Akonadi::Item::UrlWithMimeType).url();
308         case PendingCutRole:
309             return d->m_pendingCutItems.contains(node->id);
310         case Qt::BackgroundRole:
311             if (const auto *const attr = item.attribute<EntityDisplayAttribute>(); attr && attr->backgroundColor().isValid()) {
312                 return attr->backgroundColor();
313             }
314             Q_FALLTHROUGH();
315         default:
316             return entityData(item, index.column(), role);
317         }
318     }
319 
320     return QVariant();
321 }
322 
flags(const QModelIndex & index) const323 Qt::ItemFlags EntityTreeModel::flags(const QModelIndex &index) const
324 {
325     Q_D(const EntityTreeModel);
326     // Pass modeltest.
327     if (!index.isValid()) {
328         return {};
329     }
330 
331     Qt::ItemFlags flags = QAbstractItemModel::flags(index);
332 
333     const Node *node = reinterpret_cast<Node *>(index.internalPointer());
334 
335     if (Node::Collection == node->type) {
336         const Collection collection = d->m_collections.value(node->id);
337         if (collection.isValid()) {
338             if (collection == Collection::root()) {
339                 // Selectable and displayable only.
340                 return flags;
341             }
342 
343             const int rights = collection.rights();
344 
345             if (rights & Collection::CanChangeCollection) {
346                 if (index.column() == 0) {
347                     flags |= Qt::ItemIsEditable;
348                 }
349                 // Changing the collection includes changing the metadata (child entityordering).
350                 // Need to allow this by drag and drop.
351                 flags |= Qt::ItemIsDropEnabled;
352             }
353             if (rights & (Collection::CanCreateCollection | Collection::CanCreateItem | Collection::CanLinkItem)) {
354                 // Can we drop new collections and items into this collection?
355                 flags |= Qt::ItemIsDropEnabled;
356             }
357 
358             // dragging is always possible, even for read-only objects, but they can only be copied, not moved.
359             flags |= Qt::ItemIsDragEnabled;
360         }
361     } else if (Node::Item == node->type) {
362         // cut out entities are shown as disabled
363         // TODO: Not sure this is wanted, it prevents any interaction with them, better
364         // solution would be to move this to the delegate, as was done for collections.
365         if (d->m_pendingCutItems.contains(node->id)) {
366             return Qt::ItemIsSelectable;
367         }
368 
369         // Rights come from the parent collection.
370 
371         Collection parentCollection;
372         if (!index.parent().isValid()) {
373             parentCollection = d->m_rootCollection;
374         } else {
375             const Node *parentNode = reinterpret_cast<Node *>(index.parent().internalPointer());
376             parentCollection = d->m_collections.value(parentNode->id);
377         }
378         if (parentCollection.isValid()) {
379             const int rights = parentCollection.rights();
380 
381             // Can't drop onto items.
382             if (rights & Collection::CanChangeItem && index.column() == 0) {
383                 flags |= Qt::ItemIsEditable;
384             }
385             // dragging is always possible, even for read-only objects, but they can only be copied, not moved.
386             flags |= Qt::ItemIsDragEnabled;
387         }
388     }
389 
390     return flags;
391 }
392 
supportedDropActions() const393 Qt::DropActions EntityTreeModel::supportedDropActions() const
394 {
395     return (Qt::CopyAction | Qt::MoveAction | Qt::LinkAction);
396 }
397 
mimeTypes() const398 QStringList EntityTreeModel::mimeTypes() const
399 {
400     // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example.
401     return {QStringLiteral("text/uri-list")};
402 }
403 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)404 bool EntityTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
405 {
406     Q_UNUSED(row)
407     Q_UNUSED(column)
408     Q_D(EntityTreeModel);
409 
410     // Can't drop onto Collection::root.
411     if (!parent.isValid()) {
412         return false;
413     }
414 
415     // TODO Use action and collection rights and return false if necessary
416 
417     // if row and column are -1, then the drop was on parent directly.
418     // data should then be appended on the end of the items of the collections as appropriate.
419     // That will mean begin insert rows etc.
420     // Otherwise it was a sibling of the row^th item of parent.
421     // Needs to be handled when ordering is accounted for.
422 
423     // Handle dropping between items as well as on items.
424     //   if ( row != -1 && column != -1 )
425     //   {
426     //   }
427 
428     if (action == Qt::IgnoreAction) {
429         return true;
430     }
431 
432     // Shouldn't do this. Need to be able to drop vcards for example.
433     //   if ( !data->hasFormat( "text/uri-list" ) )
434     //       return false;
435 
436     Node *node = reinterpret_cast<Node *>(parent.internalId());
437 
438     Q_ASSERT(node);
439 
440     if (Node::Item == node->type) {
441         if (!parent.parent().isValid()) {
442             // The drop is somehow on an item with no parent (shouldn't happen)
443             // The drop should be considered handled anyway.
444             qCWarning(AKONADICORE_LOG) << "Dropped onto item with no parent collection";
445             return true;
446         }
447 
448         // A drop onto an item should be considered as a drop onto its parent collection
449         node = reinterpret_cast<Node *>(parent.parent().internalId());
450     }
451 
452     if (Node::Collection == node->type) {
453         const Collection destCollection = d->m_collections.value(node->id);
454 
455         // Applications can't create new collections in root. Only resources can.
456         if (destCollection == Collection::root()) {
457             // Accept the event so that it doesn't propagate.
458             return true;
459         }
460 
461         if (data->hasFormat(QStringLiteral("text/uri-list"))) {
462             MimeTypeChecker mimeChecker;
463             mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes());
464 
465             const QList<QUrl> urls = data->urls();
466             for (const QUrl &url : urls) {
467                 const Collection collection = d->m_collections.value(Collection::fromUrl(url).id());
468                 if (collection.isValid()) {
469                     if (collection.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) {
470                         qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same.";
471                         return false;
472                     }
473 
474                     if (!mimeChecker.isWantedCollection(collection)) {
475                         qCDebug(AKONADICORE_LOG) << "unwanted collection" << mimeChecker.wantedMimeTypes() << collection.contentMimeTypes();
476                         return false;
477                     }
478 
479                     QUrlQuery query(url);
480                     if (query.hasQueryItem(QStringLiteral("name"))) {
481                         const QString collectionName = query.queryItemValue(QStringLiteral("name"));
482                         const QStringList collectionNames = d->childCollectionNames(destCollection);
483 
484                         if (collectionNames.contains(collectionName)) {
485                             QMessageBox::critical(
486                                 nullptr,
487                                 i18nc("@window:title", "Error"),
488                                 i18n("The target collection '%1' contains already\na collection with name '%2'.", destCollection.name(), collection.name()));
489                             return false;
490                         }
491                     }
492                 } else {
493                     const Item item = d->m_items.value(Item::fromUrl(url).id());
494                     if (item.isValid()) {
495                         if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) {
496                             qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same.";
497                             return false;
498                         }
499 
500                         if (!mimeChecker.isWantedItem(item)) {
501                             qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType();
502                             return false;
503                         }
504                     }
505                 }
506             }
507 
508             KJob *job = PasteHelper::pasteUriList(data, destCollection, action, d->m_session);
509             if (!job) {
510                 return false;
511             }
512 
513             connect(job, SIGNAL(result(KJob *)), SLOT(pasteJobDone(KJob *)));
514 
515             // Accept the event so that it doesn't propagate.
516             return true;
517         } else {
518             //       not a set of uris. Maybe vcards etc. Check if the parent supports them, and maybe do
519             // fromMimeData for them. Hmm, put it in the same transaction with the above?
520             // TODO: This should be handled first, not last.
521         }
522     }
523 
524     return false;
525 }
526 
index(int row,int column,const QModelIndex & parent) const527 QModelIndex EntityTreeModel::index(int row, int column, const QModelIndex &parent) const
528 {
529     Q_D(const EntityTreeModel);
530 
531     if (parent.column() > 0) {
532         return QModelIndex();
533     }
534 
535     // TODO: don't use column count here? Use some d-> func.
536     if (column >= columnCount() || column < 0) {
537         return QModelIndex();
538     }
539 
540     QList<Node *> childEntities;
541 
542     const Node *parentNode = reinterpret_cast<Node *>(parent.internalPointer());
543     if (!parentNode || !parent.isValid()) {
544         if (d->m_showRootCollection) {
545             childEntities << d->m_childEntities.value(-1);
546         } else {
547             childEntities = d->m_childEntities.value(d->m_rootCollection.id());
548         }
549     } else if (parentNode->id >= 0) {
550         childEntities = d->m_childEntities.value(parentNode->id);
551     }
552 
553     const int size = childEntities.size();
554     if (row < 0 || row >= size) {
555         return QModelIndex();
556     }
557 
558     Node *node = childEntities.at(row);
559     return createIndex(row, column, reinterpret_cast<void *>(node));
560 }
561 
parent(const QModelIndex & index) const562 QModelIndex EntityTreeModel::parent(const QModelIndex &index) const
563 {
564     Q_D(const EntityTreeModel);
565 
566     if (!index.isValid()) {
567         return QModelIndex();
568     }
569 
570     if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) {
571         return QModelIndex();
572     }
573 
574     const Node *node = reinterpret_cast<Node *>(index.internalPointer());
575 
576     if (!node) {
577         return QModelIndex();
578     }
579 
580     const Collection collection = d->m_collections.value(node->parent);
581 
582     if (!collection.isValid()) {
583         return QModelIndex();
584     }
585 
586     if (collection.id() == d->m_rootCollection.id()) {
587         if (!d->m_showRootCollection) {
588             return QModelIndex();
589         } else {
590             return createIndex(0, 0, reinterpret_cast<void *>(d->m_rootNode));
591         }
592     }
593 
594     Q_ASSERT(collection.parentCollection().isValid());
595     const int row = d->indexOf<Node::Collection>(d->m_childEntities.value(collection.parentCollection().id()), collection.id());
596 
597     Q_ASSERT(row >= 0);
598     Node *parentNode = d->m_childEntities.value(collection.parentCollection().id()).at(row);
599 
600     return createIndex(row, 0, reinterpret_cast<void *>(parentNode));
601 }
602 
rowCount(const QModelIndex & parent) const603 int EntityTreeModel::rowCount(const QModelIndex &parent) const
604 {
605     Q_D(const EntityTreeModel);
606 
607     if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) {
608         if (parent.isValid()) {
609             return 0;
610         } else {
611             return d->m_items.size();
612         }
613     }
614 
615     if (!parent.isValid()) {
616         // If we're showing the root collection then it will be the only child of the root.
617         if (d->m_showRootCollection) {
618             return d->m_childEntities.value(-1).size();
619         }
620         return d->m_childEntities.value(d->m_rootCollection.id()).size();
621     }
622 
623     if (parent.column() != 0) {
624         return 0;
625     }
626 
627     const Node *node = reinterpret_cast<Node *>(parent.internalPointer());
628 
629     if (!node) {
630         return 0;
631     }
632 
633     if (Node::Item == node->type) {
634         return 0;
635     }
636 
637     Q_ASSERT(parent.isValid());
638     return d->m_childEntities.value(node->id).size();
639 }
640 
entityColumnCount(HeaderGroup headerGroup) const641 int EntityTreeModel::entityColumnCount(HeaderGroup headerGroup) const
642 {
643     // Not needed in this model.
644     Q_UNUSED(headerGroup)
645 
646     return 1;
647 }
648 
entityHeaderData(int section,Qt::Orientation orientation,int role,HeaderGroup headerGroup) const649 QVariant EntityTreeModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const
650 {
651     Q_D(const EntityTreeModel);
652     // Not needed in this model.
653     Q_UNUSED(headerGroup)
654 
655     if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
656         if (d->m_rootCollection == Collection::root()) {
657             return i18nc("@title:column Name of a thing", "Name");
658         }
659         return d->m_rootCollection.name();
660     }
661 
662     return QAbstractItemModel::headerData(section, orientation, role);
663 }
664 
headerData(int section,Qt::Orientation orientation,int role) const665 QVariant EntityTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
666 {
667     const auto headerGroup = static_cast<HeaderGroup>((role / static_cast<int>(TerminalUserRole)));
668 
669     role %= TerminalUserRole;
670     return entityHeaderData(section, orientation, role, headerGroup);
671 }
672 
mimeData(const QModelIndexList & indexes) const673 QMimeData *EntityTreeModel::mimeData(const QModelIndexList &indexes) const
674 {
675     Q_D(const EntityTreeModel);
676 
677     auto *data = new QMimeData();
678     QList<QUrl> urls;
679     for (const QModelIndex &index : indexes) {
680         if (index.column() != 0) {
681             continue;
682         }
683 
684         if (!index.isValid()) {
685             continue;
686         }
687 
688         const Node *node = reinterpret_cast<Node *>(index.internalPointer());
689 
690         if (Node::Collection == node->type) {
691             urls << d->m_collections.value(node->id).url(Collection::UrlWithName);
692         } else if (Node::Item == node->type) {
693             QUrl url = d->m_items.value(node->id).url(Item::Item::UrlWithMimeType);
694             QUrlQuery query(url);
695             query.addQueryItem(QStringLiteral("parent"), QString::number(node->parent));
696             url.setQuery(query);
697             urls << url;
698         } else { // if that happens something went horrible wrong
699             Q_ASSERT(false);
700         }
701     }
702 
703     data->setUrls(urls);
704 
705     return data;
706 }
707 
708 // Always return false for actions which take place asynchronously, eg via a Job.
setData(const QModelIndex & index,const QVariant & value,int role)709 bool EntityTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
710 {
711     Q_D(EntityTreeModel);
712 
713     const Node *node = reinterpret_cast<Node *>(index.internalPointer());
714 
715     if (role == PendingCutRole) {
716         if (index.isValid() && value.toBool()) {
717             if (Node::Collection == node->type) {
718                 d->m_pendingCutCollections.append(node->id);
719             } else if (Node::Item == node->type) {
720                 d->m_pendingCutItems.append(node->id);
721             }
722         } else {
723             d->m_pendingCutCollections.clear();
724             d->m_pendingCutItems.clear();
725         }
726         return true;
727     }
728 
729     if (index.isValid() && node->type == Node::Collection && (role == CollectionRefRole || role == CollectionDerefRole)) {
730         const Collection collection = index.data(CollectionRole).value<Collection>();
731         Q_ASSERT(collection.isValid());
732 
733         if (role == CollectionDerefRole) {
734             d->deref(collection.id());
735         } else if (role == CollectionRefRole) {
736             d->ref(collection.id());
737         }
738         return true;
739     }
740 
741     if (index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole))) {
742         if (Node::Collection == node->type) {
743             Collection collection = d->m_collections.value(node->id);
744             if (!collection.isValid() || !value.isValid()) {
745                 return false;
746             }
747 
748             if (Qt::EditRole == role) {
749                 collection.setName(value.toString());
750                 if (collection.hasAttribute<EntityDisplayAttribute>()) {
751                     auto *displayAttribute = collection.attribute<EntityDisplayAttribute>();
752                     displayAttribute->setDisplayName(value.toString());
753                 }
754             } else if (Qt::BackgroundRole == role) {
755                 auto color = value.value<QColor>();
756                 if (!color.isValid()) {
757                     return false;
758                 }
759 
760                 auto *eda = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
761                 eda->setBackgroundColor(color);
762             } else if (CollectionRole == role) {
763                 collection = value.value<Collection>();
764             }
765 
766             auto *job = new CollectionModifyJob(collection, d->m_session);
767             connect(job, SIGNAL(result(KJob *)), SLOT(updateJobDone(KJob *)));
768 
769             return false;
770         } else if (Node::Item == node->type) {
771             Item item = d->m_items.value(node->id);
772             if (!item.isValid() || !value.isValid()) {
773                 return false;
774             }
775 
776             if (Qt::EditRole == role) {
777                 if (item.hasAttribute<EntityDisplayAttribute>()) {
778                     auto *displayAttribute = item.attribute<EntityDisplayAttribute>(Item::AddIfMissing);
779                     displayAttribute->setDisplayName(value.toString());
780                 }
781             } else if (Qt::BackgroundRole == role) {
782                 auto color = value.value<QColor>();
783                 if (!color.isValid()) {
784                     return false;
785                 }
786 
787                 auto *eda = item.attribute<EntityDisplayAttribute>(Item::AddIfMissing);
788                 eda->setBackgroundColor(color);
789             } else if (ItemRole == role) {
790                 item = value.value<Item>();
791                 Q_ASSERT(item.id() == node->id);
792             }
793 
794             auto *itemModifyJob = new ItemModifyJob(item, d->m_session);
795             connect(itemModifyJob, SIGNAL(result(KJob *)), SLOT(updateJobDone(KJob *)));
796 
797             return false;
798         }
799     }
800 
801     return QAbstractItemModel::setData(index, value, role);
802 }
803 
canFetchMore(const QModelIndex & parent) const804 bool EntityTreeModel::canFetchMore(const QModelIndex &parent) const
805 {
806     Q_UNUSED(parent)
807     return false;
808 }
809 
fetchMore(const QModelIndex & parent)810 void EntityTreeModel::fetchMore(const QModelIndex &parent)
811 {
812     Q_D(EntityTreeModel);
813 
814     if (!d->canFetchMore(parent)) {
815         return;
816     }
817 
818     if (d->m_collectionFetchStrategy == InvisibleCollectionFetch) {
819         return;
820     }
821 
822     if (d->m_itemPopulation == ImmediatePopulation) {
823         // Nothing to do. The items are already in the model.
824         return;
825     } else if (d->m_itemPopulation == LazyPopulation) {
826         const Collection collection = parent.data(CollectionRole).value<Collection>();
827 
828         if (!collection.isValid()) {
829             return;
830         }
831 
832         d->fetchItems(collection);
833     }
834 }
835 
hasChildren(const QModelIndex & parent) const836 bool EntityTreeModel::hasChildren(const QModelIndex &parent) const
837 {
838     Q_D(const EntityTreeModel);
839 
840     if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) {
841         return parent.isValid() ? false : !d->m_items.isEmpty();
842     }
843 
844     // TODO: Empty collections right now will return true and get a little + to expand.
845     // There is probably no way to tell if a collection
846     // has child items in akonadi without first attempting an itemFetchJob...
847     // Figure out a way to fix this. (Statistics)
848     return ((rowCount(parent) > 0) || (d->canFetchMore(parent) && d->m_itemPopulation == LazyPopulation));
849 }
850 
isCollectionTreeFetched() const851 bool EntityTreeModel::isCollectionTreeFetched() const
852 {
853     Q_D(const EntityTreeModel);
854     return d->m_collectionTreeFetched;
855 }
856 
isCollectionPopulated(Collection::Id id) const857 bool EntityTreeModel::isCollectionPopulated(Collection::Id id) const
858 {
859     Q_D(const EntityTreeModel);
860     return d->m_populatedCols.contains(id);
861 }
862 
isFullyPopulated() const863 bool EntityTreeModel::isFullyPopulated() const
864 {
865     Q_D(const EntityTreeModel);
866     return d->m_collectionTreeFetched && d->m_pendingCollectionRetrieveJobs.isEmpty();
867 }
868 
match(const QModelIndex & start,int role,const QVariant & value,int hits,Qt::MatchFlags flags) const869 QModelIndexList EntityTreeModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const
870 {
871     Q_D(const EntityTreeModel);
872 
873     if (role == CollectionIdRole || role == CollectionRole) {
874         Collection::Id id;
875         if (role == CollectionRole) {
876             const Collection collection = value.value<Collection>();
877             id = collection.id();
878         } else {
879             id = value.toLongLong();
880         }
881 
882         const Collection collection = d->m_collections.value(id);
883         if (!collection.isValid()) {
884             return {};
885         }
886 
887         const QModelIndex collectionIndex = d->indexForCollection(collection);
888         Q_ASSERT(collectionIndex.isValid());
889         return {collectionIndex};
890     } else if (role == ItemIdRole || role == ItemRole) {
891         Item::Id id;
892         if (role == ItemRole) {
893             id = value.value<Item>().id();
894         } else {
895             id = value.toLongLong();
896         }
897 
898         const Item item = d->m_items.value(id);
899         if (!item.isValid()) {
900             return {};
901         }
902         return d->indexesForItem(item);
903     } else if (role == EntityUrlRole) {
904         const QUrl url(value.toString());
905         const Item item = Item::fromUrl(url);
906 
907         if (item.isValid()) {
908             return d->indexesForItem(d->m_items.value(item.id()));
909         }
910 
911         const Collection collection = Collection::fromUrl(url);
912         if (!collection.isValid()) {
913             return {};
914         }
915         return {d->indexForCollection(collection)};
916     }
917 
918     return QAbstractItemModel::match(start, role, value, hits, flags);
919 }
920 
insertRows(int,int,const QModelIndex &)921 bool EntityTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/)
922 {
923     return false;
924 }
925 
insertColumns(int,int,const QModelIndex &)926 bool EntityTreeModel::insertColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/)
927 {
928     return false;
929 }
930 
removeRows(int,int,const QModelIndex &)931 bool EntityTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/)
932 {
933     return false;
934 }
935 
removeColumns(int,int,const QModelIndex &)936 bool EntityTreeModel::removeColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/)
937 {
938     return false;
939 }
940 
setItemPopulationStrategy(ItemPopulationStrategy strategy)941 void EntityTreeModel::setItemPopulationStrategy(ItemPopulationStrategy strategy)
942 {
943     Q_D(EntityTreeModel);
944     d->beginResetModel();
945     d->m_itemPopulation = strategy;
946 
947     if (strategy == NoItemPopulation) {
948         disconnect(d->m_monitor, SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection)), this, SLOT(monitoredItemAdded(Akonadi::Item, Akonadi::Collection)));
949         disconnect(d->m_monitor, SIGNAL(itemChanged(Akonadi::Item, QSet<QByteArray>)), this, SLOT(monitoredItemChanged(Akonadi::Item, QSet<QByteArray>)));
950         disconnect(d->m_monitor, SIGNAL(itemRemoved(Akonadi::Item)), this, SLOT(monitoredItemRemoved(Akonadi::Item)));
951         disconnect(d->m_monitor,
952                    SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)),
953                    this,
954                    SLOT(monitoredItemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)));
955 
956         disconnect(d->m_monitor, SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection)), this, SLOT(monitoredItemLinked(Akonadi::Item, Akonadi::Collection)));
957         disconnect(d->m_monitor,
958                    SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection)),
959                    this,
960                    SLOT(monitoredItemUnlinked(Akonadi::Item, Akonadi::Collection)));
961     }
962 
963     d->m_monitor->d_ptr->useRefCounting = (strategy == LazyPopulation);
964 
965     d->endResetModel();
966 }
967 
itemPopulationStrategy() const968 EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const
969 {
970     Q_D(const EntityTreeModel);
971     return d->m_itemPopulation;
972 }
973 
setIncludeRootCollection(bool include)974 void EntityTreeModel::setIncludeRootCollection(bool include)
975 {
976     Q_D(EntityTreeModel);
977     d->beginResetModel();
978     d->m_showRootCollection = include;
979     d->endResetModel();
980 }
981 
includeRootCollection() const982 bool EntityTreeModel::includeRootCollection() const
983 {
984     Q_D(const EntityTreeModel);
985     return d->m_showRootCollection;
986 }
987 
setRootCollectionDisplayName(const QString & displayName)988 void EntityTreeModel::setRootCollectionDisplayName(const QString &displayName)
989 {
990     Q_D(EntityTreeModel);
991     d->m_rootCollectionDisplayName = displayName;
992 
993     // TODO: Emit datachanged if it is being shown.
994 }
995 
rootCollectionDisplayName() const996 QString EntityTreeModel::rootCollectionDisplayName() const
997 {
998     Q_D(const EntityTreeModel);
999     return d->m_rootCollectionDisplayName;
1000 }
1001 
setCollectionFetchStrategy(CollectionFetchStrategy strategy)1002 void EntityTreeModel::setCollectionFetchStrategy(CollectionFetchStrategy strategy)
1003 {
1004     Q_D(EntityTreeModel);
1005     d->beginResetModel();
1006     d->m_collectionFetchStrategy = strategy;
1007 
1008     if (strategy == FetchNoCollections || strategy == InvisibleCollectionFetch) {
1009         disconnect(d->m_monitor, SIGNAL(collectionChanged(Akonadi::Collection)), this, SLOT(monitoredCollectionChanged(Akonadi::Collection)));
1010         disconnect(d->m_monitor,
1011                    SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection)),
1012                    this,
1013                    SLOT(monitoredCollectionAdded(Akonadi::Collection, Akonadi::Collection)));
1014         disconnect(d->m_monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), this, SLOT(monitoredCollectionRemoved(Akonadi::Collection)));
1015         disconnect(d->m_monitor,
1016                    SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)),
1017                    this,
1018                    SLOT(monitoredCollectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)));
1019         d->m_monitor->fetchCollection(false);
1020     } else {
1021         d->m_monitor->fetchCollection(true);
1022     }
1023 
1024     d->endResetModel();
1025 }
1026 
collectionFetchStrategy() const1027 EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const
1028 {
1029     Q_D(const EntityTreeModel);
1030     return d->m_collectionFetchStrategy;
1031 }
1032 
proxiesAndModel(const QAbstractItemModel * model)1033 static QPair<QList<const QAbstractProxyModel *>, const EntityTreeModel *> proxiesAndModel(const QAbstractItemModel *model)
1034 {
1035     QList<const QAbstractProxyModel *> proxyChain;
1036     const auto *proxy = qobject_cast<const QAbstractProxyModel *>(model);
1037     const QAbstractItemModel *_model = model;
1038     while (proxy) {
1039         proxyChain.prepend(proxy);
1040         _model = proxy->sourceModel();
1041         proxy = qobject_cast<const QAbstractProxyModel *>(_model);
1042     }
1043 
1044     const auto *etm = qobject_cast<const EntityTreeModel *>(_model);
1045     return qMakePair(proxyChain, etm);
1046 }
1047 
proxiedIndex(const QModelIndex & idx,const QList<const QAbstractProxyModel * > & proxyChain)1048 static QModelIndex proxiedIndex(const QModelIndex &idx, const QList<const QAbstractProxyModel *> &proxyChain)
1049 {
1050     QModelIndex _idx = idx;
1051     for (const auto *proxy : proxyChain) {
1052         _idx = proxy->mapFromSource(_idx);
1053     }
1054     return _idx;
1055 }
1056 
modelIndexForCollection(const QAbstractItemModel * model,const Collection & collection)1057 QModelIndex EntityTreeModel::modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
1058 {
1059     const auto &[proxy, etm] = proxiesAndModel(model);
1060     if (!etm) {
1061         qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM.";
1062         return {};
1063     }
1064 
1065     QModelIndex idx = etm->d_ptr->indexForCollection(collection);
1066     return proxiedIndex(idx, proxy);
1067 }
1068 
modelIndexesForItem(const QAbstractItemModel * model,const Item & item)1069 QModelIndexList EntityTreeModel::modelIndexesForItem(const QAbstractItemModel *model, const Item &item)
1070 {
1071     const auto &[proxy, etm] = proxiesAndModel(model);
1072 
1073     if (!etm) {
1074         qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM.";
1075         return QModelIndexList();
1076     }
1077 
1078     const QModelIndexList list = etm->d_ptr->indexesForItem(item);
1079     QModelIndexList proxyList;
1080     for (const QModelIndex &idx : list) {
1081         const QModelIndex pIdx = proxiedIndex(idx, proxy);
1082         if (pIdx.isValid()) {
1083             proxyList.push_back(pIdx);
1084         }
1085     }
1086     return proxyList;
1087 }
1088 
updatedCollection(const QAbstractItemModel * model,qint64 collectionId)1089 Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, qint64 collectionId)
1090 {
1091     const auto *proxy = qobject_cast<const QAbstractProxyModel *>(model);
1092     const QAbstractItemModel *_model = model;
1093     while (proxy) {
1094         _model = proxy->sourceModel();
1095         proxy = qobject_cast<const QAbstractProxyModel *>(_model);
1096     }
1097 
1098     const auto *etm = qobject_cast<const EntityTreeModel *>(_model);
1099     if (etm) {
1100         return etm->d_ptr->m_collections.value(collectionId);
1101     } else {
1102         return Collection{collectionId};
1103     }
1104 }
1105 
updatedCollection(const QAbstractItemModel * model,const Collection & collection)1106 Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, const Collection &collection)
1107 {
1108     return updatedCollection(model, collection.id());
1109 }
1110 
1111 #include "moc_entitytreemodel.cpp"
1112