Akonadi Calendar

collectioncalendar.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "collectioncalendar.h"
8using namespace Qt::Literals::StringLiterals;
9
10#include "akonadicalendar_debug.h"
11#include "calendarbase_p.h"
12
13#include <Akonadi/CalendarUtils>
14#include <Akonadi/EntityTreeModel>
15#include <Akonadi/ItemFetchJob>
16#include <Akonadi/ItemFetchScope>
17#include <Akonadi/Monitor>
18
19#include <QAbstractProxyModel>
20
21using namespace KCalendarCore;
22
23namespace Akonadi
24{
25
26namespace
27{
28
29Akonadi::EntityTreeModel *findETM(QAbstractItemModel *model)
30{
31 while (model) {
32 if (auto etm = qobject_cast<Akonadi::EntityTreeModel *>(model); etm != nullptr) {
33 return etm;
34 }
35 if (auto proxy = qobject_cast<QAbstractProxyModel *>(model); proxy != nullptr) {
36 model = proxy->sourceModel();
37 } else {
38 break;
39 }
40 }
41
42 Q_ASSERT_X(false, "CollectionCalendar", "Model is not ETM or a proxy on top of an ETM!");
43 return nullptr;
44}
45
46}
47
48class CollectionCalendarPrivate : public CalendarBasePrivate
49{
50 Q_OBJECT
51public:
52 CollectionCalendarPrivate(QAbstractItemModel *model, CollectionCalendar *qq)
53 : CalendarBasePrivate(qq)
54 , m_model(model)
55 , m_etm(findETM(model))
56 , q(qq)
57 {
58 }
59
60 void setCollection(const Collection &col)
61 {
62 if (!col.isValid()) {
63 return;
64 }
65
66 Q_ASSERT(!m_collection.isValid());
67 m_collection = col;
68
69 if (m_monitor) {
70 m_monitor->setCollectionMonitored(m_collection);
71 }
72
73 init();
74 }
75
76 Collection m_collection;
77 QAbstractItemModel *m_model = nullptr;
78 Akonadi::EntityTreeModel *m_etm = nullptr;
79 Monitor *m_monitor = nullptr;
80 bool m_populatedFromEtm = false;
81
82 QHash<Item::Id, Item> m_itemById;
83
84private:
85 void init()
86 {
87 if (!m_collection.isValid()) {
88 return;
89 }
90
91 if (!m_model) {
92 m_model = createEtm();
93 }
94
95 connect(m_model, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
96 if (!isMatchingCollection(parent)) {
97 return;
98 }
99
100 handleRowsInsertedUnchecked(parent, first, last);
101 });
102 connect(m_model,
104 this,
105 [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) {
106 Q_UNUSED(newParent);
107 Q_UNUSED(row);
108 // If rows are about to be moved from collection we monitor, it's like removal from our point of view.
109 // Rows being moved into the collection we monitor is handled in rowsMoved signal handler.
110 if (!isMatchingCollection(parent)) {
111 return;
112 }
113
114 handleRowsRemovedUnchecked(parent, start, end);
115 });
116 connect(m_model, &QAbstractItemModel::rowsMoved, this, [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) {
117 Q_UNUSED(parent);
118 // If rows were moved into the collection we monitor, it's like they were added.
119 // Rows being moved from the collection we monitor is handled in rowsAboutToBeRemoved signal handler.
120 if (!isMatchingCollection(newParent)) {
121 return;
122 }
123
124 handleRowsInsertedUnchecked(newParent, row, row + (end - start));
125 });
126 connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
127 if (!isMatchingCollection(parent)) {
128 return;
129 }
130
131 handleRowsRemovedUnchecked(parent, first, last);
132 });
133 connect(m_model, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
134 if (!isMatchingCollection(topLeft.parent())) {
135 return;
136 }
137
138 auto index = topLeft;
139 for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
140 index = index.sibling(row, 0);
141 const auto item = m_model->data(index, EntityTreeModel::ItemRole).value<Item>();
142 if (item.isValid() || item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
143 updateItem(item);
144 }
145 }
146 });
147 connect(m_model, &QAbstractItemModel::modelReset, this, [this]() {
148 for (const auto &item : q->items()) {
149 internalRemove(item);
150 }
151 m_populatedFromEtm = false;
152 populateFromETM();
153 });
154 connect(m_model, &QAbstractItemModel::layoutChanged, this, &CollectionCalendarPrivate::populateFromETM);
155
156 populateFromETM();
157 }
158
159 void handleRowsRemovedUnchecked(const QModelIndex &parent, int first, int last)
160 {
161 for (int row = first; row <= last; ++row) {
162 const auto index = m_model->index(row, 0, parent);
163 const auto item = m_model->data(index, EntityTreeModel::ItemRole).value<Item>();
164 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
165 m_itemById.remove(item.id());
166 internalRemove(item);
167 }
168 }
169 }
170
171 void handleRowsInsertedUnchecked(const QModelIndex &parent, int first, int last)
172 {
173 for (int row = first; row <= last; ++row) {
174 const auto index = m_model->index(row, 0, parent);
175 const auto item = m_model->data(index, EntityTreeModel::ItemRole).value<Item>();
176 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
177 m_itemById.insert(item.id(), item);
178 internalInsert(item);
179 }
180 }
181 }
182
183 bool isMatchingCollection(const QModelIndex &index) const
184 {
185 const auto colId = m_model->data(index, EntityTreeModel::CollectionIdRole).toLongLong();
186 return colId == m_collection.id();
187 }
188
189 void updateItem(const Item &item)
190 {
191 Incidence::Ptr newIncidence = CalendarUtils::incidence(item);
192 newIncidence->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(item.id()));
193 IncidenceBase::Ptr existingIncidence = q->incidence(newIncidence->uid(), newIncidence->recurrenceId());
194
195 auto oldItem = m_itemById.value(item.id()); // if not found, seenItem will be invalid
196
197 if (!existingIncidence && !oldItem.isValid()) {
198 // We don't know about this one because it was discarded, for example because of not having DTSTART
199 return;
200 }
201
202 if (existingIncidence) {
203 // We set the payload so that the internal incidence pointer and the one in m_itemById stay the same
204 auto updatedItem = item;
205 updatedItem.setPayload(existingIncidence.staticCast<KCalendarCore::Incidence>());
206 m_itemById.insert(item.id(), updatedItem);
207
208 (*existingIncidence.data()) = *(newIncidence.data());
209 } else { // seenItem must be valid
210 m_itemById.insert(item.id(), item);
211 // The item changed it's UID, update our maps, the Google resource changes the UID when we create incidences.
212 handleUidChange(oldItem, item, newIncidence->instanceIdentifier());
213 }
214 }
215
216 void populateFromETM()
217 {
218 if (m_populatedFromEtm) {
219 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - already populated";
220 return;
221 }
222 m_populatedFromEtm = true;
223
224 if (!m_etm->isCollectionTreeFetched()) {
225 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - collection tree not fetched";
226 return;
227 }
228
229 if (!m_etm->isCollectionPopulated(m_collection.id())) {
230 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - target collection not populated yet";
231 return;
232 }
233
234 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar populating from ETM";
235
236 q->setIsLoading(true);
237 const auto colIdx = EntityTreeModel::modelIndexForCollection(m_model, m_collection);
238 Q_ASSERT(colIdx.isValid());
239 if (!colIdx.isValid()) {
240 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar failed to populate from ETM - couldn't find model index for our Collection"
241 << m_collection.id();
242 return;
243 }
244
245 q->startBatchAdding();
246 auto idx = m_model->index(0, 0, colIdx);
247 std::size_t itemCount = 0;
248 while (idx.isValid()) {
249 const auto item = m_model->data(idx, EntityTreeModel::ItemRole).value<Item>();
250 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
251 internalInsert(item);
252 ++itemCount;
253 }
254 idx = idx.siblingAtRow(idx.row() + 1);
255 }
256 q->endBatchAdding();
257
258 q->setIsLoading(false);
259
260 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar for Collection" << m_collection.id() << "populated from ETM with" << itemCount << "incidences";
261 }
262
263 EntityTreeModel *createEtm()
264 {
265 m_monitor = new Monitor(this);
266 m_monitor->setCollectionMonitored(m_collection);
267 m_monitor->itemFetchScope().fetchFullPayload();
268 m_monitor->itemFetchScope().setCacheOnly(true);
269 m_monitor->itemFetchScope().setAncestorRetrieval(ItemFetchScope::AncestorRetrieval::Parent);
270 for (const auto &mt : KCalendarCore::Incidence::mimeTypes()) {
271 m_monitor->setMimeTypeMonitored(mt, true);
272 }
273
274 return new EntityTreeModel(m_monitor, this);
275 }
276
277 CollectionCalendar *const q;
278};
279
280} // namespace Akonadi
281
282using namespace Akonadi;
283
284CollectionCalendar::CollectionCalendar(const Akonadi::Collection &col, QObject *parent)
285 : Akonadi::CalendarBase(new CollectionCalendarPrivate(nullptr, this), parent)
286{
287 setCollection(col);
288
289 incidenceChanger()->setDefaultCollection(col);
290 incidenceChanger()->setGroupwareCommunication(false);
291 incidenceChanger()->setDestinationPolicy(Akonadi::IncidenceChanger::DestinationPolicyNeverAsk);
292}
293
294CollectionCalendar::CollectionCalendar(QAbstractItemModel *model, const Akonadi::Collection &col, QObject *parent)
295 : Akonadi::CalendarBase(new CollectionCalendarPrivate(model, this), parent)
296{
297 setCollection(col);
298}
299
300CollectionCalendar::~CollectionCalendar() = default;
301
302Akonadi::Collection CollectionCalendar::collection() const
303{
304 Q_D(const CollectionCalendar);
305 return d->m_collection;
306}
307
308void CollectionCalendar::setCollection(const Akonadi::Collection &c)
309{
310 Q_D(CollectionCalendar);
311
312 if (c.id() == d->m_collection.id()) {
313 return;
314 }
315
316 Q_ASSERT(!d->m_collection.isValid());
317 if (d->m_collection.isValid()) {
318 qCWarning(AKONADICALENDAR_LOG) << "Cannot change collection of CollectionCalendar at runtime yet, sorry.";
319 return;
320 }
321
324 : KCalendarCore::ReadOnly);
325 d->setCollection(c);
326}
327
328Akonadi::EntityTreeModel *CollectionCalendar::model() const
329{
330 Q_D(const CollectionCalendar);
331 return d->m_etm;
332}
333
335{
336 Q_D(CollectionCalendar);
337
338 if (d->m_collection.contentMimeTypes().contains(event->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
340 }
341 return false;
342}
343
345{
346 Q_D(CollectionCalendar);
347
348 if (d->m_collection.contentMimeTypes().contains(todo->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
350 }
351 return false;
352}
353
355{
356 Q_D(CollectionCalendar);
357
358 if (d->m_collection.contentMimeTypes().contains(journal->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
360 }
361 return false;
362}
363
364bool CollectionCalendar::hasRight(Akonadi::Collection::Right right) const
365{
366 Q_D(const CollectionCalendar);
367 const auto fullCollection = Akonadi::EntityTreeModel::updatedCollection(d->m_model, d->m_collection);
368 Q_ASSERT(fullCollection.isValid());
369 if (!fullCollection.isValid()) {
370 return false;
371 }
372
373 return (fullCollection.rights() & right) == right;
374}
375
376#include "collectioncalendar.moc"
377#include "moc_collectioncalendar.cpp"
The base class for all akonadi aware calendars.
bool addEvent(const KCalendarCore::Event::Ptr &event) override
Adds an Event to the calendar.
bool addTodo(const KCalendarCore::Todo::Ptr &todo) override
Adds a Todo to the calendar.
bool addJournal(const KCalendarCore::Journal::Ptr &journal) override
Adds a Journal to the calendar.
Calendar representing a single Akonadi::Collection.
bool addJournal(const KCalendarCore::Journal::Ptr &journal) override
Adds a Journal to the calendar.
bool addEvent(const KCalendarCore::Event::Ptr &event) override
Adds an Event to the calendar.
bool addTodo(const KCalendarCore::Todo::Ptr &todo) override
Adds a Todo to the calendar.
bool isValid() const
Rights rights() const
static Collection updatedCollection(const QAbstractItemModel *model, qint64 collectionId)
static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
void setAccessMode(const AccessMode mode)
void setName(const QString &name)
QSharedPointer< Event > Ptr
QSharedPointer< IncidenceBase > Ptr
static QStringList mimeTypes()
QSharedPointer< Incidence > Ptr
QSharedPointer< Journal > Ptr
Todo::Ptr todo(const QString &uid, const QDateTime &recurrenceId={}) const override
Event::Ptr event(const QString &uid, const QDateTime &recurrenceId={}) const override
Journal::Ptr journal(const QString &uid, const QDateTime &recurrenceId={}) const override
QSharedPointer< Todo > Ptr
Q_SCRIPTABLE Q_NOREPLY void start()
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
Returns the incidence from an Akonadi item, or a null pointer if the item has no such payload.
AKONADI_CALENDAR_EXPORT QString displayName(Akonadi::ETMCalendar *calendar, const Akonadi::Collection &collection)
Returns a suitable display name for the calendar (or calendar folder) collection.
FreeBusyManager::Singleton.
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void rowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
QModelIndex parent() const const
int row() const const
T * data() const const
QSharedPointer< X > staticCast() const const
QString number(double n, char format, int precision)
QTextStream & right(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:58:31 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.