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()
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
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
QVariant data() const const
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 Fri Sep 27 2024 11:56:15 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.