SVN_SILENT made messages (after extraction)
[kdepim.git] / kalarm / collectionmodel.cpp
blobac107812a30d60cb057133901965bd635428f895
1 /*
2 * collectionmodel.cpp - Akonadi collection models
3 * Program: kalarm
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>
38 #include <QUrl>
39 #include <QApplication>
40 #include <QMouseEvent>
41 #include <QHelpEvent>
42 #include <QToolTip>
43 #include <QTimer>
44 #include <QObject>
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
61 Q_OBJECT
62 public:
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;
71 protected:
72 bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const Q_DECL_OVERRIDE;
74 private:
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),
83 mWritableOnly(false),
84 mEnabledOnly(false)
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)
95 mAlarmType = type;
96 invalidateFilter();
100 void CollectionMimeTypeFilterModel::setFilterWritable(bool writable)
102 if (writable != mWritableOnly)
104 mWritableOnly = writable;
105 invalidateFilter();
109 void CollectionMimeTypeFilterModel::setFilterEnabled(bool enabled)
111 if (enabled != mEnabledOnly)
113 Q_EMIT layoutAboutToBeChanged();
114 mEnabledOnly = enabled;
115 invalidateFilter();
116 Q_EMIT layoutChanged();
120 bool CollectionMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
122 if (!EntityMimeTypeFilterModel::filterAcceptsRow(sourceRow, sourceParent))
123 return false;
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())
128 return false;
129 if (!mWritableOnly && mAlarmType == CalEvent::EMPTY)
130 return true;
131 if (mWritableOnly && (collection.rights() & writableRights) != writableRights)
132 return false;
133 if (mAlarmType != CalEvent::EMPTY && !collection.contentMimeTypes().contains(CalEvent::mimeType(mAlarmType)))
134 return false;
135 if ((mWritableOnly || mEnabledOnly) && !collection.hasAttribute<CollectionAttribute>())
136 return false;
137 if (mWritableOnly && (!collection.hasAttribute<CompatibilityAttribute>()
138 || collection.attribute<CompatibilityAttribute>()->compatibility() != KACalendar::Current))
139 return false;
140 if (mEnabledOnly && !collection.attribute<CollectionAttribute>()->isEnabled(mAlarmType))
141 return false;
142 return true;
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
223 switch (role)
225 case Qt::BackgroundRole:
226 if (!mUseCollectionColour)
227 role = AkonadiModel::BaseColourRole;
228 break;
229 default:
230 break;
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),
250 mAlarmType(type)
252 ++mInstanceCount;
253 if (!mModel)
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
272 // being deleted.
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)
281 delete mModel;
282 mModel = Q_NULLPTR;
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
309 switch (role)
311 case Qt::ForegroundRole:
313 const QString mimeType = CalEvent::mimeType(mAlarmType);
314 if (collection.contentMimeTypes().contains(mimeType))
315 return AkonadiModel::foregroundColor(collection, QStringList(mimeType));
316 break;
318 case Qt::FontRole:
320 if (!collection.hasAttribute<CollectionAttribute>()
321 || !AkonadiModel::isCompatible(collection))
322 break;
323 const CollectionAttribute* attr = collection.attribute<CollectionAttribute>();
324 if (!attr->enabled())
325 break;
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));
331 font.setBold(true);
332 return font;
334 break;
336 default:
337 break;
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))
359 QString errmsg;
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)
379 return false;
381 if (!errmsg.isEmpty())
383 KAMessageBox::sorry(messageParent, errmsg);
384 return false;
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())
434 return;
435 switch (change)
437 case AkonadiModel::Enabled:
438 qCDebug(KALARM_LOG) << "Enabled" << collection.id();
439 break;
440 case AkonadiModel::AlarmTypes:
441 qCDebug(KALARM_LOG) << "AlarmTypes" << collection.id();
442 break;
443 default:
444 return;
446 const QModelIndex ix = mModel->collectionIndex(collection);
447 if (ix.isValid())
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;
489 switch (type)
491 case CalEvent::ACTIVE: newModel = mActiveModel; break;
492 case CalEvent::ARCHIVED: newModel = mArchivedModel; break;
493 case CalEvent::TEMPLATE: newModel = mTemplateModel; break;
494 default:
495 return;
497 mAlarmType = type;
498 setSourceModel(newModel);
499 invalidate();
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
518 switch (role)
520 case Qt::ToolTipRole:
522 const Collection col = collection(index);
523 if (col.isValid())
524 return AkonadiModel::instance()->tooltip(col, mAlarmType);
525 break;
527 default:
528 break;
530 return QSortFilterProxyModel::data(index, role);
533 bool CollectionFilterCheckListModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
535 if (mAlarmType == CalEvent::EMPTY)
536 return true;
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())
549 invalidateFilter();
553 /*=============================================================================
554 = Class: CollectionView
555 = View displaying a list of collections.
556 =============================================================================*/
557 CollectionView::CollectionView(CollectionFilterCheckListModel* model, QWidget* parent)
558 : QListView(parent)
560 setModel(model);
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())
588 clearSelection();
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('@'));
606 if (i > 0)
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.
626 if (k > 0)
627 toolTip.remove(i, k + 1 - i);
629 else
631 toolTip.remove(k, 1);
632 toolTip.remove(i, 1);
635 QToolTip::showText(he->globalPos(), toolTip, this);
636 return true;
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
650 = collection models.
651 =============================================================================*/
653 CollectionControlModel* CollectionControlModel::mInstance = Q_NULLPTR;
654 bool CollectionControlModel::mAskDestination = false;
656 CollectionControlModel* CollectionControlModel::instance()
658 if (!mInstance)
659 mInstance = new CollectionControlModel(qApp);
660 return mInstance;
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);
709 if (canEnable)
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))
719 return false;
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);
725 return false;
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();
746 if (enabled)
747 alarmTypes |= static_cast<CalEvent::Types>(types & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE));
748 else
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
771 if (canEnable)
773 bool inList = false;
774 const Collection::List cols = collections();
775 foreach (const Collection& c, cols)
777 if (c.id() == collection.id())
779 inList = true;
780 break;
783 if (!inList)
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()
789 : CalEvent::EMPTY;
790 if (stdTypes)
792 foreach (const Collection& col, cols)
794 Collection c(col);
795 AkonadiModel::instance()->refresh(c); // update with latest data
796 if (c.isValid())
798 const CalEvent::Types t = stdTypes & CalEvent::types(c.contentMimeTypes());
799 if (t)
801 if (c.hasAttribute<CollectionAttribute>()
802 && AkonadiModel::isCompatible(c))
804 disallowedStdTypes |= c.attribute<CollectionAttribute>()->standard() & t;
805 if (disallowedStdTypes == stdTypes)
806 break;
812 addCollection(collection);
815 else
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);
831 return canEnable;
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
837 * list.
839 void CollectionControlModel::statusChanged(const Collection& collection, AkonadiModel::Change change, const QVariant& value, bool inserted)
841 if (!collection.isValid())
842 return;
844 switch (change)
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);
851 break;
853 case AkonadiModel::ReadOnly:
855 bool readOnly = value.toBool();
856 qCDebug(KALARM_LOG) << "id:" << collection.id() << ", readOnly=" << readOnly;
857 if (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;
867 QString msg;
868 switch (std)
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.",
873 collection.name());
874 break;
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.",
878 collection.name());
879 break;
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.",
883 collection.name());
884 break;
885 default:
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));
890 singleType = false;
891 break;
893 if (singleType)
894 msg = xi18nc("@info", "<para>%1</para><para>Please select a new default calendar.</para>", msg);
895 KAMessageBox::information(messageParent, msg);
898 break;
900 default:
901 break;
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.
911 * Parameters:
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);
920 if (types)
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();
936 if (!types)
937 break;
942 return types;
945 /******************************************************************************
946 * Create a bulleted list of alarm types for insertion into <para>...</para>.
948 QString CollectionControlModel::typeListForDisplay(CalEvent::Types alarmTypes)
950 QString list;
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>");
957 if (!list.isEmpty())
958 list = QStringLiteral("<list>") + list + QStringLiteral("</list>");
959 return list;
962 /******************************************************************************
963 * Return whether a collection is both enabled and fully writable for a given
964 * alarm type.
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);
978 if (writable == -1)
979 return -1;
981 // Check the collection's enabled status
982 if (!instance()->collections().contains(collection)
983 || !collection.hasAttribute<CollectionAttribute>())
984 return -1;
985 if (!collection.attribute<CollectionAttribute>()->isEnabled(type))
986 return -1;
987 return writable;
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);
998 int defalt = -1;
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]))
1009 return 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
1018 * mime type.
1020 bool CollectionControlModel::isStandard(Akonadi::Collection& collection, CalEvent::Type type)
1022 if (!instance()->collections().contains(collection))
1023 return false;
1024 AkonadiModel::instance()->refresh(collection); // update with latest data
1025 if (!collection.hasAttribute<CollectionAttribute>()
1026 || !AkonadiModel::isCompatible(collection))
1027 return false;
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()
1044 : CalEvent::EMPTY;
1045 if (useDefault)
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)
1052 if (cols[i] == col)
1053 continue;
1054 AkonadiModel::instance()->refresh(cols[i]); // update with latest data
1055 if (cols[i].isValid())
1056 wantedTypes &= ~AkonadiModel::types(cols[i]);
1058 stdTypes |= wantedTypes;
1060 return stdTypes;
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
1074 if (standard)
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))
1080 return;
1081 const CalEvent::Types ctypes = collection.hasAttribute<CollectionAttribute>()
1082 ? collection.attribute<CollectionAttribute>()->standard() : CalEvent::EMPTY;
1083 if (ctypes & type)
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;
1093 else
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))
1099 continue;
1100 types &= ~type;
1102 const QModelIndex index = model->collectionIndex(cols[i]);
1103 model->setData(index, static_cast<int>(types), AkonadiModel::IsStandardRole);
1106 else
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;
1112 if (types & type)
1114 types &= ~type;
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
1132 if (types)
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))
1138 return;
1139 const CalEvent::Types t = collection.hasAttribute<CollectionAttribute>()
1140 ? collection.attribute<CollectionAttribute>()->standard() : CalEvent::EMPTY;
1141 if (t == types)
1142 return; // there's no change to the collection's status
1143 for (int i = 0, count = cols.count(); i < count; ++i)
1145 CalEvent::Types t;
1146 if (cols[i] == collection)
1148 cols[i] = collection; // update with latest data
1149 t = types;
1151 else
1153 model->refresh(cols[i]); // update with latest data
1154 t = cols[i].hasAttribute<CollectionAttribute>()
1155 ? cols[i].attribute<CollectionAttribute>()->standard() : CalEvent::EMPTY;
1156 if (!(t & types))
1157 continue;
1158 t &= ~types;
1160 const QModelIndex index = model->collectionIndex(cols[i]);
1161 model->setData(index, static_cast<int>(t), AkonadiModel::IsStandardRole);
1164 else
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)
1184 if (cancelled)
1185 *cancelled = false;
1186 Collection standard;
1187 if (type == CalEvent::EMPTY)
1188 return standard;
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()))
1193 return standard;
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);
1201 Collection col;
1202 switch (model->rowCount())
1204 case 0:
1205 break;
1206 case 1:
1207 col = model->collection(0);
1208 break;
1209 default:
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)));
1218 if (dlg->exec())
1219 col = dlg->selectedCollection();
1220 if (!col.isValid() && cancelled)
1221 *cancelled = true;
1224 return col;
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)))
1241 result += cols[i];
1243 return result;
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)
1255 return cols[i];
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)
1275 return false;
1278 return true;
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);
1288 int result = 1;
1289 AkonadiModel* model = AkonadiModel::instance();
1290 while (!model->isCollectionTreeFetched()
1291 || !isPopulated(colId))
1293 if (!mPopulatedCheckLoop)
1294 mPopulatedCheckLoop = new QEventLoop(this);
1295 if (timeout > 0)
1296 QTimer::singleShot(timeout * 1000, mPopulatedCheckLoop, &QEventLoop::quit);
1297 result = mPopulatedCheckLoop->exec();
1299 delete mPopulatedCheckLoop;
1300 mPopulatedCheckLoop = Q_NULLPTR;
1301 return result;
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"
1337 // vim: et sw=4: