File indexing completed on 2024-11-24 04:50:39

0001 //  SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
0002 //  SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
0003 //  SPDX-FileCopyrightText: 2003, 2004 Cornelius Schumacher <schumacher@kde.org>
0004 //  SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0005 //  SPDX-FileCopyrightText: 2009 Sebastian Sauer <sebsauer@kdab.net>
0006 //  SPDX-FileCopyrightText: 2010-2021 Laurent Montel <montel@kde.org>
0007 //  SPDX-FileCopyrightText: 2012 Sérgio Martins <iamsergio@gmail.com>
0008 //
0009 //  SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-Commercial-exception-1.0
0010 
0011 #include "calendarmanager.h"
0012 
0013 // Akonadi
0014 #include "merkuro_calendar_debug.h"
0015 
0016 #include "models/todosortfilterproxymodel.h"
0017 #include <Akonadi/AgentFilterProxyModel>
0018 #include <Akonadi/AgentInstanceModel>
0019 #include <Akonadi/AgentManager>
0020 #include <Akonadi/AttributeFactory>
0021 #include <Akonadi/Collection>
0022 #include <Akonadi/CollectionColorAttribute>
0023 #include <Akonadi/CollectionDeleteJob>
0024 #include <Akonadi/CollectionIdentificationAttribute>
0025 #include <Akonadi/CollectionModifyJob>
0026 #include <Akonadi/CollectionPropertiesDialog>
0027 #include <Akonadi/CollectionUtils>
0028 #include <Akonadi/Control>
0029 #include <Akonadi/ETMViewStateSaver>
0030 #include <Akonadi/EntityDisplayAttribute>
0031 #include <Akonadi/EntityRightsFilterModel>
0032 #include <Akonadi/EntityTreeModel>
0033 #include <Akonadi/History>
0034 #include <Akonadi/ItemModifyJob>
0035 #include <Akonadi/ItemMoveJob>
0036 #include <Akonadi/Monitor>
0037 #include <KCheckableProxyModel>
0038 #include <KDescendantsProxyModel>
0039 #include <KLocalizedString>
0040 #include <QApplication>
0041 #include <QPointer>
0042 
0043 #include "colorproxymodel.h"
0044 #include "incidencewrapper.h"
0045 #include "sortedcollectionproxymodel.h"
0046 
0047 using namespace Akonadi;
0048 
0049 static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model)
0050 {
0051     QAbstractProxyModel *proxyModel = nullptr;
0052     while (model) {
0053         proxyModel = qobject_cast<QAbstractProxyModel *>(model);
0054         if (proxyModel && proxyModel->sourceModel()) {
0055             model = proxyModel->sourceModel();
0056         } else {
0057             break;
0058         }
0059     }
0060     return qobject_cast<Akonadi::EntityTreeModel *>(model);
0061 }
0062 
0063 /**
0064  * Automatically checks new calendar entries
0065  */
0066 class NewCalendarChecker : public QObject
0067 {
0068     Q_OBJECT
0069 public:
0070     NewCalendarChecker(QAbstractItemModel *model)
0071         : QObject(model)
0072         , mCheckableProxy(model)
0073     {
0074         connect(model, &QAbstractItemModel::rowsInserted, this, &NewCalendarChecker::onSourceRowsInserted);
0075         qRegisterMetaType<QPersistentModelIndex>("QPersistentModelIndex");
0076     }
0077 
0078 private Q_SLOTS:
0079     void onSourceRowsInserted(const QModelIndex &parent, int start, int end)
0080     {
0081         Akonadi::EntityTreeModel *etm = findEtm(mCheckableProxy);
0082         // Only check new collections and not during initial population
0083         if (!etm || !etm->isCollectionTreeFetched()) {
0084             return;
0085         }
0086         for (int i = start; i <= end; ++i) {
0087             // qCDebug(KORGANIZER_LOG) << "checking " << i << parent << mCheckableProxy->index(i, 0, parent).data().toString();
0088             const QModelIndex index = mCheckableProxy->index(i, 0, parent);
0089             QMetaObject::invokeMethod(this, "setCheckState", Qt::QueuedConnection, Q_ARG(QPersistentModelIndex, index));
0090         }
0091     }
0092 
0093     void setCheckState(const QPersistentModelIndex &index)
0094     {
0095         mCheckableProxy->setData(index, Qt::Checked, Qt::CheckStateRole);
0096         if (mCheckableProxy->hasChildren(index)) {
0097             onSourceRowsInserted(index, 0, mCheckableProxy->rowCount(index) - 1);
0098         }
0099     }
0100 
0101 private:
0102     QAbstractItemModel *const mCheckableProxy;
0103 };
0104 
0105 class CollectionFilter : public QSortFilterProxyModel
0106 {
0107 public:
0108     explicit CollectionFilter(QObject *parent = nullptr)
0109         : QSortFilterProxyModel(parent)
0110     {
0111         setDynamicSortFilter(true);
0112     }
0113 
0114 protected:
0115     bool filterAcceptsRow(int row, const QModelIndex &sourceParent) const override
0116     {
0117         const QModelIndex sourceIndex = sourceModel()->index(row, 0, sourceParent);
0118         Q_ASSERT(sourceIndex.isValid());
0119 
0120         const Akonadi::Collection &col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0121         const auto attr = col.attribute<Akonadi::CollectionIdentificationAttribute>();
0122 
0123         // We filter the user folders because we insert person nodes for user folders.
0124         if ((attr && attr->collectionNamespace().startsWith("usertoplevel")) || col.name().contains(QLatin1StringView("Other Users"))) {
0125             return false;
0126         }
0127         return true;
0128     }
0129 };
0130 
0131 CalendarManager::CalendarManager(QObject *parent)
0132     : QObject(parent)
0133     , m_calendar(nullptr)
0134     , m_config(new CalendarConfig(this))
0135 {
0136     if (!Akonadi::Control::start()) {
0137         qApp->exit(-1);
0138         return;
0139     }
0140 
0141     auto colorProxy = new ColorProxyModel(this);
0142     colorProxy->setObjectName(QLatin1StringView("Show calendar colors"));
0143     colorProxy->setDynamicSortFilter(true);
0144     colorProxy->setStandardCollectionId(m_config->lastUsedEventCollection());
0145 
0146     connect(m_config, &CalendarConfig::lastUsedEventCollectionChanged, this, [this, colorProxy]() {
0147         colorProxy->setStandardCollectionId(m_config->lastUsedEventCollection());
0148     });
0149     m_baseModel = colorProxy;
0150 
0151     // Hide collections that are not required
0152     auto collectionFilter = new CollectionFilter(this);
0153     collectionFilter->setSourceModel(colorProxy);
0154 
0155     m_calendar = QSharedPointer<Akonadi::ETMCalendar>::create(); // QSharedPointer
0156     setCollectionSelectionProxyModel(m_calendar->checkableProxyModel());
0157     connect(m_calendar->checkableProxyModel(), &KCheckableProxyModel::dataChanged, this, &CalendarManager::refreshEnabledTodoCollections);
0158 
0159     m_changer = m_calendar->incidenceChanger();
0160     m_changer->setHistoryEnabled(true);
0161     connect(m_changer->history(), &Akonadi::History::changed, this, &CalendarManager::undoRedoDataChanged);
0162 
0163     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0164     mCollectionSelectionModelStateSaver = new Akonadi::ETMViewStateSaver(); // not a leak
0165     KConfigGroup selectionGroup = config->group(QStringLiteral("GlobalCollectionSelection"));
0166     mCollectionSelectionModelStateSaver->setView(nullptr);
0167     mCollectionSelectionModelStateSaver->setSelectionModel(m_calendar->checkableProxyModel()->selectionModel());
0168     mCollectionSelectionModelStateSaver->restoreState(selectionGroup);
0169 
0170     m_allCalendars = new Akonadi::CollectionFilterProxyModel(this);
0171     m_allCalendars->setSourceModel(collectionFilter);
0172     m_allCalendars->setExcludeVirtualCollections(true);
0173 
0174     // Filter it by mimetype again, to only keep
0175     // Kolab / Inbox / Calendar
0176     m_eventMimeTypeFilterModel = new Akonadi::CollectionFilterProxyModel(this);
0177     m_eventMimeTypeFilterModel->setSourceModel(collectionFilter);
0178     m_eventMimeTypeFilterModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.event"));
0179 
0180     // text/calendar mimetype includes todo cals
0181     m_todoMimeTypeFilterModel = new Akonadi::CollectionFilterProxyModel(this);
0182     m_todoMimeTypeFilterModel->setSourceModel(collectionFilter);
0183     m_todoMimeTypeFilterModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.todo"));
0184     m_todoMimeTypeFilterModel->setExcludeVirtualCollections(true);
0185 
0186     // Filter by access rights
0187     m_allCollectionsRightsFilterModel = new Akonadi::EntityRightsFilterModel(this);
0188     m_allCollectionsRightsFilterModel->setAccessRights(Collection::CanCreateItem);
0189     m_allCollectionsRightsFilterModel->setSourceModel(collectionFilter);
0190 
0191     m_eventRightsFilterModel = new Akonadi::EntityRightsFilterModel(this);
0192     m_eventRightsFilterModel->setAccessRights(Collection::CanCreateItem);
0193     m_eventRightsFilterModel->setSourceModel(m_eventMimeTypeFilterModel);
0194 
0195     m_todoRightsFilterModel = new Akonadi::EntityRightsFilterModel(this);
0196     m_todoRightsFilterModel->setAccessRights(Collection::CanCreateItem);
0197     m_todoRightsFilterModel->setSourceModel(m_todoMimeTypeFilterModel);
0198 
0199     // Model for todo via collection picker
0200     m_todoViewCollectionModel = new SortedCollectionProxModel(this);
0201     m_todoViewCollectionModel->setSourceModel(collectionFilter);
0202     m_todoViewCollectionModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.todo"));
0203     m_todoViewCollectionModel->setExcludeVirtualCollections(true);
0204     m_todoViewCollectionModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0205     m_todoViewCollectionModel->sort(0, Qt::AscendingOrder);
0206 
0207     // Model for the mainDrawer
0208     m_viewCollectionModel = new SortedCollectionProxModel(this);
0209     m_viewCollectionModel->setSourceModel(collectionFilter);
0210     m_viewCollectionModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.event"));
0211     m_viewCollectionModel->addMimeTypeFilter(QStringLiteral("application/x-vnd.akonadi.calendar.todo"));
0212     m_viewCollectionModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0213     m_viewCollectionModel->sort(0, Qt::AscendingOrder);
0214 
0215     m_flatCollectionTreeModel = new KDescendantsProxyModel(this);
0216     m_flatCollectionTreeModel->setSourceModel(m_viewCollectionModel);
0217     m_flatCollectionTreeModel->setExpandsByDefault(true);
0218 
0219     auto refreshColors = [this, colorProxy]() {
0220         for (auto i = 0; i < m_flatCollectionTreeModel->rowCount(); i++) {
0221             auto idx = m_flatCollectionTreeModel->index(i, 0, {});
0222             colorProxy->getCollectionColor(Akonadi::CollectionUtils::fromIndex(idx));
0223         }
0224     };
0225     connect(m_flatCollectionTreeModel, &QSortFilterProxyModel::rowsInserted, this, refreshColors);
0226 
0227     KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors"));
0228     m_colorWatcher = KConfigWatcher::create(config);
0229     connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &CalendarManager::collectionColorsChanged);
0230 
0231     connect(m_calendar.data(), &Akonadi::ETMCalendar::calendarChanged, this, &CalendarManager::calendarChanged);
0232 }
0233 
0234 CalendarManager::~CalendarManager()
0235 {
0236     save();
0237     // delete mCollectionSelectionModelStateSaver;
0238 }
0239 
0240 void CalendarManager::save()
0241 {
0242     Akonadi::ETMViewStateSaver treeStateSaver;
0243     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0244     KConfigGroup group = config->group(QStringLiteral("GlobalCollectionSelection"));
0245     treeStateSaver.setView(nullptr);
0246     treeStateSaver.setSelectionModel(m_calendar->checkableProxyModel()->selectionModel());
0247     treeStateSaver.saveState(group);
0248 
0249     config->sync();
0250 }
0251 
0252 void CalendarManager::delayedInit()
0253 {
0254     Q_EMIT loadingChanged();
0255 }
0256 
0257 QAbstractProxyModel *CalendarManager::collections()
0258 {
0259     return static_cast<QAbstractProxyModel *>(m_flatCollectionTreeModel->sourceModel());
0260 }
0261 
0262 QAbstractItemModel *CalendarManager::todoCollections()
0263 {
0264     return m_todoViewCollectionModel;
0265 }
0266 
0267 QAbstractItemModel *CalendarManager::viewCollections()
0268 {
0269     return m_viewCollectionModel;
0270 }
0271 
0272 QList<qint64> CalendarManager::enabledTodoCollections()
0273 {
0274     return m_enabledTodoCollections;
0275 }
0276 
0277 void CalendarManager::refreshEnabledTodoCollections()
0278 {
0279     m_enabledTodoCollections.clear();
0280     const auto selectedIndexes = m_calendar->checkableProxyModel()->selectionModel()->selectedIndexes();
0281     for (auto selectedIndex : selectedIndexes) {
0282         auto collection = selectedIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0283         if (collection.contentMimeTypes().contains(QStringLiteral("application/x-vnd.akonadi.calendar.todo"))) {
0284             m_enabledTodoCollections.append(collection.id());
0285         }
0286     }
0287 
0288     Q_EMIT enabledTodoCollectionsChanged();
0289 }
0290 
0291 bool CalendarManager::loading() const
0292 {
0293     return m_calendar->isLoading();
0294 }
0295 
0296 void CalendarManager::setCollectionSelectionProxyModel(KCheckableProxyModel *m)
0297 {
0298     if (m_selectionProxyModel == m) {
0299         return;
0300     }
0301 
0302     m_selectionProxyModel = m;
0303     if (!m_selectionProxyModel) {
0304         return;
0305     }
0306 
0307     new NewCalendarChecker(m);
0308     m_baseModel->setSourceModel(m_selectionProxyModel);
0309 }
0310 
0311 KCheckableProxyModel *CalendarManager::collectionSelectionProxyModel() const
0312 {
0313     return m_selectionProxyModel;
0314 }
0315 
0316 Akonadi::ETMCalendar::Ptr CalendarManager::calendar() const
0317 {
0318     return m_calendar;
0319 }
0320 
0321 Akonadi::IncidenceChanger *CalendarManager::incidenceChanger() const
0322 {
0323     return m_changer;
0324 }
0325 
0326 Akonadi::CollectionFilterProxyModel *CalendarManager::allCalendars()
0327 {
0328     return m_allCalendars;
0329 }
0330 
0331 qint64 CalendarManager::defaultCalendarId(IncidenceWrapper *incidenceWrapper)
0332 {
0333     // Checks if default collection accepts this type of incidence
0334     auto mimeType = incidenceWrapper->incidencePtr()->mimeType();
0335     Akonadi::Collection collection = m_calendar->collection(m_config->lastUsedEventCollection());
0336     bool supportsMimeType = collection.contentMimeTypes().contains(mimeType) || mimeType == QLatin1StringView("");
0337     bool hasRights = collection.rights() & Akonadi::Collection::CanCreateItem;
0338     if (collection.isValid() && supportsMimeType && hasRights) {
0339         return collection.id();
0340     }
0341 
0342     // Should add last used collection by mimetype somewhere.
0343 
0344     // Searches for first collection that will accept this incidence
0345     for (int i = 0; i < m_allCalendars->rowCount(); i++) {
0346         QModelIndex idx = m_allCalendars->index(i, 0);
0347         collection = idx.data(Akonadi::EntityTreeModel::Roles::CollectionRole).value<Akonadi::Collection>();
0348         supportsMimeType = collection.contentMimeTypes().contains(mimeType) || mimeType == QLatin1StringView("");
0349         hasRights = collection.rights() & Akonadi::Collection::CanCreateItem;
0350         if (collection.isValid() && supportsMimeType && hasRights) {
0351             return collection.id();
0352         }
0353     }
0354 
0355     return -1;
0356 }
0357 
0358 QVariant CalendarManager::getIncidenceSubclassed(KCalendarCore::Incidence::Ptr incidencePtr)
0359 {
0360     switch (incidencePtr->type()) {
0361     case (KCalendarCore::IncidenceBase::TypeEvent):
0362         return QVariant::fromValue(m_calendar->event(incidencePtr->instanceIdentifier()));
0363         break;
0364     case (KCalendarCore::IncidenceBase::TypeTodo):
0365         return QVariant::fromValue(m_calendar->todo(incidencePtr->instanceIdentifier()));
0366         break;
0367     case (KCalendarCore::IncidenceBase::TypeJournal):
0368         return QVariant::fromValue(m_calendar->journal(incidencePtr->instanceIdentifier()));
0369         break;
0370     default:
0371         return QVariant::fromValue(incidencePtr);
0372         break;
0373     }
0374 }
0375 
0376 QVariantMap CalendarManager::undoRedoData()
0377 {
0378     if (!m_changer || !m_changer->history()) {
0379         return {
0380             {QStringLiteral("undoAvailable"), false},
0381             {QStringLiteral("redoAvailable"), false},
0382             {QStringLiteral("nextUndoDescription"), QString()},
0383             {QStringLiteral("nextRedoDescription"), QString()},
0384         };
0385     }
0386     return QVariantMap{
0387         {QStringLiteral("undoAvailable"), m_changer->history()->undoAvailable()},
0388         {QStringLiteral("redoAvailable"), m_changer->history()->redoAvailable()},
0389         {QStringLiteral("nextUndoDescription"), m_changer->history()->nextUndoDescription()},
0390         {QStringLiteral("nextRedoDescription"), m_changer->history()->nextRedoDescription()},
0391     };
0392 }
0393 
0394 Akonadi::Item CalendarManager::incidenceItem(KCalendarCore::Incidence::Ptr incidence) const
0395 {
0396     return m_calendar->item(incidence);
0397 }
0398 
0399 Akonadi::Item CalendarManager::incidenceItem(const QString &uid) const
0400 {
0401     return incidenceItem(m_calendar->incidence(uid));
0402 }
0403 
0404 KCalendarCore::Incidence::List CalendarManager::childIncidences(const QString &uid) const
0405 {
0406     return m_calendar->childIncidences(uid);
0407 }
0408 
0409 void CalendarManager::addIncidence(IncidenceWrapper *incidenceWrapper)
0410 {
0411     if (incidenceWrapper->collectionId() < 0) {
0412         const auto sharedConfig = KSharedConfig::openConfig();
0413         const auto editorConfigSection = sharedConfig->group(QStringLiteral("Editor"));
0414 
0415         const auto lastUsedCollectionType = incidenceWrapper->incidenceType() == KCalendarCore::IncidenceBase::TypeTodo
0416             ? QStringLiteral("lastUsedTodoCollection")
0417             : QStringLiteral("lastUsedEventCollection");
0418         const auto lastUsedCollectionId = editorConfigSection.readEntry(lastUsedCollectionType, -1);
0419 
0420         if (lastUsedCollectionId > -1) {
0421             incidenceWrapper->setCollectionId(lastUsedCollectionId);
0422         }
0423     }
0424 
0425     Akonadi::Collection collection(incidenceWrapper->collectionId());
0426 
0427     switch (incidenceWrapper->incidencePtr()->type()) {
0428     case (KCalendarCore::IncidenceBase::TypeEvent): {
0429         KCalendarCore::Event::Ptr event = incidenceWrapper->incidencePtr().staticCast<KCalendarCore::Event>();
0430         m_changer->createIncidence(event, collection);
0431         break;
0432     }
0433     case (KCalendarCore::IncidenceBase::TypeTodo): {
0434         KCalendarCore::Todo::Ptr todo = incidenceWrapper->incidencePtr().staticCast<KCalendarCore::Todo>();
0435         m_changer->createIncidence(todo, collection);
0436         break;
0437     }
0438     default:
0439         m_changer->createIncidence(KCalendarCore::Incidence::Ptr(incidenceWrapper->incidencePtr()->clone()), collection);
0440         break;
0441     }
0442     // This will fritz if you don't choose a valid *calendar*
0443 }
0444 
0445 // Replicates IncidenceDialogPrivate::save
0446 void CalendarManager::editIncidence(IncidenceWrapper *incidenceWrapper)
0447 {
0448     // We need to use the incidenceChanger manually to get the change recorded in the history
0449     // For undo/redo to work properly we need to change the ownership of the incidence pointers
0450     KCalendarCore::Incidence::Ptr changedIncidence(incidenceWrapper->incidencePtr()->clone());
0451     KCalendarCore::Incidence::Ptr originalPayload(incidenceWrapper->originalIncidencePtr()->clone());
0452 
0453     Akonadi::Item modifiedItem = m_calendar->item(changedIncidence->instanceIdentifier());
0454     modifiedItem.setPayload<KCalendarCore::Incidence::Ptr>(changedIncidence);
0455 
0456     m_changer->modifyIncidence(modifiedItem, originalPayload);
0457 
0458     if (!incidenceWrapper->collectionId() || incidenceWrapper->collectionId() < 0 || modifiedItem.parentCollection().id() == incidenceWrapper->collectionId()) {
0459         return;
0460     }
0461 
0462     changeIncidenceCollection(modifiedItem, incidenceWrapper->collectionId());
0463 }
0464 
0465 void CalendarManager::updateIncidenceDates(IncidenceWrapper *incidenceWrapper, int startOffset, int endOffset, int occurrences, const QDateTime &occurrenceDate)
0466 { // start and end offsets are in msecs
0467 
0468     Akonadi::Item item = m_calendar->item(incidenceWrapper->incidencePtr());
0469     item.setPayload(incidenceWrapper->incidencePtr());
0470 
0471     auto setNewDates = [&](KCalendarCore::Incidence::Ptr incidence) {
0472         if (incidence->type() == KCalendarCore::Incidence::TypeTodo) {
0473             // For to-dos endOffset is ignored because it will always be == to startOffset because we only
0474             // support moving to-dos, not resizing them. There are no multi-day to-dos.
0475             // Lets just call it offset to reduce confusion.
0476             const int offset = startOffset;
0477 
0478             KCalendarCore::Todo::Ptr todo = incidence.staticCast<KCalendarCore::Todo>();
0479             QDateTime due = todo->dtDue();
0480             QDateTime start = todo->dtStart();
0481             if (due.isValid()) { // Due has priority over start.
0482                 // We will only move the due date, unlike events where we move both.
0483                 due = due.addMSecs(offset);
0484                 todo->setDtDue(due);
0485 
0486                 if (start.isValid() && start > due) {
0487                     // Start can't be bigger than due.
0488                     todo->setDtStart(due);
0489                 }
0490             } else if (start.isValid()) {
0491                 // So we're displaying a to-do that doesn't have due date, only start...
0492                 start = start.addMSecs(offset);
0493                 todo->setDtStart(start);
0494             } else {
0495                 // This never happens
0496                 // qCWarning(CALENDARVIEW_LOG) << "Move what? uid:" << todo->uid() << "; summary=" << todo->summary();
0497             }
0498         } else {
0499             incidence->setDtStart(incidence->dtStart().addMSecs(startOffset));
0500             if (incidence->type() == KCalendarCore::Incidence::TypeEvent) {
0501                 KCalendarCore::Event::Ptr event = incidence.staticCast<KCalendarCore::Event>();
0502                 event->setDtEnd(event->dtEnd().addMSecs(endOffset));
0503             }
0504         }
0505     };
0506 
0507     if (incidenceWrapper->incidencePtr()->recurs()) {
0508         switch (occurrences) {
0509         case KCalUtils::RecurrenceActions::AllOccurrences: {
0510             // All occurrences
0511             KCalendarCore::Incidence::Ptr oldIncidence(incidenceWrapper->incidencePtr()->clone());
0512             setNewDates(incidenceWrapper->incidencePtr());
0513             qCDebug(MERKURO_CALENDAR_LOG) << incidenceWrapper->incidenceStart();
0514             m_changer->modifyIncidence(item, oldIncidence);
0515             break;
0516         }
0517         case KCalUtils::RecurrenceActions::SelectedOccurrence: // Just this occurrence
0518         case KCalUtils::RecurrenceActions::FutureOccurrences: { // All future occurrences
0519             const bool thisAndFuture = (occurrences == KCalUtils::RecurrenceActions::FutureOccurrences);
0520             auto tzedOccurrenceDate = occurrenceDate.toTimeZone(incidenceWrapper->incidenceStart().timeZone());
0521             KCalendarCore::Incidence::Ptr newIncidence(
0522                 KCalendarCore::Calendar::createException(incidenceWrapper->incidencePtr(), tzedOccurrenceDate, thisAndFuture));
0523 
0524             if (newIncidence) {
0525                 m_changer->startAtomicOperation(i18n("Move occurrence(s)"));
0526                 setNewDates(newIncidence);
0527                 m_changer->createIncidence(newIncidence, m_calendar->collection(incidenceWrapper->collectionId()));
0528                 m_changer->endAtomicOperation();
0529             } else {
0530                 qCDebug(MERKURO_CALENDAR_LOG) << i18n("Unable to add the exception item to the calendar. No change will be done.");
0531             }
0532             break;
0533         }
0534         }
0535     } else { // Doesn't recur
0536         KCalendarCore::Incidence::Ptr oldIncidence(incidenceWrapper->incidencePtr()->clone());
0537         setNewDates(incidenceWrapper->incidencePtr());
0538         m_changer->modifyIncidence(item, oldIncidence);
0539     }
0540 
0541     Q_EMIT updateIncidenceDatesCompleted();
0542 }
0543 
0544 bool CalendarManager::hasChildren(KCalendarCore::Incidence::Ptr incidence)
0545 {
0546     return !m_calendar->childIncidences(incidence->uid()).isEmpty();
0547 }
0548 
0549 void CalendarManager::deleteAllChildren(KCalendarCore::Incidence::Ptr incidence)
0550 {
0551     const auto allChildren = m_calendar->childIncidences(incidence->uid());
0552 
0553     for (const auto &child : allChildren) {
0554         if (!m_calendar->childIncidences(child->uid()).isEmpty()) {
0555             deleteAllChildren(child);
0556         }
0557     }
0558 
0559     for (const auto &child : allChildren) {
0560         m_calendar->deleteIncidence(child);
0561     }
0562 }
0563 
0564 void CalendarManager::deleteIncidence(KCalendarCore::Incidence::Ptr incidence, bool deleteChildren)
0565 {
0566     const auto directChildren = m_calendar->childIncidences(incidence->uid());
0567 
0568     if (!directChildren.isEmpty()) {
0569         if (deleteChildren) {
0570             m_changer->startAtomicOperation(i18n("Delete task and its sub-tasks"));
0571             deleteAllChildren(incidence);
0572         } else {
0573             m_changer->startAtomicOperation(i18n("Delete task and make sub-tasks independent"));
0574             for (const auto &child : directChildren) {
0575                 const auto instances = m_calendar->instances(child);
0576                 for (const auto &instance : instances) {
0577                     KCalendarCore::Incidence::Ptr oldInstance(instance->clone());
0578                     instance->setRelatedTo(QString());
0579                     m_changer->modifyIncidence(m_calendar->item(instance), oldInstance);
0580                 }
0581 
0582                 KCalendarCore::Incidence::Ptr oldInc(child->clone());
0583                 child->setRelatedTo(QString());
0584                 m_changer->modifyIncidence(m_calendar->item(child), oldInc);
0585             }
0586         }
0587 
0588         m_calendar->deleteIncidence(incidence);
0589         m_changer->endAtomicOperation();
0590         return;
0591     }
0592 
0593     m_calendar->deleteIncidence(incidence);
0594 }
0595 
0596 void CalendarManager::changeIncidenceCollection(KCalendarCore::Incidence::Ptr incidence, qint64 collectionId)
0597 {
0598     KCalendarCore::Incidence::Ptr incidenceClone(incidence->clone());
0599     Akonadi::Item modifiedItem = m_calendar->item(incidence->instanceIdentifier());
0600     modifiedItem.setPayload<KCalendarCore::Incidence::Ptr>(incidenceClone);
0601 
0602     if (modifiedItem.parentCollection().id() != collectionId) {
0603         changeIncidenceCollection(modifiedItem, collectionId);
0604     }
0605 }
0606 
0607 void CalendarManager::changeIncidenceCollection(Akonadi::Item item, qint64 collectionId)
0608 {
0609     if (item.parentCollection().id() == collectionId) {
0610         return;
0611     }
0612 
0613     Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
0614 
0615     Akonadi::Collection newCollection(collectionId);
0616     item.setParentCollection(newCollection);
0617 
0618     auto job = new Akonadi::ItemMoveJob(item, newCollection);
0619     // Add some type of check here?
0620     connect(job, &KJob::result, job, [this, job, item, collectionId]() {
0621         qCDebug(MERKURO_CALENDAR_LOG) << job->error();
0622 
0623         if (!job->error()) {
0624             const auto allChildren = m_calendar->childIncidences(item.id());
0625             for (const auto &child : allChildren) {
0626                 changeIncidenceCollection(m_calendar->item(child), collectionId);
0627             }
0628 
0629             auto parent = item.payload<KCalendarCore::Incidence::Ptr>()->relatedTo();
0630             if (!parent.isEmpty()) {
0631                 changeIncidenceCollection(m_calendar->item(parent), collectionId);
0632             }
0633         }
0634     });
0635 }
0636 
0637 QVariantMap CalendarManager::getCollectionDetails(QVariant collectionId)
0638 {
0639     QVariantMap collectionDetails;
0640     Akonadi::Collection collection = m_calendar->collection(collectionId.toInt());
0641     bool isFiltered = false;
0642     int allCalendarsRow = 0;
0643 
0644     for (int i = 0; i < m_allCalendars->rowCount(); i++) {
0645         if (m_allCalendars->data(m_allCalendars->index(i, 0), Akonadi::EntityTreeModel::CollectionIdRole).toInt() == collectionId) {
0646             isFiltered = !m_allCalendars->data(m_allCalendars->index(i, 0), Qt::CheckStateRole).toBool();
0647             allCalendarsRow = i;
0648             break;
0649         }
0650     }
0651 
0652     collectionDetails[QLatin1StringView("id")] = collection.id();
0653     collectionDetails[QLatin1StringView("name")] = collection.name();
0654     collectionDetails[QLatin1StringView("displayName")] = collection.displayName();
0655     collectionDetails[QLatin1StringView("color")] = m_baseModel->color(collection.id());
0656     collectionDetails[QLatin1StringView("count")] = collection.statistics().count();
0657     collectionDetails[QLatin1StringView("isResource")] = Akonadi::CollectionUtils::isResource(collection);
0658     collectionDetails[QLatin1StringView("resource")] = collection.resource();
0659     collectionDetails[QLatin1StringView("readOnly")] = collection.rights().testFlag(Collection::ReadOnly);
0660     collectionDetails[QLatin1StringView("canChange")] = collection.rights().testFlag(Collection::CanChangeCollection);
0661     collectionDetails[QLatin1StringView("canCreate")] = collection.rights().testFlag(Collection::CanCreateCollection);
0662     collectionDetails[QLatin1StringView("canDelete")] =
0663         collection.rights().testFlag(Collection::CanDeleteCollection) && !Akonadi::CollectionUtils::isResource(collection);
0664     collectionDetails[QLatin1StringView("isFiltered")] = isFiltered;
0665     collectionDetails[QLatin1StringView("allCalendarsRow")] = allCalendarsRow;
0666 
0667     return collectionDetails;
0668 }
0669 
0670 void CalendarManager::setCollectionColor(qint64 collectionId, const QColor &color)
0671 {
0672     auto collection = m_calendar->collection(collectionId);
0673     auto colorAttr = collection.attribute<Akonadi::CollectionColorAttribute>(Akonadi::Collection::AddIfMissing);
0674     colorAttr->setColor(color);
0675     auto modifyJob = new Akonadi::CollectionModifyJob(collection);
0676     connect(modifyJob, &Akonadi::CollectionModifyJob::result, this, [this, collectionId, color](KJob *job) {
0677         if (job->error()) {
0678             qCWarning(MERKURO_CALENDAR_LOG) << "Error occurred modifying collection color: " << job->errorString();
0679         } else {
0680             m_baseModel->setColor(collectionId, color);
0681         }
0682     });
0683 }
0684 
0685 void CalendarManager::undoAction()
0686 {
0687     m_changer->history()->undo();
0688 }
0689 
0690 void CalendarManager::redoAction()
0691 {
0692     m_changer->history()->redo();
0693 }
0694 
0695 void CalendarManager::updateAllCollections()
0696 {
0697     for (int i = 0; i < collections()->rowCount(); i++) {
0698         auto collection = collections()->data(collections()->index(i, 0), Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
0699         Akonadi::AgentManager::self()->synchronizeCollection(collection, true);
0700     }
0701 }
0702 
0703 void CalendarManager::updateCollection(qint64 collectionId)
0704 {
0705     auto collection = m_calendar->collection(collectionId);
0706     Akonadi::AgentManager::self()->synchronizeCollection(collection, false);
0707 }
0708 
0709 void CalendarManager::deleteCollection(qint64 collectionId)
0710 {
0711     auto collection = m_calendar->collection(collectionId);
0712     const bool isTopLevel = collection.parentCollection() == Akonadi::Collection::root();
0713 
0714     if (!isTopLevel) {
0715         // deletes contents
0716         auto job = new Akonadi::CollectionDeleteJob(collection, this);
0717         connect(job, &Akonadi::CollectionDeleteJob::result, this, [](KJob *job) {
0718             if (job->error()) {
0719                 qCWarning(MERKURO_CALENDAR_LOG) << "Error occurred deleting collection: " << job->errorString();
0720             }
0721         });
0722         return;
0723     }
0724     // deletes the agent, not the contents
0725     const Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(collection.resource());
0726     if (instance.isValid()) {
0727         Akonadi::AgentManager::self()->removeInstance(instance);
0728     }
0729 }
0730 
0731 void CalendarManager::editCollection(qint64 collectionId)
0732 { // TODO: Reimplement this dialog in QML
0733     auto collection = m_calendar->collection(collectionId);
0734     QPointer<Akonadi::CollectionPropertiesDialog> dlg = new Akonadi::CollectionPropertiesDialog(collection);
0735     dlg->setWindowTitle(i18nc("@title:window", "Properties of Calendar %1", collection.name()));
0736     dlg->show();
0737 }
0738 
0739 void CalendarManager::toggleCollection(qint64 collectionId)
0740 {
0741     const auto matches = m_calendar->checkableProxyModel()->match(m_calendar->checkableProxyModel()->index(0, 0),
0742                                                                   Akonadi::EntityTreeModel::CollectionIdRole,
0743                                                                   collectionId,
0744                                                                   1,
0745                                                                   Qt::MatchExactly | Qt::MatchWrap | Qt::MatchRecursive);
0746     if (!matches.isEmpty()) {
0747         const auto collectionIndex = matches.first();
0748         const auto collectionChecked = collectionIndex.data(Qt::CheckStateRole).toInt() == Qt::Checked;
0749         const auto checkStateToSet = collectionChecked ? Qt::Unchecked : Qt::Checked;
0750         m_calendar->checkableProxyModel()->setData(collectionIndex, checkStateToSet, Qt::CheckStateRole);
0751     }
0752 }
0753 
0754 IncidenceWrapper *CalendarManager::createIncidenceWrapper()
0755 {
0756     // Ownership is transfered to the qml engine
0757     return new IncidenceWrapper(this, nullptr);
0758 }
0759 
0760 #ifndef UNITY_CMAKE_SUPPORT
0761 Q_DECLARE_METATYPE(KCalendarCore::Incidence::Ptr)
0762 #endif
0763 
0764 #include "calendarmanager.moc"
0765 
0766 #include "moc_calendarmanager.cpp"