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"