File indexing completed on 2024-11-24 04:50:37

0001 // Copyright (c) 2018 Michael Bohlender <michael.bohlender@kdemail.net>
0002 // Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
0003 // Copyright (c) 2018 RĂ©mi Nicole <minijackson@riseup.net>
0004 // Copyright (c) 2021 Carl Schwan <carlschwan@kde.org>
0005 // SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
0006 // SPDX-License-Identifier: LGPL-2.0-or-later
0007 
0008 #include "multidayincidencemodel.h"
0009 #include <QBitArray>
0010 
0011 using namespace std::chrono_literals;
0012 
0013 MultiDayIncidenceModel::MultiDayIncidenceModel(QObject *parent)
0014     : QAbstractListModel(parent)
0015 {
0016     m_refreshTimer.setSingleShot(true);
0017     m_refreshTimer.setInterval(m_active ? 200ms : 1000ms);
0018     m_refreshTimer.callOnTimeout(this, [this]() {
0019         Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
0020     });
0021 }
0022 
0023 void MultiDayIncidenceModel::classBegin()
0024 {
0025 }
0026 
0027 void MultiDayIncidenceModel::componentComplete()
0028 {
0029     beginResetModel();
0030     m_initialized = true;
0031     endResetModel();
0032 }
0033 
0034 void MultiDayIncidenceModel::scheduleReset()
0035 {
0036     if (!m_refreshTimer.isActive()) {
0037         m_refreshTimer.start();
0038     }
0039 }
0040 
0041 int MultiDayIncidenceModel::rowCount(const QModelIndex &parent) const
0042 {
0043     if (parent.isValid() || !mSourceModel || !m_initialized) {
0044         return 0;
0045     }
0046 
0047     // Number of weeks
0048     return qMax(mSourceModel->length() / mPeriodLength, 1);
0049 }
0050 
0051 static long long getDuration(const QDate &start, const QDate &end)
0052 {
0053     return qMax(start.daysTo(end) + 1, 1ll);
0054 }
0055 
0056 // We first sort all occurrences so we get all-day first (sorted by duration),
0057 // and then the rest sorted by start-date.
0058 QList<QModelIndex> MultiDayIncidenceModel::sortedIncidencesFromSourceModel(const QDate &rowStart) const
0059 {
0060     // Don't add days if we are going for a daily period
0061     const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
0062     QList<QModelIndex> sorted;
0063     sorted.reserve(mSourceModel->rowCount());
0064     // Get incidences from source model
0065     for (int row = 0; row < mSourceModel->rowCount(); row++) {
0066         const auto srcIdx = mSourceModel->index(row, 0, {});
0067         const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0068         const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
0069 
0070         // Skip incidences not part of the week
0071         if (end < rowStart || start > rowEnd) {
0072             // qCWarning(MERKURO_CALENDAR_LOG) << "Skipping because not part of this week";
0073             continue;
0074         }
0075 
0076         if (!incidencePassesFilter(srcIdx)) {
0077             continue;
0078         }
0079 
0080         // qCWarning(MERKURO_CALENDAR_LOG) << "found " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
0081         // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
0082         sorted.append(srcIdx);
0083     }
0084 
0085     // Sort incidences by date
0086     std::sort(sorted.begin(), sorted.end(), [&](const QModelIndex &left, const QModelIndex &right) {
0087         // All-day first, sorted by duration (in the hope that we can fit multiple on the same line)
0088         const auto leftAllDay = left.data(IncidenceOccurrenceModel::AllDay).toBool();
0089         const auto rightAllDay = right.data(IncidenceOccurrenceModel::AllDay).toBool();
0090 
0091         const auto leftDuration =
0092             getDuration(left.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), left.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
0093         const auto rightDuration =
0094             getDuration(right.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), right.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
0095 
0096         const auto leftDt = left.data(IncidenceOccurrenceModel::StartTime).toDateTime();
0097         const auto rightDt = right.data(IncidenceOccurrenceModel::StartTime).toDateTime();
0098 
0099         if (leftAllDay && !rightAllDay) {
0100             return true;
0101         }
0102         if (!leftAllDay && rightAllDay) {
0103             return false;
0104         }
0105         if (leftAllDay && rightAllDay) {
0106             return leftDuration < rightDuration;
0107         }
0108 
0109         // The rest sorted by start date
0110         return leftDt < rightDt && leftDuration <= rightDuration;
0111     });
0112 
0113     return sorted;
0114 }
0115 
0116 /*
0117  * Layout the lines:
0118  *
0119  * The line grouping algorithm then always picks the first incidence,
0120  * and tries to add more to the same line.
0121  *
0122  * We never mix all-day and non-all day, and otherwise try to fit as much as possible
0123  * on the same line. Same day time-order should be preserved because of the sorting.
0124  */
0125 QVariantList MultiDayIncidenceModel::layoutLines(const QDate &rowStart) const
0126 {
0127     auto getStart = [&rowStart](const QDate &start) {
0128         return qMax(rowStart.daysTo(start), 0ll);
0129     };
0130 
0131     QList<QModelIndex> sorted = sortedIncidencesFromSourceModel(rowStart);
0132 
0133     // for (const auto &srcIdx : sorted) {
0134     //     qCWarning(MERKURO_CALENDAR_LOG) << "sorted " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
0135     //     srcIdx.data(IncidenceOccurrenceModel::Summary).toString()
0136     //     << srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
0137     // }
0138 
0139     QVariantList result;
0140     while (!sorted.isEmpty()) {
0141         const auto srcIdx = sorted.takeFirst();
0142         const auto startDate = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
0143             ? rowStart
0144             : srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0145         const auto start = getStart(srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
0146         const auto duration = qMin(getDuration(startDate, srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0147 
0148         // qCWarning(MERKURO_CALENDAR_LOG) << "First of line " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() << duration <<
0149         // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
0150         QVariantList currentLine;
0151 
0152         auto addToLine = [&currentLine](const QModelIndex &idx, int start, int duration) {
0153             currentLine.append(QVariantMap{
0154                 {QStringLiteral("text"), idx.data(IncidenceOccurrenceModel::Summary)},
0155                 {QStringLiteral("description"), idx.data(IncidenceOccurrenceModel::Description)},
0156                 {QStringLiteral("location"), idx.data(IncidenceOccurrenceModel::Location)},
0157                 {QStringLiteral("startTime"), idx.data(IncidenceOccurrenceModel::StartTime)},
0158                 {QStringLiteral("endTime"), idx.data(IncidenceOccurrenceModel::EndTime)},
0159                 {QStringLiteral("allDay"), idx.data(IncidenceOccurrenceModel::AllDay)},
0160                 {QStringLiteral("todoCompleted"), idx.data(IncidenceOccurrenceModel::TodoCompleted)},
0161                 {QStringLiteral("priority"), idx.data(IncidenceOccurrenceModel::Priority)},
0162                 {QStringLiteral("starts"), start},
0163                 {QStringLiteral("duration"), duration},
0164                 {QStringLiteral("durationString"), idx.data(IncidenceOccurrenceModel::DurationString)},
0165                 {QStringLiteral("recurs"), idx.data(IncidenceOccurrenceModel::Recurs)},
0166                 {QStringLiteral("hasReminders"), idx.data(IncidenceOccurrenceModel::HasReminders)},
0167                 {QStringLiteral("isOverdue"), idx.data(IncidenceOccurrenceModel::IsOverdue)},
0168                 {QStringLiteral("isReadOnly"), idx.data(IncidenceOccurrenceModel::IsReadOnly)},
0169                 {QStringLiteral("color"), idx.data(IncidenceOccurrenceModel::Color)},
0170                 {QStringLiteral("collectionId"), idx.data(IncidenceOccurrenceModel::CollectionId)},
0171                 {QStringLiteral("incidenceId"), idx.data(IncidenceOccurrenceModel::IncidenceId)},
0172                 {QStringLiteral("incidenceType"), idx.data(IncidenceOccurrenceModel::IncidenceType)},
0173                 {QStringLiteral("incidenceTypeStr"), idx.data(IncidenceOccurrenceModel::IncidenceTypeStr)},
0174                 {QStringLiteral("incidenceTypeIcon"), idx.data(IncidenceOccurrenceModel::IncidenceTypeIcon)},
0175                 {QStringLiteral("incidencePtr"), idx.data(IncidenceOccurrenceModel::IncidencePtr)},
0176                 {QStringLiteral("incidenceOccurrence"), idx.data(IncidenceOccurrenceModel::IncidenceOccurrence)},
0177             });
0178         };
0179 
0180         if (start >= mPeriodLength) {
0181             // qCWarning(MERKURO_CALENDAR_LOG) << "Skipping " << srcIdx.data(IncidenceOccurrenceModel::Summary);
0182             continue;
0183         }
0184 
0185         // Add first incidence of line
0186         addToLine(srcIdx, start, duration);
0187         // const bool allDayLine = srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
0188 
0189         // Fill line with incidences that fit
0190         QBitArray takenSpaces(mPeriodLength);
0191         // Set this incidence's space as taken
0192         for (int i = start; i < start + duration; i++) {
0193             takenSpaces[i] = true;
0194         }
0195 
0196         auto doesIntersect = [&](int start, int end) {
0197             for (int i = start; i < end; i++) {
0198                 if (takenSpaces[i]) {
0199                     // qCWarning(MERKURO_CALENDAR_LOG) << "Found intersection " << start << end;
0200                     return true;
0201                 }
0202             }
0203 
0204             // If incidence fits on line, set its space as taken
0205             for (int i = start; i < end; i++) {
0206                 takenSpaces[i] = true;
0207             }
0208             return false;
0209         };
0210 
0211         for (auto it = sorted.begin(); it != sorted.end();) {
0212             const auto idx = *it;
0213             const auto startDate = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
0214                 ? rowStart
0215                 : idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0216             const auto start = getStart(idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
0217             const auto duration = qMin(getDuration(startDate, idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0218             const auto end = start + duration;
0219 
0220             // This leaves a space in rows with all day events, making this y area of the row exclusively for all day events
0221             /*if (allDayLine && !idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
0222                 continue;
0223             }*/
0224 
0225             if (doesIntersect(start, end)) {
0226                 it++;
0227             } else {
0228                 addToLine(idx, start, duration);
0229                 it = sorted.erase(it);
0230             }
0231         }
0232         // qCWarning(MERKURO_CALENDAR_LOG) << "Appending line " << currentLine;
0233         result.append(QVariant::fromValue(currentLine));
0234     }
0235     return result;
0236 }
0237 
0238 QVariant MultiDayIncidenceModel::data(const QModelIndex &index, int role) const
0239 {
0240     Q_ASSERT(hasIndex(index.row(), index.column()) && mSourceModel);
0241 
0242     const auto rowStart = mSourceModel->start().addDays(index.row() * mPeriodLength);
0243 
0244     switch (role) {
0245     case PeriodStartDateRole:
0246         return rowStart.startOfDay();
0247     case IncidencesRole:
0248         return layoutLines(rowStart);
0249     default:
0250         return {};
0251     }
0252 }
0253 
0254 IncidenceOccurrenceModel *MultiDayIncidenceModel::model() const
0255 {
0256     return mSourceModel;
0257 }
0258 
0259 void MultiDayIncidenceModel::setModel(IncidenceOccurrenceModel *model)
0260 {
0261     beginResetModel();
0262     mSourceModel = model;
0263     Q_EMIT modelChanged();
0264     endResetModel();
0265 
0266     auto resetModel = [this] {
0267         if (!m_refreshTimer.isActive()) {
0268             m_refreshTimer.start();
0269         }
0270     };
0271 
0272     connect(model, &QAbstractItemModel::dataChanged, this, &MultiDayIncidenceModel::slotSourceDataChanged);
0273     connect(model, &QAbstractItemModel::layoutChanged, this, resetModel);
0274     connect(model, &QAbstractItemModel::modelReset, this, resetModel);
0275     connect(model, &QAbstractItemModel::rowsMoved, this, resetModel);
0276     connect(model, &QAbstractItemModel::rowsInserted, this, resetModel);
0277     connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel);
0278     connect(model, &IncidenceOccurrenceModel::lengthChanged, this, [this] {
0279         beginResetModel();
0280         endResetModel();
0281     });
0282 }
0283 
0284 void MultiDayIncidenceModel::slotSourceDataChanged(const QModelIndex &upperLeft, const QModelIndex &bottomRight)
0285 {
0286     if (m_refreshTimer.isActive()) {
0287         // We don't care resetting will be done soon
0288         return;
0289     }
0290 
0291     QSet<int> rows;
0292 
0293     for (int i = upperLeft.row(); i <= bottomRight.row(); ++i) {
0294         const auto sourceModelIndex = mSourceModel->index(i, 0, {});
0295         const auto occurrence = sourceModelIndex.data(IncidenceOccurrenceModel::IncidenceOccurrence).value<IncidenceOccurrenceModel::Occurrence>();
0296 
0297         const auto sourceModelStartDate = mSourceModel->start();
0298         const auto startDaysFromSourceStart = sourceModelStartDate.daysTo(occurrence.start.date());
0299         const auto endDaysFromSourceStart = sourceModelStartDate.daysTo(occurrence.end.date());
0300 
0301         const auto firstPeriodOccurrenceAppears = startDaysFromSourceStart / mPeriodLength;
0302         const auto lastPeriodOccurrenceAppears = endDaysFromSourceStart / mPeriodLength;
0303 
0304         if (firstPeriodOccurrenceAppears > rowCount() || lastPeriodOccurrenceAppears < 0) {
0305             continue;
0306         }
0307 
0308         const auto lastRow = rowCount() - 1;
0309         rows.insert(qMin(qMax(static_cast<int>(firstPeriodOccurrenceAppears), 0), lastRow));
0310         rows.insert(qMin(static_cast<int>(lastPeriodOccurrenceAppears), lastRow));
0311     }
0312 
0313     for (const auto row : std::as_const(rows)) {
0314         Q_EMIT dataChanged(index(row, 0), index(row, 0), {IncidencesRole});
0315     }
0316 }
0317 
0318 int MultiDayIncidenceModel::periodLength() const
0319 {
0320     return mPeriodLength;
0321 }
0322 
0323 void MultiDayIncidenceModel::setPeriodLength(int periodLength)
0324 {
0325     beginResetModel();
0326     if (mPeriodLength == periodLength) {
0327         return;
0328     }
0329     mPeriodLength = periodLength;
0330     Q_EMIT periodLengthChanged();
0331     endResetModel();
0332 }
0333 
0334 MultiDayIncidenceModel::Filters MultiDayIncidenceModel::filters() const
0335 {
0336     return m_filters;
0337 }
0338 
0339 void MultiDayIncidenceModel::setFilters(MultiDayIncidenceModel::Filters filters)
0340 {
0341     if (m_filters == filters) {
0342         return;
0343     }
0344     m_filters = filters;
0345     Q_EMIT filtersChanged();
0346 
0347     scheduleReset();
0348 }
0349 
0350 bool MultiDayIncidenceModel::showTodos() const
0351 {
0352     return m_showTodos;
0353 }
0354 
0355 void MultiDayIncidenceModel::setShowTodos(const bool showTodos)
0356 {
0357     if (showTodos == m_showTodos) {
0358         return;
0359     }
0360 
0361     m_showTodos = showTodos;
0362     Q_EMIT showTodosChanged();
0363 
0364     scheduleReset();
0365 }
0366 
0367 bool MultiDayIncidenceModel::showSubTodos() const
0368 {
0369     return m_showSubTodos;
0370 }
0371 
0372 void MultiDayIncidenceModel::setShowSubTodos(const bool showSubTodos)
0373 {
0374     if (showSubTodos == m_showSubTodos) {
0375         return;
0376     }
0377 
0378     m_showSubTodos = showSubTodos;
0379     Q_EMIT showSubTodosChanged();
0380 
0381     scheduleReset();
0382 }
0383 
0384 bool MultiDayIncidenceModel::incidencePassesFilter(const QModelIndex &idx) const
0385 {
0386     if (!m_filters && m_showTodos && m_showSubTodos) {
0387         return true;
0388     }
0389 
0390     bool include = true;
0391 
0392     if (m_filters) {
0393         // Start out assuming the worst, filter everything out
0394         include = false;
0395 
0396         const auto start = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0397 
0398         if (m_filters.testFlag(AllDayOnly) && idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
0399             include = true;
0400         }
0401 
0402         if (m_filters.testFlag(NoStartDateOnly) && !start.isValid()) {
0403             include = true;
0404         }
0405 
0406         if (m_filters.testFlag(MultiDayOnly) && idx.data(IncidenceOccurrenceModel::Duration).value<KCalendarCore::Duration>().asDays() >= 1) {
0407             include = true;
0408         }
0409     }
0410 
0411     const auto incidencePtr = idx.data(IncidenceOccurrenceModel::IncidencePtr).value<KCalendarCore::Incidence::Ptr>();
0412     const auto incidenceIsTodo = incidencePtr->type() == Incidence::TypeTodo;
0413     if (!m_showTodos && incidenceIsTodo) {
0414         include = false;
0415     } else if (m_showTodos && incidenceIsTodo && !m_showSubTodos && !incidencePtr->relatedTo().isEmpty()) {
0416         include = false;
0417     }
0418 
0419     return include;
0420 }
0421 
0422 int MultiDayIncidenceModel::incidenceCount() const
0423 {
0424     int count = 0;
0425 
0426     for (int i = 0; i < rowCount(); i++) {
0427         const auto rowStart = mSourceModel->start().addDays(i * mPeriodLength);
0428         const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
0429 
0430         for (int row = 0; row < mSourceModel->rowCount(); row++) {
0431             const auto srcIdx = mSourceModel->index(row, 0, {});
0432             const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0433             const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
0434 
0435             // Skip incidences not part of the week
0436             if (end < rowStart || start > rowEnd) {
0437                 // qCWarning(MERKURO_CALENDAR_LOG) << "Skipping because not part of this week";
0438                 continue;
0439             }
0440 
0441             if (!incidencePassesFilter(srcIdx)) {
0442                 continue;
0443             }
0444 
0445             count++;
0446         }
0447     }
0448 
0449     return count;
0450 }
0451 
0452 bool MultiDayIncidenceModel::active() const
0453 {
0454     return m_active;
0455 }
0456 
0457 void MultiDayIncidenceModel::setActive(const bool active)
0458 {
0459     if (active == m_active) {
0460         return;
0461     }
0462 
0463     m_active = active;
0464     Q_EMIT activeChanged();
0465 
0466     if (active && m_refreshTimer.isActive() && std::chrono::milliseconds(m_refreshTimer.remainingTime()) > 200ms) {
0467         Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
0468         m_refreshTimer.stop();
0469     }
0470     m_refreshTimer.setInterval(active ? 200ms : 1000ms);
0471 }
0472 
0473 QHash<int, QByteArray> MultiDayIncidenceModel::roleNames() const
0474 {
0475     return {
0476         {IncidencesRole, QByteArrayLiteral("incidences")},
0477         {PeriodStartDateRole, QByteArrayLiteral("periodStartDate")},
0478     };
0479 }
0480 
0481 #include "moc_multidayincidencemodel.cpp"