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