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 }