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 // Copyright (c) 2021 Claudio Cambra <claudio.cambra@gmail.com>
0006 // SPDX-License-Identifier: LGPL-2.0-or-later
0007 
0008 #include "incidenceoccurrencemodel.h"
0009 #include "merkuro_calendar_debug.h"
0010 
0011 #include "../filter.h"
0012 #include "../utils.h"
0013 #include <Akonadi/CollectionColorAttribute>
0014 #include <Akonadi/EntityTreeModel>
0015 #include <KCalendarCore/OccurrenceIterator>
0016 #include <KConfigGroup>
0017 #include <KLocalizedString>
0018 #include <KSharedConfig>
0019 #include <QMetaEnum>
0020 
0021 IncidenceOccurrenceModel::IncidenceOccurrenceModel(QObject *parent)
0022     : QAbstractListModel(parent)
0023     , m_coreCalendar(nullptr)
0024 {
0025     m_resetThrottlingTimer.setSingleShot(true);
0026     QObject::connect(&m_resetThrottlingTimer, &QTimer::timeout, this, &IncidenceOccurrenceModel::resetFromSource);
0027 
0028     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0029     KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors"));
0030     m_colorWatcher = KConfigWatcher::create(config);
0031 
0032     // This is quite slow; would be nice to find a quicker way
0033     connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &IncidenceOccurrenceModel::resetFromSource);
0034 }
0035 
0036 void IncidenceOccurrenceModel::setStart(const QDate &start)
0037 {
0038     if (start == mStart) {
0039         return;
0040     }
0041 
0042     mStart = start;
0043     Q_EMIT startChanged();
0044 
0045     mEnd = mStart.addDays(mLength);
0046     scheduleReset();
0047 }
0048 
0049 QDate IncidenceOccurrenceModel::start() const
0050 {
0051     return mStart;
0052 }
0053 
0054 void IncidenceOccurrenceModel::setLength(int length)
0055 {
0056     if (mLength == length) {
0057         return;
0058     }
0059     mLength = length;
0060     Q_EMIT lengthChanged();
0061 
0062     mEnd = mStart.addDays(mLength);
0063     scheduleReset();
0064 }
0065 
0066 int IncidenceOccurrenceModel::length() const
0067 {
0068     return mLength;
0069 }
0070 
0071 Filter *IncidenceOccurrenceModel::filter() const
0072 {
0073     return mFilter;
0074 }
0075 
0076 void IncidenceOccurrenceModel::setFilter(Filter *filter)
0077 {
0078     mFilter = filter;
0079     Q_EMIT filterChanged();
0080 
0081     scheduleReset();
0082 }
0083 
0084 bool IncidenceOccurrenceModel::loading() const
0085 {
0086     return m_loading;
0087 }
0088 
0089 void IncidenceOccurrenceModel::setLoading(const bool loading)
0090 {
0091     if (loading == m_loading) {
0092         return;
0093     }
0094 
0095     m_loading = loading;
0096     Q_EMIT loadingChanged();
0097 }
0098 
0099 int IncidenceOccurrenceModel::resetThrottleInterval() const
0100 {
0101     return m_resetThrottleInterval;
0102 }
0103 
0104 void IncidenceOccurrenceModel::setResetThrottleInterval(const int resetThrottleInterval)
0105 {
0106     if (resetThrottleInterval == m_resetThrottleInterval) {
0107         return;
0108     }
0109 
0110     m_resetThrottleInterval = resetThrottleInterval;
0111     Q_EMIT resetThrottleIntervalChanged();
0112 }
0113 
0114 void IncidenceOccurrenceModel::scheduleReset()
0115 {
0116     if (!m_resetThrottlingTimer.isActive()) {
0117         // Instant update, but then only refresh every interval at most.
0118         m_resetThrottlingTimer.start(m_resetThrottleInterval);
0119     }
0120 }
0121 
0122 void IncidenceOccurrenceModel::resetFromSource()
0123 {
0124     if (!m_coreCalendar) {
0125         qCWarning(MERKURO_CALENDAR_LOG) << "Not resetting IOC from source as no core calendar set.";
0126         return;
0127     }
0128 
0129     setLoading(true);
0130 
0131     if (m_resetThrottlingTimer.isActive() || m_coreCalendar->isLoading()) {
0132         // If calendar is still loading then just schedule a refresh later
0133         // If refresh timer already active this won't restart it
0134         scheduleReset();
0135         return;
0136     }
0137 
0138     loadColors();
0139 
0140     beginResetModel();
0141 
0142     m_incidences.clear();
0143 
0144     KCalendarCore::OccurrenceIterator occurrenceIterator(*m_coreCalendar, QDateTime(mStart, {0, 0, 0}), QDateTime(mEnd, {12, 59, 59}));
0145 
0146     while (occurrenceIterator.hasNext()) {
0147         occurrenceIterator.next();
0148         const auto incidence = occurrenceIterator.incidence();
0149 
0150         if (!incidencePassesFilter(incidence)) {
0151             continue;
0152         }
0153 
0154         const auto occurrenceStartEnd = incidenceOccurrenceStartEnd(occurrenceIterator.occurrenceStartDate(), incidence);
0155         const auto start = occurrenceStartEnd.first;
0156         const auto end = occurrenceStartEnd.second;
0157 
0158         const Occurrence occurrence{
0159             start,
0160             end,
0161             incidence,
0162             getColor(incidence),
0163             getCollectionId(incidence),
0164             incidence->allDay(),
0165         };
0166 
0167         m_incidences.append(occurrence);
0168     }
0169 
0170     endResetModel();
0171 
0172     setLoading(false);
0173 }
0174 
0175 int IncidenceOccurrenceModel::rowCount(const QModelIndex &parent) const
0176 {
0177     if (parent.isValid()) {
0178         return 0;
0179     }
0180     return m_incidences.size();
0181 }
0182 
0183 qint64 IncidenceOccurrenceModel::getCollectionId(const KCalendarCore::Incidence::Ptr &incidence)
0184 {
0185     auto item = m_coreCalendar->item(incidence);
0186     if (!item.isValid()) {
0187         return {};
0188     }
0189     auto collection = item.parentCollection();
0190     if (!collection.isValid()) {
0191         return {};
0192     }
0193     return collection.id();
0194 }
0195 
0196 QColor IncidenceOccurrenceModel::getColor(const KCalendarCore::Incidence::Ptr &incidence)
0197 {
0198     if (!incidence->color().isEmpty()) {
0199         return incidence->color();
0200     }
0201 
0202     const auto item = m_coreCalendar->item(incidence);
0203     if (!item.isValid()) {
0204         return {};
0205     }
0206 
0207     const auto collection = item.parentCollection();
0208     if (!collection.isValid()) {
0209         return {};
0210     }
0211 
0212     const auto id = collection.id();
0213 
0214     if (collection.hasAttribute<Akonadi::CollectionColorAttribute>()) {
0215         const auto colorAttr = collection.attribute<Akonadi::CollectionColorAttribute>();
0216         if (colorAttr && colorAttr->color().isValid()) {
0217             m_colors[id] = colorAttr->color();
0218             return colorAttr->color();
0219         }
0220     }
0221 
0222     if (m_colors.contains(id)) {
0223         return m_colors[id];
0224     }
0225 
0226     return {};
0227 }
0228 
0229 QVariant IncidenceOccurrenceModel::data(const QModelIndex &idx, int role) const
0230 {
0231     Q_ASSERT(hasIndex(idx.row(), idx.column()));
0232 
0233     const auto occurrence = m_incidences.at(idx.row());
0234     const auto incidence = occurrence.incidence;
0235 
0236     switch (role) {
0237     case Qt::DisplayRole:
0238     case Qt::EditRole:
0239     case Summary:
0240         return incidence->summary();
0241     case Qt::DecorationRole:
0242         switch (incidence->type()) {
0243         case KCalendarCore::IncidenceBase::TypeTodo:
0244             return QIcon::fromTheme(QStringLiteral("view-pim-tasks"));
0245         case KCalendarCore::IncidenceBase::TypeEvent:
0246             return QIcon::fromTheme(QStringLiteral("view-pim-calendar"));
0247         case KCalendarCore::IncidenceBase::TypeJournal:
0248             return QIcon::fromTheme(QStringLiteral("view-pim-journal"));
0249         default:
0250             Q_UNREACHABLE();
0251         }
0252     case Description:
0253         return incidence->description();
0254     case Location:
0255         return incidence->location();
0256     case StartTime:
0257         return occurrence.start;
0258     case EndTime:
0259         return occurrence.end;
0260     case Duration: {
0261         const KCalendarCore::Duration duration(occurrence.start, occurrence.end);
0262         return QVariant::fromValue(duration);
0263     }
0264     case DurationString: {
0265         const KCalendarCore::Duration duration(occurrence.start, occurrence.end);
0266         return Utils::formatSpelloutDuration(duration, m_format, occurrence.allDay);
0267     }
0268     case Recurs:
0269         return incidence->recurs();
0270     case HasReminders:
0271         return incidence->alarms().length() > 0;
0272     case Priority:
0273         return incidence->priority();
0274     case Color:
0275         return occurrence.color;
0276     case CollectionId:
0277         return occurrence.collectionId;
0278     case AllDay:
0279         return occurrence.allDay;
0280     case TodoCompleted: {
0281         if (incidence->type() != KCalendarCore::IncidenceBase::TypeTodo) {
0282             return false;
0283         }
0284 
0285         auto todo = incidence.staticCast<KCalendarCore::Todo>();
0286         return todo->isCompleted();
0287     }
0288     case IsOverdue: {
0289         if (incidence->type() != KCalendarCore::IncidenceBase::TypeTodo) {
0290             return false;
0291         }
0292 
0293         auto todo = incidence.staticCast<KCalendarCore::Todo>();
0294         return todo->isOverdue();
0295     }
0296     case IsReadOnly: {
0297         const auto collection = m_coreCalendar->collection(occurrence.collectionId);
0298         return collection.rights().testFlag(Akonadi::Collection::ReadOnly);
0299     }
0300     case IncidenceId:
0301         return incidence->uid();
0302     case IncidenceType:
0303         return incidence->type();
0304     case IncidenceTypeStr:
0305         return incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(incidence->typeStr().constData());
0306     case IncidenceTypeIcon:
0307         return incidence->iconName();
0308     case IncidencePtr:
0309         return QVariant::fromValue(incidence);
0310     case IncidenceOccurrence:
0311         return QVariant::fromValue(occurrence);
0312     default:
0313         return {};
0314     }
0315 }
0316 
0317 void IncidenceOccurrenceModel::setCalendar(Akonadi::ETMCalendar::Ptr calendar)
0318 {
0319     if (m_coreCalendar == calendar) {
0320         return;
0321     }
0322     m_coreCalendar = calendar;
0323 
0324     connect(m_coreCalendar->model(), &QAbstractItemModel::dataChanged, this, &IncidenceOccurrenceModel::scheduleReset);
0325     connect(m_coreCalendar->model(), &QAbstractItemModel::rowsInserted, this, &IncidenceOccurrenceModel::scheduleReset);
0326     connect(m_coreCalendar->model(), &QAbstractItemModel::rowsRemoved, this, &IncidenceOccurrenceModel::scheduleReset);
0327     connect(m_coreCalendar->model(), &QAbstractItemModel::layoutChanged, this, &IncidenceOccurrenceModel::scheduleReset);
0328     connect(m_coreCalendar->model(), &QAbstractItemModel::modelReset, this, &IncidenceOccurrenceModel::scheduleReset);
0329     connect(m_coreCalendar->model(), &QAbstractItemModel::rowsMoved, this, &IncidenceOccurrenceModel::scheduleReset);
0330     connect(m_coreCalendar.get(), &Akonadi::ETMCalendar::collectionsRemoved, this, &IncidenceOccurrenceModel::scheduleReset);
0331 
0332     Q_EMIT calendarChanged();
0333 
0334     scheduleReset();
0335 }
0336 
0337 Akonadi::ETMCalendar::Ptr IncidenceOccurrenceModel::calendar() const
0338 {
0339     return m_coreCalendar;
0340 }
0341 
0342 void IncidenceOccurrenceModel::loadColors()
0343 {
0344     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0345     KConfigGroup rColorsConfig(config, QStringLiteral("Resources Colors"));
0346     const QStringList colorKeyList = rColorsConfig.keyList();
0347 
0348     for (const QString &key : colorKeyList) {
0349         const auto keyId = key.toLong();
0350         QColor color = rColorsConfig.readEntry(key, QColor("blue"));
0351         m_colors[keyId] = color;
0352     }
0353 }
0354 
0355 std::pair<QDateTime, QDateTime> IncidenceOccurrenceModel::incidenceOccurrenceStartEnd(const QDateTime &ocStart, const KCalendarCore::Incidence::Ptr &incidence)
0356 {
0357     auto start = ocStart;
0358     const auto end = incidence->endDateForStart(start);
0359 
0360     if (incidence->type() == KCalendarCore::Incidence::IncidenceType::TypeTodo) {
0361         KCalendarCore::Todo::Ptr todo = incidence.staticCast<KCalendarCore::Todo>();
0362 
0363         if (!start.isValid()) { // Todos are very likely not to have a set start date
0364             start = todo->dtDue();
0365         }
0366     }
0367 
0368     return {start, end};
0369 }
0370 
0371 bool IncidenceOccurrenceModel::incidencePassesFilter(const KCalendarCore::Incidence::Ptr &incidence)
0372 {
0373     if (!mFilter || mFilter->tags().empty()) {
0374         return true;
0375     }
0376 
0377     const auto tags = mFilter->tags();
0378     return std::any_of(tags.cbegin(), tags.cend(), [&incidence](const QString &tag) {
0379         return incidence->categories().contains(tag);
0380     });
0381 }
0382 
0383 #include "moc_incidenceoccurrencemodel.cpp"