File indexing completed on 2024-05-12 05:37:11
0001 /* 0002 SPDX-FileCopyrightText: 2013 Mark Gaiser <markg85@gmail.com> 0003 SPDX-FileCopyrightText: 2016 Martin Klapetek <mklapetek@kde.org> 0004 SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "daysmodel.h" 0010 #include "eventdatadecorator.h" 0011 0012 #include <QByteArray> 0013 #include <QDebug> 0014 #include <QDir> 0015 #include <QMetaObject> 0016 0017 constexpr int maxEventDisplayed = 5; 0018 0019 class DaysModelPrivate 0020 { 0021 public: 0022 explicit DaysModelPrivate(); 0023 0024 QList<DayData> *data = nullptr; 0025 QList<QObject *> qmlData; 0026 QMultiHash<QDate, CalendarEvents::EventData> eventsData; 0027 QHash<QDate /* Gregorian */, QCalendar::YearMonthDay> alternateDatesData; 0028 QHash<QDate, CalendarEvents::CalendarEventsPlugin::SubLabel> subLabelsData; 0029 0030 QDate lastRequestedAgendaDate; 0031 bool agendaNeedsUpdate = false; 0032 0033 // QML Ownership 0034 EventPluginsManager *pluginsManager = nullptr; 0035 }; 0036 0037 DaysModelPrivate::DaysModelPrivate() 0038 { 0039 } 0040 0041 DaysModel::DaysModel(QObject *parent) 0042 : QAbstractItemModel(parent) 0043 , d(new DaysModelPrivate) 0044 { 0045 } 0046 0047 DaysModel::~DaysModel() 0048 { 0049 delete d; 0050 } 0051 0052 void DaysModel::setSourceData(QList<DayData> *data) 0053 { 0054 if (d->data != data) { 0055 beginResetModel(); 0056 d->data = data; 0057 endResetModel(); 0058 } 0059 } 0060 0061 int DaysModel::rowCount(const QModelIndex &parent) const 0062 { 0063 if (!parent.isValid()) { 0064 // day count 0065 if (d->data->size() <= 0) { 0066 return 0; 0067 } else { 0068 return d->data->size(); 0069 } 0070 } else { 0071 // event count 0072 const auto &eventDatas = data(parent, Roles::Events).value<QList<CalendarEvents::EventData>>(); 0073 Q_ASSERT(eventDatas.count() <= maxEventDisplayed); 0074 return eventDatas.count(); 0075 } 0076 } 0077 0078 int DaysModel::columnCount(const QModelIndex &parent) const 0079 { 0080 Q_UNUSED(parent) 0081 return 1; 0082 } 0083 0084 QVariant DaysModel::data(const QModelIndex &index, int role) const 0085 { 0086 if (!index.isValid()) { 0087 return {}; 0088 } 0089 0090 const int row = index.row(); 0091 0092 if (!index.parent().isValid()) { 0093 // Fetch days in month 0094 const DayData ¤tData = d->data->at(row); 0095 const QDate currentDate(currentData.yearNumber, currentData.monthNumber, currentData.dayNumber); 0096 0097 switch (role) { 0098 case isCurrent: 0099 return currentData.isCurrent; 0100 case containsEventItems: 0101 return d->eventsData.contains(currentDate); 0102 case Events: 0103 return QVariant::fromValue(d->eventsData.values(currentDate)); 0104 case EventCount: 0105 return d->eventsData.values(currentDate).count(); 0106 case containsMajorEventItems: 0107 return hasMajorEventAtDate(currentDate); 0108 case containsMinorEventItems: 0109 return hasMinorEventAtDate(currentDate); 0110 case dayNumber: 0111 return currentData.dayNumber; 0112 case monthNumber: 0113 return currentData.monthNumber; 0114 case yearNumber: 0115 return currentData.yearNumber; 0116 default: 0117 break; 0118 } 0119 0120 if (d->alternateDatesData.count(currentDate)) { 0121 switch (role) { 0122 case AlternateYearNumber: 0123 return d->alternateDatesData.value(currentDate).year; 0124 case AlternateMonthNumber: 0125 return d->alternateDatesData.value(currentDate).month; 0126 case AlternateDayNumber: 0127 return d->alternateDatesData.value(currentDate).day; 0128 default: 0129 break; 0130 } 0131 } 0132 0133 if (d->subLabelsData.count(currentDate)) { 0134 switch (role) { 0135 case SubLabel: 0136 return d->subLabelsData.value(currentDate).label; 0137 case SubYearLabel: 0138 return d->subLabelsData.value(currentDate).yearLabel; 0139 case SubMonthLabel: 0140 return d->subLabelsData.value(currentDate).monthLabel; 0141 case SubDayLabel: 0142 return d->subLabelsData.value(currentDate).dayLabel; 0143 default: 0144 break; 0145 } 0146 } 0147 } else { 0148 // Fetch event in day 0149 const auto &eventDatas = data(index.parent(), Roles::Events).value<QList<CalendarEvents::EventData>>(); 0150 if (eventDatas.count() < row) { 0151 return {}; 0152 } 0153 0154 const auto &eventData = eventDatas[row]; 0155 switch (role) { 0156 case EventColor: 0157 return eventData.eventColor(); 0158 } 0159 } 0160 return {}; 0161 } 0162 0163 void DaysModel::update() 0164 { 0165 if (d->data->size() <= 0) { 0166 return; 0167 } 0168 0169 // We need to reset the model since m_data has already been changed here 0170 // and we can't remove the events manually with beginRemoveRows() since 0171 // we don't know where the old events were located. 0172 beginResetModel(); 0173 d->eventsData.clear(); 0174 d->alternateDatesData.clear(); 0175 d->subLabelsData.clear(); 0176 endResetModel(); 0177 0178 if (d->pluginsManager) { 0179 const QDate modelFirstDay(d->data->at(0).yearNumber, d->data->at(0).monthNumber, d->data->at(0).dayNumber); 0180 const auto plugins = d->pluginsManager->plugins(); 0181 for (CalendarEvents::CalendarEventsPlugin *eventsPlugin : plugins) { 0182 eventsPlugin->loadEventsForDateRange(modelFirstDay, modelFirstDay.addDays(42)); 0183 } 0184 } 0185 0186 // We always have 42 items (or weeks * num of days in week) so we only have to tell the view that the data changed. 0187 Q_EMIT dataChanged(index(0, 0), index(d->data->count() - 1, 0)); 0188 } 0189 0190 void DaysModel::onDataReady(const QMultiHash<QDate, CalendarEvents::EventData> &data) 0191 { 0192 d->eventsData.reserve(d->eventsData.size() + data.size()); 0193 for (int i = 0; i < d->data->count(); i++) { 0194 const DayData ¤tData = d->data->at(i); 0195 const QDate currentDate(currentData.yearNumber, currentData.monthNumber, currentData.dayNumber); 0196 if (!data.values(currentDate).isEmpty()) { 0197 // Make sure we don't display more than maxEventDisplayed events. 0198 const int currentCount = d->eventsData.values(currentDate).count(); 0199 if (currentCount >= maxEventDisplayed) { 0200 break; 0201 } 0202 0203 const int addedEventCount = std::min<int>(currentCount + data.values(currentDate).count(), maxEventDisplayed) - currentCount; 0204 0205 // Add event 0206 beginInsertRows(index(i, 0), 0, addedEventCount - 1); 0207 int stopCounter = currentCount; 0208 for (const auto &dataDay : data.values(currentDate)) { 0209 if (stopCounter >= maxEventDisplayed) { 0210 break; 0211 } 0212 stopCounter++; 0213 d->eventsData.insert(currentDate, dataDay); 0214 } 0215 endInsertRows(); 0216 } 0217 } 0218 0219 if (data.contains(QDate::currentDate())) { 0220 d->agendaNeedsUpdate = true; 0221 } 0222 0223 // only the containsEventItems roles may have changed 0224 Q_EMIT dataChanged(index(0, 0), index(d->data->count() - 1, 0), {containsEventItems, containsMajorEventItems, containsMinorEventItems, Events, EventCount}); 0225 0226 Q_EMIT agendaUpdated(QDate::currentDate()); 0227 } 0228 0229 void DaysModel::onEventModified(const CalendarEvents::EventData &data) 0230 { 0231 QList<QDate> updatesList; 0232 auto i = d->eventsData.begin(); 0233 while (i != d->eventsData.end()) { 0234 if (i->uid() == data.uid()) { 0235 *i = data; 0236 updatesList << i.key(); 0237 } 0238 0239 ++i; 0240 } 0241 0242 if (!updatesList.isEmpty()) { 0243 d->agendaNeedsUpdate = true; 0244 } 0245 0246 for (const QDate date : std::as_const(updatesList)) { 0247 const QModelIndex changedIndex = indexForDate(date); 0248 if (changedIndex.isValid()) { 0249 Q_EMIT dataChanged(changedIndex, changedIndex, {containsEventItems, containsMajorEventItems, containsMinorEventItems, EventColor}); 0250 } 0251 Q_EMIT agendaUpdated(date); 0252 } 0253 } 0254 0255 void DaysModel::onEventRemoved(const QString &uid) 0256 { 0257 // HACK We should update the model with beginRemoveRows instead of 0258 // using beginResetModel() since this creates a small visual glitches 0259 // if an event is removed in Korganizer and the calendar is open. 0260 // Using beginRemoveRows instead we make the code a lot more complex 0261 // and if not done correctly will introduce bugs. 0262 beginResetModel(); 0263 QList<QDate> updatesList; 0264 auto i = d->eventsData.begin(); 0265 while (i != d->eventsData.end()) { 0266 if (i->uid() == uid) { 0267 updatesList << i.key(); 0268 i = d->eventsData.erase(i); 0269 } else { 0270 ++i; 0271 } 0272 } 0273 0274 if (!updatesList.isEmpty()) { 0275 d->agendaNeedsUpdate = true; 0276 } 0277 0278 for (const QDate date : std::as_const(updatesList)) { 0279 const QModelIndex changedIndex = indexForDate(date); 0280 if (changedIndex.isValid()) { 0281 Q_EMIT dataChanged(changedIndex, changedIndex, {containsEventItems, containsMajorEventItems, containsMinorEventItems}); 0282 } 0283 0284 Q_EMIT agendaUpdated(date); 0285 } 0286 endResetModel(); 0287 } 0288 0289 void DaysModel::onAlternateCalendarDateReady(const QHash<QDate, QCalendar::YearMonthDay> &data) 0290 { 0291 d->alternateDatesData.reserve(d->alternateDatesData.size() + data.size()); 0292 for (int i = 0; i < d->data->count(); i++) { 0293 const DayData ¤tData = d->data->at(i); 0294 const QDate currentDate(currentData.yearNumber, currentData.monthNumber, currentData.dayNumber); 0295 if (!data.contains(currentDate)) { 0296 continue; 0297 } 0298 // Add an alternate date 0299 d->alternateDatesData.insert(currentDate, data.value(currentDate)); 0300 } 0301 0302 Q_EMIT dataChanged(index(0, 0), index(d->data->count() - 1, 0), {AlternateYearNumber, AlternateMonthNumber, AlternateDayNumber}); 0303 } 0304 0305 void DaysModel::onSubLabelReady(const QHash<QDate, CalendarEvents::CalendarEventsPlugin::SubLabel> &data) 0306 { 0307 d->subLabelsData.reserve(d->subLabelsData.size() + data.size()); 0308 for (int i = 0; i < d->data->count(); i++) { 0309 const DayData ¤tData = d->data->at(i); 0310 const QDate currentDate(currentData.yearNumber, currentData.monthNumber, currentData.dayNumber); 0311 auto newValueIt = data.find(currentDate); 0312 if (newValueIt == data.end()) { 0313 continue; 0314 } 0315 // Add/Overwrite a sub-label based on priority 0316 auto oldValueIt = d->subLabelsData.find(currentDate); 0317 auto newValue = newValueIt.value(); 0318 if (oldValueIt == d->subLabelsData.end()) { 0319 // Just insert the new value 0320 d->subLabelsData.insert(currentDate, newValue); 0321 0322 } else if (newValue.priority > oldValueIt->priority) { 0323 // Sanitize labels: if the new value doesn't have dayLabel or label, keep the existing label. 0324 if (newValue.dayLabel.isEmpty()) { 0325 newValue.dayLabel = oldValueIt->dayLabel; 0326 } 0327 if (newValue.label.isEmpty()) { 0328 newValue.label = oldValueIt->label; 0329 } 0330 d->subLabelsData.insert(currentDate, newValue); 0331 0332 } else if (newValue.priority <= oldValueIt->priority) { 0333 // Fill the two empty labels 0334 if (oldValueIt->dayLabel.isEmpty()) { 0335 oldValueIt->dayLabel = newValue.dayLabel; 0336 } 0337 if (oldValueIt->label.isEmpty()) { 0338 oldValueIt->label = newValue.label; 0339 } 0340 } 0341 } 0342 0343 Q_EMIT dataChanged(index(0, 0), index(d->data->count() - 1, 0), {SubLabel, SubYearLabel, SubMonthLabel, SubDayLabel}); 0344 } 0345 0346 QList<QObject *> DaysModel::eventsForDate(const QDate &date) 0347 { 0348 if (d->lastRequestedAgendaDate == date && !d->agendaNeedsUpdate) { 0349 return d->qmlData; 0350 } 0351 0352 d->lastRequestedAgendaDate = date; 0353 qDeleteAll(d->qmlData); 0354 d->qmlData.clear(); 0355 0356 QList<CalendarEvents::EventData> events = d->eventsData.values(date); 0357 d->qmlData.reserve(events.size()); 0358 0359 // sort events by their time and type 0360 std::sort(events.begin(), events.end(), [](const CalendarEvents::EventData &a, const CalendarEvents::EventData &b) { 0361 return b.type() > a.type() || b.startDateTime() > a.startDateTime(); 0362 }); 0363 0364 for (const CalendarEvents::EventData &event : std::as_const(events)) { 0365 d->qmlData << new EventDataDecorator(event, this); 0366 } 0367 0368 d->agendaNeedsUpdate = false; 0369 return d->qmlData; 0370 } 0371 0372 QModelIndex DaysModel::indexForDate(const QDate &date) 0373 { 0374 if (!d->data) { 0375 return QModelIndex(); 0376 } 0377 0378 const DayData &firstDay = d->data->at(0); 0379 const QDate firstDate(firstDay.yearNumber, firstDay.monthNumber, firstDay.dayNumber); 0380 0381 qint64 daysTo = firstDate.daysTo(date); 0382 0383 return createIndex(daysTo, 0); 0384 } 0385 0386 bool DaysModel::hasMajorEventAtDate(const QDate &date) const 0387 { 0388 auto it = d->eventsData.find(date); 0389 while (it != d->eventsData.end() && it.key() == date) { 0390 if (!it.value().isMinor()) { 0391 return true; 0392 } 0393 ++it; 0394 } 0395 return false; 0396 } 0397 0398 bool DaysModel::hasMinorEventAtDate(const QDate &date) const 0399 { 0400 auto it = d->eventsData.find(date); 0401 while (it != d->eventsData.end() && it.key() == date) { 0402 if (it.value().isMinor()) { 0403 return true; 0404 } 0405 ++it; 0406 } 0407 return false; 0408 } 0409 0410 void DaysModel::setPluginsManager(QObject *manager) 0411 { 0412 if (d->pluginsManager) { 0413 disconnect(d->pluginsManager, nullptr, this, nullptr); 0414 } 0415 0416 EventPluginsManager *m = qobject_cast<EventPluginsManager *>(manager); 0417 0418 if (!m) { 0419 return; 0420 } 0421 0422 d->pluginsManager = m; 0423 0424 connect(d->pluginsManager, &EventPluginsManager::dataReady, this, &DaysModel::onDataReady); 0425 connect(d->pluginsManager, &EventPluginsManager::eventModified, this, &DaysModel::onEventModified); 0426 connect(d->pluginsManager, &EventPluginsManager::eventRemoved, this, &DaysModel::onEventRemoved); 0427 connect(d->pluginsManager, &EventPluginsManager::alternateCalendarDateReady, this, &DaysModel::onAlternateCalendarDateReady); 0428 connect(d->pluginsManager, &EventPluginsManager::subLabelReady, this, &DaysModel::onSubLabelReady); 0429 connect(d->pluginsManager, &EventPluginsManager::pluginsChanged, this, &DaysModel::update); 0430 0431 QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); 0432 } 0433 0434 QHash<int, QByteArray> DaysModel::roleNames() const 0435 { 0436 return { 0437 {isCurrent, "isCurrent"}, 0438 {containsEventItems, "containsEventItems"}, 0439 {containsMajorEventItems, "containsMajorEventItems"}, 0440 {containsMinorEventItems, "containsMinorEventItems"}, 0441 {dayNumber, "dayNumber"}, 0442 {monthNumber, "monthNumber"}, 0443 {yearNumber, "yearNumber"}, 0444 {EventColor, "eventColor"}, 0445 {EventCount, "eventCount"}, 0446 {Events, "events"}, 0447 {AlternateYearNumber, "alternateYearNumber"}, 0448 {AlternateMonthNumber, "alternateMonthNumber"}, 0449 {AlternateDayNumber, "alternateDayNumber"}, 0450 {SubLabel, "subLabel"}, 0451 {SubYearLabel, "subYearLabel"}, 0452 {SubMonthLabel, "subMonthLabel"}, 0453 {SubDayLabel, "subDayLabel"}, 0454 }; 0455 } 0456 0457 QModelIndex DaysModel::index(int row, int column, const QModelIndex &parent) const 0458 { 0459 if (parent.isValid()) { 0460 return createIndex(row, column, (intptr_t)parent.row()); 0461 } 0462 return createIndex(row, column, nullptr); 0463 } 0464 0465 QModelIndex DaysModel::parent(const QModelIndex &child) const 0466 { 0467 if (child.internalId()) { 0468 return createIndex(child.internalId(), 0, nullptr); 0469 } 0470 return QModelIndex(); 0471 }