File indexing completed on 2024-06-23 04:42:36

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 "kalendar_debug.h"
0010 #include <QBitArray>
0011 
0012 MultiDayIncidenceModel::MultiDayIncidenceModel(QObject *parent)
0013     : QAbstractItemModel(parent)
0014 {
0015     mRefreshTimer.setSingleShot(true);
0016 //    m_config = KalendarConfig::self();
0017 //    QObject::connect(m_config, &KalendarConfig::showSubtodosInCalendarViewsChanged, this, [&]() {
0018 //        beginResetModel();
0019 //        endResetModel();
0020 //    });
0021 }
0022 
0023 QModelIndex MultiDayIncidenceModel::index(int row, int column, const QModelIndex &parent) const
0024 {
0025     if (!hasIndex(row, column, parent)) {
0026         return {};
0027     }
0028 
0029     if (!parent.isValid()) {
0030         return createIndex(row, column);
0031     }
0032     return {};
0033 }
0034 
0035 QModelIndex MultiDayIncidenceModel::parent(const QModelIndex &) const
0036 {
0037     return {};
0038 }
0039 
0040 int MultiDayIncidenceModel::rowCount(const QModelIndex &parent) const
0041 {
0042     // Number of weeks
0043     if (!parent.isValid() && mSourceModel) {
0044         return qMax(mSourceModel->length() / mPeriodLength, 1);
0045     }
0046     return 0;
0047 }
0048 
0049 int MultiDayIncidenceModel::columnCount(const QModelIndex &) const
0050 {
0051     return 1;
0052 }
0053 
0054 static long long getDuration(const QDate &start, const QDate &end)
0055 {
0056     return qMax(start.daysTo(end) + 1, 1ll);
0057 }
0058 
0059 // We first sort all occurrences so we get all-day first (sorted by duration),
0060 // and then the rest sorted by start-date.
0061 QList<QModelIndex> MultiDayIncidenceModel::sortedIncidencesFromSourceModel(const QDate &rowStart) const
0062 {
0063     // Don't add days if we are going for a daily period
0064     const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
0065     QList<QModelIndex> sorted;
0066     sorted.reserve(mSourceModel->rowCount());
0067     // Get incidences from source model
0068     for (int row = 0; row < mSourceModel->rowCount(); row++) {
0069         const auto srcIdx = mSourceModel->index(row, 0, {});
0070         const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0071         const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
0072 
0073         // Skip incidences not part of the week
0074         if (end < rowStart || start > rowEnd) {
0075             // qWarning() << "Skipping because not part of this week";
0076             continue;
0077         }
0078 
0079         if (!incidencePassesFilter(srcIdx)) {
0080             continue;
0081         }
0082 
0083         // qWarning() << "found " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
0084         // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
0085         sorted.append(srcIdx);
0086     }
0087 
0088     // Sort incidences by date
0089     std::sort(sorted.begin(), sorted.end(), [&](const QModelIndex &left, const QModelIndex &right) {
0090         // All-day first, sorted by duration (in the hope that we can fit multiple on the same line)
0091         const auto leftAllDay = left.data(IncidenceOccurrenceModel::AllDay).toBool();
0092         const auto rightAllDay = right.data(IncidenceOccurrenceModel::AllDay).toBool();
0093 
0094         const auto leftDuration =
0095             getDuration(left.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), left.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
0096         const auto rightDuration =
0097             getDuration(right.data(IncidenceOccurrenceModel::StartTime).toDateTime().date(), right.data(IncidenceOccurrenceModel::EndTime).toDateTime().date());
0098 
0099         const auto leftDt = left.data(IncidenceOccurrenceModel::StartTime).toDateTime();
0100         const auto rightDt = right.data(IncidenceOccurrenceModel::StartTime).toDateTime();
0101 
0102         if (leftAllDay && !rightAllDay) {
0103             return true;
0104         }
0105         if (!leftAllDay && rightAllDay) {
0106             return false;
0107         }
0108         if (leftAllDay && rightAllDay) {
0109             return leftDuration < rightDuration;
0110         }
0111 
0112         // The rest sorted by start date
0113         return leftDt < rightDt && leftDuration <= rightDuration;
0114     });
0115 
0116     return sorted;
0117 }
0118 
0119 /*
0120  * Layout the lines:
0121  *
0122  * The line grouping algorithm then always picks the first incidence,
0123  * and tries to add more to the same line.
0124  *
0125  * We never mix all-day and non-all day, and otherwise try to fit as much as possible
0126  * on the same line. Same day time-order should be preserved because of the sorting.
0127  */
0128 QVariantList MultiDayIncidenceModel::layoutLines(const QDate &rowStart) const
0129 {
0130     auto getStart = [&rowStart](const QDate &start) {
0131         return qMax(rowStart.daysTo(start), 0ll);
0132     };
0133 
0134     QList<QModelIndex> sorted = sortedIncidencesFromSourceModel(rowStart);
0135 
0136     // for (const auto &srcIdx : sorted) {
0137     //     qWarning() << "sorted " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() <<
0138     //     srcIdx.data(IncidenceOccurrenceModel::Summary).toString()
0139     //     << srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
0140     // }
0141 
0142     auto result = QVariantList{};
0143     while (!sorted.isEmpty()) {
0144         const auto srcIdx = sorted.takeFirst();
0145         const auto startDate = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
0146             ? rowStart
0147             : srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0148         const auto start = getStart(srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
0149         const auto duration = qMin(getDuration(startDate, srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0150 
0151         // qWarning() << "First of line " << srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime() << duration <<
0152         // srcIdx.data(IncidenceOccurrenceModel::Summary).toString();
0153         auto currentLine = QVariantList{};
0154 
0155         auto addToLine = [&currentLine](const QModelIndex &idx, int start, int duration) {
0156             currentLine.append(QVariantMap{
0157                 {QStringLiteral("text"), idx.data(IncidenceOccurrenceModel::Summary)},
0158                 {QStringLiteral("description"), idx.data(IncidenceOccurrenceModel::Description)},
0159                 {QStringLiteral("location"), idx.data(IncidenceOccurrenceModel::Location)},
0160                 {QStringLiteral("startTime"), idx.data(IncidenceOccurrenceModel::StartTime)},
0161                 {QStringLiteral("endTime"), idx.data(IncidenceOccurrenceModel::EndTime)},
0162                 {QStringLiteral("allDay"), idx.data(IncidenceOccurrenceModel::AllDay)},
0163                 {QStringLiteral("todoCompleted"), idx.data(IncidenceOccurrenceModel::TodoCompleted)},
0164                 {QStringLiteral("priority"), idx.data(IncidenceOccurrenceModel::Priority)},
0165                 {QStringLiteral("starts"), start},
0166                 {QStringLiteral("duration"), duration},
0167                 {QStringLiteral("durationString"), idx.data(IncidenceOccurrenceModel::DurationString)},
0168                 {QStringLiteral("recurs"), idx.data(IncidenceOccurrenceModel::Recurs)},
0169                 {QStringLiteral("hasReminders"), idx.data(IncidenceOccurrenceModel::HasReminders)},
0170                 {QStringLiteral("isOverdue"), idx.data(IncidenceOccurrenceModel::IsOverdue)},
0171                 {QStringLiteral("isReadOnly"), idx.data(IncidenceOccurrenceModel::IsReadOnly)},
0172                 {QStringLiteral("color"), idx.data(IncidenceOccurrenceModel::Color)},
0173                 {QStringLiteral("collectionId"), idx.data(IncidenceOccurrenceModel::CollectionId)},
0174                 {QStringLiteral("incidenceId"), idx.data(IncidenceOccurrenceModel::IncidenceId)},
0175                 {QStringLiteral("incidenceType"), idx.data(IncidenceOccurrenceModel::IncidenceType)},
0176                 {QStringLiteral("incidenceTypeStr"), idx.data(IncidenceOccurrenceModel::IncidenceTypeStr)},
0177                 {QStringLiteral("incidenceTypeIcon"), idx.data(IncidenceOccurrenceModel::IncidenceTypeIcon)},
0178                 {QStringLiteral("incidencePtr"), idx.data(IncidenceOccurrenceModel::IncidencePtr)},
0179                 {QStringLiteral("incidenceOccurrence"), idx.data(IncidenceOccurrenceModel::IncidenceOccurrence)},
0180             });
0181         };
0182 
0183         if (start >= mPeriodLength) {
0184             // qWarning() << "Skipping " << srcIdx.data(IncidenceOccurrenceModel::Summary);
0185             continue;
0186         }
0187 
0188         // Add first incidence of line
0189         addToLine(srcIdx, start, duration);
0190         // const bool allDayLine = srcIdx.data(IncidenceOccurrenceModel::AllDay).toBool();
0191 
0192         // Fill line with incidences that fit
0193         QBitArray takenSpaces(mPeriodLength);
0194         // Set this incidence's space as taken
0195         for (int i = start; i < start + duration; i++) {
0196             takenSpaces[i] = true;
0197         }
0198 
0199         auto doesIntersect = [&](int start, int end) {
0200             for (int i = start; i < end; i++) {
0201                 if (takenSpaces[i]) {
0202                     // qWarning() << "Found intersection " << start << end;
0203                     return true;
0204                 }
0205             }
0206 
0207             // If incidence fits on line, set its space as taken
0208             for (int i = start; i < end; i++) {
0209                 takenSpaces[i] = true;
0210             }
0211             return false;
0212         };
0213 
0214         for (auto it = sorted.begin(); it != sorted.end();) {
0215             const auto idx = *it;
0216             const auto startDate = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date() < rowStart
0217                 ? rowStart
0218                 : idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0219             const auto start = getStart(idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date());
0220             const auto duration = qMin(getDuration(startDate, idx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0221             const auto end = start + duration;
0222 
0223             // This leaves a space in rows with all day events, making this y area of the row exclusively for all day events
0224             /*if (allDayLine && !idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
0225                 continue;
0226             }*/
0227 
0228             if (doesIntersect(start, end)) {
0229                 it++;
0230             } else {
0231                 addToLine(idx, start, duration);
0232                 it = sorted.erase(it);
0233             }
0234         }
0235         // qWarning() << "Appending line " << currentLine;
0236         result.append(QVariant::fromValue(currentLine));
0237     }
0238     return result;
0239 }
0240 
0241 QVariant MultiDayIncidenceModel::data(const QModelIndex &idx, int role) const
0242 {
0243     if (!hasIndex(idx.row(), idx.column())) {
0244         return {};
0245     }
0246     if (!mSourceModel) {
0247         return {};
0248     }
0249     const auto rowStart = mSourceModel->start().addDays(idx.row() * mPeriodLength);
0250     switch (role) {
0251     case PeriodStartDate:
0252         return rowStart.startOfDay();
0253     case Incidences:
0254         return layoutLines(rowStart);
0255     default:
0256         Q_ASSERT(false);
0257         return {};
0258     }
0259 }
0260 
0261 IncidenceOccurrenceModel *MultiDayIncidenceModel::model()
0262 {
0263     return mSourceModel;
0264 }
0265 
0266 void MultiDayIncidenceModel::setModel(IncidenceOccurrenceModel *model)
0267 {
0268     beginResetModel();
0269 
0270     mSourceModel = model;
0271     Q_EMIT modelChanged();
0272     auto resetModel = [this] {
0273         if (!mRefreshTimer.isActive()) {
0274             beginResetModel();
0275             endResetModel();
0276             Q_EMIT incidenceCountChanged();
0277             mRefreshTimer.start(50);
0278         }
0279     };
0280     QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel);
0281     QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel);
0282     QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel);
0283     QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel);
0284     QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel);
0285     QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel);
0286     endResetModel();
0287 }
0288 
0289 int MultiDayIncidenceModel::periodLength()
0290 {
0291     return mPeriodLength;
0292 }
0293 
0294 void MultiDayIncidenceModel::setPeriodLength(int periodLength)
0295 {
0296     mPeriodLength = periodLength;
0297 }
0298 
0299 MultiDayIncidenceModel::Filters MultiDayIncidenceModel::filters()
0300 {
0301     return m_filters;
0302 }
0303 
0304 void MultiDayIncidenceModel::setFilters(MultiDayIncidenceModel::Filters filters)
0305 {
0306     beginResetModel();
0307     m_filters = filters;
0308     Q_EMIT filtersChanged();
0309     endResetModel();
0310 }
0311 
0312 bool MultiDayIncidenceModel::incidencePassesFilter(const QModelIndex &idx) const
0313 {
0314 //    if (!m_filters && m_config->showSubtodosInCalendarViews()) {
0315 //        return true;
0316 //    }
0317     bool include = false;
0318 
0319     if (m_filters) {
0320         const auto start = idx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0321 
0322         if (m_filters.testFlag(AllDayOnly) && idx.data(IncidenceOccurrenceModel::AllDay).toBool()) {
0323             include = true;
0324         }
0325 
0326         if (m_filters.testFlag(NoStartDateOnly) && !start.isValid()) {
0327             include = true;
0328         }
0329         if (m_filters.testFlag(MultiDayOnly) && idx.data(IncidenceOccurrenceModel::Duration).value<KCalendarCore::Duration>().asDays() >= 1) {
0330             include = true;
0331         }
0332     }
0333 
0334 //    if (!m_config->showSubtodosInCalendarViews()
0335 //        && idx.data(IncidenceOccurrenceModel::IncidencePtr).value<KCalendarCore::Incidence::Ptr>()->relatedTo().isEmpty()) {
0336 //        include = true;
0337 //    }
0338 
0339     return include;
0340 }
0341 
0342 int MultiDayIncidenceModel::incidenceCount()
0343 {
0344     int count = 0;
0345 
0346     for (int i = 0; i < rowCount({}); i++) {
0347         const auto rowStart = mSourceModel->start().addDays(i * mPeriodLength);
0348         const auto rowEnd = rowStart.addDays(mPeriodLength > 1 ? mPeriodLength : 0);
0349 
0350         for (int row = 0; row < mSourceModel->rowCount(); row++) {
0351             const auto srcIdx = mSourceModel->index(row, 0, {});
0352             const auto start = srcIdx.data(IncidenceOccurrenceModel::StartTime).toDateTime().date();
0353             const auto end = srcIdx.data(IncidenceOccurrenceModel::EndTime).toDateTime().date();
0354 
0355             // Skip incidences not part of the week
0356             if (end < rowStart || start > rowEnd) {
0357                 // qWarning() << "Skipping because not part of this week";
0358                 continue;
0359             }
0360 
0361             if (!incidencePassesFilter(srcIdx)) {
0362                 continue;
0363             }
0364 
0365             count++;
0366         }
0367     }
0368 
0369     return count;
0370 }
0371 
0372 QHash<int, QByteArray> MultiDayIncidenceModel::roleNames() const
0373 {
0374     return {
0375         {Incidences, "incidences"},
0376         {PeriodStartDate, "periodStartDate"},
0377     };
0378 }