File indexing completed on 2024-05-12 17:08:53

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 */, QDate> 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 &currentData = 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 &currentData = 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::onAlternateDateReady(const QHash<QDate, QDate> &data)
0290 {
0291     d->alternateDatesData.reserve(d->alternateDatesData.size() + data.size());
0292     for (int i = 0; i < d->data->count(); i++) {
0293         const DayData &currentData = 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 &currentData = d->data->at(i);
0310         const QDate currentDate(currentData.yearNumber, currentData.monthNumber, currentData.dayNumber);
0311         if (!data.contains(currentDate)) {
0312             continue;
0313         }
0314         // Add/Overwrite a sub-label based on priority
0315         if (const auto &value = data.value(currentDate);
0316             !d->subLabelsData.count(currentDate) || (d->subLabelsData.count(currentDate) && value.priority > d->subLabelsData.value(currentDate).priority)) {
0317             d->subLabelsData.insert(currentDate, value);
0318         }
0319     }
0320 
0321     Q_EMIT dataChanged(index(0, 0), index(d->data->count() - 1, 0), {SubLabel, SubYearLabel, SubMonthLabel, SubDayLabel});
0322 }
0323 
0324 QList<QObject *> DaysModel::eventsForDate(const QDate &date)
0325 {
0326     if (d->lastRequestedAgendaDate == date && !d->agendaNeedsUpdate) {
0327         return d->qmlData;
0328     }
0329 
0330     d->lastRequestedAgendaDate = date;
0331     qDeleteAll(d->qmlData);
0332     d->qmlData.clear();
0333 
0334     QList<CalendarEvents::EventData> events = d->eventsData.values(date);
0335     d->qmlData.reserve(events.size());
0336 
0337     // sort events by their time and type
0338     std::sort(events.begin(), events.end(), [](const CalendarEvents::EventData &a, const CalendarEvents::EventData &b) {
0339         return b.type() > a.type() || b.startDateTime() > a.startDateTime();
0340     });
0341 
0342     for (const CalendarEvents::EventData &event : std::as_const(events)) {
0343         d->qmlData << new EventDataDecorator(event, this);
0344     }
0345 
0346     d->agendaNeedsUpdate = false;
0347     return d->qmlData;
0348 }
0349 
0350 QModelIndex DaysModel::indexForDate(const QDate &date)
0351 {
0352     if (!d->data) {
0353         return QModelIndex();
0354     }
0355 
0356     const DayData &firstDay = d->data->at(0);
0357     const QDate firstDate(firstDay.yearNumber, firstDay.monthNumber, firstDay.dayNumber);
0358 
0359     qint64 daysTo = firstDate.daysTo(date);
0360 
0361     return createIndex(daysTo, 0);
0362 }
0363 
0364 bool DaysModel::hasMajorEventAtDate(const QDate &date) const
0365 {
0366     auto it = d->eventsData.find(date);
0367     while (it != d->eventsData.end() && it.key() == date) {
0368         if (!it.value().isMinor()) {
0369             return true;
0370         }
0371         ++it;
0372     }
0373     return false;
0374 }
0375 
0376 bool DaysModel::hasMinorEventAtDate(const QDate &date) const
0377 {
0378     auto it = d->eventsData.find(date);
0379     while (it != d->eventsData.end() && it.key() == date) {
0380         if (it.value().isMinor()) {
0381             return true;
0382         }
0383         ++it;
0384     }
0385     return false;
0386 }
0387 
0388 void DaysModel::setPluginsManager(QObject *manager)
0389 {
0390     if (d->pluginsManager) {
0391         disconnect(d->pluginsManager, nullptr, this, nullptr);
0392     }
0393 
0394     EventPluginsManager *m = qobject_cast<EventPluginsManager *>(manager);
0395 
0396     if (!m) {
0397         return;
0398     }
0399 
0400     d->pluginsManager = m;
0401 
0402     connect(d->pluginsManager, &EventPluginsManager::dataReady, this, &DaysModel::onDataReady);
0403     connect(d->pluginsManager, &EventPluginsManager::eventModified, this, &DaysModel::onEventModified);
0404     connect(d->pluginsManager, &EventPluginsManager::eventRemoved, this, &DaysModel::onEventRemoved);
0405     connect(d->pluginsManager, &EventPluginsManager::alternateDateReady, this, &DaysModel::onAlternateDateReady);
0406     connect(d->pluginsManager, &EventPluginsManager::subLabelReady, this, &DaysModel::onSubLabelReady);
0407     connect(d->pluginsManager, &EventPluginsManager::pluginsChanged, this, &DaysModel::update);
0408 
0409     QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
0410 }
0411 
0412 QHash<int, QByteArray> DaysModel::roleNames() const
0413 {
0414     return {
0415         {isCurrent, "isCurrent"},
0416         {containsEventItems, "containsEventItems"},
0417         {containsMajorEventItems, "containsMajorEventItems"},
0418         {containsMinorEventItems, "containsMinorEventItems"},
0419         {dayNumber, "dayNumber"},
0420         {monthNumber, "monthNumber"},
0421         {yearNumber, "yearNumber"},
0422         {EventColor, "eventColor"},
0423         {EventCount, "eventCount"},
0424         {Events, "events"},
0425         {AlternateYearNumber, "alternateYearNumber"},
0426         {AlternateMonthNumber, "alternateMonthNumber"},
0427         {AlternateDayNumber, "alternateDayNumber"},
0428         {SubLabel, "subLabel"},
0429         {SubYearLabel, "subYearLabel"},
0430         {SubMonthLabel, "subMonthLabel"},
0431         {SubDayLabel, "subDayLabel"},
0432     };
0433 }
0434 
0435 QModelIndex DaysModel::index(int row, int column, const QModelIndex &parent) const
0436 {
0437     if (parent.isValid()) {
0438         return createIndex(row, column, (intptr_t)parent.row());
0439     }
0440     return createIndex(row, column, nullptr);
0441 }
0442 
0443 QModelIndex DaysModel::parent(const QModelIndex &child) const
0444 {
0445     if (child.internalId()) {
0446         return createIndex(child.internalId(), 0, nullptr);
0447     }
0448     return QModelIndex();
0449 }
0450 
0451 Q_DECLARE_METATYPE(CalendarEvents::EventData)