File indexing completed on 2024-11-24 04:41:36

0001 /*
0002   SPDX-FileCopyrightText: 2008 Bruno Virlet <bruno.virlet@gmail.com>
0003   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0004   SPDX-FileContributor: Bertjan Broeksema <broeksema@kde.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 
0009 #include "monthview.h"
0010 #include "monthgraphicsitems.h"
0011 #include "monthitem.h"
0012 #include "monthscene.h"
0013 #include "prefs.h"
0014 
0015 #include <Akonadi/CalendarBase>
0016 #include <CalendarSupport/CollectionSelection>
0017 #include <CalendarSupport/KCalPrefs>
0018 #include <CalendarSupport/Utils>
0019 
0020 #include "calendarview_debug.h"
0021 #include <KCalendarCore/OccurrenceIterator>
0022 #include <KCheckableProxyModel>
0023 #include <KLocalizedString>
0024 #include <QIcon>
0025 
0026 #include <QHBoxLayout>
0027 #include <QTimer>
0028 #include <QToolButton>
0029 #include <QWheelEvent>
0030 
0031 #include <ranges>
0032 
0033 using namespace EventViews;
0034 
0035 namespace EventViews
0036 {
0037 class MonthViewPrivate : public KCalendarCore::Calendar::CalendarObserver
0038 {
0039     MonthView *const q;
0040 
0041 public: /// Methods
0042     explicit MonthViewPrivate(MonthView *qq);
0043 
0044     MonthItem *loadCalendarIncidences(const Akonadi::CollectionCalendar::Ptr &calendar, const QDateTime &startDt, const QDateTime &endDt);
0045 
0046     void addIncidence(const Akonadi::Item &incidence);
0047     void moveStartDate(int weeks, int months);
0048     // void setUpModels();
0049     void triggerDelayedReload(EventView::Change reason);
0050 
0051 public: /// Members
0052     QTimer reloadTimer;
0053     MonthScene *scene = nullptr;
0054     QDate selectedItemDate;
0055     Akonadi::Item::Id selectedItemId;
0056     MonthGraphicsView *view = nullptr;
0057     QToolButton *fullView = nullptr;
0058 
0059     // List of uids for QDate
0060     QMap<QDate, QStringList> mBusyDays;
0061 
0062 protected:
0063     /* reimplemented from KCalendarCore::Calendar::CalendarObserver */
0064     void calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence) override;
0065     void calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence) override;
0066     void calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar) override;
0067 
0068 private:
0069     // quiet --overloaded-virtual warning
0070     using KCalendarCore::Calendar::CalendarObserver::calendarIncidenceDeleted;
0071 };
0072 }
0073 
0074 MonthViewPrivate::MonthViewPrivate(MonthView *qq)
0075     : q(qq)
0076     , scene(new MonthScene(qq))
0077     , selectedItemId(-1)
0078     , view(new MonthGraphicsView(qq))
0079     , fullView(nullptr)
0080 {
0081     reloadTimer.setSingleShot(true);
0082     view->setScene(scene);
0083 }
0084 
0085 MonthItem *MonthViewPrivate::loadCalendarIncidences(const Akonadi::CollectionCalendar::Ptr &calendar, const QDateTime &startDt, const QDateTime &endDt)
0086 {
0087     MonthItem *itemToReselect = nullptr;
0088 
0089     const bool colorMonthBusyDays = q->preferences()->colorMonthBusyDays();
0090 
0091     KCalendarCore::OccurrenceIterator occurIter(*calendar, startDt, endDt);
0092     while (occurIter.hasNext()) {
0093         occurIter.next();
0094 
0095         // Remove the two checks when filtering is done through a proxyModel, when using calendar search
0096         if (!q->preferences()->showTodosMonthView() && occurIter.incidence()->type() == KCalendarCore::Incidence::TypeTodo) {
0097             continue;
0098         }
0099         if (!q->preferences()->showJournalsMonthView() && occurIter.incidence()->type() == KCalendarCore::Incidence::TypeJournal) {
0100             continue;
0101         }
0102 
0103         const bool busyDay = colorMonthBusyDays && q->makesWholeDayBusy(occurIter.incidence());
0104         if (busyDay) {
0105             QStringList &list = mBusyDays[occurIter.occurrenceStartDate().date()];
0106             list.append(occurIter.incidence()->uid());
0107         }
0108 
0109         const Akonadi::Item item = calendar->item(occurIter.incidence());
0110         if (!item.isValid()) {
0111             continue;
0112         }
0113         Q_ASSERT(item.isValid());
0114         Q_ASSERT(item.hasPayload());
0115         MonthItem *manager = new IncidenceMonthItem(scene, calendar, item, occurIter.incidence(), occurIter.occurrenceStartDate().toLocalTime().date());
0116         scene->mManagerList.push_back(manager);
0117         if (selectedItemId == item.id() && manager->realStartDate() == selectedItemDate) {
0118             // only select it outside the loop because we are still creating items
0119             itemToReselect = manager;
0120         }
0121     }
0122 
0123     return itemToReselect;
0124 }
0125 
0126 void MonthViewPrivate::addIncidence(const Akonadi::Item &incidence)
0127 {
0128     Q_UNUSED(incidence)
0129     // TODO: add some more intelligence here...
0130     q->setChanges(q->changes() | EventView::IncidencesAdded);
0131     reloadTimer.start(50);
0132 }
0133 
0134 void MonthViewPrivate::moveStartDate(int weeks, int months)
0135 {
0136     auto start = q->startDateTime();
0137     auto end = q->endDateTime();
0138     start = start.addDays(weeks * 7);
0139     end = end.addDays(weeks * 7);
0140     start = start.addMonths(months);
0141     end = end.addMonths(months);
0142 
0143     KCalendarCore::DateList dateList;
0144     QDate d = start.date();
0145     const QDate e = end.date();
0146     dateList.reserve(d.daysTo(e) + 1);
0147     while (d <= e) {
0148         dateList.append(d);
0149         d = d.addDays(1);
0150     }
0151 
0152     /**
0153      * If we call q->setDateRange( start, end ); directly,
0154      * it will change the selected dates in month view,
0155      * but the application won't know about it.
0156      * The correct way is to Q_EMIT datesSelected()
0157      * #250256
0158      * */
0159     Q_EMIT q->datesSelected(dateList);
0160 }
0161 
0162 void MonthViewPrivate::triggerDelayedReload(EventView::Change reason)
0163 {
0164     q->setChanges(q->changes() | reason);
0165     if (!reloadTimer.isActive()) {
0166         reloadTimer.start(50);
0167     }
0168 }
0169 
0170 void MonthViewPrivate::calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &)
0171 {
0172     triggerDelayedReload(MonthView::IncidencesAdded);
0173 }
0174 
0175 void MonthViewPrivate::calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &)
0176 {
0177     triggerDelayedReload(MonthView::IncidencesEdited);
0178 }
0179 
0180 void MonthViewPrivate::calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar)
0181 {
0182     Q_UNUSED(calendar)
0183     Q_ASSERT(!incidence->uid().isEmpty());
0184     scene->removeIncidence(incidence->uid());
0185 }
0186 
0187 /// MonthView
0188 
0189 MonthView::MonthView(NavButtonsVisibility visibility, QWidget *parent)
0190     : EventView(parent)
0191     , d(new MonthViewPrivate(this))
0192 {
0193     auto topLayout = new QHBoxLayout(this);
0194     topLayout->addWidget(d->view);
0195     topLayout->setContentsMargins(0, 0, 0, 0);
0196 
0197     if (visibility == Visible) {
0198         auto rightLayout = new QVBoxLayout();
0199         rightLayout->setSpacing(0);
0200         rightLayout->setContentsMargins(0, 0, 0, 0);
0201 
0202         // push buttons to the bottom
0203         rightLayout->addStretch(1);
0204 
0205         d->fullView = new QToolButton(this);
0206         d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen")));
0207         d->fullView->setAutoRaise(true);
0208         d->fullView->setCheckable(true);
0209         d->fullView->setChecked(preferences()->fullViewMonth());
0210         d->fullView->isChecked() ? d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a normal size"))
0211                                  : d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a full window"));
0212         d->fullView->setWhatsThis(i18nc("@info:whatsthis",
0213                                         "Click this button and the month view will be enlarged to fill the "
0214                                         "maximum available window space / or shrunk back to its normal size."));
0215         connect(d->fullView, &QAbstractButton::clicked, this, &MonthView::changeFullView);
0216 
0217         auto minusMonth = new QToolButton(this);
0218         minusMonth->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up-double")));
0219         minusMonth->setAutoRaise(true);
0220         minusMonth->setToolTip(i18nc("@info:tooltip", "Go back one month"));
0221         minusMonth->setWhatsThis(i18nc("@info:whatsthis", "Click this button and the view will be scrolled back in time by 1 month."));
0222         connect(minusMonth, &QAbstractButton::clicked, this, &MonthView::moveBackMonth);
0223 
0224         auto minusWeek = new QToolButton(this);
0225         minusWeek->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
0226         minusWeek->setAutoRaise(true);
0227         minusWeek->setToolTip(i18nc("@info:tooltip", "Go back one week"));
0228         minusWeek->setWhatsThis(i18nc("@info:whatsthis", "Click this button and the view will be scrolled back in time by 1 week."));
0229         connect(minusWeek, &QAbstractButton::clicked, this, &MonthView::moveBackWeek);
0230 
0231         auto plusWeek = new QToolButton(this);
0232         plusWeek->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
0233         plusWeek->setAutoRaise(true);
0234         plusWeek->setToolTip(i18nc("@info:tooltip", "Go forward one week"));
0235         plusWeek->setWhatsThis(i18nc("@info:whatsthis", "Click this button and the view will be scrolled forward in time by 1 week."));
0236         connect(plusWeek, &QAbstractButton::clicked, this, &MonthView::moveFwdWeek);
0237 
0238         auto plusMonth = new QToolButton(this);
0239         plusMonth->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down-double")));
0240         plusMonth->setAutoRaise(true);
0241         plusMonth->setToolTip(i18nc("@info:tooltip", "Go forward one month"));
0242         plusMonth->setWhatsThis(i18nc("@info:whatsthis", "Click this button and the view will be scrolled forward in time by 1 month."));
0243         connect(plusMonth, &QAbstractButton::clicked, this, &MonthView::moveFwdMonth);
0244 
0245         rightLayout->addWidget(d->fullView);
0246         rightLayout->addWidget(minusMonth);
0247         rightLayout->addWidget(minusWeek);
0248         rightLayout->addWidget(plusWeek);
0249         rightLayout->addWidget(plusMonth);
0250 
0251         topLayout->addLayout(rightLayout);
0252     } else {
0253         d->view->setFrameStyle(QFrame::NoFrame);
0254     }
0255 
0256     connect(d->scene, &MonthScene::showIncidencePopupSignal, this, &MonthView::showIncidencePopupSignal);
0257 
0258     connect(d->scene, &MonthScene::incidenceSelected, this, &EventView::incidenceSelected);
0259 
0260     connect(d->scene, &MonthScene::newEventSignal, this, qOverload<>(&EventView::newEventSignal));
0261 
0262     connect(d->scene, &MonthScene::showNewEventPopupSignal, this, &MonthView::showNewEventPopupSignal);
0263 
0264     connect(&d->reloadTimer, &QTimer::timeout, this, &MonthView::reloadIncidences);
0265     updateConfig();
0266 
0267     // d->setUpModels();
0268     d->reloadTimer.start(50);
0269 }
0270 
0271 MonthView::~MonthView()
0272 {
0273     for (auto &calendar : calendars()) {
0274         calendar->unregisterObserver(d.get());
0275     }
0276 }
0277 
0278 void MonthView::addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0279 {
0280     EventView::addCalendar(calendar);
0281     calendar->registerObserver(d.get());
0282     setChanges(changes() | ResourcesChanged);
0283     d->reloadTimer.start(50);
0284 }
0285 
0286 void MonthView::removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0287 {
0288     EventView::removeCalendar(calendar);
0289     calendar->unregisterObserver(d.get());
0290     setChanges(changes() | ResourcesChanged);
0291     d->reloadTimer.start(50);
0292 }
0293 
0294 void MonthView::updateConfig()
0295 {
0296     d->scene->update();
0297     setChanges(changes() | ConfigChanged);
0298     d->reloadTimer.start(50);
0299 }
0300 
0301 int MonthView::currentDateCount() const
0302 {
0303     return actualStartDateTime().date().daysTo(actualEndDateTime().date());
0304 }
0305 
0306 KCalendarCore::DateList MonthView::selectedIncidenceDates() const
0307 {
0308     KCalendarCore::DateList list;
0309     if (d->scene->selectedItem()) {
0310         auto tmp = qobject_cast<IncidenceMonthItem *>(d->scene->selectedItem());
0311         if (tmp) {
0312             QDate selectedItemDate = tmp->realStartDate();
0313             if (selectedItemDate.isValid()) {
0314                 list << selectedItemDate;
0315             }
0316         }
0317     } else if (d->scene->selectedCell()) {
0318         list << d->scene->selectedCell()->date();
0319     }
0320 
0321     return list;
0322 }
0323 
0324 QDateTime MonthView::selectionStart() const
0325 {
0326     if (d->scene->selectedCell()) {
0327         return QDateTime(d->scene->selectedCell()->date().startOfDay());
0328     } else {
0329         return {};
0330     }
0331 }
0332 
0333 QDateTime MonthView::selectionEnd() const
0334 {
0335     // Only one cell can be selected (for now)
0336     return selectionStart();
0337 }
0338 
0339 void MonthView::setDateRange(const QDateTime &start, const QDateTime &end, const QDate &preferredMonth)
0340 {
0341     EventView::setDateRange(start, end, preferredMonth);
0342     setChanges(changes() | DatesChanged);
0343     d->reloadTimer.start(50);
0344 }
0345 
0346 bool MonthView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const
0347 {
0348     if (d->scene->selectedCell()) {
0349         startDt.setDate(d->scene->selectedCell()->date());
0350         endDt.setDate(d->scene->selectedCell()->date());
0351         allDay = true;
0352         return true;
0353     }
0354 
0355     return false;
0356 }
0357 
0358 void MonthView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
0359 {
0360     Q_UNUSED(incidenceList)
0361     Q_UNUSED(date)
0362 }
0363 
0364 void MonthView::changeIncidenceDisplay(const Akonadi::Item &incidence, int action)
0365 {
0366     Q_UNUSED(incidence)
0367     Q_UNUSED(action)
0368 
0369     // TODO: add some more intelligence here...
0370 
0371     // don't call reloadIncidences() directly. It would delete all
0372     // MonthItems, but this changeIncidenceDisplay()-method was probably
0373     // called by one of the MonthItem objects. So only schedule a reload
0374     // as event
0375     setChanges(changes() | IncidencesEdited);
0376     d->reloadTimer.start(50);
0377 }
0378 
0379 void MonthView::updateView()
0380 {
0381     d->view->update();
0382 }
0383 
0384 #ifndef QT_NO_WHEELEVENT
0385 void MonthView::wheelEvent(QWheelEvent *event)
0386 {
0387     // invert direction to get scroll-like behaviour
0388     if (event->angleDelta().y() > 0) {
0389         d->moveStartDate(-1, 0);
0390     } else if (event->angleDelta().y() < 0) {
0391         d->moveStartDate(1, 0);
0392     }
0393 
0394     // call accept in every case, we do not want anybody else to react
0395     event->accept();
0396 }
0397 
0398 #endif
0399 
0400 void MonthView::keyPressEvent(QKeyEvent *event)
0401 {
0402     if (event->key() == Qt::Key_PageUp) {
0403         d->moveStartDate(0, -1);
0404         event->accept();
0405     } else if (event->key() == Qt::Key_PageDown) {
0406         d->moveStartDate(0, 1);
0407         event->accept();
0408     } else if (processKeyEvent(event)) {
0409         event->accept();
0410     } else {
0411         event->ignore();
0412     }
0413 }
0414 
0415 void MonthView::keyReleaseEvent(QKeyEvent *event)
0416 {
0417     if (processKeyEvent(event)) {
0418         event->accept();
0419     } else {
0420         event->ignore();
0421     }
0422 }
0423 
0424 void MonthView::changeFullView()
0425 {
0426     bool fullView = d->fullView->isChecked();
0427 
0428     if (fullView) {
0429         d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-restore")));
0430         d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a normal size"));
0431     } else {
0432         d->fullView->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen")));
0433         d->fullView->setToolTip(i18nc("@info:tooltip", "Display calendar in a full window"));
0434     }
0435     preferences()->setFullViewMonth(fullView);
0436     preferences()->writeConfig();
0437 
0438     Q_EMIT fullViewChanged(fullView);
0439 }
0440 
0441 void MonthView::moveBackMonth()
0442 {
0443     d->moveStartDate(0, -1);
0444 }
0445 
0446 void MonthView::moveBackWeek()
0447 {
0448     d->moveStartDate(-1, 0);
0449 }
0450 
0451 void MonthView::moveFwdWeek()
0452 {
0453     d->moveStartDate(1, 0);
0454 }
0455 
0456 void MonthView::moveFwdMonth()
0457 {
0458     d->moveStartDate(0, 1);
0459 }
0460 
0461 void MonthView::showDates(const QDate &start, const QDate &end, const QDate &preferedMonth)
0462 {
0463     Q_UNUSED(start)
0464     Q_UNUSED(end)
0465     Q_UNUSED(preferedMonth)
0466     d->triggerDelayedReload(DatesChanged);
0467 }
0468 
0469 QPair<QDateTime, QDateTime> MonthView::actualDateRange(const QDateTime &start, const QDateTime &, const QDate &preferredMonth) const
0470 {
0471     QDateTime dayOne = preferredMonth.isValid() ? QDateTime(preferredMonth.startOfDay()) : start;
0472 
0473     dayOne.setDate(QDate(dayOne.date().year(), dayOne.date().month(), 1));
0474     const int weekdayCol = (dayOne.date().dayOfWeek() + 7 - preferences()->firstDayOfWeek()) % 7;
0475     QDateTime actualStart = dayOne.addDays(-weekdayCol);
0476     actualStart.setTime(QTime(0, 0, 0, 0));
0477     QDateTime actualEnd = actualStart.addDays(6 * 7 - 1);
0478     actualEnd.setTime(QTime(23, 59, 59, 99));
0479     return qMakePair(actualStart, actualEnd);
0480 }
0481 
0482 Akonadi::Item::List MonthView::selectedIncidences() const
0483 {
0484     Akonadi::Item::List selected;
0485     if (d->scene->selectedItem()) {
0486         auto tmp = qobject_cast<IncidenceMonthItem *>(d->scene->selectedItem());
0487         if (tmp) {
0488             Akonadi::Item incidenceSelected = tmp->akonadiItem();
0489             if (incidenceSelected.isValid()) {
0490                 selected.append(incidenceSelected);
0491             }
0492         }
0493     }
0494     return selected;
0495 }
0496 
0497 KHolidays::Holiday::List MonthView::holidays(QDate startDate, QDate endDate)
0498 {
0499     KHolidays::Holiday::List holidays;
0500     auto const regions = CalendarSupport::KCalPrefs::instance()->mHolidays;
0501     for (auto const &r : regions) {
0502         KHolidays::HolidayRegion region(r);
0503         if (region.isValid()) {
0504             holidays += region.rawHolidaysWithAstroSeasons(startDate, endDate);
0505         }
0506     }
0507     return holidays;
0508 }
0509 
0510 void MonthView::reloadIncidences()
0511 {
0512     if (changes() == NothingChanged) {
0513         return;
0514     }
0515     // keep selection if it exists
0516     Akonadi::Item incidenceSelected;
0517 
0518     MonthItem *itemToReselect = nullptr;
0519 
0520     if (auto tmp = qobject_cast<IncidenceMonthItem *>(d->scene->selectedItem())) {
0521         d->selectedItemId = tmp->akonadiItem().id();
0522         d->selectedItemDate = tmp->realStartDate();
0523         if (!d->selectedItemDate.isValid()) {
0524             return;
0525         }
0526     }
0527 
0528     d->scene->resetAll();
0529     d->mBusyDays.clear();
0530     // build monthcells hash
0531     int i = 0;
0532     for (QDate date = actualStartDateTime().date(); date <= actualEndDateTime().date(); date = date.addDays(1)) {
0533         d->scene->mMonthCellMap[date] = new MonthCell(i, date, d->scene);
0534         i++;
0535     }
0536 
0537     // build global event list
0538     for (const auto &calendar : calendars()) {
0539         auto *newItemToReselect = d->loadCalendarIncidences(calendar, actualStartDateTime(), actualEndDateTime());
0540         if (itemToReselect == nullptr) {
0541             itemToReselect = newItemToReselect;
0542         }
0543     }
0544 
0545     if (itemToReselect) {
0546         d->scene->selectItem(itemToReselect);
0547     }
0548 
0549     // add holidays
0550     for (auto const &h : holidays(actualStartDateTime().date(), actualEndDateTime().date())) {
0551         if (h.dayType() == KHolidays::Holiday::NonWorkday) {
0552             MonthItem *holidayItem = new HolidayMonthItem(d->scene, h.observedStartDate(), h.observedEndDate(), h.name());
0553             d->scene->mManagerList << holidayItem;
0554         }
0555     }
0556 
0557     // sort it
0558     std::sort(d->scene->mManagerList.begin(), d->scene->mManagerList.end(), MonthItem::greaterThan);
0559 
0560     // build each month's cell event list
0561     for (MonthItem *manager : std::as_const(d->scene->mManagerList)) {
0562         for (QDate date = manager->startDate(); date <= manager->endDate(); date = date.addDays(1)) {
0563             MonthCell *cell = d->scene->mMonthCellMap.value(date);
0564             if (cell) {
0565                 cell->mMonthItemList << manager;
0566             }
0567         }
0568     }
0569 
0570     for (MonthItem *manager : std::as_const(d->scene->mManagerList)) {
0571         manager->updateMonthGraphicsItems();
0572         manager->updatePosition();
0573     }
0574 
0575     for (MonthItem *manager : std::as_const(d->scene->mManagerList)) {
0576         manager->updateGeometry();
0577     }
0578 
0579     d->scene->setInitialized(true);
0580     d->view->update();
0581     d->scene->update();
0582 }
0583 
0584 void MonthView::calendarReset()
0585 {
0586     qCDebug(CALENDARVIEW_LOG);
0587     d->triggerDelayedReload(ResourcesChanged);
0588 }
0589 
0590 QDate MonthView::averageDate() const
0591 {
0592     return actualStartDateTime().date().addDays(actualStartDateTime().date().daysTo(actualEndDateTime().date()) / 2);
0593 }
0594 
0595 int MonthView::currentMonth() const
0596 {
0597     return averageDate().month();
0598 }
0599 
0600 bool MonthView::usesFullWindow()
0601 {
0602     return preferences()->fullViewMonth();
0603 }
0604 
0605 bool MonthView::isBusyDay(QDate day) const
0606 {
0607     return !d->mBusyDays[day].isEmpty();
0608 }
0609 
0610 #include "moc_monthview.cpp"