File indexing completed on 2024-06-23 04:42:35
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 // Copyright (c) 2021 Claudio Cambra <claudio.cambra@gmail.com> 0006 // SPDX-License-Identifier: LGPL-2.0-or-later 0007 0008 #include "incidenceoccurrencemodel.h" 0009 0010 #include "../filter.h" 0011 #include <Akonadi/EntityTreeModel> 0012 #include <KCalendarCore/OccurrenceIterator> 0013 #include <KConfigGroup> 0014 #include <KLocalizedString> 0015 #include <KSharedConfig> 0016 #include <QMetaEnum> 0017 0018 IncidenceOccurrenceModel::IncidenceOccurrenceModel(QObject *parent) 0019 : QAbstractListModel(parent) 0020 , m_coreCalendar(nullptr) 0021 { 0022 m_resetThrottlingTimer.setSingleShot(true); 0023 QObject::connect(&m_resetThrottlingTimer, &QTimer::timeout, this, &IncidenceOccurrenceModel::resetFromSource); 0024 0025 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0026 KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors")); 0027 m_colorWatcher = KConfigWatcher::create(config); 0028 0029 // This is quite slow; would be nice to find a quicker way 0030 connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &IncidenceOccurrenceModel::resetFromSource); 0031 } 0032 0033 void IncidenceOccurrenceModel::setStart(const QDate &start) 0034 { 0035 if(start == mStart) { 0036 return; 0037 } 0038 0039 mStart = start; 0040 Q_EMIT startChanged(); 0041 0042 mEnd = mStart.addDays(mLength); 0043 scheduleReset(); 0044 } 0045 0046 QDate IncidenceOccurrenceModel::start() const 0047 { 0048 return mStart; 0049 } 0050 0051 void IncidenceOccurrenceModel::setLength(int length) 0052 { 0053 if (mLength == length) { 0054 return; 0055 } 0056 mLength = length; 0057 Q_EMIT lengthChanged(); 0058 0059 mEnd = mStart.addDays(mLength); 0060 scheduleReset(); 0061 } 0062 0063 int IncidenceOccurrenceModel::length() const 0064 { 0065 return mLength; 0066 } 0067 0068 Filter *IncidenceOccurrenceModel::filter() const 0069 { 0070 return mFilter; 0071 } 0072 0073 void IncidenceOccurrenceModel::setFilter(Filter *filter) 0074 { 0075 mFilter = filter; 0076 Q_EMIT filterChanged(); 0077 0078 scheduleReset(); 0079 } 0080 0081 bool IncidenceOccurrenceModel::loading() const 0082 { 0083 return m_loading; 0084 } 0085 0086 void IncidenceOccurrenceModel::setLoading(const bool loading) 0087 { 0088 if(loading == m_loading) { 0089 return; 0090 } 0091 0092 m_loading = loading; 0093 Q_EMIT loadingChanged(); 0094 } 0095 0096 int IncidenceOccurrenceModel::resetThrottleInterval() const 0097 { 0098 return m_resetThrottleInterval; 0099 } 0100 0101 void IncidenceOccurrenceModel::setResetThrottleInterval(const int resetThrottleInterval) 0102 { 0103 if(resetThrottleInterval == m_resetThrottleInterval) { 0104 return; 0105 } 0106 0107 m_resetThrottleInterval = resetThrottleInterval; 0108 Q_EMIT resetThrottleIntervalChanged(); 0109 } 0110 0111 void IncidenceOccurrenceModel::scheduleReset() 0112 { 0113 if (!m_resetThrottlingTimer.isActive()) { 0114 // Instant update, but then only refresh every interval at most. 0115 m_resetThrottlingTimer.start(m_resetThrottleInterval); 0116 } 0117 } 0118 0119 void IncidenceOccurrenceModel::resetFromSource() 0120 { 0121 if (!m_coreCalendar) { 0122 qWarning() << "Not resetting IOC from source as no core calendar set."; 0123 return; 0124 } 0125 0126 setLoading(true); 0127 0128 if (m_resetThrottlingTimer.isActive() || m_coreCalendar->isLoading()) { 0129 // If calendar is still loading then just schedule a refresh later 0130 // If refresh timer already active this won't restart it 0131 scheduleReset(); 0132 return; 0133 } 0134 0135 loadColors(); 0136 0137 beginResetModel(); 0138 0139 m_incidences.clear(); 0140 m_occurrenceIndexHash.clear(); 0141 0142 KCalendarCore::OccurrenceIterator occurrenceIterator(*m_coreCalendar, QDateTime(mStart, {0, 0, 0}), QDateTime(mEnd, {12, 59, 59})); 0143 0144 while (occurrenceIterator.hasNext()) { 0145 occurrenceIterator.next(); 0146 const auto incidence = occurrenceIterator.incidence(); 0147 0148 if(!incidencePassesFilter(incidence)) { 0149 continue; 0150 } 0151 0152 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence); 0153 const auto start = occurrenceStartEnd.first; 0154 const auto end = occurrenceStartEnd.second; 0155 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid()); 0156 const Occurrence occurrence{ 0157 start, 0158 end, 0159 incidence, 0160 getColor(incidence), 0161 getCollectionId(incidence), 0162 incidence->allDay(), 0163 }; 0164 0165 const auto indexRow = m_incidences.count(); 0166 m_incidences.append(occurrence); 0167 0168 const auto occurrenceIndex = index(indexRow); 0169 const QPersistentModelIndex persistentIndex(occurrenceIndex); 0170 0171 m_occurrenceIndexHash.insert(occurrenceHashKey, persistentIndex); 0172 } 0173 0174 endResetModel(); 0175 0176 setLoading(false); 0177 } 0178 0179 void IncidenceOccurrenceModel::slotSourceDataChanged(const QModelIndex &upperLeft, const QModelIndex &bottomRight) 0180 { 0181 if (!m_coreCalendar || !upperLeft.isValid() || !bottomRight.isValid() || m_resetThrottlingTimer.isActive()) { 0182 return; 0183 } 0184 0185 setLoading(true); 0186 0187 const auto startRow = upperLeft.row(); 0188 const auto endRow = bottomRight.row(); 0189 0190 for (int i = startRow; i <= endRow; ++i) { 0191 const auto sourceModelIndex = m_coreCalendar->model()->index(i, 0, upperLeft.parent()); 0192 const auto incidenceItem = sourceModelIndex.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>(); 0193 0194 if(!incidenceItem.isValid() || !incidenceItem.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0195 continue; 0196 } 0197 0198 const auto incidence = incidenceItem.payload<KCalendarCore::Incidence::Ptr>(); 0199 KCalendarCore::OccurrenceIterator occurrenceIterator{*m_coreCalendar, incidence, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}}; 0200 0201 while (occurrenceIterator.hasNext()) { 0202 occurrenceIterator.next(); 0203 0204 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence); 0205 const auto start = occurrenceStartEnd.first; 0206 const auto end = occurrenceStartEnd.second; 0207 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid()); 0208 0209 if(!m_occurrenceIndexHash.contains(occurrenceHashKey)) { 0210 continue; 0211 } 0212 0213 const Occurrence occurrence{ 0214 start, 0215 end, 0216 incidence, 0217 getColor(incidence), 0218 getCollectionId(incidence), 0219 incidence->allDay(), 0220 }; 0221 0222 const auto existingOccurrenceIndex = m_occurrenceIndexHash.value(occurrenceHashKey); 0223 const auto existingOccurrenceRow = existingOccurrenceIndex.row(); 0224 0225 m_incidences.replace(existingOccurrenceRow, occurrence); 0226 Q_EMIT dataChanged(existingOccurrenceIndex, existingOccurrenceIndex); 0227 } 0228 } 0229 0230 setLoading(false); 0231 } 0232 0233 void IncidenceOccurrenceModel::slotSourceRowsInserted(const QModelIndex &parent, const int first, const int last) 0234 { 0235 if (!m_coreCalendar || m_resetThrottlingTimer.isActive()) { 0236 return; 0237 } else if (m_coreCalendar->isLoading()) { 0238 m_resetThrottlingTimer.start(m_resetThrottleInterval); 0239 return; 0240 } 0241 0242 setLoading(true); 0243 0244 for (int i = first; i <= last; ++i) { 0245 const auto sourceModelIndex = m_coreCalendar->model()->index(i, 0, parent); 0246 const auto incidenceItem = sourceModelIndex.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>(); 0247 0248 if(!incidenceItem.isValid() || !incidenceItem.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0249 continue; 0250 } 0251 0252 const auto incidence = incidenceItem.payload<KCalendarCore::Incidence::Ptr>(); 0253 0254 if(!incidencePassesFilter(incidence)) { 0255 continue; 0256 } 0257 0258 KCalendarCore::OccurrenceIterator occurrenceIterator{*m_coreCalendar, incidence, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}}; 0259 0260 while (occurrenceIterator.hasNext()) { 0261 occurrenceIterator.next(); 0262 0263 const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence); 0264 const auto start = occurrenceStartEnd.first; 0265 const auto end = occurrenceStartEnd.second; 0266 const auto occurrenceHashKey = incidenceOccurrenceHash(start, end, incidence->uid()); 0267 0268 if(m_occurrenceIndexHash.contains(occurrenceHashKey)) { 0269 continue; 0270 } 0271 0272 const Occurrence occurrence{ 0273 start, 0274 end, 0275 incidence, 0276 getColor(incidence), 0277 getCollectionId(incidence), 0278 incidence->allDay(), 0279 }; 0280 0281 const auto indexRow = m_incidences.count(); 0282 0283 beginInsertRows({}, indexRow, indexRow); 0284 m_incidences.append(occurrence); 0285 endInsertRows(); 0286 0287 const auto occurrenceIndex = index(indexRow); 0288 const QPersistentModelIndex persistentIndex(occurrenceIndex); 0289 0290 m_occurrenceIndexHash.insert(occurrenceHashKey, persistentIndex); 0291 } 0292 } 0293 0294 setLoading(false); 0295 } 0296 0297 int IncidenceOccurrenceModel::rowCount(const QModelIndex &parent) const 0298 { 0299 if (!parent.isValid()) { 0300 return m_incidences.size(); 0301 } 0302 return 0; 0303 } 0304 0305 qint64 IncidenceOccurrenceModel::getCollectionId(const KCalendarCore::Incidence::Ptr &incidence) 0306 { 0307 auto item = m_coreCalendar->item(incidence); 0308 if (!item.isValid()) { 0309 return {}; 0310 } 0311 auto collection = item.parentCollection(); 0312 if (!collection.isValid()) { 0313 return {}; 0314 } 0315 return collection.id(); 0316 } 0317 0318 QColor IncidenceOccurrenceModel::getColor(const KCalendarCore::Incidence::Ptr &incidence) 0319 { 0320 auto item = m_coreCalendar->item(incidence); 0321 if (!item.isValid()) { 0322 return {}; 0323 } 0324 auto collection = item.parentCollection(); 0325 if (!collection.isValid()) { 0326 return {}; 0327 } 0328 const QString id = QString::number(collection.id()); 0329 // qDebug() << "Collection id: " << collection.id(); 0330 0331 if (m_colors.contains(id)) { 0332 // qDebug() << collection.id() << "Found in m_colors"; 0333 return m_colors[id]; 0334 } 0335 0336 return {}; 0337 } 0338 0339 QVariant IncidenceOccurrenceModel::data(const QModelIndex &idx, int role) const 0340 { 0341 if (!hasIndex(idx.row(), idx.column())) { 0342 return {}; 0343 } 0344 0345 const auto occurrence = m_incidences.at(idx.row()); 0346 const auto incidence = occurrence.incidence; 0347 0348 switch (role) { 0349 case Summary: 0350 return incidence->summary(); 0351 case Description: 0352 return incidence->description(); 0353 case Location: 0354 return incidence->location(); 0355 case StartTime: 0356 return occurrence.start; 0357 case EndTime: 0358 return occurrence.end; 0359 case Duration: 0360 { 0361 const KCalendarCore::Duration duration(occurrence.start, occurrence.end); 0362 return QVariant::fromValue(duration); 0363 } 0364 case DurationString: { 0365 const KCalendarCore::Duration duration(occurrence.start, occurrence.end); 0366 0367 if (duration.asSeconds() == 0) { 0368 return QString(); 0369 } 0370 0371 return m_format.formatSpelloutDuration(duration.asSeconds() * 1000); 0372 } 0373 case Recurs: 0374 return incidence->recurs(); 0375 case HasReminders: 0376 return incidence->alarms().length() > 0; 0377 case Priority: 0378 return incidence->priority(); 0379 case Color: 0380 return occurrence.color; 0381 case CollectionId: 0382 return occurrence.collectionId; 0383 case AllDay: 0384 return occurrence.allDay; 0385 case TodoCompleted: { 0386 if (incidence->type() != KCalendarCore::IncidenceBase::TypeTodo) { 0387 return false; 0388 } 0389 0390 auto todo = incidence.staticCast<KCalendarCore::Todo>(); 0391 return todo->isCompleted(); 0392 } 0393 case IsOverdue: { 0394 if (incidence->type() != KCalendarCore::IncidenceBase::TypeTodo) { 0395 return false; 0396 } 0397 0398 auto todo = incidence.staticCast<KCalendarCore::Todo>(); 0399 return todo->isOverdue(); 0400 } 0401 case IsReadOnly: { 0402 const auto collection = m_coreCalendar->collection(occurrence.collectionId); 0403 return collection.rights().testFlag(Akonadi::Collection::ReadOnly); 0404 } 0405 case IncidenceId: 0406 return incidence->uid(); 0407 case IncidenceType: 0408 return incidence->type(); 0409 case IncidenceTypeStr: 0410 return incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(incidence->typeStr().constData()); 0411 case IncidenceTypeIcon: 0412 return incidence->iconName(); 0413 case IncidencePtr: 0414 return QVariant::fromValue(incidence); 0415 case IncidenceOccurrence: 0416 return QVariant::fromValue(occurrence); 0417 default: 0418 qWarning( 0419 0420 ) << "Unknown role for occurrence:" << QMetaEnum::fromType<Roles>().valueToKey(role); 0421 return {}; 0422 } 0423 } 0424 0425 void IncidenceOccurrenceModel::setCalendar(Akonadi::ETMCalendar::Ptr calendar) 0426 { 0427 if (m_coreCalendar == calendar) { 0428 return; 0429 } 0430 m_coreCalendar = calendar; 0431 0432 connect(m_coreCalendar->model(), &QAbstractItemModel::dataChanged, this, &IncidenceOccurrenceModel::slotSourceDataChanged); 0433 connect(m_coreCalendar->model(), &QAbstractItemModel::rowsInserted, this, &IncidenceOccurrenceModel::slotSourceRowsInserted); 0434 connect(m_coreCalendar->model(), &QAbstractItemModel::rowsRemoved, this, &IncidenceOccurrenceModel::scheduleReset); 0435 connect(m_coreCalendar->model(), &QAbstractItemModel::modelReset, this, &IncidenceOccurrenceModel::scheduleReset); 0436 connect(m_coreCalendar.get(), &Akonadi::ETMCalendar::collectionsRemoved, this, &IncidenceOccurrenceModel::scheduleReset); 0437 0438 Q_EMIT calendarChanged(); 0439 0440 scheduleReset(); 0441 } 0442 0443 Akonadi::ETMCalendar::Ptr IncidenceOccurrenceModel::calendar() const 0444 { 0445 return m_coreCalendar; 0446 } 0447 0448 void IncidenceOccurrenceModel::loadColors() 0449 { 0450 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0451 KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors")); 0452 const QStringList colorKeyList = rColorsConfig.keyList(); 0453 0454 for (const QString &key : colorKeyList) { 0455 QColor color = rColorsConfig.readEntry(key, QColor("blue")); 0456 m_colors[key] = color; 0457 } 0458 } 0459 0460 std::pair<QDateTime, QDateTime> IncidenceOccurrenceModel::incidenceOccurrenceStartEnd(const QDateTime &ocStart, const KCalendarCore::Incidence::Ptr &incidence) 0461 { 0462 auto start = ocStart; 0463 const auto end = incidence->endDateForStart(start); 0464 0465 if (incidence->type() == KCalendarCore::Incidence::IncidenceType::TypeTodo) { 0466 KCalendarCore::Todo::Ptr todo = incidence.staticCast<KCalendarCore::Todo>(); 0467 0468 if (!start.isValid()) { // Todos are very likely not to have a set start date 0469 start = todo->dtDue(); 0470 } 0471 } 0472 0473 return {start, end}; 0474 } 0475 0476 uint IncidenceOccurrenceModel::incidenceOccurrenceHash(const QDateTime &ocStart, const QDateTime &ocEnd, const QString &incidenceUid) 0477 { 0478 return qHash(QString::number(ocStart.toSecsSinceEpoch()) + 0479 QString::number(ocEnd.toSecsSinceEpoch()) + 0480 incidenceUid); 0481 } 0482 0483 bool IncidenceOccurrenceModel::incidencePassesFilter(const KCalendarCore::Incidence::Ptr &incidence) 0484 { 0485 if(!mFilter || mFilter->tags().empty()) { 0486 return true; 0487 } 0488 0489 auto match = false; 0490 const auto tags = mFilter->tags(); 0491 for (const auto &tag : tags) { 0492 if (incidence->categories().contains(tag)) { 0493 match = true; 0494 break; 0495 } 0496 } 0497 0498 return match; 0499 } 0500 0501 QHash<int, QByteArray> IncidenceOccurrenceModel::roleNames() const 0502 { 0503 return { 0504 {IncidenceOccurrenceModel::Summary, "summary"}, 0505 {IncidenceOccurrenceModel::StartTime, "startTime"}, 0506 }; 0507 }