File indexing completed on 2025-01-19 04:51:58
0001 /* 0002 Copyright (c) 2018 Michael Bohlender <michael.bohlender@kdemail.net> 0003 Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com> 0004 Copyright (c) 2018 RĂ©mi Nicole <minijackson@riseup.net> 0005 0006 This library is free software; you can redistribute it and/or modify it 0007 under the terms of the GNU Library General Public License as published by 0008 the Free Software Foundation; either version 2 of the License, or (at your 0009 option) any later version. 0010 0011 This library is distributed in the hope that it will be useful, but WITHOUT 0012 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 0013 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 0014 License for more details. 0015 0016 You should have received a copy of the GNU Library General Public License 0017 along with this library; see the file COPYING.LIB. If not, write to the 0018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0019 02110-1301, USA. 0020 */ 0021 0022 #include "eventoccurrencemodel.h" 0023 0024 #include <sink/log.h> 0025 #include <sink/query.h> 0026 #include <sink/store.h> 0027 #include <sink/applicationdomaintype.h> 0028 0029 #include <QMetaEnum> 0030 0031 #include <KCalendarCore/ICalFormat> 0032 #include <KCalendarCore/OccurrenceIterator> 0033 #include <KCalendarCore/MemoryCalendar> 0034 0035 #include <entitycache.h> 0036 0037 #include <algorithm> 0038 #include <vector> 0039 #include <iterator> 0040 0041 using namespace Sink; 0042 0043 EventOccurrenceModel::EventOccurrenceModel(QObject *parent) 0044 : QAbstractItemModel(parent), 0045 mCalendarCache{EntityCache<ApplicationDomain::Calendar>::Ptr::create(QByteArrayList{{ApplicationDomain::Calendar::Color::name}})}, 0046 mUpdateFromSourceDebouncer{100,[this] { this->updateFromSource(); }, this} 0047 { 0048 } 0049 0050 void EventOccurrenceModel::setStart(const QDate &start) 0051 { 0052 if (start != mStart) { 0053 mStart = start; 0054 updateQuery(); 0055 } 0056 } 0057 0058 QDate EventOccurrenceModel::start() const 0059 { 0060 return mStart; 0061 } 0062 0063 void EventOccurrenceModel::setLength(int length) 0064 { 0065 mLength = length; 0066 updateQuery(); 0067 } 0068 0069 int EventOccurrenceModel::length() const 0070 { 0071 return mLength; 0072 } 0073 0074 void EventOccurrenceModel::setCalendarFilter(const QList<QString> &calendarFilter) 0075 { 0076 mCalendarFilter.clear(); 0077 for (const auto &id : calendarFilter) { 0078 mCalendarFilter << id.toLatin1(); 0079 } 0080 updateQuery(); 0081 } 0082 0083 void EventOccurrenceModel::setFilter(const QVariantMap &filter) 0084 { 0085 mFilter = filter; 0086 updateQuery(); 0087 } 0088 0089 void EventOccurrenceModel::updateQuery() 0090 { 0091 using namespace Sink::ApplicationDomain; 0092 mInitialItemsLoaded = false; 0093 if (mCalendarFilter.isEmpty() || !mLength || !mStart.isValid()) { 0094 mSourceModel.clear(); 0095 refreshView(); 0096 return; 0097 } 0098 mEnd = mStart.addDays(mLength); 0099 0100 Sink::Query query; 0101 query.setFlags(Sink::Query::LiveQuery); 0102 query.request<Event::Summary>(); 0103 query.request<Event::Description>(); 0104 query.request<Event::StartTime>(); 0105 query.request<Event::EndTime>(); 0106 query.request<Event::Calendar>(); 0107 query.request<Event::Ical>(); 0108 query.request<Event::AllDay>(); 0109 0110 query.filter<Event::StartTime, Event::EndTime>(Sink::Query::Comparator(QVariantList{mStart, mEnd}, Sink::Query::Comparator::Overlap)); 0111 0112 query.setPostQueryFilter([=, filter = mFilter, calendarFilter = mCalendarFilter] (const ApplicationDomain::ApplicationDomainType &entity){ 0113 const Sink::ApplicationDomain::Event event(entity); 0114 if (!calendarFilter.contains(event.getCalendar())) { 0115 return false; 0116 } 0117 0118 for (auto it = filter.constBegin(); it!= filter.constEnd(); it++) { 0119 if (event.getProperty(it.key().toLatin1()) != it.value()) { 0120 return false; 0121 } 0122 } 0123 0124 return true; 0125 }); 0126 0127 mSourceModel = Store::loadModel<ApplicationDomain::Event>(query); 0128 0129 QObject::connect(mSourceModel.data(), &QAbstractItemModel::dataChanged, this, &EventOccurrenceModel::refreshView); 0130 QObject::connect(mSourceModel.data(), &QAbstractItemModel::layoutChanged, this, &EventOccurrenceModel::refreshView); 0131 QObject::connect(mSourceModel.data(), &QAbstractItemModel::modelReset, this, &EventOccurrenceModel::refreshView); 0132 QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsInserted, this, &EventOccurrenceModel::refreshView); 0133 QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsMoved, this, &EventOccurrenceModel::refreshView); 0134 QObject::connect(mSourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &EventOccurrenceModel::refreshView); 0135 0136 refreshView(); 0137 } 0138 0139 void EventOccurrenceModel::refreshView() 0140 { 0141 mUpdateFromSourceDebouncer.trigger(); 0142 } 0143 0144 void EventOccurrenceModel::updateFromSource() 0145 { 0146 QList<Occurrence> newEvents; 0147 0148 if (mSourceModel) { 0149 QMap<QByteArray, KCalendarCore::Incidence::Ptr> recurringEvents; 0150 QMultiMap<QByteArray, KCalendarCore::Incidence::Ptr> exceptions; 0151 QMap<QByteArray, QSharedPointer<Sink::ApplicationDomain::Event>> events; 0152 for (int i = 0; i < mSourceModel->rowCount(); ++i) { 0153 auto event = mSourceModel->index(i, 0).data(Sink::Store::DomainObjectRole).value<ApplicationDomain::Event::Ptr>(); 0154 Q_ASSERT(event); 0155 0156 //Parse the event 0157 auto icalEvent = KCalendarCore::ICalFormat().readIncidence(event->getIcal()).dynamicCast<KCalendarCore::Event>(); 0158 if(!icalEvent) { 0159 SinkWarning() << "Invalid ICal to process, ignoring..."; 0160 continue; 0161 } 0162 0163 //Collect recurring events and add the rest immediately 0164 if (icalEvent->recurs()) { 0165 recurringEvents.insert(icalEvent->uid().toLatin1(), icalEvent); 0166 events.insert(icalEvent->instanceIdentifier().toLatin1(), event); 0167 } else if(icalEvent->recurrenceId().isValid()) { 0168 exceptions.insert(icalEvent->uid().toLatin1(), icalEvent); 0169 events.insert(icalEvent->instanceIdentifier().toLatin1(), event); 0170 } else { 0171 if (icalEvent->dtStart().date() < mEnd && icalEvent->dtEnd().date() >= mStart) { 0172 newEvents.append({icalEvent->dtStart(), icalEvent->dtEnd(), icalEvent, getColor(event->getCalendar()), event->getAllDay(), event}); 0173 } 0174 } 0175 } 0176 //process all recurring events and their exceptions. 0177 for (const auto &uid : recurringEvents.keys()) { 0178 KCalendarCore::MemoryCalendar calendar{QTimeZone::systemTimeZone()}; 0179 calendar.addIncidence(recurringEvents.value(uid)); 0180 for (const auto &event : exceptions.values(uid)) { 0181 calendar.addIncidence(event); 0182 } 0183 KCalendarCore::OccurrenceIterator occurrenceIterator{calendar, QDateTime{mStart, {0, 0, 0}}, QDateTime{mEnd, {12, 59, 59}}}; 0184 while (occurrenceIterator.hasNext()) { 0185 occurrenceIterator.next(); 0186 const auto incidence = occurrenceIterator.incidence(); 0187 Q_ASSERT(incidence); 0188 const auto event = events.value(incidence->instanceIdentifier().toLatin1()); 0189 const auto start = occurrenceIterator.occurrenceStartDate(); 0190 const auto end = incidence->endDateForStart(start); 0191 if (start.date() < mEnd && end.date() >= mStart) { 0192 newEvents.append({start, end, incidence, getColor(event->getCalendar()), event->getAllDay(), event}); 0193 } 0194 } 0195 } 0196 //Process all exceptions that had no main event present in the current query 0197 for (const auto &uid : exceptions.keys()) { 0198 const auto icalEvent = exceptions.value(uid).dynamicCast<KCalendarCore::Event>(); 0199 Q_ASSERT(icalEvent); 0200 const auto event = events.value(icalEvent->instanceIdentifier().toLatin1()); 0201 if (icalEvent->dtStart().date() < mEnd && icalEvent->dtEnd().date() >= mStart) { 0202 newEvents.append({icalEvent->dtStart(), icalEvent->dtEnd(), icalEvent, getColor(event->getCalendar()), event->getAllDay(), event}); 0203 } 0204 } 0205 } 0206 0207 { 0208 auto it = std::begin(mEvents); 0209 while (it != std::end(mEvents)) { 0210 const auto event = *it; 0211 auto itToRemove = std::find_if(std::begin(newEvents), std::end(newEvents), [&] (const auto &e) { 0212 Q_ASSERT(e.incidence); 0213 Q_ASSERT(event.incidence); 0214 return e.incidence->uid() == event.incidence->uid() && e.start == event.start; 0215 }); 0216 // Can't find the vevent in newEvents anymore, so remove from list 0217 if (itToRemove == std::end(newEvents)) { 0218 //Removed 0219 const int startIndex = std::distance(std::begin(mEvents), it); 0220 beginRemoveRows(QModelIndex(), startIndex, startIndex); 0221 it = mEvents.erase(it); 0222 endRemoveRows(); 0223 } else { 0224 it++; 0225 } 0226 } 0227 } 0228 0229 for (auto newIt = std::begin(newEvents); newIt != std::end(newEvents); newIt++) { 0230 const auto event = *newIt; 0231 auto it = std::find_if(std::begin(mEvents), std::end(mEvents), [&] (const auto &e) { 0232 Q_ASSERT(e.incidence); 0233 return e.incidence->uid() == event.incidence->uid() && e.start == event.start; 0234 }); 0235 if (it == std::end(mEvents)) { 0236 //New event 0237 const int startIndex = std::distance(std::begin(newEvents), newIt); 0238 beginInsertRows(QModelIndex(), startIndex, startIndex); 0239 mEvents.insert(startIndex, event); 0240 endInsertRows(); 0241 } else { 0242 if (*(newIt->incidence) != *(it->incidence)) { 0243 const int startIndex = std::distance(std::begin(mEvents), it); 0244 mEvents[startIndex] = event; 0245 emit dataChanged(index(startIndex, 0), index(startIndex, 0), {}); 0246 } 0247 } 0248 } 0249 0250 if (!mInitialItemsLoaded && (!mSourceModel || mSourceModel->data({}, Sink::Store::ChildrenFetchedRole).toBool())) { 0251 mInitialItemsLoaded = true; 0252 emit initialItemsLoaded(); 0253 } 0254 } 0255 0256 0257 bool EventOccurrenceModel::initialItemsComplete() const 0258 { 0259 return mInitialItemsLoaded; 0260 } 0261 0262 QModelIndex EventOccurrenceModel::index(int row, int column, const QModelIndex &parent) const 0263 { 0264 if (!hasIndex(row, column, parent)) { 0265 return {}; 0266 } 0267 0268 if (!parent.isValid()) { 0269 return createIndex(row, column); 0270 } 0271 return {}; 0272 } 0273 0274 QModelIndex EventOccurrenceModel::parent(const QModelIndex &) const 0275 { 0276 return {}; 0277 } 0278 0279 int EventOccurrenceModel::rowCount(const QModelIndex &parent) const 0280 { 0281 if (!parent.isValid()) { 0282 return mEvents.size(); 0283 } 0284 return 0; 0285 } 0286 0287 int EventOccurrenceModel::columnCount(const QModelIndex &) const 0288 { 0289 return 1; 0290 } 0291 0292 QByteArray EventOccurrenceModel::getColor(const QByteArray &calendar) const 0293 { 0294 const auto color = mCalendarCache->getProperty(calendar, "color").toByteArray(); 0295 if (color.isEmpty()) { 0296 qWarning() << "Failed to get color for calendar " << calendar; 0297 } 0298 return color; 0299 } 0300 0301 QVariant EventOccurrenceModel::data(const QModelIndex &idx, int role) const 0302 { 0303 if (!hasIndex(idx.row(), idx.column())) { 0304 return {}; 0305 } 0306 auto event = mEvents.at(idx.row()); 0307 auto icalEvent = event.incidence; 0308 switch (role) { 0309 case Summary: 0310 return icalEvent->summary(); 0311 case Description: 0312 return icalEvent->description(); 0313 case StartTime: 0314 return event.start; 0315 case EndTime: 0316 return event.end; 0317 case Color: 0318 return event.color; 0319 case AllDay: 0320 return event.allDay; 0321 case Event: 0322 return QVariant::fromValue(event.domainObject); 0323 case EventOccurrence: 0324 return QVariant::fromValue(event); 0325 default: 0326 SinkWarning() << "Unknown role for event:" << QMetaEnum::fromType<Roles>().valueToKey(role); 0327 return {}; 0328 } 0329 } 0330