File indexing completed on 2024-05-12 05:10:43

0001 /*
0002     SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
0003     SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectioncalendar.h"
0008 #include "akonadicalendar_debug.h"
0009 #include "calendarbase_p.h"
0010 
0011 #include <Akonadi/CalendarUtils>
0012 #include <Akonadi/EntityTreeModel>
0013 #include <Akonadi/ItemFetchJob>
0014 #include <Akonadi/ItemFetchScope>
0015 #include <Akonadi/Monitor>
0016 
0017 using namespace KCalendarCore;
0018 
0019 namespace Akonadi
0020 {
0021 
0022 class CollectionCalendarPrivate : public CalendarBasePrivate
0023 {
0024     Q_OBJECT
0025 public:
0026     CollectionCalendarPrivate(EntityTreeModel *etm, CollectionCalendar *qq)
0027         : CalendarBasePrivate(qq)
0028         , m_etm(etm)
0029         , q(qq)
0030     {
0031     }
0032 
0033     void setCollection(const Collection &col)
0034     {
0035         if (!col.isValid()) {
0036             return;
0037         }
0038 
0039         Q_ASSERT(!m_collection.isValid());
0040         m_collection = col;
0041 
0042         if (m_monitor) {
0043             m_monitor->setCollectionMonitored(m_collection);
0044         }
0045 
0046         init();
0047     }
0048 
0049     Collection m_collection;
0050     EntityTreeModel *m_etm = nullptr;
0051     Monitor *m_monitor = nullptr;
0052     bool m_populatedFromEtm = false;
0053 
0054     QHash<Item::Id, Item> m_itemById;
0055 
0056 private:
0057     void init()
0058     {
0059         if (!m_collection.isValid()) {
0060             return;
0061         }
0062 
0063         if (!m_etm) {
0064             m_etm = createEtm();
0065         }
0066 
0067         connect(m_etm, &EntityTreeModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
0068             if (!isMatchingCollection(parent)) {
0069                 return;
0070             }
0071 
0072             handleRowsInsertedUnchecked(parent, first, last);
0073         });
0074         connect(m_etm,
0075                 &EntityTreeModel::rowsAboutToBeMoved,
0076                 this,
0077                 [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) {
0078                     Q_UNUSED(newParent);
0079                     Q_UNUSED(row);
0080                     // If rows are about to be moved from collection we monitor, it's like removal from our point of view.
0081                     // Rows being moved into the collection we monitor is handled in rowsMoved signal handler.
0082                     if (!isMatchingCollection(parent)) {
0083                         return;
0084                     }
0085 
0086                     handleRowsRemovedUnchecked(parent, start, end);
0087                 });
0088         connect(m_etm, &EntityTreeModel::rowsMoved, this, [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) {
0089             Q_UNUSED(parent);
0090             // If rows were moved into the collection we monitor, it's like they were added.
0091             // Rows being moved from the collection we monitor is handled in rowsAboutToBeRemoved signal handler.
0092             if (!isMatchingCollection(newParent)) {
0093                 return;
0094             }
0095 
0096             handleRowsInsertedUnchecked(newParent, row, row + (end - start));
0097         });
0098         connect(m_etm, &EntityTreeModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
0099             if (!isMatchingCollection(parent)) {
0100                 return;
0101             }
0102 
0103             handleRowsRemovedUnchecked(parent, first, last);
0104         });
0105         connect(m_etm, &EntityTreeModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
0106             if (!isMatchingCollection(topLeft.parent())) {
0107                 return;
0108             }
0109 
0110             auto index = topLeft;
0111             for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
0112                 index = index.sibling(row, 0);
0113                 const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>();
0114                 if (item.isValid() || item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0115                     updateItem(item);
0116                 }
0117             }
0118         });
0119         connect(m_etm, &EntityTreeModel::modelReset, this, [this]() {
0120             for (const auto &item : q->items()) {
0121                 internalRemove(item);
0122             }
0123             m_populatedFromEtm = false;
0124             populateFromETM();
0125         });
0126         connect(m_etm, &EntityTreeModel::layoutChanged, this, &CollectionCalendarPrivate::populateFromETM);
0127 
0128         populateFromETM();
0129     }
0130 
0131     void handleRowsRemovedUnchecked(const QModelIndex &parent, int first, int last)
0132     {
0133         for (int row = first; row <= last; ++row) {
0134             const auto index = m_etm->index(row, 0, parent);
0135             const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>();
0136             if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0137                 m_itemById.remove(item.id());
0138                 internalRemove(item);
0139             }
0140         }
0141     }
0142 
0143     void handleRowsInsertedUnchecked(const QModelIndex &parent, int first, int last)
0144     {
0145         for (int row = first; row <= last; ++row) {
0146             const auto index = m_etm->index(row, 0, parent);
0147             const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>();
0148             if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0149                 m_itemById.insert(item.id(), item);
0150                 internalInsert(item);
0151             }
0152         }
0153     }
0154 
0155     bool isMatchingCollection(const QModelIndex &index) const
0156     {
0157         const auto colId = m_etm->data(index, EntityTreeModel::CollectionIdRole).toLongLong();
0158         return colId == m_collection.id();
0159     }
0160 
0161     void updateItem(const Item &item)
0162     {
0163         Incidence::Ptr newIncidence = CalendarUtils::incidence(item);
0164         newIncidence->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(item.id()));
0165         IncidenceBase::Ptr existingIncidence = q->incidence(newIncidence->uid(), newIncidence->recurrenceId());
0166 
0167         auto oldItem = m_itemById.value(item.id());
0168 
0169         if (existingIncidence) {
0170             auto updatedItem = item;
0171             updatedItem.setPayload(existingIncidence.staticCast<KCalendarCore::Incidence>());
0172             m_itemById.insert(item.id(), updatedItem);
0173 
0174             (*existingIncidence.data()) = *(newIncidence.data());
0175         } else {
0176             m_itemById.insert(item.id(), item);
0177             handleUidChange(oldItem, item, newIncidence->instanceIdentifier());
0178         }
0179     }
0180 
0181     void populateFromETM()
0182     {
0183         if (m_populatedFromEtm) {
0184             qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - already populated";
0185             return;
0186         }
0187         m_populatedFromEtm = true;
0188 
0189         if (!m_etm->isCollectionTreeFetched()) {
0190             qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - collection tree not fetched";
0191             return;
0192         }
0193 
0194         if (!m_etm->isCollectionPopulated(m_collection.id())) {
0195             qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - target collection not populated yet";
0196             return;
0197         }
0198 
0199         qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar populating from ETM";
0200 
0201         q->setIsLoading(true);
0202         const auto colIdx = EntityTreeModel::modelIndexForCollection(m_etm, m_collection);
0203         Q_ASSERT(colIdx.isValid());
0204         if (!colIdx.isValid()) {
0205             qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar failed to populate from ETM - couldn't find model index for our Collection"
0206                                          << m_collection.id();
0207             return;
0208         }
0209 
0210         q->startBatchAdding();
0211         auto idx = m_etm->index(0, 0, colIdx);
0212         std::size_t itemCount = 0;
0213         while (idx.isValid()) {
0214             const auto item = m_etm->data(idx, EntityTreeModel::ItemRole).value<Item>();
0215             if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0216                 internalInsert(item);
0217                 ++itemCount;
0218             }
0219             idx = idx.siblingAtRow(idx.row() + 1);
0220         }
0221         q->endBatchAdding();
0222 
0223         q->setIsLoading(false);
0224 
0225         qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar for Collection" << m_collection.id() << "populated from ETM with" << itemCount << "incidences";
0226     }
0227 
0228     EntityTreeModel *createEtm()
0229     {
0230         m_monitor = new Monitor(this);
0231         m_monitor->setCollectionMonitored(m_collection);
0232         m_monitor->itemFetchScope().fetchFullPayload();
0233         m_monitor->itemFetchScope().setCacheOnly(true);
0234         m_monitor->itemFetchScope().setAncestorRetrieval(ItemFetchScope::AncestorRetrieval::Parent);
0235         for (const auto &mt : KCalendarCore::Incidence::mimeTypes()) {
0236             m_monitor->setMimeTypeMonitored(mt, true);
0237         }
0238 
0239         return new EntityTreeModel(m_monitor, this);
0240     }
0241 
0242     CollectionCalendar *const q;
0243 };
0244 
0245 } // namespace Akonadi
0246 
0247 using namespace Akonadi;
0248 
0249 CollectionCalendar::CollectionCalendar(const Akonadi::Collection &col, QObject *parent)
0250     : Akonadi::CalendarBase(new CollectionCalendarPrivate(nullptr, this), parent)
0251 {
0252     setCollection(col);
0253 
0254     incidenceChanger()->setDefaultCollection(col);
0255     incidenceChanger()->setGroupwareCommunication(false);
0256     incidenceChanger()->setDestinationPolicy(Akonadi::IncidenceChanger::DestinationPolicyNeverAsk);
0257 }
0258 
0259 CollectionCalendar::CollectionCalendar(Akonadi::EntityTreeModel *etm, const Akonadi::Collection &col, QObject *parent)
0260     : Akonadi::CalendarBase(new CollectionCalendarPrivate(etm, this), parent)
0261 {
0262     setCollection(col);
0263 }
0264 
0265 CollectionCalendar::~CollectionCalendar() = default;
0266 
0267 Akonadi::Collection CollectionCalendar::collection() const
0268 {
0269     Q_D(const CollectionCalendar);
0270     return d->m_collection;
0271 }
0272 
0273 void CollectionCalendar::setCollection(const Akonadi::Collection &c)
0274 {
0275     Q_D(CollectionCalendar);
0276 
0277     if (c.id() == d->m_collection.id()) {
0278         return;
0279     }
0280 
0281     Q_ASSERT(!d->m_collection.isValid());
0282     if (d->m_collection.isValid()) {
0283         qCWarning(AKONADICALENDAR_LOG) << "Cannot change collection of CollectionCalendar at runtime yet, sorry.";
0284         return;
0285     }
0286 
0287     setName(Akonadi::CalendarUtils::displayName(d->m_etm, c));
0288     setAccessMode((c.rights() & (Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanChangeItem)) ? KCalendarCore::ReadWrite
0289                                                                                                            : KCalendarCore::ReadOnly);
0290     d->setCollection(c);
0291 }
0292 
0293 Akonadi::EntityTreeModel *CollectionCalendar::model() const
0294 {
0295     Q_D(const CollectionCalendar);
0296     return d->m_etm;
0297 }
0298 
0299 bool CollectionCalendar::addEvent(const KCalendarCore::Event::Ptr &event)
0300 {
0301     Q_D(CollectionCalendar);
0302 
0303     if (d->m_collection.contentMimeTypes().contains(event->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) {
0304         return CalendarBase::addEvent(event);
0305     }
0306     return false;
0307 }
0308 
0309 bool CollectionCalendar::addTodo(const KCalendarCore::Todo::Ptr &todo)
0310 {
0311     Q_D(CollectionCalendar);
0312 
0313     if (d->m_collection.contentMimeTypes().contains(todo->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) {
0314         return CalendarBase::addTodo(todo);
0315     }
0316     return false;
0317 }
0318 
0319 bool CollectionCalendar::addJournal(const KCalendarCore::Journal::Ptr &journal)
0320 {
0321     Q_D(CollectionCalendar);
0322 
0323     if (d->m_collection.contentMimeTypes().contains(journal->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) {
0324         return CalendarBase::addJournal(journal);
0325     }
0326     return false;
0327 }
0328 
0329 bool CollectionCalendar::hasRight(Akonadi::Collection::Right right) const
0330 {
0331     Q_D(const CollectionCalendar);
0332     const auto fullCollection = Akonadi::EntityTreeModel::updatedCollection(d->m_etm, d->m_collection);
0333     Q_ASSERT(fullCollection.isValid());
0334     if (!fullCollection.isValid()) {
0335         return false;
0336     }
0337 
0338     return (fullCollection.rights() & right) == right;
0339 }
0340 
0341 #include "collectioncalendar.moc"
0342 #include "moc_collectioncalendar.cpp"