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