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