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"