MauiKit Calendar

multidayincidencemodel.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// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
6// SPDX-License-Identifier: LGPL-2.0-or-later
7
8#include "multidayincidencemodel.h"
9//#include "kalendar_debug.h"
10#include <QBitArray>
11
12MultiDayIncidenceModel::MultiDayIncidenceModel(QObject *parent)
13 : QAbstractItemModel(parent)
14{
15 mRefreshTimer.setSingleShot(true);
16// m_config = KalendarConfig::self();
17// QObject::connect(m_config, &KalendarConfig::showSubtodosInCalendarViewsChanged, this, [&]() {
18// beginResetModel();
19// endResetModel();
20// });
21}
22
23QModelIndex MultiDayIncidenceModel::index(int row, int column, const QModelIndex &parent) const
24{
25 if (!hasIndex(row, column, parent)) {
26 return {};
27 }
28
29 if (!parent.isValid()) {
30 return createIndex(row, column);
31 }
32 return {};
33}
34
36{
37 return {};
38}
39
40int MultiDayIncidenceModel::rowCount(const QModelIndex &parent) const
41{
42 // Number of weeks
43 if (!parent.isValid() && mSourceModel) {
44 return qMax(mSourceModel->length() / mPeriodLength, 1);
45 }
46 return 0;
47}
48
49int MultiDayIncidenceModel::columnCount(const QModelIndex &) const
50{
51 return 1;
52}
53
54static long long getDuration(const QDate &start, const QDate &end)
55{
56 return qMax(start.daysTo(end) + 1, 1ll);
57}
58
59// We first sort all occurrences so we get all-day first (sorted by duration),
60// and then the rest sorted by start-date.
61QList<QModelIndex> MultiDayIncidenceModel::sortedIncidencesFromSourceModel(const QDate &rowStart) const
62{
63 // Don't add days if we are going for a daily period
64 const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
65 QList<QModelIndex> sorted;
66 sorted.reserve(mSourceModel->rowCount());
67 // Get incidences from source model
68 for (int row = 0; row < mSourceModel->rowCount(); row++) {
69 const auto srcIdx = mSourceModel->index(row, 0, {});
70 const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
71 const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
72
73 // Skip incidences not part of the week
74 if (end < rowStart || start > rowEnd) {
75 // qWarning() << "Skipping because not part of this week";
76 continue;
77 }
78
79 if (!incidencePassesFilter(srcIdx)) {
80 continue;
81 }
82
83 // qWarning() << "found " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
84 // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
85 sorted.append(srcIdx);
86 }
87
88 // Sort incidences by date
89 std::sort(sorted.begin(), sorted.end(), [&](const QModelIndex &left, const QModelIndex &right) {
90 // All-day first, sorted by duration (in the hope that we can fit multiple on the same line)
91 const auto leftAllDay = left.data(IncidenceOccurrenceModel::AllDay).toBool();
92 const auto rightAllDay = right.data(IncidenceOccurrenceModel::AllDay).toBool();
93
94 const auto leftDuration =
95 getDuration(left.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), left.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
96 const auto rightDuration =
97 getDuration(right.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), right.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
98
99 const auto leftDt = left.data(IncidenceOccurrenceModel::StartTime).toDateTime();
100 const auto rightDt = right.data(IncidenceOccurrenceModel::StartTime).toDateTime();
101
102 if (leftAllDay && !rightAllDay) {
103 return true;
104 }
105 if (!leftAllDay && rightAllDay) {
106 return false;
107 }
108 if (leftAllDay && rightAllDay) {
109 return leftDuration < rightDuration;
110 }
111
112 // The rest sorted by start date
113 return leftDt < rightDt && leftDuration <= rightDuration;
114 });
115
116 return sorted;
117}
118
119/*
120 * Layout the lines:
121 *
122 * The line grouping algorithm then always picks the first incidence,
123 * and tries to add more to the same line.
124 *
125 * We never mix all-day and non-all day, and otherwise try to fit as much as possible
126 * on the same line. Same day time-order should be preserved because of the sorting.
127 */
128QVariantList MultiDayIncidenceModel::layoutLines(const QDate &rowStart) const
129{
130 auto getStart = [&rowStart](const QDate &start) {
131 return qMax(rowStart.daysTo(start), 0ll);
132 };
133
134 QList<QModelIndex> sorted = sortedIncidencesFromSourceModel(rowStart);
135
136 // for (const auto &srcIdx : sorted) {
137 // qWarning() << "sorted " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
138 // srcIdx.data(IncidenceOccurrenceModel::Summary).toString()
139 // << srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
140 // }
141
142 auto result = QVariantList{};
143 while (!sorted.isEmpty()) {
144 const auto srcIdx = sorted.takeFirst();
145 const auto startDate = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
146 ? rowStart
147 : srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
148 const auto start = getStart(srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
149 const auto duration = qMin(getDuration(startDate, srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
150
151 // qWarning() << "First of line " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() << duration <<
152 // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
153 auto currentLine = QVariantList{};
154
155 auto addToLine = [&currentLine](const QModelIndex &idx, int start, int duration) {
156 currentLine.append(QVariantMap{
157 {QStringLiteral("text"), idx.data(IncidenceOccurrenceModel::Summary)},
158 {QStringLiteral("description"), idx.data(IncidenceOccurrenceModel::Description)},
159 {QStringLiteral("location"), idx.data(IncidenceOccurrenceModel::Location)},
160 {QStringLiteral("startTime"), idx.data(IncidenceOccurrenceModel::StartTime)},
161 {QStringLiteral("endTime"), idx.data(IncidenceOccurrenceModel::EndTime)},
162 {QStringLiteral("allDay"), idx.data(IncidenceOccurrenceModel::AllDay)},
163 {QStringLiteral("todoCompleted"), idx.data(IncidenceOccurrenceModel::TodoCompleted)},
164 {QStringLiteral("priority"), idx.data(IncidenceOccurrenceModel::Priority)},
165 {QStringLiteral("starts"), start},
166 {QStringLiteral("duration"), duration},
167 {QStringLiteral("durationString"), idx.data(IncidenceOccurrenceModel::DurationString)},
168 {QStringLiteral("recurs"), idx.data(IncidenceOccurrenceModel::Recurs)},
169 {QStringLiteral("hasReminders"), idx.data(IncidenceOccurrenceModel::HasReminders)},
170 {QStringLiteral("isOverdue"), idx.data(IncidenceOccurrenceModel::IsOverdue)},
171 {QStringLiteral("isReadOnly"), idx.data(IncidenceOccurrenceModel::IsReadOnly)},
172 {QStringLiteral("color"), idx.data(IncidenceOccurrenceModel::Color)},
173 {QStringLiteral("collectionId"), idx.data(IncidenceOccurrenceModel::CollectionId)},
174 {QStringLiteral("incidenceId"), idx.data(IncidenceOccurrenceModel::IncidenceId)},
175 {QStringLiteral("incidenceType"), idx.data(IncidenceOccurrenceModel::IncidenceType)},
176 {QStringLiteral("incidenceTypeStr"), idx.data(IncidenceOccurrenceModel::IncidenceTypeStr)},
177 {QStringLiteral("incidenceTypeIcon"), idx.data(IncidenceOccurrenceModel::IncidenceTypeIcon)},
178 {QStringLiteral("incidencePtr"), idx.data(IncidenceOccurrenceModel::IncidencePtr)},
179 {QStringLiteral("incidenceOccurrence"), idx.data(IncidenceOccurrenceModel::IncidenceOccurrence)},
180 });
181 };
182
183 if (start >= mPeriodLength) {
184 // qWarning() << "Skipping " << srcIdx.data(IncidenceOccurrenceModel::Summary);
185 continue;
186 }
187
188 // Add first incidence of line
189 addToLine(srcIdx, start, duration);
190 // const bool allDayLine = srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
191
192 // Fill line with incidences that fit
193 QBitArray takenSpaces(mPeriodLength);
194 // Set this incidence's space as taken
195 for (int i = start; i < start + duration; i++) {
196 takenSpaces[i] = true;
197 }
198
199 auto doesIntersect = [&](int start, int end) {
200 for (int i = start; i < end; i++) {
201 if (takenSpaces[i]) {
202 // qWarning() << "Found intersection " << start << end;
203 return true;
204 }
205 }
206
207 // If incidence fits on line, set its space as taken
208 for (int i = start; i < end; i++) {
209 takenSpaces[i] = true;
210 }
211 return false;
212 };
213
214 for (auto it = sorted.begin(); it != sorted.end();) {
215 const auto idx = *it;
216 const auto startDate = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
217 ? rowStart
218 : idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
219 const auto start = getStart(idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
220 const auto duration = qMin(getDuration(startDate, idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
221 const auto end = start + duration;
222
223 // This leaves a space in rows with all day events, making this y area of the row exclusively for all day events
224 /*if (allDayLine && !idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
225 continue;
226 }*/
227
228 if (doesIntersect(start, end)) {
229 it++;
230 } else {
231 addToLine(idx, start, duration);
232 it = sorted.erase(it);
233 }
234 }
235 // qWarning() << "Appending line " << currentLine;
236 result.append(QVariant::fromValue(currentLine));
237 }
238 return result;
239}
240
241QVariant MultiDayIncidenceModel::data(const QModelIndex &idx, int role) const
242{
243 if (!hasIndex(idx.row(), idx.column())) {
244 return {};
245 }
246 if (!mSourceModel) {
247 return {};
248 }
249 const auto rowStart = mSourceModel->start().addDays(idx.row() * mPeriodLength);
250 switch (role) {
251 case PeriodStartDate:
252 return rowStart.startOfDay();
253 case Incidences:
254 return layoutLines(rowStart);
255 default:
256 Q_ASSERT(false);
257 return {};
258 }
259}
260
261IncidenceOccurrenceModel *MultiDayIncidenceModel::model()
262{
263 return mSourceModel;
264}
265
266void MultiDayIncidenceModel::setModel(IncidenceOccurrenceModel *model)
267{
269
270 mSourceModel = model;
271 Q_EMIT modelChanged();
272 auto resetModel = [this] {
273 if (!mRefreshTimer.isActive()) {
276 Q_EMIT incidenceCountChanged();
277 mRefreshTimer.start(50);
278 }
279 };
280 QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel);
281 QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel);
282 QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel);
283 QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel);
284 QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel);
285 QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel);
287}
288
289int MultiDayIncidenceModel::periodLength()
290{
291 return mPeriodLength;
292}
293
294void MultiDayIncidenceModel::setPeriodLength(int periodLength)
295{
296 mPeriodLength = periodLength;
297}
298
299MultiDayIncidenceModel::Filters MultiDayIncidenceModel::filters()
300{
301 return m_filters;
302}
303
304void MultiDayIncidenceModel::setFilters(MultiDayIncidenceModel::Filters filters)
305{
307 m_filters = filters;
308 Q_EMIT filtersChanged();
310}
311
312bool MultiDayIncidenceModel::incidencePassesFilter(const QModelIndex &idx) const
313{
314// if (!m_filters && m_config->showSubtodosInCalendarViews()) {
315// return true;
316// }
317 bool include = false;
318
319 if (m_filters) {
320 const auto start = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
321
322 if (m_filters.testFlag(AllDayOnly) && idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
323 include = true;
324 }
325
326 if (m_filters.testFlag(NoStartDateOnly) && !start.isValid()) {
327 include = true;
328 }
329 if (m_filters.testFlag(MultiDayOnly) && idx.data(IncidenceOccurrenceModel::Duration).value<KCalendarCore::Duration>().asDays() >= 1) {
330 include = true;
331 }
332 }
333
334// if (!m_config->showSubtodosInCalendarViews()
335// && idx.data(IncidenceOccurrenceModel::IncidencePtr).value<KCalendarCore::Incidence::Ptr>()->relatedTo().isEmpty()) {
336// include = true;
337// }
338
339 return include;
340}
341
342int MultiDayIncidenceModel::incidenceCount()
343{
344 int count = 0;
345
346 for (int i = 0; i < rowCount({}); i++) {
347 const auto rowStart = mSourceModel->start().addDays(i * mPeriodLength);
348 const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
349
350 for (int row = 0; row < mSourceModel->rowCount(); row++) {
351 const auto srcIdx = mSourceModel->index(row, 0, {});
352 const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
353 const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
354
355 // Skip incidences not part of the week
356 if (end < rowStart || start > rowEnd) {
357 // qWarning() << "Skipping because not part of this week";
358 continue;
359 }
360
361 if (!incidencePassesFilter(srcIdx)) {
362 continue;
363 }
364
365 count++;
366 }
367 }
368
369 return count;
370}
371
372QHash<int, QByteArray> MultiDayIncidenceModel::roleNames() const
373{
374 return {
375 {Incidences, "incidences"},
376 {PeriodStartDate, "periodStartDate"},
377 };
378}
Loads all event occurrences within the given period and matching the given filter.
Q_SCRIPTABLE Q_NOREPLY void start()
const QList< QKeySequence > & end()
QModelIndex createIndex(int row, int column, const void *ptr) const const
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
bool hasIndex(int row, int column, const QModelIndex &parent) const const
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
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 daysTo(QDate d) const const
QDateTime startOfDay() const const
QDate date() const const
bool testFlag(Enum flag) const const
void append(QList< T > &&value)
iterator begin()
iterator end()
iterator erase(const_iterator begin, const_iterator end)
bool isEmpty() const const
void reserve(qsizetype size)
value_type takeFirst()
int column() const const
QVariant data(int role) const const
int row() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
bool isActive() const const
void start()
QVariant fromValue(T &&value)
bool toBool() const const
QDateTime toDateTime() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:15:37 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.