2 * collectionmodel.cpp - Akonadi collection models
4 * Copyright © 2007-2014 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "collectionmodel.h"
22 #include "autoqpointer.h"
23 #include "messagebox.h"
24 #include "preferences.h"
26 #include <kalarmcal/collectionattribute.h>
27 #include <kalarmcal/compatibilityattribute.h>
29 #include <AkonadiCore/agentmanager.h>
30 #include <AkonadiCore/collectiondeletejob.h>
31 #include <AkonadiCore/collectionmodifyjob.h>
32 #include <AkonadiCore/entitymimetypefiltermodel.h>
33 #include <AkonadiWidgets/collectiondialog.h>
35 #include <KLocalizedString>
36 #include <KSharedConfig>
39 #include <QApplication>
40 #include <QMouseEvent>
45 #include "kalarm_debug.h"
47 using namespace Akonadi
;
48 using namespace KAlarmCal
;
50 static Collection::Rights writableRights
= Collection::CanChangeItem
| Collection::CanCreateItem
| Collection::CanDeleteItem
;
53 /*=============================================================================
54 = Class: CollectionMimeTypeFilterModel
55 = Proxy model to filter AkonadiModel to restrict its contents to Collections,
56 = not Items, containing specified KAlarm content mime types.
57 = It can optionally be restricted to writable and/or enabled Collections.
58 =============================================================================*/
59 class CollectionMimeTypeFilterModel
: public Akonadi::EntityMimeTypeFilterModel
63 explicit CollectionMimeTypeFilterModel(QObject
* parent
= Q_NULLPTR
);
64 void setEventTypeFilter(CalEvent::Type
);
65 void setFilterWritable(bool writable
);
66 void setFilterEnabled(bool enabled
);
67 Akonadi::Collection
collection(int row
) const;
68 Akonadi::Collection
collection(const QModelIndex
&) const;
69 QModelIndex
collectionIndex(const Akonadi::Collection
&) const;
72 bool filterAcceptsRow(int sourceRow
, const QModelIndex
& sourceParent
) const Q_DECL_OVERRIDE
;
75 CalEvent::Type mAlarmType
; // collection content type contained in this model
76 bool mWritableOnly
; // only include writable collections in this model
77 bool mEnabledOnly
; // only include enabled collections in this model
80 CollectionMimeTypeFilterModel::CollectionMimeTypeFilterModel(QObject
* parent
)
81 : EntityMimeTypeFilterModel(parent
),
82 mAlarmType(CalEvent::EMPTY
),
86 addMimeTypeInclusionFilter(Collection::mimeType()); // select collections, not items
87 setHeaderGroup(EntityTreeModel::CollectionTreeHeaders
);
88 setSourceModel(AkonadiModel::instance());
91 void CollectionMimeTypeFilterModel::setEventTypeFilter(CalEvent::Type type
)
93 if (type
!= mAlarmType
)
100 void CollectionMimeTypeFilterModel::setFilterWritable(bool writable
)
102 if (writable
!= mWritableOnly
)
104 mWritableOnly
= writable
;
109 void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled
)
111 if (enabled
!= mEnabledOnly
)
113 Q_EMIT
layoutAboutToBeChanged();
114 mEnabledOnly
= enabled
;
116 Q_EMIT
layoutChanged();
120 bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow
, const QModelIndex
& sourceParent
) const
122 if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow
, sourceParent
))
124 AkonadiModel
* model
= AkonadiModel::instance();
125 const QModelIndex ix
= model
->index(sourceRow
, 0, sourceParent
);
126 const Collection collection
= model
->data(ix
, AkonadiModel::CollectionRole
).value
<Collection
>();
127 if (!AgentManager::self()->instance(collection
.resource()).isValid())
129 if (!mWritableOnly
&& mAlarmType
== CalEvent::EMPTY
)
131 if (mWritableOnly
&& (collection
.rights() & writableRights
) != writableRights
)
133 if (mAlarmType
!= CalEvent::EMPTY
&& !collection
.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType
)))
135 if ((mWritableOnly
|| mEnabledOnly
) && !collection
.hasAttribute
<CollectionAttribute
>())
137 if (mWritableOnly
&& (!collection
.hasAttribute
<CompatibilityAttribute
>()
138 || collection
.attribute
<CompatibilityAttribute
>()->compatibility() != KACalendar::Current
))
140 if (mEnabledOnly
&& !collection
.attribute
<CollectionAttribute
>()->isEnabled(mAlarmType
))
145 /******************************************************************************
146 * Return the collection for a given row.
148 Collection
CollectionMimeTypeFilterModel::collection(int row
) const
150 return static_cast<AkonadiModel
*>(sourceModel())->data(mapToSource(index(row
, 0)), EntityTreeModel::CollectionRole
).value
<Collection
>();
153 Collection
CollectionMimeTypeFilterModel::collection(const QModelIndex
& index
) const
155 return static_cast<AkonadiModel
*>(sourceModel())->data(mapToSource(index
), EntityTreeModel::CollectionRole
).value
<Collection
>();
158 QModelIndex
CollectionMimeTypeFilterModel::collectionIndex(const Collection
& collection
) const
160 return mapFromSource(static_cast<AkonadiModel
*>(sourceModel())->collectionIndex(collection
));
164 /*=============================================================================
165 = Class: CollectionListModel
166 = Proxy model converting the AkonadiModel collection tree into a flat list.
167 = The model may be restricted to specified content mime types.
168 = It can optionally be restricted to writable and/or enabled Collections.
169 =============================================================================*/
171 CollectionListModel::CollectionListModel(QObject
* parent
)
172 : KDescendantsProxyModel(parent
),
173 mUseCollectionColour(true)
175 setSourceModel(new CollectionMimeTypeFilterModel(this));
176 setDisplayAncestorData(false);
179 /******************************************************************************
180 * Return the collection for a given row.
182 Collection
CollectionListModel::collection(int row
) const
184 return data(index(row
, 0), EntityTreeModel::CollectionRole
).value
<Collection
>();
187 Collection
CollectionListModel::collection(const QModelIndex
& index
) const
189 return data(index
, EntityTreeModel::CollectionRole
).value
<Collection
>();
192 QModelIndex
CollectionListModel::collectionIndex(const Collection
& collection
) const
194 return mapFromSource(static_cast<CollectionMimeTypeFilterModel
*>(sourceModel())->collectionIndex(collection
));
197 void CollectionListModel::setEventTypeFilter(CalEvent::Type type
)
199 static_cast<CollectionMimeTypeFilterModel
*>(sourceModel())->setEventTypeFilter(type
);
202 void CollectionListModel::setFilterWritable(bool writable
)
204 static_cast<CollectionMimeTypeFilterModel
*>(sourceModel())->setFilterWritable(writable
);
207 void CollectionListModel::setFilterEnabled(bool enabled
)
209 static_cast<CollectionMimeTypeFilterModel
*>(sourceModel())->setFilterEnabled(enabled
);
212 bool CollectionListModel::isDescendantOf(const QModelIndex
& ancestor
, const QModelIndex
& descendant
) const
214 Q_UNUSED(descendant
);
215 return !ancestor
.isValid();
218 /******************************************************************************
219 * Return the data for a given role, for a specified item.
221 QVariant
CollectionListModel::data(const QModelIndex
& index
, int role
) const
225 case Qt::BackgroundRole
:
226 if (!mUseCollectionColour
)
227 role
= AkonadiModel::BaseColourRole
;
232 return KDescendantsProxyModel::data(index
, role
);
236 /*=============================================================================
237 = Class: CollectionCheckListModel
238 = Proxy model providing a checkable list of all Collections. A Collection's
239 = checked status is equivalent to whether it is selected or not.
240 = An alarm type is specified, whereby Collections which are enabled for that
241 = alarm type are checked; Collections which do not contain that alarm type, or
242 = which are disabled for that alarm type, are unchecked.
243 =============================================================================*/
245 CollectionListModel
* CollectionCheckListModel::mModel
= Q_NULLPTR
;
246 int CollectionCheckListModel::mInstanceCount
= 0;
248 CollectionCheckListModel::CollectionCheckListModel(CalEvent::Type type
, QObject
* parent
)
249 : KCheckableProxyModel(parent
),
254 mModel
= new CollectionListModel(Q_NULLPTR
);
255 setSourceModel(mModel
); // the source model is NOT filtered by alarm type
256 mSelectionModel
= new QItemSelectionModel(mModel
);
257 setSelectionModel(mSelectionModel
);
258 connect(mSelectionModel
, &QItemSelectionModel::selectionChanged
,
259 this, &CollectionCheckListModel::selectionChanged
);
260 connect(mModel
, SIGNAL(rowsAboutToBeInserted(QModelIndex
,int,int)), SIGNAL(layoutAboutToBeChanged()));
261 connect(mModel
, &QAbstractItemModel::rowsInserted
, this, &CollectionCheckListModel::slotRowsInserted
);
262 // This is probably needed to make CollectionFilterCheckListModel update
263 // (similarly to when rows are inserted).
264 connect(mModel
, SIGNAL(rowsAboutToBeRemoved(QModelIndex
,int,int)), SIGNAL(layoutAboutToBeChanged()));
265 connect(mModel
, SIGNAL(rowsRemoved(QModelIndex
,int,int)), SIGNAL(layoutChanged()));
267 connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged
,
268 this, &CollectionCheckListModel::collectionStatusChanged
);
270 // Initialise checked status for all collections.
271 // Note that this is only necessary if the model is recreated after
273 for (int row
= 0, count
= mModel
->rowCount(); row
< count
; ++row
)
274 setSelectionStatus(mModel
->collection(row
), mModel
->index(row
, 0));
277 CollectionCheckListModel::~CollectionCheckListModel()
279 if (--mInstanceCount
<= 0)
287 /******************************************************************************
288 * Return the collection for a given row.
290 Collection
CollectionCheckListModel::collection(int row
) const
292 return mModel
->collection(mapToSource(index(row
, 0)));
295 Collection
CollectionCheckListModel::collection(const QModelIndex
& index
) const
297 return mModel
->collection(mapToSource(index
));
300 /******************************************************************************
301 * Return model data for one index.
303 QVariant
CollectionCheckListModel::data(const QModelIndex
& index
, int role
) const
305 const Collection collection
= mModel
->collection(index
);
306 if (collection
.isValid())
308 // This is a Collection row
311 case Qt::ForegroundRole
:
313 const QString mimeType
= CalEvent::mimeType(mAlarmType
);
314 if (collection
.contentMimeTypes().contains(mimeType
))
315 return AkonadiModel::foregroundColor(collection
, QStringList(mimeType
));
320 if (!collection
.hasAttribute
<CollectionAttribute
>()
321 || !AkonadiModel::isCompatible(collection
))
323 const CollectionAttribute
* attr
= collection
.attribute
<CollectionAttribute
>();
324 if (!attr
->enabled())
326 const QStringList mimeTypes
= collection
.contentMimeTypes();
327 if (attr
->isStandard(mAlarmType
) && mimeTypes
.contains(CalEvent::mimeType(mAlarmType
)))
329 // It's the standard collection for a mime type
330 QFont font
= qvariant_cast
<QFont
>(KCheckableProxyModel::data(index
, role
));
340 return KCheckableProxyModel::data(index
, role
);
343 /******************************************************************************
344 * Set model data for one index.
345 * If the change is to disable a collection, check for eligibility and prevent
346 * the change if necessary.
348 bool CollectionCheckListModel::setData(const QModelIndex
& index
, const QVariant
& value
, int role
)
350 if (role
== Qt::CheckStateRole
&& static_cast<Qt::CheckState
>(value
.toInt()) != Qt::Checked
)
352 // A collection is to be disabled.
353 const Collection collection
= mModel
->collection(index
);
354 if (collection
.isValid() && collection
.hasAttribute
<CollectionAttribute
>())
356 const CollectionAttribute
* attr
= collection
.attribute
<CollectionAttribute
>();
357 if (attr
->isEnabled(mAlarmType
))
360 QWidget
* messageParent
= qobject_cast
<QWidget
*>(QObject::parent());
361 if (attr
->isStandard(mAlarmType
)
362 && AkonadiModel::isCompatible(collection
))
364 // It's the standard collection for some alarm type.
365 if (mAlarmType
== CalEvent::ACTIVE
)
367 errmsg
= i18nc("@info", "You cannot disable your default active alarm calendar.");
369 else if (mAlarmType
== CalEvent::ARCHIVED
&& Preferences::archivedKeepDays())
371 // Only allow the archived alarms standard collection to be disabled if
372 // we're not saving expired alarms.
373 errmsg
= i18nc("@info", "You cannot disable your default archived alarm calendar "
374 "while expired alarms are configured to be kept.");
376 else if (KAMessageBox::warningContinueCancel(messageParent
,
377 i18nc("@info", "Do you really want to disable your default calendar?"))
378 == KMessageBox::Cancel
)
381 if (!errmsg
.isEmpty())
383 KAMessageBox::sorry(messageParent
, errmsg
);
389 return KCheckableProxyModel::setData(index
, value
, role
);
392 /******************************************************************************
393 * Called when rows have been inserted into the model.
394 * Select or deselect them according to their enabled status.
396 void CollectionCheckListModel::slotRowsInserted(const QModelIndex
& parent
, int start
, int end
)
398 for (int row
= start
; row
<= end
; ++row
)
400 const QModelIndex ix
= mapToSource(index(row
, 0, parent
));
401 const Collection collection
= mModel
->collection(ix
);
402 if (collection
.isValid())
403 setSelectionStatus(collection
, ix
);
405 Q_EMIT
layoutChanged(); // this is needed to make CollectionFilterCheckListModel update
408 /******************************************************************************
409 * Called when the user has ticked/unticked a collection to enable/disable it
410 * (or when the selection changes for any other reason).
412 void CollectionCheckListModel::selectionChanged(const QItemSelection
& selected
, const QItemSelection
& deselected
)
414 const QModelIndexList sel
= selected
.indexes();
415 foreach (const QModelIndex
& ix
, sel
)
417 // Try to enable the collection, but untick it if not possible
418 if (!CollectionControlModel::setEnabled(mModel
->collection(ix
), mAlarmType
, true))
419 mSelectionModel
->select(ix
, QItemSelectionModel::Deselect
);
421 const QModelIndexList desel
= deselected
.indexes();
422 foreach (const QModelIndex
& ix
, desel
)
423 CollectionControlModel::setEnabled(mModel
->collection(ix
), mAlarmType
, false);
426 /******************************************************************************
427 * Called when a collection parameter or status has changed.
428 * If the collection's alarm types have been reconfigured, ensure that the
429 * model views are updated to reflect this.
431 void CollectionCheckListModel::collectionStatusChanged(const Collection
& collection
, AkonadiModel::Change change
, const QVariant
&, bool inserted
)
433 if (inserted
|| !collection
.isValid())
437 case AkonadiModel::Enabled
:
438 qCDebug(KALARM_LOG
) << "Enabled" << collection
.id();
440 case AkonadiModel::AlarmTypes
:
441 qCDebug(KALARM_LOG
) << "AlarmTypes" << collection
.id();
446 const QModelIndex ix
= mModel
->collectionIndex(collection
);
448 setSelectionStatus(collection
, ix
);
449 if (change
== AkonadiModel::AlarmTypes
)
450 Q_EMIT
collectionTypeChange(this);
453 /******************************************************************************
454 * Select or deselect an index according to its enabled status.
456 void CollectionCheckListModel::setSelectionStatus(const Collection
& collection
, const QModelIndex
& sourceIndex
)
458 const QItemSelectionModel::SelectionFlags sel
= (collection
.hasAttribute
<CollectionAttribute
>()
459 && collection
.attribute
<CollectionAttribute
>()->isEnabled(mAlarmType
))
460 ? QItemSelectionModel::Select
: QItemSelectionModel::Deselect
;
461 mSelectionModel
->select(sourceIndex
, sel
);
465 /*=============================================================================
466 = Class: CollectionFilterCheckListModel
467 = Proxy model providing a checkable collection list. The model contains all
468 = alarm types, but returns only one type at any given time. The selected alarm
469 = type may be changed as desired.
470 =============================================================================*/
471 CollectionFilterCheckListModel::CollectionFilterCheckListModel(QObject
* parent
)
472 : QSortFilterProxyModel(parent
),
473 mActiveModel(new CollectionCheckListModel(CalEvent::ACTIVE
, this)),
474 mArchivedModel(new CollectionCheckListModel(CalEvent::ARCHIVED
, this)),
475 mTemplateModel(new CollectionCheckListModel(CalEvent::TEMPLATE
, this)),
476 mAlarmType(CalEvent::EMPTY
)
478 setDynamicSortFilter(true);
479 connect(mActiveModel
, &CollectionCheckListModel::collectionTypeChange
, this, &CollectionFilterCheckListModel::collectionTypeChanged
);
480 connect(mArchivedModel
, &CollectionCheckListModel::collectionTypeChange
, this, &CollectionFilterCheckListModel::collectionTypeChanged
);
481 connect(mTemplateModel
, &CollectionCheckListModel::collectionTypeChange
, this, &CollectionFilterCheckListModel::collectionTypeChanged
);
484 void CollectionFilterCheckListModel::setEventTypeFilter(CalEvent::Type type
)
486 if (type
!= mAlarmType
)
488 CollectionCheckListModel
* newModel
;
491 case CalEvent::ACTIVE
: newModel
= mActiveModel
; break;
492 case CalEvent::ARCHIVED
: newModel
= mArchivedModel
; break;
493 case CalEvent::TEMPLATE
: newModel
= mTemplateModel
; break;
498 setSourceModel(newModel
);
503 /******************************************************************************
504 * Return the collection for a given row.
506 Collection
CollectionFilterCheckListModel::collection(int row
) const
508 return static_cast<CollectionCheckListModel
*>(sourceModel())->collection(mapToSource(index(row
, 0)));
511 Collection
CollectionFilterCheckListModel::collection(const QModelIndex
& index
) const
513 return static_cast<CollectionCheckListModel
*>(sourceModel())->collection(mapToSource(index
));
516 QVariant
CollectionFilterCheckListModel::data(const QModelIndex
& index
, int role
) const
520 case Qt::ToolTipRole
:
522 const Collection col
= collection(index
);
524 return AkonadiModel::instance()->tooltip(col
, mAlarmType
);
530 return QSortFilterProxyModel::data(index
, role
);
533 bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow
, const QModelIndex
& sourceParent
) const
535 if (mAlarmType
== CalEvent::EMPTY
)
537 const CollectionCheckListModel
* model
= static_cast<CollectionCheckListModel
*>(sourceModel());
538 const Collection collection
= model
->collection(model
->index(sourceRow
, 0, sourceParent
));
539 return collection
.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType
));
542 /******************************************************************************
543 * Called when a collection alarm type has changed.
544 * Ensure that the collection is removed from or added to the current model view.
546 void CollectionFilterCheckListModel::collectionTypeChanged(CollectionCheckListModel
* model
)
548 if (model
== sourceModel())
553 /*=============================================================================
554 = Class: CollectionView
555 = View displaying a list of collections.
556 =============================================================================*/
557 CollectionView::CollectionView(CollectionFilterCheckListModel
* model
, QWidget
* parent
)
563 void CollectionView::setModel(QAbstractItemModel
* model
)
565 QListView::setModel(model
);
568 /******************************************************************************
569 * Return the collection for a given row.
571 Collection
CollectionView::collection(int row
) const
573 return static_cast<CollectionFilterCheckListModel
*>(model())->collection(row
);
576 Collection
CollectionView::collection(const QModelIndex
& index
) const
578 return static_cast<CollectionFilterCheckListModel
*>(model())->collection(index
);
581 /******************************************************************************
582 * Called when a mouse button is released.
583 * Any currently selected collection is deselected.
585 void CollectionView::mouseReleaseEvent(QMouseEvent
* e
)
587 if (!indexAt(e
->pos()).isValid())
589 QListView::mouseReleaseEvent(e
);
592 /******************************************************************************
593 * Called when a ToolTip or WhatsThis event occurs.
595 bool CollectionView::viewportEvent(QEvent
* e
)
597 if (e
->type() == QEvent::ToolTip
&& isActiveWindow())
599 const QHelpEvent
* he
= static_cast<QHelpEvent
*>(e
);
600 const QModelIndex index
= indexAt(he
->pos());
601 QVariant value
= static_cast<CollectionFilterCheckListModel
*>(model())->data(index
, Qt::ToolTipRole
);
602 if (qVariantCanConvert
<QString
>(value
))
604 QString toolTip
= value
.toString();
605 int i
= toolTip
.indexOf(QLatin1Char('@'));
608 int j
= toolTip
.indexOf(QRegExp(QLatin1String("<(nl|br)"), Qt::CaseInsensitive
), i
+ 1);
609 int k
= toolTip
.indexOf(QLatin1Char('@'), j
);
610 const QString name
= toolTip
.mid(i
+ 1, j
- i
- 1);
611 value
= model()->data(index
, Qt::FontRole
);
612 const QFontMetrics
fm(qvariant_cast
<QFont
>(value
).resolve(viewOptions().font
));
613 int textWidth
= fm
.boundingRect(name
).width() + 1;
614 const int margin
= QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin
) + 1;
615 QStyleOptionButton opt
;
616 opt
.QStyleOption::operator=(viewOptions());
617 opt
.rect
= rectForIndex(index
);
618 int checkWidth
= QApplication::style()->subElementRect(QStyle::SE_ViewItemCheckIndicator
, &opt
).width();
619 int left
= spacing() + 3*margin
+ checkWidth
+ viewOptions().decorationSize
.width(); // left offset of text
620 int right
= left
+ textWidth
;
621 if (left
>= horizontalOffset() + spacing()
622 && right
<= horizontalOffset() + width() - spacing() - 2*frameWidth())
624 // The whole of the collection name is already displayed,
625 // so omit it from the tooltip.
627 toolTip
.remove(i
, k
+ 1 - i
);
631 toolTip
.remove(k
, 1);
632 toolTip
.remove(i
, 1);
635 QToolTip::showText(he
->globalPos(), toolTip
, this);
639 return QListView::viewportEvent(e
);
643 /*=============================================================================
644 = Class: CollectionControlModel
645 = Proxy model to select which Collections will be enabled. Disabled Collections
646 = are not populated or monitored; their contents are ignored. The set of
647 = enabled Collections is stored in the config file's "Collections" group.
648 = Note that this model is not used directly for displaying - its purpose is to
649 = allow collections to be disabled, which will remove them from the other
651 =============================================================================*/
653 CollectionControlModel
* CollectionControlModel::mInstance
= Q_NULLPTR
;
654 bool CollectionControlModel::mAskDestination
= false;
656 CollectionControlModel
* CollectionControlModel::instance()
659 mInstance
= new CollectionControlModel(qApp
);
663 CollectionControlModel::CollectionControlModel(QObject
* parent
)
664 : FavoriteCollectionsModel(AkonadiModel::instance(), KConfigGroup(KSharedConfig::openConfig(), "Collections"), parent
),
665 mPopulatedCheckLoop(Q_NULLPTR
)
667 // Initialise the list of enabled collections
668 EntityMimeTypeFilterModel
* filter
= new EntityMimeTypeFilterModel(this);
669 filter
->addMimeTypeInclusionFilter(Collection::mimeType());
670 filter
->setSourceModel(AkonadiModel::instance());
671 Collection::List collections
;
672 findEnabledCollections(filter
, QModelIndex(), collections
);
673 setCollections(collections
);
675 connect(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged
,
676 this, &CollectionControlModel::statusChanged
);
677 connect(AkonadiModel::instance(), &EntityTreeModel::collectionTreeFetched
,
678 this, &CollectionControlModel::collectionPopulated
);
679 connect(AkonadiModel::instance(), &EntityTreeModel::collectionPopulated
,
680 this, &CollectionControlModel::collectionPopulated
);
681 connect(AkonadiModel::instance(), SIGNAL(serverStopped()), SLOT(reset()));
684 /******************************************************************************
685 * Recursive function to check all collections' enabled status, and to compile a
686 * list of all collections which have any alarm types enabled.
687 * Collections which duplicate the same backend storage are filtered out, to
688 * avoid crashes due to duplicate events in different resources.
690 void CollectionControlModel::findEnabledCollections(const EntityMimeTypeFilterModel
* filter
, const QModelIndex
& parent
, Collection::List
& collections
) const
692 AkonadiModel
* model
= AkonadiModel::instance();
693 for (int row
= 0, count
= filter
->rowCount(parent
); row
< count
; ++row
)
695 const QModelIndex ix
= filter
->index(row
, 0, parent
);
696 const Collection collection
= model
->data(filter
->mapToSource(ix
), AkonadiModel::CollectionRole
).value
<Collection
>();
697 if (!AgentManager::self()->instance(collection
.resource()).isValid())
698 continue; // the collection doesn't belong to a resource, so omit it
699 const CalEvent::Types enabled
= !collection
.hasAttribute
<CollectionAttribute
>() ? CalEvent::EMPTY
700 : collection
.attribute
<CollectionAttribute
>()->enabled();
701 const CalEvent::Types canEnable
= checkTypesToEnable(collection
, collections
, enabled
);
702 if (canEnable
!= enabled
)
704 // There is another collection which uses the same backend
705 // storage. Disable alarm types enabled in the other collection.
706 if (!model
->isCollectionBeingDeleted(collection
.id()))
707 model
->setData(model
->collectionIndex(collection
), static_cast<int>(canEnable
), AkonadiModel::EnabledTypesRole
);
710 collections
+= collection
;
711 if (filter
->rowCount(ix
) > 0)
712 findEnabledCollections(filter
, ix
, collections
);
716 bool CollectionControlModel::isEnabled(const Collection
& collection
, CalEvent::Type type
)
718 if (!collection
.isValid() || !instance()->collections().contains(collection
))
720 if (!AgentManager::self()->instance(collection
.resource()).isValid())
722 // The collection doesn't belong to a resource, so it can't be used.
723 // Remove it from the list of collections.
724 instance()->removeCollection(collection
);
727 Collection col
= collection
;
728 AkonadiModel::instance()->refresh(col
); // update with latest data
729 return col
.hasAttribute
<CollectionAttribute
>()
730 && col
.attribute
<CollectionAttribute
>()->isEnabled(type
);
733 /******************************************************************************
734 * Enable or disable the specified alarm types for a collection.
735 * Reply = alarm types which can be enabled
737 CalEvent::Types
CollectionControlModel::setEnabled(const Collection
& collection
, CalEvent::Types types
, bool enabled
)
739 qCDebug(KALARM_LOG
) << "id:" << collection
.id() << ", alarm types" << types
<< "->" << enabled
;
740 if (!collection
.isValid() || (!enabled
&& !instance()->collections().contains(collection
)))
741 return CalEvent::EMPTY
;
742 Collection col
= collection
;
743 AkonadiModel::instance()->refresh(col
); // update with latest data
744 CalEvent::Types alarmTypes
= !col
.hasAttribute
<CollectionAttribute
>() ? CalEvent::EMPTY
745 : col
.attribute
<CollectionAttribute
>()->enabled();
747 alarmTypes
|= static_cast<CalEvent::Types
>(types
& (CalEvent::ACTIVE
| CalEvent::ARCHIVED
| CalEvent::TEMPLATE
));
749 alarmTypes
&= ~types
;
751 return instance()->setEnabledStatus(collection
, alarmTypes
, false);
754 /******************************************************************************
755 * Change the collection's enabled status.
756 * Add or remove the collection to/from the enabled list.
757 * Reply = alarm types which can be enabled
759 CalEvent::Types
CollectionControlModel::setEnabledStatus(const Collection
& collection
, CalEvent::Types types
, bool inserted
)
761 qCDebug(KALARM_LOG
) << "id:" << collection
.id() << ", types=" << types
;
762 CalEvent::Types
disallowedStdTypes(0);
763 CalEvent::Types
stdTypes(0);
765 // Prevent the enabling of duplicate alarm types if another collection
766 // uses the same backend storage.
767 const Collection::List cols
= collections();
768 const CalEvent::Types canEnable
= checkTypesToEnable(collection
, cols
, types
);
770 // Update the list of enabled collections
774 const Collection::List cols
= collections();
775 foreach (const Collection
& c
, cols
)
777 if (c
.id() == collection
.id())
785 // It's a new collection.
786 // Prevent duplicate standard collections being created for any alarm type.
787 stdTypes
= collection
.hasAttribute
<CollectionAttribute
>()
788 ? collection
.attribute
<CollectionAttribute
>()->standard()
792 foreach (const Collection
& col
, cols
)
795 AkonadiModel::instance()->refresh(c
); // update with latest data
798 const CalEvent::Types t
= stdTypes
& CalEvent::types(c
.contentMimeTypes());
801 if (c
.hasAttribute
<CollectionAttribute
>()
802 && AkonadiModel::isCompatible(c
))
804 disallowedStdTypes
|= c
.attribute
<CollectionAttribute
>()->standard() & t
;
805 if (disallowedStdTypes
== stdTypes
)
812 addCollection(collection
);
816 removeCollection(collection
);
818 if (disallowedStdTypes
|| !inserted
|| canEnable
!= types
)
820 // Update the collection's status
821 AkonadiModel
* model
= static_cast<AkonadiModel
*>(sourceModel());
822 if (!model
->isCollectionBeingDeleted(collection
.id()))
824 const QModelIndex ix
= model
->collectionIndex(collection
);
825 if (!inserted
|| canEnable
!= types
)
826 model
->setData(ix
, static_cast<int>(canEnable
), AkonadiModel::EnabledTypesRole
);
827 if (disallowedStdTypes
)
828 model
->setData(ix
, static_cast<int>(stdTypes
& ~disallowedStdTypes
), AkonadiModel::IsStandardRole
);
834 /******************************************************************************
835 * Called when a collection parameter or status has changed.
836 * If it's the enabled status, add or remove the collection to/from the enabled
839 void CollectionControlModel::statusChanged(const Collection
& collection
, AkonadiModel::Change change
, const QVariant
& value
, bool inserted
)
841 if (!collection
.isValid())
846 case AkonadiModel::Enabled
:
848 const CalEvent::Types enabled
= static_cast<CalEvent::Types
>(value
.toInt());
849 qCDebug(KALARM_LOG
) << "id:" << collection
.id() << ", enabled=" << enabled
<< ", inserted=" << inserted
;
850 setEnabledStatus(collection
, enabled
, inserted
);
853 case AkonadiModel::ReadOnly
:
855 bool readOnly
= value
.toBool();
856 qCDebug(KALARM_LOG
) << "id:" << collection
.id() << ", readOnly=" << readOnly
;
859 // A read-only collection can't be the default for any alarm type
860 const CalEvent::Types std
= standardTypes(collection
, false);
861 if (std
!= CalEvent::EMPTY
)
863 Collection
c(collection
);
864 setStandard(c
, CalEvent::Types(CalEvent::EMPTY
));
865 QWidget
* messageParent
= qobject_cast
<QWidget
*>(QObject::parent());
866 bool singleType
= true;
870 case CalEvent::ACTIVE
:
871 msg
= xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
872 "This was the default calendar for active alarms.",
875 case CalEvent::ARCHIVED
:
876 msg
= xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
877 "This was the default calendar for archived alarms.",
880 case CalEvent::TEMPLATE
:
881 msg
= xi18nc("@info", "The calendar <resource>%1</resource> has been made read-only. "
882 "This was the default calendar for alarm templates.",
886 msg
= xi18nc("@info", "<para>The calendar <resource>%1</resource> has been made read-only. "
887 "This was the default calendar for:%2</para>"
888 "<para>Please select new default calendars.</para>",
889 collection
.name(), typeListForDisplay(std
));
894 msg
= xi18nc("@info", "<para>%1</para><para>Please select a new default calendar.</para>", msg
);
895 KAMessageBox::information(messageParent
, msg
);
905 /******************************************************************************
906 * Check which alarm types can be enabled for a specified collection.
907 * If the collection uses the same backend storage as another collection, any
908 * alarm types already enabled in the other collection must be disabled in this
909 * collection. This is to avoid duplicating events between different resources,
910 * which causes user confusion and annoyance, and causes crashes.
912 * collection - must be up to date (using AkonadiModel::refresh() etc.)
913 * collections = list of collections to search for duplicates.
914 * types = alarm types to be enabled for the collection.
915 * Reply = alarm types which can be enabled without duplicating other collections.
917 CalEvent::Types
CollectionControlModel::checkTypesToEnable(const Collection
& collection
, const Collection::List
& collections
, CalEvent::Types types
)
919 types
&= (CalEvent::ACTIVE
| CalEvent::ARCHIVED
| CalEvent::TEMPLATE
);
922 // At least on alarm type is to be enabled
923 const QUrl location
= QUrl::fromUserInput(collection
.remoteId(), QString(), QUrl::AssumeLocalFile
);
924 foreach (const Collection
& c
, collections
)
926 const QUrl cLocation
= QUrl::fromUserInput(c
.remoteId(), QString(), QUrl::AssumeLocalFile
);
927 if (c
.id() != collection
.id() && cLocation
== location
)
929 // The collection duplicates the backend storage
930 // used by another enabled collection.
931 // N.B. don't refresh this collection - assume no change.
932 qCDebug(KALARM_LOG
) << "Collection" << c
.id() << "duplicates backend for" << collection
.id();
933 if (c
.hasAttribute
<CollectionAttribute
>())
935 types
&= ~c
.attribute
<CollectionAttribute
>()->enabled();
945 /******************************************************************************
946 * Create a bulleted list of alarm types for insertion into <para>...</para>.
948 QString
CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes
)
951 if (alarmTypes
& CalEvent::ACTIVE
)
952 list
+= QLatin1String("<item>") + i18nc("@info", "Active Alarms") + QLatin1String("</item>");
953 if (alarmTypes
& CalEvent::ARCHIVED
)
954 list
+= QLatin1String("<item>") + i18nc("@info", "Archived Alarms") + QLatin1String("</item>");
955 if (alarmTypes
& CalEvent::TEMPLATE
)
956 list
+= QLatin1String("<item>") + i18nc("@info", "Alarm Templates") + QLatin1String("</item>");
958 list
= QStringLiteral("<list>") + list
+ QStringLiteral("</list>");
962 /******************************************************************************
963 * Return whether a collection is both enabled and fully writable for a given
965 * Optionally, the enabled status can be ignored.
966 * Reply: 1 = fully enabled and writable,
967 * 0 = enabled and writable except that backend calendar is in an old KAlarm format,
968 * -1 = not enabled, read-only, or incompatible format.
970 int CollectionControlModel::isWritableEnabled(const Akonadi::Collection
& collection
, CalEvent::Type type
)
972 KACalendar::Compat format
;
973 return isWritableEnabled(collection
, type
, format
);
975 int CollectionControlModel::isWritableEnabled(const Akonadi::Collection
& collection
, CalEvent::Type type
, KACalendar::Compat
& format
)
977 int writable
= AkonadiModel::isWritable(collection
, format
);
981 // Check the collection's enabled status
982 if (!instance()->collections().contains(collection
)
983 || !collection
.hasAttribute
<CollectionAttribute
>())
985 if (!collection
.attribute
<CollectionAttribute
>()->isEnabled(type
))
990 /******************************************************************************
991 * Return the standard collection for a specified mime type.
992 * If 'useDefault' is true and there is no standard collection, the only
993 * collection for the mime type will be returned as a default.
995 Collection
CollectionControlModel::getStandard(CalEvent::Type type
, bool useDefault
)
997 const QString mimeType
= CalEvent::mimeType(type
);
999 Collection::List cols
= instance()->collections();
1000 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1002 AkonadiModel::instance()->refresh(cols
[i
]); // update with latest data
1003 if (cols
[i
].isValid()
1004 && cols
[i
].contentMimeTypes().contains(mimeType
))
1006 if (cols
[i
].hasAttribute
<CollectionAttribute
>()
1007 && (cols
[i
].attribute
<CollectionAttribute
>()->standard() & type
)
1008 && AkonadiModel::isCompatible(cols
[i
]))
1010 defalt
= (defalt
== -1) ? i
: -2;
1013 return (useDefault
&& defalt
>= 0) ? cols
[defalt
] : Collection();
1016 /******************************************************************************
1017 * Return whether a collection is the standard collection for a specified
1020 bool CollectionControlModel::isStandard(Akonadi::Collection
& collection
, CalEvent::Type type
)
1022 if (!instance()->collections().contains(collection
))
1024 AkonadiModel::instance()->refresh(collection
); // update with latest data
1025 if (!collection
.hasAttribute
<CollectionAttribute
>()
1026 || !AkonadiModel::isCompatible(collection
))
1028 return collection
.attribute
<CollectionAttribute
>()->isStandard(type
);
1031 /******************************************************************************
1032 * Return the alarm type(s) for which a collection is the standard collection.
1034 CalEvent::Types
CollectionControlModel::standardTypes(const Collection
& collection
, bool useDefault
)
1036 if (!instance()->collections().contains(collection
))
1037 return CalEvent::EMPTY
;
1038 Collection col
= collection
;
1039 AkonadiModel::instance()->refresh(col
); // update with latest data
1040 if (!AkonadiModel::isCompatible(col
))
1041 return CalEvent::EMPTY
;
1042 CalEvent::Types stdTypes
= col
.hasAttribute
<CollectionAttribute
>()
1043 ? col
.attribute
<CollectionAttribute
>()->standard()
1047 // Also return alarm types for which this is the only collection.
1048 CalEvent::Types wantedTypes
= AkonadiModel::types(collection
) & ~stdTypes
;
1049 Collection::List cols
= instance()->collections();
1050 for (int i
= 0, count
= cols
.count(); wantedTypes
&& i
< count
; ++i
)
1054 AkonadiModel::instance()->refresh(cols
[i
]); // update with latest data
1055 if (cols
[i
].isValid())
1056 wantedTypes
&= ~AkonadiModel::types(cols
[i
]);
1058 stdTypes
|= wantedTypes
;
1063 /******************************************************************************
1064 * Set or clear a collection as the standard collection for a specified mime
1065 * type. If it is being set as standard, the standard status for the mime type
1066 * is cleared for all other collections.
1068 void CollectionControlModel::setStandard(Akonadi::Collection
& collection
, CalEvent::Type type
, bool standard
)
1070 AkonadiModel
* model
= AkonadiModel::instance();
1071 model
->refresh(collection
); // update with latest data
1072 if (!AkonadiModel::isCompatible(collection
))
1073 standard
= false; // the collection isn't writable
1076 // The collection is being set as standard.
1077 // Clear the 'standard' status for all other collections.
1078 Collection::List cols
= instance()->collections();
1079 if (!cols
.contains(collection
))
1081 const CalEvent::Types ctypes
= collection
.hasAttribute
<CollectionAttribute
>()
1082 ? collection
.attribute
<CollectionAttribute
>()->standard() : CalEvent::EMPTY
;
1084 return; // it's already the standard collection for this type
1085 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1087 CalEvent::Types types
;
1088 if (cols
[i
] == collection
)
1090 cols
[i
] = collection
; // update with latest data
1091 types
= ctypes
| type
;
1095 model
->refresh(cols
[i
]); // update with latest data
1096 types
= cols
[i
].hasAttribute
<CollectionAttribute
>()
1097 ? cols
[i
].attribute
<CollectionAttribute
>()->standard() : CalEvent::EMPTY
;
1098 if (!(types
& type
))
1102 const QModelIndex index
= model
->collectionIndex(cols
[i
]);
1103 model
->setData(index
, static_cast<int>(types
), AkonadiModel::IsStandardRole
);
1108 // The 'standard' status is being cleared for the collection.
1109 // The collection doesn't have to be in this model's list of collections.
1110 CalEvent::Types types
= collection
.hasAttribute
<CollectionAttribute
>()
1111 ? collection
.attribute
<CollectionAttribute
>()->standard() : CalEvent::EMPTY
;
1115 const QModelIndex index
= model
->collectionIndex(collection
);
1116 model
->setData(index
, static_cast<int>(types
), AkonadiModel::IsStandardRole
);
1121 /******************************************************************************
1122 * Set which mime types a collection is the standard collection for.
1123 * If it is being set as standard for any mime types, the standard status for
1124 * those mime types is cleared for all other collections.
1126 void CollectionControlModel::setStandard(Akonadi::Collection
& collection
, CalEvent::Types types
)
1128 AkonadiModel
* model
= AkonadiModel::instance();
1129 model
->refresh(collection
); // update with latest data
1130 if (!AkonadiModel::isCompatible(collection
))
1131 types
= CalEvent::EMPTY
; // the collection isn't writable
1134 // The collection is being set as standard for at least one mime type.
1135 // Clear the 'standard' status for all other collections.
1136 Collection::List cols
= instance()->collections();
1137 if (!cols
.contains(collection
))
1139 const CalEvent::Types t
= collection
.hasAttribute
<CollectionAttribute
>()
1140 ? collection
.attribute
<CollectionAttribute
>()->standard() : CalEvent::EMPTY
;
1142 return; // there's no change to the collection's status
1143 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1146 if (cols
[i
] == collection
)
1148 cols
[i
] = collection
; // update with latest data
1153 model
->refresh(cols
[i
]); // update with latest data
1154 t
= cols
[i
].hasAttribute
<CollectionAttribute
>()
1155 ? cols
[i
].attribute
<CollectionAttribute
>()->standard() : CalEvent::EMPTY
;
1160 const QModelIndex index
= model
->collectionIndex(cols
[i
]);
1161 model
->setData(index
, static_cast<int>(t
), AkonadiModel::IsStandardRole
);
1166 // The 'standard' status is being cleared for the collection.
1167 // The collection doesn't have to be in this model's list of collections.
1168 if (collection
.hasAttribute
<CollectionAttribute
>()
1169 && collection
.attribute
<CollectionAttribute
>()->standard())
1171 const QModelIndex index
= model
->collectionIndex(collection
);
1172 model
->setData(index
, static_cast<int>(types
), AkonadiModel::IsStandardRole
);
1177 /******************************************************************************
1178 * Get the collection to use for storing an alarm.
1179 * Optionally, the standard collection for the alarm type is returned. If more
1180 * than one collection is a candidate, the user is prompted.
1182 Collection
CollectionControlModel::destination(CalEvent::Type type
, QWidget
* promptParent
, bool noPrompt
, bool* cancelled
)
1186 Collection standard
;
1187 if (type
== CalEvent::EMPTY
)
1189 standard
= getStandard(type
);
1190 // Archived alarms are always saved in the default resource,
1191 // else only prompt if necessary.
1192 if (type
== CalEvent::ARCHIVED
|| noPrompt
|| (!mAskDestination
&& standard
.isValid()))
1195 // Prompt for which collection to use
1196 CollectionListModel
* model
= new CollectionListModel(promptParent
);
1197 model
->setFilterWritable(true);
1198 model
->setFilterEnabled(true);
1199 model
->setEventTypeFilter(type
);
1200 model
->useCollectionColour(false);
1202 switch (model
->rowCount())
1207 col
= model
->collection(0);
1211 // Use AutoQPointer to guard against crash on application exit while
1212 // the dialogue is still open. It prevents double deletion (both on
1213 // deletion of 'promptParent', and on return from this function).
1214 AutoQPointer
<CollectionDialog
> dlg
= new CollectionDialog(model
, promptParent
);
1215 dlg
->setWindowTitle(i18nc("@title:window", "Choose Calendar"));
1216 dlg
->setDefaultCollection(standard
);
1217 dlg
->setMimeTypeFilter(QStringList(CalEvent::mimeType(type
)));
1219 col
= dlg
->selectedCollection();
1220 if (!col
.isValid() && cancelled
)
1227 /******************************************************************************
1228 * Return the enabled collections which contain a specified mime type.
1229 * If 'writable' is true, only writable collections are included.
1231 Collection::List
CollectionControlModel::enabledCollections(CalEvent::Type type
, bool writable
)
1233 const QString mimeType
= CalEvent::mimeType(type
);
1234 Collection::List cols
= instance()->collections();
1235 Collection::List result
;
1236 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1238 AkonadiModel::instance()->refresh(cols
[i
]); // update with latest data
1239 if (cols
[i
].contentMimeTypes().contains(mimeType
)
1240 && (!writable
|| ((cols
[i
].rights() & writableRights
) == writableRights
)))
1246 /******************************************************************************
1247 * Return the collection ID for a given resource ID.
1249 Collection
CollectionControlModel::collectionForResource(const QString
& resourceId
)
1251 const Collection::List cols
= instance()->collections();
1252 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1254 if (cols
[i
].resource() == resourceId
)
1257 return Collection();
1260 /******************************************************************************
1261 * Return whether all enabled collections have been populated.
1263 bool CollectionControlModel::isPopulated(Collection::Id colId
)
1265 AkonadiModel
* model
= AkonadiModel::instance();
1266 Collection::List cols
= instance()->collections();
1267 for (int i
= 0, count
= cols
.count(); i
< count
; ++i
)
1269 if ((colId
== -1 || colId
== cols
[i
].id())
1270 && !model
->data(model
->collectionIndex(cols
[i
].id()), AkonadiModel::IsPopulatedRole
).toBool())
1272 model
->refresh(cols
[i
]); // update with latest data
1273 if (!cols
[i
].hasAttribute
<CollectionAttribute
>()
1274 || cols
[i
].attribute
<CollectionAttribute
>()->enabled() == CalEvent::EMPTY
)
1281 /******************************************************************************
1282 * Wait for one or all enabled collections to be populated.
1283 * Reply = true if successful.
1285 bool CollectionControlModel::waitUntilPopulated(Collection::Id colId
, int timeout
)
1287 qCDebug(KALARM_LOG
);
1289 AkonadiModel
* model
= AkonadiModel::instance();
1290 while (!model
->isCollectionTreeFetched()
1291 || !isPopulated(colId
))
1293 if (!mPopulatedCheckLoop
)
1294 mPopulatedCheckLoop
= new QEventLoop(this);
1296 QTimer::singleShot(timeout
* 1000, mPopulatedCheckLoop
, &QEventLoop::quit
);
1297 result
= mPopulatedCheckLoop
->exec();
1299 delete mPopulatedCheckLoop
;
1300 mPopulatedCheckLoop
= Q_NULLPTR
;
1304 /******************************************************************************
1305 * Called when the Akonadi server has stopped. Reset the model.
1307 void CollectionControlModel::reset()
1309 delete mPopulatedCheckLoop
;
1310 mPopulatedCheckLoop
= Q_NULLPTR
;
1312 // Clear the collections list. This is required because addCollection() or
1313 // setCollections() don't work if the collections which they specify are
1314 // already in the list.
1315 setCollections(Collection::List());
1318 /******************************************************************************
1319 * Exit from the populated event loop when a collection has been populated.
1321 void CollectionControlModel::collectionPopulated()
1323 if (mPopulatedCheckLoop
)
1324 mPopulatedCheckLoop
->exit(1);
1327 /******************************************************************************
1328 * Return the data for a given role, for a specified item.
1330 QVariant
CollectionControlModel::data(const QModelIndex
& index
, int role
) const
1332 return sourceModel()->data(mapToSource(index
), role
);
1335 #include "collectionmodel.moc"