MauiKit Calendar

hourlyincidencemodel.cpp
1// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
2// SPDX-License-Identifier: LGPL-2.0-or-later
3
4#include "hourlyincidencemodel.h"
5#include <QTimeZone>
6#include <cmath>
7
8HourlyIncidenceModel::HourlyIncidenceModel(QObject *parent)
9 : QAbstractItemModel(parent)
10{
11 mRefreshTimer.setSingleShot(true);
12
13// m_config = KalendarConfig::self();
14// QObject::connect(m_config, &KalendarConfig::showSubtodosInCalendarViewsChanged, this, [&]() {
15// beginResetModel();
16// endResetModel();
17// });
18}
19
20QModelIndex HourlyIncidenceModel::index(int row, int column, const QModelIndex &parent) const
21{
22 if (!hasIndex(row, column, parent)) {
23 return {};
24 }
25
26 if (!parent.isValid()) {
27 return createIndex(row, column);
28 }
29 return {};
30}
31
33{
34 return {};
35}
36
37int HourlyIncidenceModel::rowCount(const QModelIndex &parent) const
38{
39 // Number of weeks
40 if (!parent.isValid() && mSourceModel) {
41 return qMax(mSourceModel->length(), 1);
42 }
43 return 0;
44}
45
46int HourlyIncidenceModel::columnCount(const QModelIndex &) const
47{
48 return 1;
49}
50
51static double getDuration(const QDateTime &start, const QDateTime &end, int periodLength)
52{
53 return ((start.secsTo(end) * 1.0) / 60.0) / periodLength;
54}
55
56// We first sort all occurrences so we get all-day first (sorted by duration),
57// and then the rest sorted by start-date.
58QList<QModelIndex> HourlyIncidenceModel::sortedIncidencesFromSourceModel(const QDateTime &rowStart) const
59{
60 // Don't add days if we are going for a daily period
61 const auto rowEnd = rowStart.date().endOfDay();
62 QList<QModelIndex> sorted;
63 sorted.reserve(mSourceModel->rowCount());
64 // Get incidences from source model
65 for (int row = 0; row < mSourceModel->rowCount(); row++) {
66 const auto srcIdx = mSourceModel->index(row, 0, {});
67 const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone());
68 const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone());
69
70 // Skip incidences not part of the week
71 if (end < rowStart || start > rowEnd) {
72 // qCWarning(KALENDAR_LOG) << "Skipping because not part of this week";
73 continue;
74 }
75
76 if (m_filters.testFlag(NoAllDay) && srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
77 continue;
78 }
79
80 if (m_filters.testFlag(NoMultiDay) && srcIdx.data(IncidenceOccurrenceModel::Duration).value<KCalendarCore::Duration>().asDays() >= 1) {
81 continue;
82 }
83
84// if (!m_config->showSubtodosInCalendarViews()
85// && !srcIdx.data(IncidenceOccurrenceModel::IncidencePtr).value<KCalendarCore::Incidence::Ptr>()->relatedTo().isEmpty()) {
86// continue;
87// }
88 // qCWarning(KALENDAR_LOG) << "found " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
89 // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
90 sorted.append(srcIdx);
91 }
92
93 // Sort incidences by date
94 std::sort(sorted.begin(), sorted.end(), [&](const QModelIndex &left, const QModelIndex &right) {
95 // All-day first
96 const auto leftAllDay = left.data(IncidenceOccurrenceModel::AllDay).toBool();
97 const auto rightAllDay = right.data(IncidenceOccurrenceModel::AllDay).toBool();
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
109 // The rest sorted by start date
110 return leftDt < rightDt;
111 });
112
113 return sorted;
114}
115
116/*
117 * Layout the lines:
118 *
119 * The line grouping algorithm then always picks the first incidence,
120 * and tries to add more to the same line.
121 *
122 */
123QVariantList HourlyIncidenceModel::layoutLines(const QDateTime &rowStart) const
124{
125 QList<QModelIndex> sorted = sortedIncidencesFromSourceModel(rowStart);
126 const auto rowEnd = rowStart.date().endOfDay();
127 const int periodsPerDay = (24 * 60) / mPeriodLength;
128
129 // for (const auto &srcIdx : sorted) {
130 // qCWarning(KALENDAR_LOG) << "sorted " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
131 // srcIdx.data(IncidenceOccurrenceModel::Summary).toString()
132 // << srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
133 // }
134 auto result = QVariantList{};
135
136 auto addToResults = [&result](const QModelIndex &idx, double start, double duration) {
137 auto incidenceMap = QVariantMap{
138 {QStringLiteral("text"), idx.data(IncidenceOccurrenceModel::Summary)},
139 {QStringLiteral("description"), idx.data(IncidenceOccurrenceModel::Description)},
140 {QStringLiteral("location"), idx.data(IncidenceOccurrenceModel::Location)},
141 {QStringLiteral("startTime"), idx.data(IncidenceOccurrenceModel::StartTime)},
142 {QStringLiteral("endTime"), idx.data(IncidenceOccurrenceModel::EndTime)},
143 {QStringLiteral("allDay"), idx.data(IncidenceOccurrenceModel::AllDay)},
144 {QStringLiteral("todoCompleted"), idx.data(IncidenceOccurrenceModel::TodoCompleted)},
145 {QStringLiteral("priority"), idx.data(IncidenceOccurrenceModel::Priority)},
146 {QStringLiteral("starts"), start},
147 {QStringLiteral("duration"), duration},
148 {QStringLiteral("durationString"), idx.data(IncidenceOccurrenceModel::DurationString)},
149 {QStringLiteral("recurs"), idx.data(IncidenceOccurrenceModel::Recurs)},
150 {QStringLiteral("hasReminders"), idx.data(IncidenceOccurrenceModel::HasReminders)},
151 {QStringLiteral("isOverdue"), idx.data(IncidenceOccurrenceModel::IsOverdue)},
152 {QStringLiteral("isReadOnly"), idx.data(IncidenceOccurrenceModel::IsReadOnly)},
153 {QStringLiteral("color"), idx.data(IncidenceOccurrenceModel::Color)},
154 {QStringLiteral("collectionId"), idx.data(IncidenceOccurrenceModel::CollectionId)},
155 {QStringLiteral("incidenceId"), idx.data(IncidenceOccurrenceModel::IncidenceId)},
156 {QStringLiteral("incidenceType"), idx.data(IncidenceOccurrenceModel::IncidenceType)},
157 {QStringLiteral("incidenceTypeStr"), idx.data(IncidenceOccurrenceModel::IncidenceTypeStr)},
158 {QStringLiteral("incidenceTypeIcon"), idx.data(IncidenceOccurrenceModel::IncidenceTypeIcon)},
159 {QStringLiteral("incidencePtr"), idx.data(IncidenceOccurrenceModel::IncidencePtr)},
160 {QStringLiteral("incidenceOccurrence"), idx.data(IncidenceOccurrenceModel::IncidenceOccurrence)},
161 };
162
163 result.append(incidenceMap);
164 };
165
166 // Since our hourly view displays by the minute, we need to know how many incidences there are in each minute.
167 // This hash's keys are the minute of the given day, as the view has accuracy down to the minute. Each value
168 // for each key is the number of incidences that occupy that minute's spot.
169 QHash<int, int> takenSpaces;
170 auto setTakenSpaces = [&](int start, int end) {
171 for (int i = start; i < end; i++) {
172 if (!takenSpaces.contains(i)) {
173 takenSpaces[i] = 1;
174 } else {
175 takenSpaces[i]++;
176 }
177 }
178 };
179
180 while (!sorted.isEmpty()) {
181 const auto idx = sorted.takeFirst();
182 const auto startDT = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone()) > rowStart
183 ? idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone())
184 : rowStart;
185 const auto endDT = idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone()) < rowEnd
186 ? idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone())
187 : rowEnd;
188 // Need to convert ints into doubles to get more accurate starting positions
189 // We get a start position relative to the number of period spaces there are in a day
190 const auto start = ((startDT.time().hour() * 1.0) * (60.0 / mPeriodLength)) + ((startDT.time().minute() * 1.0) / mPeriodLength);
191 auto duration = // Give a minimum acceptable height or otherwise have unclickable incidence
192 qMax(getDuration(startDT, idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().toTimeZone(QTimeZone::systemTimeZone()), mPeriodLength), 1.0);
193
194 // Make sure incidence doesn't extend past the end of the day
195 if (start + duration > periodsPerDay) {
196 duration = periodsPerDay - start;
197 }
198
199 const auto realEndMinutesFromDayStart = qMin((endDT.time().hour() * 60) + endDT.time().minute(), 24 * 60 * 60);
200 // Todos likely won't have end date
201 const auto startMinutesFromDayStart =
202 startDT.isValid() ? (startDT.time().hour() * 60) + startDT.time().minute() : qMax(realEndMinutesFromDayStart - mPeriodLength, 0);
203 const auto displayedEndMinutesFromDayStart = floor(startMinutesFromDayStart + (mPeriodLength * duration));
204
205 addToResults(idx, start, duration);
206 setTakenSpaces(startMinutesFromDayStart, displayedEndMinutesFromDayStart);
207 }
208
209 QHash<int, double> takenWidth; // We need this for potential movers
210 QHash<int, double> startX;
211 // Potential movers are incidences that are placed at first but might need to be moved later as more incidences get placed to
212 // the left of them. Rather than loop more than once over our incidences, we create a record of these and then deal with them
213 // later, storing the needed data in a struct.
214 struct PotentialMover {
215 QVariantMap incidenceMap;
216 int resultIterator;
217 int startMinutesFromDayStart;
218 int endMinutesFromDayStart;
219 };
220 QVector<PotentialMover> potentialMovers;
221
222 // Calculate the width and x position of each incidence rectangle
223 for (int i = 0; i < result.length(); i++) {
224 auto incidence = result[i].value<QVariantMap>();
225 int concurrentIncidences = 1;
226
227 const auto startDT = incidence[QLatin1String("startTime")].toDateTime().toTimeZone(QTimeZone::systemTimeZone()) > rowStart
228 ? incidence[QLatin1String("startTime")].toDateTime().toTimeZone(QTimeZone::systemTimeZone())
229 : rowStart;
230 const auto endDT = incidence[QLatin1String("endTime")].toDateTime().toTimeZone(QTimeZone::systemTimeZone()) < rowEnd
231 ? incidence[QLatin1String("endTime")].toDateTime().toTimeZone(QTimeZone::systemTimeZone())
232 : rowEnd;
233 const auto duration = incidence[QLatin1String("duration")].toDouble();
234
235 // We need a "real" and "displayed" end time for two reasons:
236 // 1. We need the real end minutes to give a fake start time to todos which do not have a start time
237 // 2. We need the displayed end minutes to be able to properly position those incidences which are displayed as longer
238 // than they actually are
239 const auto realEndMinutesFromDayStart = qMin((endDT.time().hour() * 60) + endDT.time().minute(), 24 * 60 * 60);
240 // Todos likely won't have end date
241 const auto startMinutesFromDayStart =
242 startDT.isValid() ? (startDT.time().hour() * 60) + startDT.time().minute() : qMax(realEndMinutesFromDayStart - mPeriodLength, 0);
243 const int displayedEndMinutesFromDayStart = floor(startMinutesFromDayStart + (mPeriodLength * duration));
244
245 // Get max number of incidences that happen at the same time as this
246 // (there can be different numbers of concurrent incidences during the time)
247 for (int i = startMinutesFromDayStart; i < displayedEndMinutesFromDayStart; i++) {
248 concurrentIncidences = qMax(concurrentIncidences, takenSpaces[i]);
249 }
250
251 incidence[QLatin1String("maxConcurrentIncidences")] = concurrentIncidences;
252 double widthShare = 1.0 / (concurrentIncidences * 1.0); // Width as a fraction of the whole day column width
253 incidence[QLatin1String("widthShare")] = widthShare;
254
255 // This is the value that the QML view will use to position the incidence rectangle on the day column's X axis.
256 double priorTakenWidthShare = 0.0;
257 // If we have empty space at the very left of the column we want to take advantage and place an incidence there
258 // even if there have been other incidences that take up space further to the right. For this we use minStartX,
259 // which gathers the lowest x starting position in a given minute; if this is higher than 0, it means that there
260 // is empty space at the left of the day column.
261 double minStartX = 1.0;
262
263 for (int i = startMinutesFromDayStart; i < displayedEndMinutesFromDayStart - 1; i++) {
264 // If this is the first incidence that has taken up this minute position, set details
265 if (!startX.contains(i)) {
266 takenWidth[i] = widthShare;
267 startX[i] = priorTakenWidthShare;
268 } else {
269 priorTakenWidthShare = qMax(priorTakenWidthShare, takenWidth[i]); // Get maximum prior space taken so we do not overlap with anything
270 minStartX = qMin(minStartX, startX[i]);
271
272 if (startX[i] > 0) {
273 takenWidth[i] = widthShare; // Reset as there is space available at the beginning of the column
274 } else {
275 takenWidth[i] += widthShare; // Increase the taken width at this minute position
276 }
277 }
278 }
279
280 if (minStartX > 0) {
281 priorTakenWidthShare = 0;
282 for (int i = startMinutesFromDayStart; i < displayedEndMinutesFromDayStart; i++) {
283 startX[i] = 0;
284 }
285 }
286
287 incidence[QLatin1String("priorTakenWidthShare")] = priorTakenWidthShare;
288
289 if (takenSpaces[startMinutesFromDayStart] < takenSpaces[displayedEndMinutesFromDayStart - 1] && priorTakenWidthShare > 0) {
290 potentialMovers.append(PotentialMover{incidence, i, startMinutesFromDayStart, displayedEndMinutesFromDayStart});
291 }
292
293 result[i] = incidence;
294 }
295
296 for (auto &potentialMover : potentialMovers) {
297 double maxTakenWidth = 0;
298 for (int i = potentialMover.startMinutesFromDayStart; i < potentialMover.endMinutesFromDayStart; i++) {
299 maxTakenWidth = qMax(maxTakenWidth, takenWidth[i]);
300 }
301
302 if (maxTakenWidth < 0.98) {
303 potentialMover.incidenceMap[QLatin1String("priorTakenWidthShare")] =
304 potentialMover.incidenceMap[QLatin1String("widthShare")].toDouble() * (takenSpaces[potentialMover.endMinutesFromDayStart - 1] - 1);
305
306 result[potentialMover.resultIterator] = potentialMover.incidenceMap;
307 }
308 }
309
310 return result;
311}
312
313QVariant HourlyIncidenceModel::data(const QModelIndex &idx, int role) const
314{
315 if (!hasIndex(idx.row(), idx.column())) {
316 return {};
317 }
318 if (!mSourceModel) {
319 return {};
320 }
321 const auto rowStart = mSourceModel->start().addDays(idx.row()).startOfDay();
322 switch (role) {
323 case PeriodStartDateTime:
324 return rowStart;
325 case Incidences:
326 return layoutLines(rowStart);
327 default:
328 Q_ASSERT(false);
329 return {};
330 }
331}
332
333IncidenceOccurrenceModel *HourlyIncidenceModel::model()
334{
335 return mSourceModel;
336}
337
338void HourlyIncidenceModel::setModel(IncidenceOccurrenceModel *model)
339{
341 mSourceModel = model;
342 auto resetModel = [this] {
343 if (!mRefreshTimer.isActive()) {
346 mRefreshTimer.start(50);
347 }
348 };
349 QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel);
350 QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel);
351 QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel);
352 QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel);
353 QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel);
354 QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel);
356}
357
358int HourlyIncidenceModel::periodLength()
359{
360 return mPeriodLength;
361}
362
363void HourlyIncidenceModel::setPeriodLength(int periodLength)
364{
365 mPeriodLength = periodLength;
366}
367
368HourlyIncidenceModel::Filters HourlyIncidenceModel::filters()
369{
370 return m_filters;
371}
372
373void HourlyIncidenceModel::setFilters(HourlyIncidenceModel::Filters filters)
374{
376 m_filters = filters;
377 Q_EMIT filtersChanged();
379}
380
381QHash<int, QByteArray> HourlyIncidenceModel::roleNames() const
382{
383 return {
384 {Incidences, "incidences"},
385 {PeriodStartDateTime, "periodStartDateTime"},
386 };
387}
Loads all event occurrences within the given period and matching the given filter.
Q_SCRIPTABLE Q_NOREPLY void start()
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
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
QDateTime endOfDay() const const
QDate date() const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
bool testFlag(Enum flag) const const
bool contains(const Key &key) const const
void append(QList< T > &&value)
iterator begin()
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()
QTimeZone systemTimeZone()
QDateTime toDateTime() 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.