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 &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::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 &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         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 }