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 = [¤tLine](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 }