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
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());
196
197 if (existingIncidence) {
198 auto updatedItem = item;
199 updatedItem.setPayload(existingIncidence.staticCast<KCalendarCore::Incidence>());
200 m_itemById.insert(item.id(), updatedItem);
201
202 (*existingIncidence.data()) = *(newIncidence.data());
203 } else {
204 m_itemById.insert(item.id(), item);
205 handleUidChange(oldItem, item, newIncidence->instanceIdentifier());
206 }
207 }
208
209 void populateFromETM()
210 {
211 if (m_populatedFromEtm) {
212 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - already populated";
213 return;
214 }
215 m_populatedFromEtm = true;
216
217 if (!m_etm->isCollectionTreeFetched()) {
218 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - collection tree not fetched";
219 return;
220 }
221
222 if (!m_etm->isCollectionPopulated(m_collection.id())) {
223 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - target collection not populated yet";
224 return;
225 }
226
227 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar populating from ETM";
228
229 q->setIsLoading(true);
230 const auto colIdx = EntityTreeModel::modelIndexForCollection(m_model, m_collection);
231 Q_ASSERT(colIdx.isValid());
232 if (!colIdx.isValid()) {
233 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar failed to populate from ETM - couldn't find model index for our Collection"
234 << m_collection.id();
235 return;
236 }
237
238 q->startBatchAdding();
239 auto idx = m_model->index(0, 0, colIdx);
240 std::size_t itemCount = 0;
241 while (idx.isValid()) {
242 const auto item = m_model->data(idx, EntityTreeModel::ItemRole).value<Item>();
243 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
244 internalInsert(item);
245 ++itemCount;
246 }
247 idx = idx.siblingAtRow(idx.row() + 1);
248 }
249 q->endBatchAdding();
250
251 q->setIsLoading(false);
252
253 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar for Collection" << m_collection.id() << "populated from ETM with" << itemCount << "incidences";
254 }
255
256 EntityTreeModel *createEtm()
257 {
258 m_monitor = new Monitor(this);
259 m_monitor->setCollectionMonitored(m_collection);
260 m_monitor->itemFetchScope().fetchFullPayload();
261 m_monitor->itemFetchScope().setCacheOnly(true);
262 m_monitor->itemFetchScope().setAncestorRetrieval(ItemFetchScope::AncestorRetrieval::Parent);
263 for (const auto &mt : KCalendarCore::Incidence::mimeTypes()) {
264 m_monitor->setMimeTypeMonitored(mt, true);
265 }
266
267 return new EntityTreeModel(m_monitor, this);
268 }
269
270 CollectionCalendar *const q;
271};
272
273} // namespace Akonadi
274
275using namespace Akonadi;
276
277CollectionCalendar::CollectionCalendar(const Akonadi::Collection &col, QObject *parent)
278 : Akonadi::CalendarBase(new CollectionCalendarPrivate(nullptr, this), parent)
279{
280 setCollection(col);
281
282 incidenceChanger()->setDefaultCollection(col);
283 incidenceChanger()->setGroupwareCommunication(false);
284 incidenceChanger()->setDestinationPolicy(Akonadi::IncidenceChanger::DestinationPolicyNeverAsk);
285}
286
287CollectionCalendar::CollectionCalendar(QAbstractItemModel *model, const Akonadi::Collection &col, QObject *parent)
288 : Akonadi::CalendarBase(new CollectionCalendarPrivate(model, this), parent)
289{
290 setCollection(col);
291}
292
293CollectionCalendar::~CollectionCalendar() = default;
294
295Akonadi::Collection CollectionCalendar::collection() const
296{
297 Q_D(const CollectionCalendar);
298 return d->m_collection;
299}
300
301void CollectionCalendar::setCollection(const Akonadi::Collection &c)
302{
304
305 if (c.id() == d->m_collection.id()) {
306 return;
307 }
308
309 Q_ASSERT(!d->m_collection.isValid());
310 if (d->m_collection.isValid()) {
311 qCWarning(AKONADICALENDAR_LOG) << "Cannot change collection of CollectionCalendar at runtime yet, sorry.";
312 return;
313 }
314
317 : KCalendarCore::ReadOnly);
318 d->setCollection(c);
319}
320
321Akonadi::EntityTreeModel *CollectionCalendar::model() const
322{
323 Q_D(const CollectionCalendar);
324 return d->m_etm;
325}
326
328{
330
331 if (d->m_collection.contentMimeTypes().contains(event->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
333 }
334 return false;
335}
336
338{
340
341 if (d->m_collection.contentMimeTypes().contains(todo->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
343 }
344 return false;
345}
346
348{
350
351 if (d->m_collection.contentMimeTypes().contains(journal->mimeType()) || d->m_collection.contentMimeTypes().contains("text/calendar"_L1)) {
353 }
354 return false;
355}
356
357bool CollectionCalendar::hasRight(Akonadi::Collection::Right right) const
358{
359 Q_D(const CollectionCalendar);
360 const auto fullCollection = Akonadi::EntityTreeModel::updatedCollection(d->m_model, d->m_collection);
361 Q_ASSERT(fullCollection.isValid());
362 if (!fullCollection.isValid()) {
363 return false;
364 }
365
366 return (fullCollection.rights() & right) == right;
367}
368
369#include "collectioncalendar.moc"
370#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.
void endBatchAdding() override
Tells the Calendar that you stopped adding a batch of incidences.
void startBatchAdding() override
Call this to tell the calendar that you're adding a batch of incidences.
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
bool isCollectionPopulated(Akonadi::Collection::Id) const
static Collection updatedCollection(const QAbstractItemModel *model, qint64 collectionId)
static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
bool isCollectionTreeFetched() const
void setCacheOnly(bool cacheOnly)
void setAncestorRetrieval(AncestorRetrieval ancestorDepth)
void fetchFullPayload(bool fetch=true)
void setMimeTypeMonitored(const QString &mimetype, bool monitored=true)
ItemFetchScope & itemFetchScope()
void setCollectionMonitored(const Collection &collection, bool monitored=true)
Incidence::Ptr incidence(const QString &uid, const QDateTime &recurrenceId={}) const
void setIsLoading(bool isLoading)
void setAccessMode(const AccessMode mode)
void setName(const QString &name)
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
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.
virtual QVariant data(const QModelIndex &index, int role) const const=0
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
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)
iterator insert(const Key &key, const T &value)
bool remove(const Key &key)
T value(const Key &key) const const
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)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
qlonglong toLongLong(bool *ok) const const
T value() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:48:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.