MauiKit Calendar

incidenceoccurrencemodel.cpp
1// Copyright (c) 2018 Michael Bohlender <michael.bohlender@kdemail.net>
2// Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
3// Copyright (c) 2018 RĂ©mi Nicole <minijackson@riseup.net>
4// Copyright (c) 2021 Carl Schwan <carlschwan@kde.org>
5// Copyright (c) 2021 Claudio Cambra <claudio.cambra@gmail.com>
6// SPDX-License-Identifier: LGPL-2.0-or-later
7
8#include "incidenceoccurrencemodel.h"
9
10#include "../filter.h"
11#include <Akonadi/EntityTreeModel>
12#include <KCalendarCore/OccurrenceIterator>
13#include <KConfigGroup>
14#include <KLocalizedString>
15#include <KSharedConfig>
16#include <QMetaEnum>
17
18IncidenceOccurrenceModel::IncidenceOccurrenceModel(QObject *parent)
19 : QAbstractListModel(parent)
20 , m_coreCalendar(nullptr)
21{
22 m_resetThrottlingTimer.setSingleShot(true);
23 QObject::connect(&m_resetThrottlingTimer, &QTimer::timeout, this, &IncidenceOccurrenceModel::resetFromSource);
24
25 KSharedConfig::Ptr config = KSharedConfig::openConfig();
26 KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors"));
27 m_colorWatcher = KConfigWatcher::create(config);
28
29 // This is quite slow; would be nice to find a quicker way
30 connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &IncidenceOccurrenceModel::resetFromSource);
31}
32
33void IncidenceOccurrenceModel::setStart(const QDate &start)
34{
35 if(start == mStart) {
36 return;
37 }
38
39 mStart = start;
40 Q_EMIT startChanged();
41
42 mEnd = mStart.addDays(mLength);
43 scheduleReset();
44}
45
46QDate IncidenceOccurrenceModel::start() const
47{
48 return mStart;
49}
50
51void IncidenceOccurrenceModel::setLength(int length)
52{
53 if (mLength == length) {
54 return;
55 }
56 mLength = length;
57 Q_EMIT lengthChanged();
58
59 mEnd = mStart.addDays(mLength);
60 scheduleReset();
61}
62
63int IncidenceOccurrenceModel::length() const
64{
65 return mLength;
66}
67
68Filter *IncidenceOccurrenceModel::filter() const
69{
70 return mFilter;
71}
72
73void IncidenceOccurrenceModel::setFilter(Filter *filter)
74{
75 mFilter = filter;
76 Q_EMIT filterChanged();
77
78 scheduleReset();
79}
80
81bool IncidenceOccurrenceModel::loading() const
82{
83 return m_loading;
84}
85
86void IncidenceOccurrenceModel::setLoading(const bool loading)
87{
88 if(loading == m_loading) {
89 return;
90 }
91
92 m_loading = loading;
93 Q_EMIT loadingChanged();
94}
95
96int IncidenceOccurrenceModel::resetThrottleInterval() const
97{
98 return m_resetThrottleInterval;
99}
100
101void IncidenceOccurrenceModel::setResetThrottleInterval(const int resetThrottleInterval)
102{
103 if(resetThrottleInterval == m_resetThrottleInterval) {
104 return;
105 }
106
107 m_resetThrottleInterval = resetThrottleInterval;
108 Q_EMIT resetThrottleIntervalChanged();
109}
110
111void IncidenceOccurrenceModel::scheduleReset()
112{
113 if (!m_resetThrottlingTimer.isActive()) {
114 // Instant update, but then only refresh every interval at most.
115 m_resetThrottlingTimer.start(m_resetThrottleInterval);
116 }
117}
118
119void IncidenceOccurrenceModel::resetFromSource()
120{
121 if (!m_coreCalendar) {
122 qWarning() << "Not resetting IOC from source as no core calendar set.";
123 return;
124 }
125
126 setLoading(true);
127
128 if (m_resetThrottlingTimer.isActive() || m_coreCalendar->isLoading()) {
129 // If calendar is still loading then just schedule a refresh later
130 // If refresh timer already active this won't restart it
131 scheduleReset();
132 return;
133 }
134
135 loadColors();
136
138
139 m_incidences.clear();
140 m_occurrenceIndexHash.clear();
141
142 KCalendarCore::OccurrenceIterator occurrenceIterator(*m_coreCalendar, QDateTime(mStart, {0, 0, 0}), QDateTime(mEnd, {12, 59, 59}));
143
144 while (occurrenceIterator.hasNext()) {
145 occurrenceIterator.next();
146 const auto incidence = occurrenceIterator.incidence();
147
148 if(!incidencePassesFilter(incidence)) {
149 continue;
150 }
151
152 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence);
153 const auto start = occurrenceStartEnd.first;
154 const auto end = occurrenceStartEnd.second;
155 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid());
156 const Occurrence occurrence{
157 start,
158 end,
159 incidence,
160 getColor(incidence),
161 getCollectionId(incidence),
162 incidence->allDay(),
163 };
164
165 const auto indexRow = m_incidences.count();
166 m_incidences.append(occurrence);
167
168 const auto occurrenceIndex = index(indexRow);
169 const QPersistentModelIndex persistentIndex(occurrenceIndex);
170
171 m_occurrenceIndexHash.insert(occurrenceHashKey, persistentIndex);
172 }
173
175
176 setLoading(false);
177}
178
179void IncidenceOccurrenceModel::slotSourceDataChanged(const QModelIndex &upperLeft, const QModelIndex &bottomRight)
180{
181 if (!m_coreCalendar || !upperLeft.isValid() || !bottomRight.isValid() || m_resetThrottlingTimer.isActive()) {
182 return;
183 }
184
185 setLoading(true);
186
187 const auto startRow = upperLeft.row();
188 const auto endRow = bottomRight.row();
189
190 for (int i = startRow; i <= endRow; ++i) {
191 const auto sourceModelIndex = m_coreCalendar->model()->index(i, 0, upperLeft.parent());
192 const auto incidenceItem = sourceModelIndex.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
193
194 if(!incidenceItem.isValid() || !incidenceItem.hasPayload<KCalendarCore::Incidence::Ptr>()) {
195 continue;
196 }
197
198 const auto incidence = incidenceItem.payload<KCalendarCore::Incidence::Ptr>();
199 KCalendarCore::OccurrenceIterator occurrenceIterator{*m_coreCalendar, incidence, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}};
200
201 while (occurrenceIterator.hasNext()) {
202 occurrenceIterator.next();
203
204 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence);
205 const auto start = occurrenceStartEnd.first;
206 const auto end = occurrenceStartEnd.second;
207 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid());
208
209 if(!m_occurrenceIndexHash.contains(occurrenceHashKey)) {
210 continue;
211 }
212
213 const Occurrence occurrence{
214 start,
215 end,
216 incidence,
217 getColor(incidence),
218 getCollectionId(incidence),
219 incidence->allDay(),
220 };
221
222 const auto existingOccurrenceIndex = m_occurrenceIndexHash.value(occurrenceHashKey);
223 const auto existingOccurrenceRow = existingOccurrenceIndex.row();
224
225 m_incidences.replace(existingOccurrenceRow, occurrence);
226 Q_EMIT dataChanged(existingOccurrenceIndex, existingOccurrenceIndex);
227 }
228 }
229
230 setLoading(false);
231}
232
233void IncidenceOccurrenceModel::slotSourceRowsInserted(const QModelIndex &parent, const int first, const int last)
234{
235 if (!m_coreCalendar || m_resetThrottlingTimer.isActive()) {
236 return;
237 } else if (m_coreCalendar->isLoading()) {
238 m_resetThrottlingTimer.start(m_resetThrottleInterval);
239 return;
240 }
241
242 setLoading(true);
243
244 for (int i = first; i <= last; ++i) {
245 const auto sourceModelIndex = m_coreCalendar->model()->index(i, 0, parent);
246 const auto incidenceItem = sourceModelIndex.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
247
248 if(!incidenceItem.isValid() || !incidenceItem.hasPayload<KCalendarCore::Incidence::Ptr>()) {
249 continue;
250 }
251
252 const auto incidence = incidenceItem.payload<KCalendarCore::Incidence::Ptr>();
253
254 if(!incidencePassesFilter(incidence)) {
255 continue;
256 }
257
258 KCalendarCore::OccurrenceIterator occurrenceIterator{*m_coreCalendar, incidence, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}};
259
260 while (occurrenceIterator.hasNext()) {
261 occurrenceIterator.next();
262
263 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence);
264 const auto start = occurrenceStartEnd.first;
265 const auto end = occurrenceStartEnd.second;
266 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid());
267
268 if(m_occurrenceIndexHash.contains(occurrenceHashKey)) {
269 continue;
270 }
271
272 const Occurrence occurrence{
273 start,
274 end,
275 incidence,
276 getColor(incidence),
277 getCollectionId(incidence),
278 incidence->allDay(),
279 };
280
281 const auto indexRow = m_incidences.count();
282
283 beginInsertRows({}, indexRow, indexRow);
284 m_incidences.append(occurrence);
286
287 const auto occurrenceIndex = index(indexRow);
288 const QPersistentModelIndex persistentIndex(occurrenceIndex);
289
290 m_occurrenceIndexHash.insert(occurrenceHashKey, persistentIndex);
291 }
292 }
293
294 setLoading(false);
295}
296
297int IncidenceOccurrenceModel::rowCount(const QModelIndex &parent) const
298{
299 if (!parent.isValid()) {
300 return m_incidences.size();
301 }
302 return 0;
303}
304
305qint64 IncidenceOccurrenceModel::getCollectionId(const KCalendarCore::Incidence::Ptr &incidence)
306{
307 auto item = m_coreCalendar->item(incidence);
308 if (!item.isValid()) {
309 return {};
310 }
311 auto collection = item.parentCollection();
312 if (!collection.isValid()) {
313 return {};
314 }
315 return collection.id();
316}
317
318QColor IncidenceOccurrenceModel::getColor(const KCalendarCore::Incidence::Ptr &incidence)
319{
320 auto item = m_coreCalendar->item(incidence);
321 if (!item.isValid()) {
322 return {};
323 }
324 auto collection = item.parentCollection();
325 if (!collection.isValid()) {
326 return {};
327 }
328 const QString id = QString::number(collection.id());
329 // qDebug() << "Collection id: " << collection.id();
330
331 if (m_colors.contains(id)) {
332 // qDebug() << collection.id() << "Found in m_colors";
333 return m_colors[id];
334 }
335
336 return {};
337}
338
339QVariant IncidenceOccurrenceModel::data(const QModelIndex &idx, int role) const
340{
341 if (!hasIndex(idx.row(), idx.column())) {
342 return {};
343 }
344
345 const auto occurrence = m_incidences.at(idx.row());
346 const auto incidence = occurrence.incidence;
347
348 switch (role) {
349 case Summary:
350 return incidence->summary();
351 case Description:
352 return incidence->description();
353 case Location:
354 return incidence->location();
355 case StartTime:
356 return occurrence.start;
357 case EndTime:
358 return occurrence.end;
359 case Duration:
360 {
361 const KCalendarCore::Duration duration(occurrence.start, occurrence.end);
362 return QVariant::fromValue(duration);
363 }
364 case DurationString: {
365 const KCalendarCore::Duration duration(occurrence.start, occurrence.end);
366
367 if (duration.asSeconds() == 0) {
368 return QString();
369 }
370
371 return m_format.formatSpelloutDuration(duration.asSeconds() * 1000);
372 }
373 case Recurs:
374 return incidence->recurs();
375 case HasReminders:
376 return incidence->alarms().length() > 0;
377 case Priority:
378 return incidence->priority();
379 case Color:
380 return occurrence.color;
381 case CollectionId:
382 return occurrence.collectionId;
383 case AllDay:
384 return occurrence.allDay;
385 case TodoCompleted: {
387 return false;
388 }
389
391 return todo->isCompleted();
392 }
393 case IsOverdue: {
395 return false;
396 }
397
399 return todo->isOverdue();
400 }
401 case IsReadOnly: {
402 const auto collection = m_coreCalendar->collection(occurrence.collectionId);
403 return collection.rights().testFlag(Akonadi::Collection::ReadOnly);
404 }
405 case IncidenceId:
406 return incidence->uid();
407 case IncidenceType:
408 return incidence->type();
409 case IncidenceTypeStr:
410 return incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(incidence->typeStr().constData());
411 case IncidenceTypeIcon:
412 return incidence->iconName();
413 case IncidencePtr:
414 return QVariant::fromValue(incidence);
415 case IncidenceOccurrence:
416 return QVariant::fromValue(occurrence);
417 default:
418 qWarning(
419
420 ) << "Unknown role for occurrence:" << QMetaEnum::fromType<Roles>().valueToKey(role);
421 return {};
422 }
423}
424
425void IncidenceOccurrenceModel::setCalendar(Akonadi::ETMCalendar::Ptr calendar)
426{
427 if (m_coreCalendar == calendar) {
428 return;
429 }
430 m_coreCalendar = calendar;
431
432 connect(m_coreCalendar->model(), &QAbstractItemModel::dataChanged, this, &IncidenceOccurrenceModel::slotSourceDataChanged);
433 connect(m_coreCalendar->model(), &QAbstractItemModel::rowsInserted, this, &IncidenceOccurrenceModel::slotSourceRowsInserted);
434 connect(m_coreCalendar->model(), &QAbstractItemModel::rowsRemoved, this, &IncidenceOccurrenceModel::scheduleReset);
435 connect(m_coreCalendar->model(), &QAbstractItemModel::modelReset, this, &IncidenceOccurrenceModel::scheduleReset);
436 connect(m_coreCalendar.get(), &Akonadi::ETMCalendar::collectionsRemoved, this, &IncidenceOccurrenceModel::scheduleReset);
437
438 Q_EMIT calendarChanged();
439
440 scheduleReset();
441}
442
443Akonadi::ETMCalendar::Ptr IncidenceOccurrenceModel::calendar() const
444{
445 return m_coreCalendar;
446}
447
448void IncidenceOccurrenceModel::loadColors()
449{
450 KSharedConfig::Ptr config = KSharedConfig::openConfig();
451 KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors"));
452 const QStringList colorKeyList = rColorsConfig.keyList();
453
454 for (const QString &key : colorKeyList) {
455 QColor color = rColorsConfig.readEntry(key, QColor("blue"));
456 m_colors[key] = color;
457 }
458}
459
460std::pair<QDateTime, QDateTime> IncidenceOccurrenceModel::incidenceOccurrenceStartEnd(const QDateTime &ocStart, const KCalendarCore::Incidence::Ptr &incidence)
461{
462 auto start = ocStart;
463 const auto end = incidence->endDateForStart(start);
464
465 if (incidence->type() == KCalendarCore::Incidence::IncidenceType::TypeTodo) {
467
468 if (!start.isValid()) { // Todos are very likely not to have a set start date
469 start = todo->dtDue();
470 }
471 }
472
473 return {start, end};
474}
475
476uint IncidenceOccurrenceModel::incidenceOccurrenceHash(const QDateTime &ocStart, const QDateTime &ocEnd, const QString &incidenceUid)
477{
478 return qHash(QString::number(ocStart.toSecsSinceEpoch()) +
480 incidenceUid);
481}
482
483bool IncidenceOccurrenceModel::incidencePassesFilter(const KCalendarCore::Incidence::Ptr &incidence)
484{
485 if(!mFilter || mFilter->tags().empty()) {
486 return true;
487 }
488
489 auto match = false;
490 const auto tags = mFilter->tags();
491 for (const auto &tag : tags) {
492 if (incidence->categories().contains(tag)) {
493 match = true;
494 break;
495 }
496 }
497
498 return match;
499}
500
501QHash<int, QByteArray> IncidenceOccurrenceModel::roleNames() const
502{
503 return {
504 {IncidenceOccurrenceModel::Summary, "summary"},
505 {IncidenceOccurrenceModel::StartTime, "startTime"},
506 };
507}
void collectionsRemoved(const Akonadi::Collection::List &collection)
Incidence::Ptr incidence() const
QDateTime occurrenceStartDate() const
static Ptr create(const KSharedConfig::Ptr &config)
void configChanged(const KConfigGroup &group, const QByteArrayList &names)
QString formatSpelloutDuration(quint64 msecs) const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
Q_SCRIPTABLE QString start(QString train="")
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Todo::Ptr todo(const Akonadi::Item &item)
KCALENDARCORE_EXPORT size_t qHash(const KCalendarCore::Period &key, size_t seed=0)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
void beginInsertRows(const QModelIndex &parent, int first, int last)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
bool hasIndex(int row, int column, const QModelIndex &parent) const const
virtual QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const const
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
QDate addDays(qint64 ndays) const const
qint64 toSecsSinceEpoch() const const
void clear()
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
bool empty() const const
QMetaEnum fromType()
const char * valueToKey(int value) const const
int column() const const
bool isValid() const const
QModelIndex parent() const const
int row() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
T * get() const const
QSharedPointer< X > staticCast() const const
QString first(qsizetype n) const const
QString number(double n, char format, int precision)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isActive() const const
void start()
void timeout()
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:35:11 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.