Eventviews

agendaview.cpp
1/*
2 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
3 SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
4 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
5 SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
6 SPDX-FileContributor: Kevin Krammer <krake@kdab.com>
7 SPDX-FileContributor: Sergio Martins <sergio@kdab.com>
8
9 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
10*/
11#include "agendaview.h"
12#include "agenda.h"
13#include "agendaitem.h"
14#include "alternatelabel.h"
15#include "calendardecoration.h"
16#include "decorationlabel.h"
17#include "prefs.h"
18#include "timelabels.h"
19#include "timelabelszone.h"
20
21#include "calendarview_debug.h"
22
23#include <Akonadi/CalendarUtils>
24#include <Akonadi/EntityTreeModel>
25#include <Akonadi/IncidenceChanger>
26#include <CalendarSupport/CollectionSelection>
27#include <CalendarSupport/KCalPrefs>
28#include <CalendarSupport/Utils>
29
30#include <KCalendarCore/CalFilter>
31#include <KCalendarCore/CalFormat>
32#include <KCalendarCore/OccurrenceIterator>
33
34#include <KIconLoader> // for SmallIcon()
35#include <KMessageBox>
36#include <KPluginFactory>
37#include <KSqueezedTextLabel>
38
39#include <KLocalizedString>
40#include <QApplication>
41#include <QDrag>
42#include <QGridLayout>
43#include <QLabel>
44#include <QPainter>
45#include <QScrollBar>
46#include <QSplitter>
47#include <QStyle>
48#include <QTimer>
49
50#include <chrono>
51#include <vector>
52
53using namespace std::chrono_literals;
54
55using namespace EventViews;
56
57enum {
58 SPACING = 2
59};
60enum {
61 SHRINKDOWN = 2 // points less for the timezone font
62};
63
64// Layout which places the widgets in equally sized columns,
65// matching the calculation of the columns in the agenda.
66class AgendaHeaderLayout : public QLayout
67{
68public:
69 explicit AgendaHeaderLayout(QWidget *parent);
70 ~AgendaHeaderLayout() override;
71
72public: // QLayout API
73 int count() const override;
74 QLayoutItem *itemAt(int index) const override;
75
76 void addItem(QLayoutItem *item) override;
77 QLayoutItem *takeAt(int index) override;
78
79public: // QLayoutItem API
80 QSize sizeHint() const override;
81 QSize minimumSize() const override;
82
83 void invalidate() override;
84 void setGeometry(const QRect &rect) override;
85
86private:
87 void updateCache() const;
88
89private:
91
92 mutable bool mIsDirty = false;
93 mutable QSize mSizeHint;
94 mutable QSize mMinSize;
95};
96
97AgendaHeaderLayout::AgendaHeaderLayout(QWidget *parent)
98 : QLayout(parent)
99{
100}
101
102AgendaHeaderLayout::~AgendaHeaderLayout()
103{
104 while (!mItems.isEmpty()) {
105 delete mItems.takeFirst();
106 }
107}
108
109void AgendaHeaderLayout::addItem(QLayoutItem *item)
110{
111 mItems.append(item);
112 invalidate();
113}
114
115int AgendaHeaderLayout::count() const
116{
117 return mItems.size();
118}
119
120QLayoutItem *AgendaHeaderLayout::itemAt(int index) const
121{
122 return mItems.value(index);
123}
124
125QLayoutItem *AgendaHeaderLayout::takeAt(int index)
126{
127 if (index < 0 || index >= mItems.size()) {
128 return nullptr;
129 }
130
131 auto item = mItems.takeAt(index);
132 if (item) {
133 invalidate();
134 }
135 return item;
136}
137
138void AgendaHeaderLayout::invalidate()
139{
141 mIsDirty = true;
142}
143
144void AgendaHeaderLayout::setGeometry(const QRect &rect)
145{
147
148 if (mItems.isEmpty()) {
149 return;
150 }
151
152 const QMargins margins = contentsMargins();
153
154 // same logic as Agenda uses to distribute the width
155 const int contentWidth = rect.width() - margins.left() - margins.right();
156 const double agendaGridSpacingX = static_cast<double>(contentWidth) / mItems.size();
157 int x = margins.left();
158 const int contentHeight = rect.height() - margins.top() - margins.bottom();
159 const int y = rect.y() + margins.top();
160 for (int i = 0; i < mItems.size(); ++i) {
161 auto item = mItems.at(i);
162 const int nextX = margins.left() + static_cast<int>((i + 1) * agendaGridSpacingX);
163 const int width = nextX - x;
164 item->setGeometry(QRect(x, y, width, contentHeight));
165 x = nextX;
166 }
167}
168
169QSize AgendaHeaderLayout::sizeHint() const
170{
171 if (mIsDirty) {
172 updateCache();
173 }
174 return mSizeHint;
175}
176
177QSize AgendaHeaderLayout::minimumSize() const
178{
179 if (mIsDirty) {
180 updateCache();
181 }
182 return mMinSize;
183}
184
185void AgendaHeaderLayout::updateCache() const
186{
187 QSize maxItemSizeHint(0, 0);
188 QSize maxItemMinSize(0, 0);
189 for (auto &item : mItems) {
190 maxItemSizeHint = maxItemSizeHint.expandedTo(item->sizeHint());
191 maxItemMinSize = maxItemMinSize.expandedTo(item->minimumSize());
192 }
193 const QMargins margins = contentsMargins();
194 const int horizontalMargins = margins.left() + margins.right();
195 const int verticalMargins = margins.top() + margins.bottom();
196 mSizeHint = QSize(maxItemSizeHint.width() * mItems.size() + horizontalMargins, maxItemSizeHint.height() + verticalMargins);
197 mMinSize = QSize(maxItemMinSize.width() * mItems.size() + horizontalMargins, maxItemMinSize.height() + verticalMargins);
198 mIsDirty = false;
199}
200
201// Header (or footer) for the agenda.
202// Optionally has an additional week header, if isSideBySide is set
203class AgendaHeader : public QWidget
204{
206public:
207 explicit AgendaHeader(bool isSideBySide, QWidget *parent);
208
210
211public:
212 void setCalendarName(const QString &calendarName);
213 void setAgenda(Agenda *agenda);
214 bool createDayLabels(const KCalendarCore::DateList &dates, bool withDayLabel, const QStringList &decos, const QStringList &enabledDecos);
215 void setWeekWidth(int width);
216 void updateDayLabelSizes();
217 void updateMargins();
218
219protected:
220 void resizeEvent(QResizeEvent *resizeEvent) override;
221
222private:
223 static CalendarDecoration::Decoration *loadCalendarDecoration(const QString &name);
224
225 void addDay(const DecorationList &decoList, QDate date, bool withDayLabel);
226 void clear();
227 void placeDecorations(const DecorationList &decoList, QDate date, QWidget *labelBox, bool forWeek);
228 void loadDecorations(const QStringList &decorations, const QStringList &whiteList, DecorationList &decoList);
229
230private:
231 const bool mIsSideBySide;
232
233 Agenda *mAgenda = nullptr;
234 KSqueezedTextLabel *mCalendarNameLabel = nullptr;
235 QWidget *mDayLabels = nullptr;
236 AgendaHeaderLayout *mDayLabelsLayout = nullptr;
237 QWidget *mWeekLabelBox = nullptr;
238
239 QList<AlternateLabel *> mDateDayLabels;
240};
241
242AgendaHeader::AgendaHeader(bool isSideBySide, QWidget *parent)
243 : QWidget(parent)
244 , mIsSideBySide(isSideBySide)
245{
246 auto layout = new QVBoxLayout(this);
247 layout->setContentsMargins({});
248
249 if (mIsSideBySide) {
250 mCalendarNameLabel = new KSqueezedTextLabel(this);
251 mCalendarNameLabel->setAlignment(Qt::AlignCenter);
252 layout->addWidget(mCalendarNameLabel);
253 }
254
255 auto *daysWidget = new QWidget(this);
256 layout->addWidget(daysWidget);
257
258 auto daysLayout = new QHBoxLayout(daysWidget);
259 daysLayout->setContentsMargins({});
260 daysLayout->setSpacing(SPACING);
261
262 if (!mIsSideBySide) {
263 mWeekLabelBox = new QWidget(daysWidget);
264 auto weekLabelBoxLayout = new QVBoxLayout(mWeekLabelBox);
265 weekLabelBoxLayout->setContentsMargins({});
266 weekLabelBoxLayout->setSpacing(0);
267 daysLayout->addWidget(mWeekLabelBox);
268 }
269
270 mDayLabels = new QWidget(daysWidget);
271 mDayLabelsLayout = new AgendaHeaderLayout(mDayLabels);
272 mDayLabelsLayout->setContentsMargins({});
273 daysLayout->addWidget(mDayLabels);
274 daysLayout->setStretchFactor(mDayLabels, 1);
275}
276
277void AgendaHeader::setAgenda(Agenda *agenda)
278{
279 mAgenda = agenda;
280}
281
282void AgendaHeader::setCalendarName(const QString &calendarName)
283{
284 if (mCalendarNameLabel) {
285 mCalendarNameLabel->setText(calendarName);
286 }
287}
288
289void AgendaHeader::updateMargins()
290{
291 const int frameWidth = mAgenda ? mAgenda->scrollArea()->frameWidth() : 0;
292 const int scrollBarWidth = (mIsSideBySide || !mAgenda || !mAgenda->verticalScrollBar()->isVisible()) ? 0 : mAgenda->verticalScrollBar()->width();
293 const bool isLTR = (layoutDirection() == Qt::LeftToRight);
294 const int leftSpacing = SPACING + frameWidth;
295 const int rightSpacing = scrollBarWidth + frameWidth;
296 mDayLabelsLayout->setContentsMargins(isLTR ? leftSpacing : rightSpacing, 0, isLTR ? rightSpacing : leftSpacing, 0);
297}
298
299void AgendaHeader::updateDayLabelSizes()
300{
301 if (mDateDayLabels.isEmpty()) {
302 return;
303 }
304 // First, calculate the maximum text type that fits for all labels
305 AlternateLabel::TextType overallType = AlternateLabel::Extensive;
306 for (auto label : std::as_const(mDateDayLabels)) {
307 AlternateLabel::TextType type = label->largestFittingTextType();
308 if (type < overallType) {
309 overallType = type;
310 }
311 }
312
313 // Then, set that maximum text type to all the labels
314 for (auto label : std::as_const(mDateDayLabels)) {
315 label->setFixedType(overallType);
316 }
317}
318
319void AgendaHeader::resizeEvent(QResizeEvent *resizeEvent)
320{
321 QWidget::resizeEvent(resizeEvent);
322 updateDayLabelSizes();
323}
324
325void AgendaHeader::setWeekWidth(int width)
326{
327 if (!mWeekLabelBox) {
328 return;
329 }
330
331 mWeekLabelBox->setFixedWidth(width);
332}
333
334void AgendaHeader::clear()
335{
336 auto childWidgets = mDayLabels->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly);
337 qDeleteAll(childWidgets);
338 if (mWeekLabelBox) {
339 childWidgets = mWeekLabelBox->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly);
340 qDeleteAll(childWidgets);
341 }
342 mDateDayLabels.clear();
343}
344
345bool AgendaHeader::createDayLabels(const KCalendarCore::DateList &dates, bool withDayLabel, const QStringList &decoNames, const QStringList &enabledPlugins)
346{
347 clear();
348
350 loadDecorations(decoNames, enabledPlugins, decos);
351 const bool hasDecos = !decos.isEmpty();
352
353 for (const QDate &date : dates) {
354 addDay(decos, date, withDayLabel);
355 }
356
357 // Week decoration labels
358 if (mWeekLabelBox) {
359 placeDecorations(decos, dates.first(), mWeekLabelBox, true);
360 }
361
362 qDeleteAll(decos);
363
364 // trigger an update after all layout has been done and the final sizes are known
365 QTimer::singleShot(0, this, &AgendaHeader::updateDayLabelSizes);
366
367 return hasDecos;
368}
369
370void AgendaHeader::addDay(const DecorationList &decoList, QDate date, bool withDayLabel)
371{
372 auto topDayLabelBox = new QWidget(mDayLabels);
373 auto topDayLabelBoxLayout = new QVBoxLayout(topDayLabelBox);
374 topDayLabelBoxLayout->setContentsMargins({});
375 topDayLabelBoxLayout->setSpacing(0);
376
377 mDayLabelsLayout->addWidget(topDayLabelBox);
378
379 if (withDayLabel) {
380 int dW = date.dayOfWeek();
382 QString longstr = i18nc("short_weekday short_monthname date (e.g. Mon Aug 13)",
383 "%1 %2 %3",
385 QLocale::system().monthName(date.month(), QLocale::ShortFormat),
386 date.day());
387 const QString shortstr = QString::number(date.day());
388
389 auto dayLabel = new AlternateLabel(shortstr, longstr, veryLongStr, topDayLabelBox);
390 topDayLabelBoxLayout->addWidget(dayLabel);
391 dayLabel->setAlignment(Qt::AlignHCenter);
392 if (date == QDate::currentDate()) {
393 QFont font = dayLabel->font();
394 font.setBold(true);
395 dayLabel->setFont(font);
396 }
397 mDateDayLabels.append(dayLabel);
398
399 // if a holiday region is selected, show the holiday name
400 const QStringList texts = CalendarSupport::holiday(date);
401 for (const QString &text : texts) {
402 auto label = new KSqueezedTextLabel(text, topDayLabelBox);
403 label->setTextElideMode(Qt::ElideRight);
404 topDayLabelBoxLayout->addWidget(label);
405 label->setAlignment(Qt::AlignCenter);
406 }
407 }
408
409 placeDecorations(decoList, date, topDayLabelBox, false);
410}
411
412void AgendaHeader::placeDecorations(const DecorationList &decoList, QDate date, QWidget *labelBox, bool forWeek)
413{
414 for (CalendarDecoration::Decoration *deco : std::as_const(decoList)) {
415 const CalendarDecoration::Element::List elements = forWeek ? deco->weekElements(date) : deco->dayElements(date);
416 if (!elements.isEmpty()) {
417 auto decoHBox = new QWidget(labelBox);
418 labelBox->layout()->addWidget(decoHBox);
419 auto layout = new QHBoxLayout(decoHBox);
420 layout->setSpacing(0);
422 decoHBox->setMinimumWidth(1);
423
424 for (CalendarDecoration::Element *it : elements) {
425 auto label = new DecorationLabel(it, decoHBox);
426 label->setAlignment(Qt::AlignBottom);
427 label->setMinimumWidth(1);
428 layout->addWidget(label);
429 }
430 }
431 }
432}
433
434void AgendaHeader::loadDecorations(const QStringList &decorations, const QStringList &whiteList, DecorationList &decoList)
435{
436 for (const QString &decoName : decorations) {
437 if (whiteList.contains(decoName)) {
438 CalendarDecoration::Decoration *deco = loadCalendarDecoration(decoName);
439 if (deco != nullptr) {
440 decoList << deco;
441 }
442 }
443 }
444}
445
446CalendarDecoration::Decoration *AgendaHeader::loadCalendarDecoration(const QString &name)
447{
448 auto result = KPluginFactory::instantiatePlugin<CalendarDecoration::Decoration>(KPluginMetaData(QStringLiteral("pim6/korganizer/") + name));
449
450 if (result) {
451 return result.plugin;
452 } else {
453 qCDebug(CALENDARVIEW_LOG) << "Factory creation failed" << result.errorString;
454 return nullptr;
455 }
456}
457
458class EventViews::EventIndicatorPrivate
459{
460public:
461 EventIndicatorPrivate(EventIndicator *parent, EventIndicator::Location loc)
462 : mLocation(loc)
463 , q(parent)
464 {
465 mEnabled.resize(mColumns);
466
467 QChar ch;
468 // Dashed up and down arrow characters
469 ch = QChar(mLocation == EventIndicator::Top ? 0x21e1 : 0x21e3);
470 QFont font = q->font();
472 QFontMetrics fm(font);
473 QRect rect = fm.boundingRect(ch).adjusted(-2, -2, 2, 2);
474 mPixmap = QPixmap(rect.size());
475 mPixmap.fill(Qt::transparent);
476 QPainter p(&mPixmap);
477 p.setOpacity(0.33);
478 p.setFont(font);
479 p.setPen(q->palette().text().color());
480 p.drawText(-rect.left(), -rect.top(), ch);
481 }
482
483 void adjustGeometry()
484 {
485 QRect rect;
486 rect.setWidth(q->parentWidget()->width());
487 rect.setHeight(q->height());
488 rect.setLeft(0);
489 rect.setTop(mLocation == EventIndicator::Top ? 0 : q->parentWidget()->height() - rect.height());
490 q->setGeometry(rect);
491 }
492
493public:
494 int mColumns = 1;
495 const EventIndicator::Location mLocation;
496 QPixmap mPixmap;
497 QList<bool> mEnabled;
498
499private:
500 EventIndicator *const q;
501};
502
503EventIndicator::EventIndicator(Location loc, QWidget *parent)
504 : QWidget(parent)
505 , d(new EventIndicatorPrivate(this, loc))
506{
508 setFixedHeight(d->mPixmap.height());
509 parent->installEventFilter(this);
510}
511
512EventIndicator::~EventIndicator() = default;
513
514void EventIndicator::paintEvent(QPaintEvent *)
515{
516 QPainter painter(this);
517
518 const double cellWidth = static_cast<double>(width()) / d->mColumns;
519 const bool isRightToLeft = QApplication::isRightToLeft();
520 const uint pixmapOffset = isRightToLeft ? 0 : (cellWidth - d->mPixmap.width());
521 for (int i = 0; i < d->mColumns; ++i) {
522 if (d->mEnabled[i]) {
523 const int xOffset = (isRightToLeft ? (d->mColumns - 1 - i) : i) * cellWidth;
524 painter.drawPixmap(xOffset + pixmapOffset, 0, d->mPixmap);
525 }
526 }
527}
528
529bool EventIndicator::eventFilter(QObject *, QEvent *event)
530{
531 if (event->type() == QEvent::Resize) {
532 d->adjustGeometry();
533 }
534 return false;
535}
536
537void EventIndicator::changeColumns(int columns)
538{
539 d->mColumns = columns;
540 d->mEnabled.resize(d->mColumns);
541
542 show();
543 raise();
544 update();
545}
546
547void EventIndicator::enableColumn(int column, bool enable)
548{
549 Q_ASSERT(column < d->mEnabled.count());
550 d->mEnabled[column] = enable;
551}
552
553////////////////////////////////////////////////////////////////////////////
554////////////////////////////////////////////////////////////////////////////
555
556class EventViews::AgendaViewPrivate : public KCalendarCore::Calendar::CalendarObserver
557{
558 AgendaView *const q;
559
560public:
561 explicit AgendaViewPrivate(AgendaView *parent, bool isInteractive, bool isSideBySide)
562 : q(parent)
563 , mUpdateItem(0)
564 , mIsSideBySide(isSideBySide)
565 , mIsInteractive(isInteractive)
566 , mViewCalendar(MultiViewCalendar::Ptr(new MultiViewCalendar()))
567 {
568 mViewCalendar->mAgendaView = q;
569 }
570
571public:
572 // view widgets
573 QVBoxLayout *mMainLayout = nullptr;
574 AgendaHeader *mTopDayLabelsFrame = nullptr;
575 AgendaHeader *mBottomDayLabelsFrame = nullptr;
576 QWidget *mAllDayFrame = nullptr;
577 QSpacerItem *mAllDayRightSpacer = nullptr;
578 QWidget *mTimeBarHeaderFrame = nullptr;
579 QSplitter *mSplitterAgenda = nullptr;
580 QList<QLabel *> mTimeBarHeaders;
581
582 Agenda *mAllDayAgenda = nullptr;
583 Agenda *mAgenda = nullptr;
584
585 TimeLabelsZone *mTimeLabelsZone = nullptr;
586
587 KCalendarCore::DateList mSelectedDates; // List of dates to be displayed
588 KCalendarCore::DateList mSaveSelectedDates; // Save the list of dates between updateViews
589 int mViewType;
590 EventIndicator *mEventIndicatorTop = nullptr;
591 EventIndicator *mEventIndicatorBottom = nullptr;
592
593 QList<int> mMinY;
594 QList<int> mMaxY;
595
596 QList<bool> mHolidayMask;
597
598 QDateTime mTimeSpanBegin;
599 QDateTime mTimeSpanEnd;
600 bool mTimeSpanInAllDay = true;
601 bool mAllowAgendaUpdate = true;
602
603 Akonadi::Item mUpdateItem;
604
605 const bool mIsSideBySide;
606
607 QWidget *mDummyAllDayLeft = nullptr;
608 bool mUpdateAllDayAgenda = true;
609 bool mUpdateAgenda = true;
610 bool mIsInteractive;
611 bool mUpdateEventIndicatorsScheduled = false;
612
613 // Contains days that have at least one all-day Event with TRANSP: OPAQUE (busy)
614 // that has you as organizer or attendee so we can color background with a different
615 // color
617
619 bool makesWholeDayBusy(const KCalendarCore::Incidence::Ptr &incidence) const;
620 void clearView();
621 void setChanges(EventView::Changes changes, const KCalendarCore::Incidence::Ptr &incidence = KCalendarCore::Incidence::Ptr());
622
623 /**
624 Returns a list of consecutive dates, starting with @p start and ending
625 with @p end. If either start or end are invalid, a list with
626 QDate::currentDate() is returned */
627 static QList<QDate> generateDateList(QDate start, QDate end);
628
629 void changeColumns(int numColumns);
630
631 AgendaItem::List agendaItems(const QString &uid) const;
632
633 // insertAtDateTime is in the view's timezone
634 void insertIncidence(const KCalendarCore::Incidence::Ptr &, const QDateTime &recurrenceId, const QDateTime &insertAtDateTime, bool createSelected);
635 void reevaluateIncidence(const KCalendarCore::Incidence::Ptr &incidence);
636
637 bool datesEqual(const KCalendarCore::Incidence::Ptr &one, const KCalendarCore::Incidence::Ptr &two) const;
638
639 /**
640 * Returns false if the incidence is for sure outside of the visible timespan.
641 * Returns true if it might be, meaning that to be sure, timezones must be
642 * taken into account.
643 * This is a very fast way of discarding incidences that are outside of the
644 * timespan and only performing expensive timezone operations on the ones
645 * that might be viisble
646 */
647 bool mightBeVisible(const KCalendarCore::Incidence::Ptr &incidence) const;
648
649 void updateAllDayRightSpacer();
650
651protected:
652 /* reimplemented from KCalendarCore::Calendar::CalendarObserver */
653 void calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence) override;
654 void calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence) override;
655 void calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar) override;
656
657private:
658 // quiet --overloaded-virtual warning
660};
661
662bool AgendaViewPrivate::datesEqual(const KCalendarCore::Incidence::Ptr &one, const KCalendarCore::Incidence::Ptr &two) const
663{
664 const auto start1 = one->dtStart();
665 const auto start2 = two->dtStart();
666 const auto end1 = one->dateTime(KCalendarCore::Incidence::RoleDisplayEnd);
667 const auto end2 = two->dateTime(KCalendarCore::Incidence::RoleDisplayEnd);
668
669 if (start1.isValid() ^ start2.isValid()) {
670 return false;
671 }
672
673 if (end1.isValid() ^ end2.isValid()) {
674 return false;
675 }
676
677 if (start1.isValid() && start1 != start2) {
678 return false;
679 }
680
681 if (end1.isValid() && end1 != end2) {
682 return false;
683 }
684
685 return true;
686}
687
688AgendaItem::List AgendaViewPrivate::agendaItems(const QString &uid) const
689{
690 AgendaItem::List allDayAgendaItems = mAllDayAgenda->agendaItems(uid);
691 return allDayAgendaItems.isEmpty() ? mAgenda->agendaItems(uid) : allDayAgendaItems;
692}
693
694bool AgendaViewPrivate::mightBeVisible(const KCalendarCore::Incidence::Ptr &incidence) const
695{
697
698 // KDateTime::toTimeSpec() is expensive, so lets first compare only the date,
699 // to see if the incidence is visible.
700 // If it's more than 48h of diff, then for sure it won't be visible,
701 // independently of timezone.
702 // The largest difference between two timezones is about 24 hours.
703
704 if (todo && todo->isOverdue()) {
705 // Don't optimize this case. Overdue to-dos have their own rules for displaying themselves
706 return true;
707 }
708
709 if (!incidence->recurs()) {
710 // If DTEND/DTDUE is before the 1st visible column
711 const QDate tdate = incidence->dateTime(KCalendarCore::Incidence::RoleEnd).date();
712 if (tdate.daysTo(mSelectedDates.first()) > 2) {
713 return false;
714 }
715
716 // if DTSTART is after the last visible column
717 if (!todo && mSelectedDates.last().daysTo(incidence->dtStart().date()) > 2) {
718 return false;
719 }
720
721 // if DTDUE is after the last visible column
722 if (todo && mSelectedDates.last().daysTo(todo->dtDue().date()) > 2) {
723 return false;
724 }
725 }
726
727 return true;
728}
729
730void AgendaViewPrivate::changeColumns(int numColumns)
731{
732 // mMinY, mMaxY and mEnabled must all have the same size.
733 // Make sure you preserve this order because mEventIndicatorTop->changeColumns()
734 // can trigger a lot of stuff, and code will be executed when mMinY wasn't resized yet.
735 mMinY.resize(numColumns);
736 mMaxY.resize(numColumns);
737 mEventIndicatorTop->changeColumns(numColumns);
738 mEventIndicatorBottom->changeColumns(numColumns);
739}
740
741/** static */
742QList<QDate> AgendaViewPrivate::generateDateList(QDate start, QDate end)
743{
745
746 if (start.isValid() && end.isValid() && end >= start && start.daysTo(end) < AgendaView::MAX_DAY_COUNT) {
747 QDate date = start;
748 list.reserve(start.daysTo(end) + 1);
749 while (date <= end) {
750 list.append(date);
751 date = date.addDays(1);
752 }
753 } else {
755 }
756
757 return list;
758}
759
760void AgendaViewPrivate::reevaluateIncidence(const KCalendarCore::Incidence::Ptr &incidence)
761{
762 if (!incidence || !mViewCalendar->isValid(incidence)) {
763 qCWarning(CALENDARVIEW_LOG) << "invalid incidence or item not found." << incidence;
764 return;
765 }
766
767 q->removeIncidence(incidence);
768 q->displayIncidence(incidence, false);
769 mAgenda->checkScrollBoundaries();
771}
772
773void AgendaViewPrivate::calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence)
774{
775 if (!incidence || !mViewCalendar->isValid(incidence)) {
776 qCCritical(CALENDARVIEW_LOG) << "AgendaViewPrivate::calendarIncidenceAdded() Invalid incidence or item:" << incidence;
777 Q_ASSERT(false);
778 return;
779 }
780
781 if (incidence->hasRecurrenceId()) {
782 const auto cal = q->calendar2(incidence);
783 if (cal) {
784 if (auto mainIncidence = cal->incidence(incidence->uid())) {
785 // Reevaluate the main event instead, if it was inserted before this one.
786 reevaluateIncidence(mainIncidence);
787 } else if (q->displayIncidence(incidence, false)) {
788 // Display disassociated occurrences because errors sometimes destroy
789 // the main recurring incidence.
790 mAgenda->checkScrollBoundaries();
791 q->scheduleUpdateEventIndicators();
792 }
793 }
794 } else if (incidence->recurs()) {
795 // Reevaluate recurring incidences to clean up any disassociated
796 // occurrences that were inserted before it.
797 reevaluateIncidence(incidence);
798 } else if (q->displayIncidence(incidence, false)) {
799 // Ordinary non-recurring non-disassociated instances.
800 mAgenda->checkScrollBoundaries();
801 q->scheduleUpdateEventIndicators();
802 }
803}
804
805void AgendaViewPrivate::calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence)
806{
807 if (!incidence || incidence->uid().isEmpty()) {
808 qCCritical(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid incidence or empty UID. " << incidence;
809 Q_ASSERT(false);
810 return;
811 }
812
813 AgendaItem::List agendaItems = this->agendaItems(incidence->uid());
814 if (agendaItems.isEmpty()) {
815 // Don't warn - it's possible the incidence has been changed in another calendar that we do not display.
816 // qCWarning(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid agendaItem for incidence " << incidence->uid();
817 return;
818 }
819
820 // Optimization: If the dates didn't change, just repaint it.
821 // This optimization for now because we need to process collisions between agenda items.
822 if (false && !incidence->recurs() && agendaItems.count() == 1) {
823 KCalendarCore::Incidence::Ptr originalIncidence = agendaItems.first()->incidence();
824
825 if (datesEqual(originalIncidence, incidence)) {
826 for (const AgendaItem::QPtr &agendaItem : std::as_const(agendaItems)) {
827 agendaItem->setIncidence(KCalendarCore::Incidence::Ptr(incidence->clone()));
828 agendaItem->update();
829 }
830 return;
831 }
832 }
833
834 if (incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence)) {
835 // Reevaluate the main event instead, if it exists
836 const auto cal = q->calendar2(incidence);
837 if (cal) {
838 KCalendarCore::Incidence::Ptr mainIncidence = cal->incidence(incidence->uid());
839 reevaluateIncidence(mainIncidence ? mainIncidence : incidence);
840 }
841 } else {
842 reevaluateIncidence(incidence);
843 }
844
845 // No need to call setChanges(), that triggers a fillAgenda()
846 // setChanges(q->changes() | IncidencesEdited, incidence);
847}
848
849void AgendaViewPrivate::calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar)
850{
851 Q_UNUSED(calendar)
852 if (!incidence || incidence->uid().isEmpty()) {
853 qCWarning(CALENDARVIEW_LOG) << "invalid incidence or empty uid: " << incidence;
854 Q_ASSERT(false);
855 return;
856 }
857
858 q->removeIncidence(incidence);
859
860 if (incidence->hasRecurrenceId()) {
861 // Reevaluate the main event, if it exists. The exception was removed so the main recurrent series
862 // will no be bigger.
863 if (mViewCalendar->isValid(incidence->uid())) {
864 const auto cal = q->calendar2(incidence->uid());
865 if (cal) {
866 KCalendarCore::Incidence::Ptr mainIncidence = cal->incidence(incidence->uid());
867 if (mainIncidence) {
868 reevaluateIncidence(mainIncidence);
869 }
870 }
871 }
872 } else if (mightBeVisible(incidence)) {
873 // No need to call setChanges(), that triggers a fillAgenda()
874 // setChanges(q->changes() | IncidencesDeleted, CalendarSupport::incidence(incidence));
875 mAgenda->checkScrollBoundaries();
876 q->scheduleUpdateEventIndicators();
877 }
878}
879
880void EventViews::AgendaViewPrivate::setChanges(EventView::Changes changes, const KCalendarCore::Incidence::Ptr &incidence)
881{
882 // We could just call EventView::setChanges(...) but we're going to do a little
883 // optimization. If only an all day item was changed, only all day agenda
884 // should be updated.
885
886 // all bits = 1
887 const int ones = ~0;
888
889 const int incidenceOperations = AgendaView::IncidencesAdded | AgendaView::IncidencesEdited | AgendaView::IncidencesDeleted;
890
891 // If changes has a flag turned on, other than incidence operations, then update both agendas
892 if ((ones ^ incidenceOperations) & changes) {
893 mUpdateAllDayAgenda = true;
894 mUpdateAgenda = true;
895 } else if (incidence) {
896 mUpdateAllDayAgenda = mUpdateAllDayAgenda | incidence->allDay();
897 mUpdateAgenda = mUpdateAgenda | !incidence->allDay();
898 }
899
900 q->EventView::setChanges(changes);
901}
902
903void AgendaViewPrivate::clearView()
904{
905 if (mUpdateAllDayAgenda) {
906 mAllDayAgenda->clear();
907 }
908
909 if (mUpdateAgenda) {
910 mAgenda->clear();
911 }
912
913 mBusyDays.clear();
914}
915
916void AgendaViewPrivate::insertIncidence(const KCalendarCore::Incidence::Ptr &incidence,
917 const QDateTime &recurrenceId,
918 const QDateTime &insertAtDateTime,
919 bool createSelected)
920{
921 if (!q->filterByCollectionSelection(incidence)) {
922 return;
923 }
924
925 KCalendarCore::Event::Ptr event = CalendarSupport::event(incidence);
926 KCalendarCore::Todo::Ptr todo = CalendarSupport::todo(incidence);
927
928 const QDate insertAtDate = insertAtDateTime.date();
929
930 // In case incidence->dtStart() isn't visible (crosses boundaries)
931 const int curCol = qMax(mSelectedDates.first().daysTo(insertAtDate), qint64(0));
932
933 // The date for the event is not displayed, just ignore it
934 if (curCol >= mSelectedDates.count()) {
935 return;
936 }
937
938 if (mMinY.count() <= curCol) {
939 mMinY.resize(mSelectedDates.count());
940 }
941
942 if (mMaxY.count() <= curCol) {
943 mMaxY.resize(mSelectedDates.count());
944 }
945
946 // Default values, which can never be reached
947 mMinY[curCol] = mAgenda->timeToY(QTime(23, 59)) + 1;
948 mMaxY[curCol] = mAgenda->timeToY(QTime(0, 0)) - 1;
949
950 int beginX;
951 int endX;
952 if (event) {
953 const QDate firstVisibleDate = mSelectedDates.first();
954 QDateTime dtEnd = event->dtEnd().toLocalTime();
955 if (!event->allDay() && dtEnd > event->dtStart()) {
956 // If dtEnd's time portion is 00:00:00, the event ends on the previous day
957 // unless it also starts at 00:00:00 (a duration of 0).
958 dtEnd = dtEnd.addMSecs(-1);
959 }
960 const int duration = event->dtStart().toLocalTime().daysTo(dtEnd);
961 if (insertAtDate < firstVisibleDate) {
962 beginX = curCol + firstVisibleDate.daysTo(insertAtDate);
963 endX = beginX + duration;
964 } else {
965 beginX = curCol;
966 endX = beginX + duration;
967 }
968 } else if (todo) {
969 if (!todo->hasDueDate()) {
970 return; // todo shall not be displayed if it has no date
971 }
972 beginX = endX = curCol;
973 } else {
974 return;
975 }
976
977 const QDate today = QDate::currentDate();
978 if (todo && todo->isOverdue() && today >= insertAtDate) {
979 mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, curCol, curCol, createSelected);
980 } else if (incidence->allDay()) {
981 mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, beginX, endX, createSelected);
982 } else if (event && event->isMultiDay(QTimeZone::systemTimeZone())) {
983 // TODO: We need a better isMultiDay(), one that receives the occurrence.
984
985 // In the single-day handling code there's a neat comment on why
986 // we're calculating the start time this way
987 const QTime startTime = insertAtDateTime.time();
988
989 // In the single-day handling code there's a neat comment on why we use the
990 // duration instead of fetching the end time directly
991 const int durationOfFirstOccurrence = event->dtStart().secsTo(event->dtEnd());
992 QTime endTime = startTime.addSecs(durationOfFirstOccurrence);
993
994 const int startY = mAgenda->timeToY(startTime);
995
996 if (endTime == QTime(0, 0, 0)) {
997 endTime = QTime(23, 59, 59);
998 }
999 const int endY = mAgenda->timeToY(endTime) - 1;
1000 if ((beginX <= 0 && curCol == 0) || beginX == curCol) {
1001 mAgenda->insertMultiItem(incidence, recurrenceId, beginX, endX, startY, endY, createSelected);
1002 }
1003 if (beginX == curCol) {
1004 mMaxY[curCol] = mAgenda->timeToY(QTime(23, 59));
1005 if (startY < mMinY[curCol]) {
1006 mMinY[curCol] = startY;
1007 }
1008 } else if (endX == curCol) {
1009 mMinY[curCol] = mAgenda->timeToY(QTime(0, 0));
1010 if (endY > mMaxY[curCol]) {
1011 mMaxY[curCol] = endY;
1012 }
1013 } else {
1014 mMinY[curCol] = mAgenda->timeToY(QTime(0, 0));
1015 mMaxY[curCol] = mAgenda->timeToY(QTime(23, 59));
1016 }
1017 } else {
1018 int startY = 0;
1019 int endY = 0;
1020 if (event) { // Single day events fall here
1021 // Don't use event->dtStart().toTimeSpec(timeSpec).time().
1022 // If it's a UTC recurring event it should have a different time when it crosses DST,
1023 // so we must use insertAtDate here, so we get the correct time.
1024 //
1025 // The nth occurrence doesn't always have the same time as the 1st occurrence.
1026 const QTime startTime = insertAtDateTime.time();
1027
1028 // We could just fetch the end time directly from dtEnd() instead of adding a duration to the
1029 // start time. This way is best because it preserves the duration of the event. There are some
1030 // corner cases where the duration would be messed up, for example a UTC event that when
1031 // converted to local has dtStart() in day light saving time, but dtEnd() outside DST.
1032 // It could create events with 0 duration.
1033 const int durationOfFirstOccurrence = event->dtStart().secsTo(event->dtEnd());
1034 QTime endTime = startTime.addSecs(durationOfFirstOccurrence);
1035
1036 startY = mAgenda->timeToY(startTime);
1037 if (durationOfFirstOccurrence != 0 && endTime == QTime(0, 0, 0)) {
1038 // If endTime is 00:00:00, the event ends on the previous day
1039 // unless it also starts at 00:00:00 (a duration of 0).
1040 endTime = endTime.addMSecs(-1);
1041 }
1042 endY = mAgenda->timeToY(endTime) - 1;
1043 }
1044 if (todo) {
1045 QTime t;
1046 if (todo->recurs()) {
1047 // The time we get depends on the insertAtDate, because of daylight savings changes
1048 const QDateTime ocurrrenceDateTime = QDateTime(insertAtDate, todo->dtDue().time(), todo->dtDue().timeZone());
1049 t = ocurrrenceDateTime.toLocalTime().time();
1050 } else {
1051 t = todo->dtDue().toLocalTime().time();
1052 }
1053
1054 if (t == QTime(0, 0) && !todo->recurs()) {
1055 // To-dos due at 00h00 are drawn at the previous day and ending at
1056 // 23h59. For recurring to-dos, that's not being done because it wasn't
1057 // implemented yet in ::fillAgenda().
1058 t = QTime(23, 59);
1059 }
1060
1061 const int halfHour = 1800;
1062 if (t.addSecs(-halfHour) < t) {
1063 startY = mAgenda->timeToY(t.addSecs(-halfHour));
1064 endY = mAgenda->timeToY(t) - 1;
1065 } else {
1066 startY = 0;
1067 endY = mAgenda->timeToY(t.addSecs(halfHour)) - 1;
1068 }
1069 }
1070 if (endY < startY) {
1071 endY = startY;
1072 }
1073 mAgenda->insertItem(incidence, recurrenceId, curCol, startY, endY, 1, 1, createSelected);
1074 if (startY < mMinY[curCol]) {
1075 mMinY[curCol] = startY;
1076 }
1077 if (endY > mMaxY[curCol]) {
1078 mMaxY[curCol] = endY;
1079 }
1080 }
1081}
1082
1083void AgendaViewPrivate::updateAllDayRightSpacer()
1084{
1085 if (!mAllDayRightSpacer) {
1086 return;
1087 }
1088
1089 // Make the all-day and normal agendas line up with each other
1090 auto verticalAgendaScrollBar = mAgenda->verticalScrollBar();
1091 int margin = verticalAgendaScrollBar->isVisible() ? verticalAgendaScrollBar->width() : 0;
1093 // Needed for some styles. Oxygen needs it, Plastique does not.
1094 margin -= mAgenda->scrollArea()->frameWidth();
1095 }
1096 mAllDayRightSpacer->changeSize(margin, 0, QSizePolicy::Fixed);
1097 mAllDayFrame->layout()->invalidate(); // needed to pick up change of space size
1098}
1099
1100////////////////////////////////////////////////////////////////////////////
1101
1102AgendaView::AgendaView(QDate start, QDate end, bool isInteractive, bool isSideBySide, QWidget *parent)
1103 : EventView(parent)
1104 , d(new AgendaViewPrivate(this, isInteractive, isSideBySide))
1105{
1106 init(start, end);
1107}
1108
1109AgendaView::AgendaView(const PrefsPtr &prefs, QDate start, QDate end, bool isInteractive, bool isSideBySide, QWidget *parent)
1110 : EventView(parent)
1111 , d(new AgendaViewPrivate(this, isInteractive, isSideBySide))
1112{
1113 setPreferences(prefs);
1114 init(start, end);
1115}
1116
1117void AgendaView::init(QDate start, QDate end)
1118{
1119 d->mSelectedDates = AgendaViewPrivate::generateDateList(start, end);
1120
1121 d->mMainLayout = new QVBoxLayout(this);
1122 d->mMainLayout->setContentsMargins({});
1123
1124 /* Create day name labels for agenda columns */
1125 d->mTopDayLabelsFrame = new AgendaHeader(d->mIsSideBySide, this);
1126 d->mMainLayout->addWidget(d->mTopDayLabelsFrame);
1127
1128 /* Create agenda splitter */
1129 d->mSplitterAgenda = new QSplitter(Qt::Vertical, this);
1130 d->mMainLayout->addWidget(d->mSplitterAgenda, 1);
1131
1132 /* Create all-day agenda widget */
1133 d->mAllDayFrame = new QWidget(d->mSplitterAgenda);
1134 auto allDayFrameLayout = new QHBoxLayout(d->mAllDayFrame);
1135 allDayFrameLayout->setContentsMargins({});
1136 allDayFrameLayout->setSpacing(SPACING);
1137
1138 // Alignment and description widgets
1139 if (!d->mIsSideBySide) {
1140 d->mTimeBarHeaderFrame = new QWidget(d->mAllDayFrame);
1141 allDayFrameLayout->addWidget(d->mTimeBarHeaderFrame);
1142 auto timeBarHeaderFrameLayout = new QHBoxLayout(d->mTimeBarHeaderFrame);
1143 timeBarHeaderFrameLayout->setContentsMargins({});
1144 timeBarHeaderFrameLayout->setSpacing(0);
1145 d->mDummyAllDayLeft = new QWidget(d->mAllDayFrame);
1146 allDayFrameLayout->addWidget(d->mDummyAllDayLeft);
1147 }
1148
1149 // The widget itself
1150 auto allDayScrollArea = new AgendaScrollArea(true, this, d->mIsInteractive, d->mAllDayFrame);
1151 allDayFrameLayout->addWidget(allDayScrollArea);
1152 d->mAllDayAgenda = allDayScrollArea->agenda();
1153
1154 /* Create the main agenda widget and the related widgets */
1155 auto agendaFrame = new QWidget(d->mSplitterAgenda);
1156 auto agendaLayout = new QHBoxLayout(agendaFrame);
1157 agendaLayout->setContentsMargins({});
1158 agendaLayout->setSpacing(SPACING);
1159
1160 // Create agenda
1161 auto scrollArea = new AgendaScrollArea(false, this, d->mIsInteractive, agendaFrame);
1162 d->mAgenda = scrollArea->agenda();
1163 d->mAgenda->verticalScrollBar()->installEventFilter(this);
1164 d->mAgenda->setCalendar(d->mViewCalendar);
1165
1166 d->mAllDayAgenda->setCalendar(d->mViewCalendar);
1167
1168 // Create event indicator bars
1169 d->mEventIndicatorTop = new EventIndicator(EventIndicator::Top, scrollArea->viewport());
1170 d->mEventIndicatorBottom = new EventIndicator(EventIndicator::Bottom, scrollArea->viewport());
1171
1172 // Create time labels
1173 d->mTimeLabelsZone = new TimeLabelsZone(this, preferences(), d->mAgenda);
1174
1175 // This timeLabelsZoneLayout is for adding some spacing
1176 // to align timelabels, to agenda's grid
1177 auto timeLabelsZoneLayout = new QVBoxLayout();
1178
1179 agendaLayout->addLayout(timeLabelsZoneLayout);
1180 agendaLayout->addWidget(scrollArea);
1181
1182 timeLabelsZoneLayout->addSpacing(scrollArea->frameWidth());
1183 timeLabelsZoneLayout->addWidget(d->mTimeLabelsZone);
1184 timeLabelsZoneLayout->addSpacing(scrollArea->frameWidth());
1185
1186 // Scrolling
1187 connect(d->mAgenda, &Agenda::zoomView, this, &AgendaView::zoomView);
1188
1189 // Event indicator updates
1190 connect(d->mAgenda, &Agenda::lowerYChanged, this, &AgendaView::updateEventIndicatorTop);
1191 connect(d->mAgenda, &Agenda::upperYChanged, this, &AgendaView::updateEventIndicatorBottom);
1192
1193 if (d->mIsSideBySide) {
1194 d->mTimeLabelsZone->hide();
1195 }
1196
1197 /* Create a frame at the bottom which may be used by decorations */
1198 d->mBottomDayLabelsFrame = new AgendaHeader(d->mIsSideBySide, this);
1199 d->mBottomDayLabelsFrame->hide();
1200
1201 d->mTopDayLabelsFrame->setAgenda(d->mAgenda);
1202 d->mBottomDayLabelsFrame->setAgenda(d->mAgenda);
1203
1204 if (!d->mIsSideBySide) {
1205 d->mAllDayRightSpacer = new QSpacerItem(0, 0);
1206 d->mAllDayFrame->layout()->addItem(d->mAllDayRightSpacer);
1207 }
1208
1209 updateTimeBarWidth();
1210
1211 // Don't call it now, bottom agenda isn't fully up yet
1212 QMetaObject::invokeMethod(this, &AgendaView::alignAgendas, Qt::QueuedConnection);
1213
1214 // Whoever changes this code, remember to leave createDayLabels()
1215 // inside the ctor, so it's always called before readSettings(), so
1216 // readSettings() works on the splitter that has the right amount of
1217 // widgets (createDayLabels() via placeDecorationFrame() removes widgets).
1218 createDayLabels(true);
1219
1220 /* Connect the agendas */
1221
1222 connect(d->mAllDayAgenda, &Agenda::newTimeSpanSignal, this, &AgendaView::newTimeSpanSelectedAllDay);
1223
1224 connect(d->mAgenda, &Agenda::newTimeSpanSignal, this, &AgendaView::newTimeSpanSelected);
1225
1226 connectAgenda(d->mAgenda, d->mAllDayAgenda);
1227 connectAgenda(d->mAllDayAgenda, d->mAgenda);
1228}
1229
1230AgendaView::~AgendaView()
1231{
1232 for (const ViewCalendar::Ptr &cal : std::as_const(d->mViewCalendar->mSubCalendars)) {
1233 if (cal->getCalendar()) {
1234 cal->getCalendar()->unregisterObserver(d.get());
1235 }
1236 }
1237}
1238
1239void AgendaView::addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
1240{
1241 EventView::addCalendar(calendar);
1242
1243 if (!d->mViewCalendar->calendarForCollection(calendar->collection()).isNull()) {
1244 return;
1245 }
1246
1248 view->mCalendar = calendar;
1249 view->mAgendaView = this;
1250 addCalendar(view);
1251}
1252
1253void AgendaView::removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
1254{
1255 EventView::removeCalendar(calendar);
1256
1257 auto cal = std::find_if(d->mViewCalendar->mSubCalendars.cbegin(), d->mViewCalendar->mSubCalendars.cend(), [calendar](const auto &subcal) {
1258 if (auto akonadiCal = qSharedPointerDynamicCast<AkonadiViewCalendar>(subcal); akonadiCal) {
1259 // TODO: FIXME: the pointer-based comparision MUST succeed here, not collection-based comparison!!!
1260 return akonadiCal->mCalendar->collection() == calendar->collection();
1261 }
1262 return false;
1263 });
1264
1265 if (cal != d->mViewCalendar->mSubCalendars.end()) {
1266 calendar->unregisterObserver(d.get());
1267 d->mViewCalendar->removeCalendar(*cal);
1268 setChanges(EventViews::EventView::ResourcesChanged);
1269 updateView();
1270 }
1271}
1272void AgendaView::showEvent(QShowEvent *showEvent)
1273{
1274 EventView::showEvent(showEvent);
1275
1276 // agenda scrollbar width only set now, so redo margin calculation
1277 d->mTopDayLabelsFrame->updateMargins();
1278 d->mBottomDayLabelsFrame->updateMargins();
1279 d->updateAllDayRightSpacer();
1280}
1281
1282bool AgendaView::eventFilter(QObject *object, QEvent *event)
1283{
1284 if ((object == d->mAgenda->verticalScrollBar()) && ((event->type() == QEvent::Show) || (event->type() == QEvent::Hide))) {
1285 d->mTopDayLabelsFrame->updateMargins();
1286 d->mBottomDayLabelsFrame->updateMargins();
1287 d->updateAllDayRightSpacer();
1288 }
1289 return false;
1290}
1291
1293{
1294 const auto cal = d->mViewCalendar->findCalendar(incidence);
1295 if (cal) {
1296 return cal->getCalendar();
1297 }
1298 return {};
1299}
1300
1301KCalendarCore::Calendar::Ptr AgendaView::calendar2(const QString &incidenceIdentifier) const
1302{
1303 const auto cal = d->mViewCalendar->findCalendar(incidenceIdentifier);
1304 if (cal) {
1305 return cal->getCalendar();
1306 }
1307 return {};
1308}
1309
1310void AgendaView::addCalendar(const ViewCalendar::Ptr &cal)
1311{
1312 const bool isFirstCalendar = d->mViewCalendar->calendarCount() == 0;
1313
1314 d->mViewCalendar->addCalendar(cal);
1315 cal->getCalendar()->registerObserver(d.get());
1316
1317 EventView::Changes changes = EventView::ResourcesChanged;
1318 if (isFirstCalendar) {
1319 changes |= EventView::DatesChanged; // we need to initialize the columns as well
1320 }
1321
1323 updateView();
1324}
1325
1326void AgendaView::connectAgenda(Agenda *agenda, Agenda *otherAgenda)
1327{
1328 connect(agenda, &Agenda::showNewEventPopupSignal, this, &AgendaView::showNewEventPopupSignal);
1329
1330 connect(agenda, &Agenda::showIncidencePopupSignal, this, &AgendaView::slotShowIncidencePopup);
1331
1332 agenda->setCalendar(d->mViewCalendar);
1333
1334 connect(agenda, &Agenda::newEventSignal, this, qOverload<>(&EventView::newEventSignal));
1335
1336 connect(agenda, &Agenda::newStartSelectSignal, otherAgenda, &Agenda::clearSelection);
1337 connect(agenda, &Agenda::newStartSelectSignal, this, &AgendaView::timeSpanSelectionChanged);
1338
1339 connect(agenda, &Agenda::editIncidenceSignal, this, &AgendaView::slotEditIncidence);
1340 connect(agenda, &Agenda::showIncidenceSignal, this, &AgendaView::slotShowIncidence);
1341 connect(agenda, &Agenda::deleteIncidenceSignal, this, &AgendaView::slotDeleteIncidence);
1342
1343 // drag signals
1344 connect(agenda, &Agenda::startDragSignal, this, [this](const KCalendarCore::Incidence::Ptr &ptr) {
1345 startDrag(ptr);
1346 });
1347
1348 // synchronize selections
1349 connect(agenda, &Agenda::incidenceSelected, otherAgenda, &Agenda::deselectItem);
1350 connect(agenda, &Agenda::incidenceSelected, this, &AgendaView::slotIncidenceSelected);
1351
1352 // rescheduling of todos by d'n'd
1353 connect(agenda,
1354 qOverload<const KCalendarCore::Incidence::List &, const QPoint &, bool>(&Agenda::droppedIncidences),
1355 this,
1356 qOverload<const KCalendarCore::Incidence::List &, const QPoint &, bool>(&AgendaView::slotIncidencesDropped));
1357 connect(agenda,
1358 qOverload<const QList<QUrl> &, const QPoint &, bool>(&Agenda::droppedIncidences),
1359 this,
1360 qOverload<const QList<QUrl> &, const QPoint &, bool>(&AgendaView::slotIncidencesDropped));
1361}
1362
1363void AgendaView::slotIncidenceSelected(const KCalendarCore::Incidence::Ptr &incidence, QDate date)
1364{
1365 Akonadi::Item item = d->mViewCalendar->item(incidence);
1366 if (item.isValid()) {
1367 Q_EMIT incidenceSelected(item, date);
1368 }
1369}
1370
1371void AgendaView::slotShowIncidencePopup(const KCalendarCore::Incidence::Ptr &incidence, QDate date)
1372{
1373 Akonadi::Item item = d->mViewCalendar->item(incidence);
1374 // qDebug() << "wanna see the popup for " << incidence->uid() << item.id();
1375 if (item.isValid()) {
1376 const auto calendar = calendar3(item);
1377 Q_EMIT showIncidencePopupSignal(calendar, item, date);
1378 }
1379}
1380
1381void AgendaView::slotShowIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1382{
1383 Akonadi::Item item = d->mViewCalendar->item(incidence);
1384 if (item.isValid()) {
1386 }
1387}
1388
1389void AgendaView::slotEditIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1390{
1391 Akonadi::Item item = d->mViewCalendar->item(incidence);
1392 if (item.isValid()) {
1394 }
1395}
1396
1397void AgendaView::slotDeleteIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1398{
1399 Akonadi::Item item = d->mViewCalendar->item(incidence);
1400 if (item.isValid()) {
1402 }
1403}
1404
1405void AgendaView::zoomInVertically()
1406{
1407 if (!d->mIsSideBySide) {
1408 preferences()->setHourSize(preferences()->hourSize() + 1);
1409 }
1410 d->mAgenda->updateConfig();
1411 d->mAgenda->checkScrollBoundaries();
1412
1413 d->mTimeLabelsZone->updateAll();
1414 setChanges(changes() | ZoomChanged);
1415 updateView();
1416}
1417
1418void AgendaView::zoomOutVertically()
1419{
1420 if (preferences()->hourSize() > 4 || d->mIsSideBySide) {
1421 if (!d->mIsSideBySide) {
1422 preferences()->setHourSize(preferences()->hourSize() - 1);
1423 }
1424 d->mAgenda->updateConfig();
1425 d->mAgenda->checkScrollBoundaries();
1426
1427 d->mTimeLabelsZone->updateAll();
1428 setChanges(changes() | ZoomChanged);
1429 updateView();
1430 }
1431}
1432
1433void AgendaView::zoomInHorizontally(QDate date)
1434{
1435 QDate begin;
1436 QDate newBegin;
1437 QDate dateToZoom = date;
1438 int ndays;
1439 int count;
1440
1441 begin = d->mSelectedDates.first();
1442 ndays = begin.daysTo(d->mSelectedDates.constLast());
1443
1444 // zoom with Action and are there a selected Incidence?, Yes, I zoom in to it.
1445 if (!dateToZoom.isValid()) {
1446 dateToZoom = d->mAgenda->selectedIncidenceDate();
1447 }
1448
1449 if (!dateToZoom.isValid()) {
1450 if (ndays > 1) {
1451 newBegin = begin.addDays(1);
1452 count = ndays - 1;
1453 Q_EMIT zoomViewHorizontally(newBegin, count);
1454 }
1455 } else {
1456 if (ndays <= 2) {
1457 newBegin = dateToZoom;
1458 count = 1;
1459 } else {
1460 newBegin = dateToZoom.addDays(-ndays / 2 + 1);
1461 count = ndays - 1;
1462 }
1463 Q_EMIT zoomViewHorizontally(newBegin, count);
1464 }
1465}
1466
1467void AgendaView::zoomOutHorizontally(QDate date)
1468{
1469 QDate begin;
1470 QDate newBegin;
1471 QDate dateToZoom = date;
1472 int ndays;
1473 int count;
1474
1475 begin = d->mSelectedDates.first();
1476 ndays = begin.daysTo(d->mSelectedDates.constLast());
1477
1478 // zoom with Action and are there a selected Incidence?, Yes, I zoom out to it.
1479 if (!dateToZoom.isValid()) {
1480 dateToZoom = d->mAgenda->selectedIncidenceDate();
1481 }
1482
1483 if (!dateToZoom.isValid()) {
1484 newBegin = begin.addDays(-1);
1485 count = ndays + 3;
1486 } else {
1487 newBegin = dateToZoom.addDays(-ndays / 2 - 1);
1488 count = ndays + 3;
1489 }
1490
1491 if (abs(count) >= 31) {
1492 qCDebug(CALENDARVIEW_LOG) << "change to the month view?";
1493 } else {
1494 // We want to center the date
1495 Q_EMIT zoomViewHorizontally(newBegin, count);
1496 }
1497}
1498
1499void AgendaView::zoomView(const int delta, QPoint pos, const Qt::Orientation orient)
1500{
1501 // TODO find out why this is necessary. seems to be some kind of performance hack
1502 static QDate zoomDate;
1503 static auto t = new QTimer(this);
1504
1505 // Zoom to the selected incidence, on the other way
1506 // zoom to the date on screen after the first mousewheel move.
1507 if (orient == Qt::Horizontal) {
1508 const QDate date = d->mAgenda->selectedIncidenceDate();
1509 if (date.isValid()) {
1510 zoomDate = date;
1511 } else {
1512 if (!t->isActive()) {
1513 zoomDate = d->mSelectedDates[pos.x()];
1514 }
1515 t->setSingleShot(true);
1516 t->start(1s);
1517 }
1518 if (delta > 0) {
1519 zoomOutHorizontally(zoomDate);
1520 } else {
1521 zoomInHorizontally(zoomDate);
1522 }
1523 } else {
1524 // Vertical zoom
1525 const QPoint posConstentsOld = d->mAgenda->gridToContents(pos);
1526 if (delta > 0) {
1527 zoomOutVertically();
1528 } else {
1529 zoomInVertically();
1530 }
1531 const QPoint posConstentsNew = d->mAgenda->gridToContents(pos);
1532 d->mAgenda->verticalScrollBar()->scroll(0, posConstentsNew.y() - posConstentsOld.y());
1533 }
1534}
1535
1537{
1538 // Check if mSelectedDates has changed, if not just return
1539 // Removes some flickering and gains speed (since this is called by each updateView())
1540 if (!force && d->mSaveSelectedDates == d->mSelectedDates) {
1541 return;
1542 }
1543 d->mSaveSelectedDates = d->mSelectedDates;
1544
1545 const QStringList topStrDecos = preferences()->decorationsAtAgendaViewTop();
1546 const QStringList botStrDecos = preferences()->decorationsAtAgendaViewBottom();
1547 const QStringList selectedPlugins = preferences()->selectedPlugins();
1548
1549 const bool hasTopDecos = d->mTopDayLabelsFrame->createDayLabels(d->mSelectedDates, true, topStrDecos, selectedPlugins);
1550 const bool hasBottomDecos = d->mBottomDayLabelsFrame->createDayLabels(d->mSelectedDates, false, botStrDecos, selectedPlugins);
1551
1552 // no splitter handle if no top deco elements, so something which needs resizing
1553 if (hasTopDecos) {
1554 // inserts in the first position, takes ownership
1555 d->mSplitterAgenda->insertWidget(0, d->mTopDayLabelsFrame);
1556 } else {
1557 d->mTopDayLabelsFrame->setParent(this);
1558 d->mMainLayout->insertWidget(0, d->mTopDayLabelsFrame);
1559 }
1560 // avoid splitter handle if no bottom labels, so something which needs resizing
1561 if (hasBottomDecos) {
1562 // inserts in the last position
1563 d->mBottomDayLabelsFrame->setParent(d->mSplitterAgenda);
1564 d->mBottomDayLabelsFrame->show();
1565 } else {
1566 d->mBottomDayLabelsFrame->setParent(this);
1567 d->mBottomDayLabelsFrame->hide();
1568 }
1569}
1570
1571void AgendaView::enableAgendaUpdate(bool enable)
1572{
1573 d->mAllowAgendaUpdate = enable;
1574}
1575
1577{
1578 return d->mSelectedDates.count();
1579}
1580
1582{
1583 Akonadi::Item::List selected;
1584
1585 KCalendarCore::Incidence::Ptr agendaitem = d->mAgenda->selectedIncidence();
1586 if (agendaitem) {
1587 selected.append(d->mViewCalendar->item(agendaitem));
1588 }
1589
1590 KCalendarCore::Incidence::Ptr dayitem = d->mAllDayAgenda->selectedIncidence();
1591 if (dayitem) {
1592 selected.append(d->mViewCalendar->item(dayitem));
1593 }
1594
1595 return selected;
1596}
1597
1599{
1600 KCalendarCore::DateList selected;
1601 QDate qd;
1602
1603 qd = d->mAgenda->selectedIncidenceDate();
1604 if (qd.isValid()) {
1605 selected.append(qd);
1606 }
1607
1608 qd = d->mAllDayAgenda->selectedIncidenceDate();
1609 if (qd.isValid()) {
1610 selected.append(qd);
1611 }
1612
1613 return selected;
1614}
1615
1616bool AgendaView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const
1617{
1618 if (selectionStart().isValid()) {
1620 QDateTime end = selectionEnd();
1621
1622 if (start.secsTo(end) == 15 * 60) {
1623 // One cell in the agenda view selected, e.g.
1624 // because of a double-click, => Use the default duration
1625 QTime defaultDuration(CalendarSupport::KCalPrefs::instance()->defaultDuration().time());
1626 int addSecs = (defaultDuration.hour() * 3600) + (defaultDuration.minute() * 60);
1627 end = start.addSecs(addSecs);
1628 }
1629
1630 startDt = start;
1631 endDt = end;
1632 allDay = selectedIsAllDay();
1633 return true;
1634 }
1635
1636 // When creating an event from the side-pane view, we have no selection in the agenda
1637 // view, so make sure the event has the default duration as well
1638 if (startDt == endDt) {
1639 QTime defaultDuration(CalendarSupport::KCalPrefs::instance()->defaultDuration().time());
1640 endDt = endDt.addDuration(std::chrono::hours(defaultDuration.hour()) + std::chrono::minutes(defaultDuration.minute()));
1641 return true;
1642 }
1643
1644 return false;
1645}
1646
1647/** returns if only a single cell is selected, or a range of cells */
1649{
1650 if (!selectionStart().isValid() || !selectionEnd().isValid()) {
1651 return false;
1652 }
1653
1654 if (selectedIsAllDay()) {
1655 int days = selectionStart().daysTo(selectionEnd());
1656 return days < 1;
1657 } else {
1658 int secs = selectionStart().secsTo(selectionEnd());
1659 return secs <= 24 * 60 * 60 / d->mAgenda->rows();
1660 }
1661}
1662
1663void AgendaView::updateView()
1664{
1665 fillAgenda();
1666}
1667
1668/*
1669 Update configuration settings for the agenda view. This method is not
1670 complete.
1671*/
1672void AgendaView::updateConfig()
1673{
1674 // Agenda can be null if setPreferences() is called inside the ctor
1675 // We don't need to update anything in this case.
1676 if (d->mAgenda && d->mAllDayAgenda) {
1677 d->mAgenda->updateConfig();
1678 d->mAllDayAgenda->updateConfig();
1679 d->mTimeLabelsZone->setPreferences(preferences());
1680 d->mTimeLabelsZone->updateAll();
1681 updateTimeBarWidth();
1683 createDayLabels(true);
1684 setChanges(changes() | ConfigChanged);
1685 updateView();
1686 }
1687}
1688
1689void AgendaView::createTimeBarHeaders()
1690{
1691 qDeleteAll(d->mTimeBarHeaders);
1692 d->mTimeBarHeaders.clear();
1693
1694 const QFont oldFont(font());
1695 QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont();
1696 labelFont.setPointSize(labelFont.pointSize() - SHRINKDOWN);
1697
1698 const auto lst = d->mTimeLabelsZone->timeLabels();
1699 for (QScrollArea *area : lst) {
1700 auto timeLabel = static_cast<TimeLabels *>(area->widget());
1701 auto label = new QLabel(timeLabel->header().replace(QLatin1Char('/'), QStringLiteral("/ ")), d->mTimeBarHeaderFrame);
1702 d->mTimeBarHeaderFrame->layout()->addWidget(label);
1703 label->setFont(labelFont);
1704 label->setAlignment(Qt::AlignBottom | Qt::AlignRight);
1705 label->setContentsMargins({});
1706 label->setWordWrap(true);
1707 label->setToolTip(timeLabel->headerToolTip());
1708 d->mTimeBarHeaders.append(label);
1709 }
1710 setFont(oldFont);
1711}
1712
1713void AgendaView::updateTimeBarWidth()
1714{
1715 if (d->mIsSideBySide) {
1716 return;
1717 }
1718
1719 createTimeBarHeaders();
1720
1721 const QFont oldFont(font());
1722 QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont();
1723 labelFont.setPointSize(labelFont.pointSize() - SHRINKDOWN);
1724 QFontMetrics fm(labelFont);
1725
1726 int width = d->mTimeLabelsZone->preferedTimeLabelsWidth();
1727 for (QLabel *l : std::as_const(d->mTimeBarHeaders)) {
1728 const auto lst = l->text().split(QLatin1Char(' '));
1729 for (const QString &word : lst) {
1730 width = qMax(width, fm.boundingRect(word).width());
1731 }
1732 }
1733 setFont(oldFont);
1734
1735 width = width + fm.boundingRect(QLatin1Char('/')).width();
1736
1737 const int timeBarWidth = width * d->mTimeBarHeaders.count();
1738
1739 d->mTimeBarHeaderFrame->setFixedWidth(timeBarWidth - SPACING);
1740 d->mTimeLabelsZone->setFixedWidth(timeBarWidth);
1741 if (d->mDummyAllDayLeft) {
1742 d->mDummyAllDayLeft->setFixedWidth(0);
1743 }
1744
1745 d->mTopDayLabelsFrame->setWeekWidth(timeBarWidth);
1746 d->mBottomDayLabelsFrame->setWeekWidth(timeBarWidth);
1747}
1748
1749// By deafult QDateTime::toTimeSpec() will turn Qt::TimeZone to Qt::LocalTime,
1750// which would turn the event's timezone into "floating". This code actually
1751// preserves the timezone, if the spec is Qt::TimeZone.
1752static QDateTime copyTimeSpec(QDateTime dt, const QDateTime &source)
1753{
1754 switch (source.timeSpec()) {
1755 case Qt::TimeZone:
1756 return dt.toTimeZone(source.timeZone());
1757 case Qt::LocalTime:
1759 case Qt::UTC:
1760 return dt.toTimeZone(QTimeZone::UTC);
1761 case Qt::OffsetFromUTC:
1762 return dt.toOffsetFromUtc(source.offsetFromUtc());
1763 }
1764
1765 Q_UNREACHABLE();
1766}
1767
1768void AgendaView::updateEventDates(AgendaItem *item, bool addIncidence, Akonadi::Collection::Id collectionId)
1769{
1770 qCDebug(CALENDARVIEW_LOG) << item->text() << "; item->cellXLeft(): " << item->cellXLeft() << "; item->cellYTop(): " << item->cellYTop()
1771 << "; item->lastMultiItem(): " << item->lastMultiItem() << "; item->itemPos(): " << item->itemPos()
1772 << "; item->itemCount(): " << item->itemCount();
1773
1774 QDateTime startDt;
1775 QDateTime endDt;
1776
1777 // Start date of this incidence, calculate the offset from it
1778 // (so recurring and non-recurring items can be treated exactly the same,
1779 // we never need to check for recurs(), because we only move the start day
1780 // by the number of days the agenda item was really moved. Smart, isn't it?)
1781 QDate thisDate;
1782 if (item->cellXLeft() < 0) {
1783 thisDate = (d->mSelectedDates.first()).addDays(item->cellXLeft());
1784 } else {
1785 thisDate = d->mSelectedDates[item->cellXLeft()];
1786 }
1787 int daysOffset = 0;
1788
1789 // daysOffset should only be calculated if item->cellXLeft() is positive which doesn't happen
1790 // if the event's start isn't visible.
1791 if (item->cellXLeft() >= 0) {
1792 daysOffset = item->occurrenceDate().daysTo(thisDate);
1793 }
1794
1795 int daysLength = 0;
1796
1797 KCalendarCore::Incidence::Ptr incidence = item->incidence();
1798 Akonadi::Item aitem = d->mViewCalendar->item(incidence);
1799 if ((!aitem.isValid() && !addIncidence) || !incidence || !changer()) {
1800 qCWarning(CALENDARVIEW_LOG) << "changer is " << changer() << " and incidence is " << incidence.data();
1801 return;
1802 }
1803
1804 QTime startTime(0, 0, 0);
1805 QTime endTime(0, 0, 0);
1806 if (incidence->allDay()) {
1807 daysLength = item->cellWidth() - 1;
1808 } else {
1809 startTime = d->mAgenda->gyToTime(item->cellYTop());
1810 if (item->lastMultiItem()) {
1811 endTime = d->mAgenda->gyToTime(item->lastMultiItem()->cellYBottom() + 1);
1812 daysLength = item->lastMultiItem()->cellXLeft() - item->cellXLeft();
1813 } else if (item->itemPos() == item->itemCount() && item->itemCount() > 1) {
1814 /* multiitem handling in agenda assumes two things:
1815 - The start (first KOAgendaItem) is always visible.
1816 - The first KOAgendaItem of the incidence has a non-null item->lastMultiItem()
1817 pointing to the last KOagendaItem.
1818
1819 But those aren't always met, for example when in day-view.
1820 kolab/issue4417
1821 */
1822
1823 // Cornercase 1: - Resizing the end of the event but the start isn't visible
1824 endTime = d->mAgenda->gyToTime(item->cellYBottom() + 1);
1825 daysLength = item->itemCount() - 1;
1826 startTime = incidence->dtStart().time();
1827 } else if (item->itemPos() == 1 && item->itemCount() > 1) {
1828 // Cornercase 2: - Resizing the start of the event but the end isn't visible
1829 endTime = incidence->dateTime(KCalendarCore::Incidence::RoleEnd).time();
1830 daysLength = item->itemCount() - 1;
1831 } else {
1832 endTime = d->mAgenda->gyToTime(item->cellYBottom() + 1);
1833 }
1834 }
1835
1836 // FIXME: use a visitor here
1837 if (const KCalendarCore::Event::Ptr ev = CalendarSupport::event(incidence)) {
1838 startDt = incidence->dtStart();
1839 // convert to calendar timespec because we then manipulate it
1840 // with time coming from the calendar
1841 startDt = startDt.toLocalTime();
1842 startDt = startDt.addDays(daysOffset);
1843 if (!incidence->allDay()) {
1844 startDt.setTime(startTime);
1845 }
1846 endDt = startDt.addDays(daysLength);
1847 if (!incidence->allDay()) {
1848 endDt.setTime(endTime);
1849 }
1850 if (incidence->dtStart().toLocalTime() == startDt && ev->dtEnd().toLocalTime() == endDt) {
1851 // No change
1852 QTimer::singleShot(0, this, &AgendaView::updateView);
1853 return;
1854 }
1855 /* setDtEnd() must be called before setDtStart(), otherwise, when moving
1856 * events, CalendarLocal::incidenceUpdated() will not remove the old hash
1857 * and that causes the event to be shown in the old date also (bug #179157).
1858 *
1859 * TODO: We need a better hashing mechanism for CalendarLocal.
1860 */
1861 ev->setDtEnd(copyTimeSpec(endDt, incidence->dateTime(KCalendarCore::Incidence::RoleEnd)));
1862 incidence->setDtStart(copyTimeSpec(startDt, incidence->dtStart()));
1863 } else if (const KCalendarCore::Todo::Ptr td = CalendarSupport::todo(incidence)) {
1864 endDt = td->dtDue(true).toLocalTime().addDays(daysOffset);
1865 endDt.setTime(td->allDay() ? QTime(00, 00, 00) : endTime);
1866
1867 if (td->dtDue(true).toLocalTime() == endDt) {
1868 // No change
1869 QMetaObject::invokeMethod(this, &AgendaView::updateView, Qt::QueuedConnection);
1870 return;
1871 }
1872
1873 const auto shift = td->dtDue(true).secsTo(endDt);
1874 startDt = td->dtStart(true).addSecs(shift);
1875 if (td->hasStartDate()) {
1876 td->setDtStart(copyTimeSpec(startDt, incidence->dtStart()));
1877 }
1878 if (td->recurs()) {
1879 td->setDtRecurrence(td->dtRecurrence().addSecs(shift));
1880 }
1881 td->setDtDue(copyTimeSpec(endDt, td->dtDue()), true);
1882 }
1883
1884 if (!incidence->hasRecurrenceId()) {
1885 item->setOccurrenceDateTime(startDt);
1886 }
1887
1888 bool result;
1889 if (addIncidence) {
1890 auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), collectionId);
1891 result = changer()->createIncidence(incidence, collection, this) != -1;
1892 } else {
1894 aitem.setPayload<KCalendarCore::Incidence::Ptr>(incidence);
1895 result = changer()->modifyIncidence(aitem, oldIncidence, this) != -1;
1896 }
1897
1898 // Update the view correctly if an agenda item move was aborted by
1899 // cancelling one of the subsequent dialogs.
1900 if (!result) {
1901 setChanges(changes() | IncidencesEdited);
1902 QMetaObject::invokeMethod(this, &AgendaView::updateView, Qt::QueuedConnection);
1903 return;
1904 }
1905
1906 // don't update the agenda as the item already has the correct coordinates.
1907 // an update would delete the current item and recreate it, but we are still
1908 // using a pointer to that item! => CRASH
1909 enableAgendaUpdate(false);
1910 // We need to do this in a timer to make sure we are not deleting the item
1911 // we are currently working on, which would lead to crashes
1912 // Only the actually moved agenda item is already at the correct position and mustn't be
1913 // recreated. All others have to!!!
1914 if (incidence->recurs() || incidence->hasRecurrenceId()) {
1915 d->mUpdateItem = aitem;
1916 QMetaObject::invokeMethod(this, &AgendaView::updateView, Qt::QueuedConnection);
1917 }
1918
1919 enableAgendaUpdate(true);
1920}
1921
1923{
1924 if (d->mSelectedDates.isEmpty()) {
1925 return {};
1926 }
1927 return d->mSelectedDates.first();
1928}
1929
1931{
1932 if (d->mSelectedDates.isEmpty()) {
1933 return {};
1934 }
1935 return d->mSelectedDates.last();
1936}
1937
1938void AgendaView::showDates(const QDate &start, const QDate &end, const QDate &preferredMonth)
1939{
1940 Q_UNUSED(preferredMonth)
1941 if (!d->mSelectedDates.isEmpty() && d->mSelectedDates.first() == start && d->mSelectedDates.last() == end) {
1942 return;
1943 }
1944
1945 if (!start.isValid() || !end.isValid() || start > end || start.daysTo(end) > MAX_DAY_COUNT) {
1946 qCWarning(CALENDARVIEW_LOG) << "got bizarre parameters: " << start << end << " - aborting here";
1947 return;
1948 }
1949
1950 d->mSelectedDates = d->generateDateList(start, end);
1951
1952 // and update the view
1953 setChanges(changes() | DatesChanged);
1954 fillAgenda();
1955 d->mTimeLabelsZone->update();
1956}
1957
1958void AgendaView::showIncidences(const Akonadi::Item::List &incidences, const QDate &date)
1959{
1960 Q_UNUSED(date)
1961
1962 QDateTime start = Akonadi::CalendarUtils::incidence(incidences.first())->dtStart().toLocalTime();
1963 QDateTime end = Akonadi::CalendarUtils::incidence(incidences.first())->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime();
1964 Akonadi::Item first = incidences.first();
1965 for (const Akonadi::Item &aitem : incidences) {
1966 // we must check if they are not filtered; if they are, remove the filter
1967 const auto &cal = d->mViewCalendar->calendarForCollection(aitem.storageCollectionId());
1968 if (cal && cal->filter()) {
1969 const bool filtered = !cal->filter()->filterIncidence(Akonadi::CalendarUtils::incidence(aitem));
1970 if (filtered) {
1971 cal->setFilter(nullptr);
1972 }
1973 }
1974
1975 if (Akonadi::CalendarUtils::incidence(aitem)->dtStart().toLocalTime() < start) {
1976 first = aitem;
1977 }
1978 start = qMin(start, Akonadi::CalendarUtils::incidence(aitem)->dtStart().toLocalTime());
1979 end = qMax(start, Akonadi::CalendarUtils::incidence(aitem)->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime());
1980 }
1981
1982 end.toTimeZone(start.timeZone()); // allow direct comparison of dates
1983 if (start.date().daysTo(end.date()) + 1 <= currentDateCount()) {
1984 showDates(start.date(), end.date());
1985 } else {
1986 showDates(start.date(), start.date().addDays(currentDateCount() - 1));
1987 }
1988
1989 d->mAgenda->selectItem(first);
1990}
1991
1993{
1994 if (changes() == NothingChanged) {
1995 return;
1996 }
1997
1998 const QString selectedAgendaId = d->mAgenda->lastSelectedItemUid();
1999 const QString selectedAllDayAgendaId = d->mAllDayAgenda->lastSelectedItemUid();
2000
2001 enableAgendaUpdate(true);
2002 d->clearView();
2003
2004 if (d->mViewCalendar->calendarCount() == 0) {
2005 return;
2006 }
2007
2008 /*
2009 qCDebug(CALENDARVIEW_LOG) << "changes = " << changes()
2010 << "; mUpdateAgenda = " << d->mUpdateAgenda
2011 << "; mUpdateAllDayAgenda = " << d->mUpdateAllDayAgenda; */
2012
2013 /* Remember the item Ids of the selected items. In case one of the
2014 * items was deleted and re-added, we want to reselect it. */
2015 if (changes().testFlag(DatesChanged)) {
2016 d->mAllDayAgenda->changeColumns(d->mSelectedDates.count());
2017 d->mAgenda->changeColumns(d->mSelectedDates.count());
2018 d->changeColumns(d->mSelectedDates.count());
2019
2020 createDayLabels(false);
2022
2023 d->mAgenda->setDateList(d->mSelectedDates);
2024 }
2025
2026 setChanges(NothingChanged);
2027
2028 bool somethingReselected = false;
2029 const KCalendarCore::Incidence::List incidences = d->mViewCalendar->incidences();
2030
2031 for (const KCalendarCore::Incidence::Ptr &incidence : incidences) {
2032 Q_ASSERT(incidence);
2033 const bool wasSelected = (incidence->uid() == selectedAgendaId) || (incidence->uid() == selectedAllDayAgendaId);
2034
2035 if ((incidence->allDay() && d->mUpdateAllDayAgenda) || (!incidence->allDay() && d->mUpdateAgenda)) {
2036 displayIncidence(incidence, wasSelected);
2037 }
2038
2039 if (wasSelected) {
2040 somethingReselected = true;
2041 }
2042 }
2043
2044 d->mAgenda->checkScrollBoundaries();
2046
2047 // mAgenda->viewport()->update();
2048 // mAllDayAgenda->viewport()->update();
2049
2050 // make invalid
2052
2053 d->mUpdateAgenda = false;
2054 d->mUpdateAllDayAgenda = false;
2055
2056 if (!somethingReselected) {
2057 Q_EMIT incidenceSelected(Akonadi::Item(), QDate());
2058 }
2059}
2060
2061bool AgendaView::displayIncidence(const KCalendarCore::Incidence::Ptr &incidence, bool createSelected)
2062{
2063 if (!incidence) {
2064 return false;
2065 }
2066
2067 if (incidence->hasRecurrenceId()) {
2068 // Normally a disassociated instance belongs to a recurring instance that
2069 // displays it.
2070 const auto cal = calendar2(incidence);
2071 if (cal && cal->incidence(incidence->uid())) {
2072 return false;
2073 }
2074 }
2075
2076 KCalendarCore::Todo::Ptr todo = CalendarSupport::todo(incidence);
2077 if (todo && (!preferences()->showTodosAgendaView() || !todo->hasDueDate())) {
2078 return false;
2079 }
2080
2081 KCalendarCore::Event::Ptr event = CalendarSupport::event(incidence);
2082 const QDate today = QDate::currentDate();
2083
2084 QDateTime firstVisibleDateTime(d->mSelectedDates.first(), QTime(0, 0, 0), QTimeZone::LocalTime);
2085 QDateTime lastVisibleDateTime(d->mSelectedDates.last(), QTime(23, 59, 59, 999), QTimeZone::LocalTime);
2086
2087 // Optimization, very cheap operation that discards incidences that aren't in the timespan
2088 if (!d->mightBeVisible(incidence)) {
2089 return false;
2090 }
2091
2092 std::vector<QDateTime> dateTimeList;
2093
2094 const QDateTime incDtStart = incidence->dtStart().toLocalTime();
2095 const QDateTime incDtEnd = incidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime();
2096
2097 bool alreadyAddedToday = false;
2098
2099 if (incidence->recurs()) {
2100 // timed incidences occur in [dtStart(), dtEnd()[
2101 // all-day incidences occur in [dtStart(), dtEnd()]
2102 // so we subtract 1 second in the timed case
2103 const int secsToAdd = incidence->allDay() ? 0 : -1;
2104 const int eventDuration = event ? incDtStart.daysTo(incDtEnd.addSecs(secsToAdd)) : 0;
2105
2106 // if there's a multiday event that starts before firstVisibleDateTime but ends after
2107 // lets include it. timesInInterval() ignores incidences that aren't totaly inside
2108 // the range
2109 const QDateTime startDateTimeWithOffset = firstVisibleDateTime.addDays(-eventDuration);
2110
2111 KCalendarCore::OccurrenceIterator rIt(*calendar2(incidence), incidence, startDateTimeWithOffset, lastVisibleDateTime);
2112 while (rIt.hasNext()) {
2113 rIt.next();
2114 auto occurrenceDate = rIt.occurrenceStartDate().toLocalTime();
2115 if (const auto todo = CalendarSupport::todo(rIt.incidence())) {
2116 // Recurrence exceptions may have durations different from the normal recurrences.
2117 occurrenceDate = occurrenceDate.addSecs(todo->dtStart().secsTo(todo->dtDue()));
2118 }
2119 const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy(rIt.incidence());
2120 if (makesDayBusy) {
2121 KCalendarCore::Event::List &busyEvents = d->mBusyDays[occurrenceDate.date()];
2122 busyEvents.append(event);
2123 }
2124
2125 if (occurrenceDate.date() == today) {
2126 alreadyAddedToday = true;
2127 }
2128 d->insertIncidence(rIt.incidence(), rIt.recurrenceId(), occurrenceDate, createSelected);
2129 }
2130 } else {
2131 QDateTime dateToAdd; // date to add to our date list
2132 QDateTime incidenceEnd;
2133
2134 if (todo && todo->hasDueDate() && !todo->isOverdue()) {
2135 // If it's not overdue it will be shown at the original date (not today)
2136 dateToAdd = todo->dtDue().toLocalTime();
2137
2138 // To-dos due at a specific time are drawn with the bottom of the rectangle at dtDue.
2139 // If dtDue is at 00:00, then it should be displayed in the previous day, at 23:59.
2140 if (!todo->allDay() && dateToAdd.time() == QTime(0, 0)) {
2141 dateToAdd = dateToAdd.addSecs(-1);
2142 }
2143
2144 incidenceEnd = dateToAdd;
2145 } else if (event) {
2146 dateToAdd = incDtStart;
2147 incidenceEnd = incDtEnd;
2148 }
2149
2150 if (dateToAdd.isValid() && incidence->allDay()) {
2151 // so comparisons with < > actually work
2152 dateToAdd.setTime(QTime(0, 0));
2153 incidenceEnd.setTime(QTime(23, 59, 59, 999));
2154 }
2155
2156 if (dateToAdd <= lastVisibleDateTime && incidenceEnd > firstVisibleDateTime) {
2157 dateTimeList.push_back(dateToAdd);
2158 }
2159 }
2160
2161 // ToDo items shall be displayed today if they are overdue
2162 const QDateTime dateTimeToday = QDateTime(today, QTime(0, 0), QTimeZone::LocalTime);
2163 if (todo && todo->isOverdue() && dateTimeToday >= firstVisibleDateTime && dateTimeToday <= lastVisibleDateTime) {
2164 /* If there's a recurring instance showing up today don't add "today" again
2165 * we don't want the event to appear duplicated */
2166 if (!alreadyAddedToday) {
2167 dateTimeList.push_back(dateTimeToday);
2168 }
2169 }
2170
2171 const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy(incidence);
2172 for (auto t = dateTimeList.begin(); t != dateTimeList.end(); ++t) {
2173 if (makesDayBusy) {
2174 KCalendarCore::Event::List &busyEvents = d->mBusyDays[(*t).date()];
2175 busyEvents.append(event);
2176 }
2177
2178 d->insertIncidence(incidence, t->toLocalTime(), t->toLocalTime(), createSelected);
2179 }
2180
2181 // Can be multiday
2182 if (event && makesDayBusy && event->isMultiDay()) {
2183 const QDate lastVisibleDate = d->mSelectedDates.last();
2184 for (QDate date = event->dtStart().date(); date <= event->dtEnd().date() && date <= lastVisibleDate; date = date.addDays(1)) {
2185 KCalendarCore::Event::List &busyEvents = d->mBusyDays[date];
2186 busyEvents.append(event);
2187 }
2188 }
2189
2190 return !dateTimeList.empty();
2191}
2192
2193void AgendaView::updateEventIndicatorTop(int newY)
2194{
2195 for (int i = 0; i < d->mMinY.size(); ++i) {
2196 d->mEventIndicatorTop->enableColumn(i, newY > d->mMinY[i]);
2197 }
2198 d->mEventIndicatorTop->update();
2199}
2200
2201void AgendaView::updateEventIndicatorBottom(int newY)
2202{
2203 for (int i = 0; i < d->mMaxY.size(); ++i) {
2204 d->mEventIndicatorBottom->enableColumn(i, newY <= d->mMaxY[i]);
2205 }
2206 d->mEventIndicatorBottom->update();
2207}
2208
2209void AgendaView::slotIncidencesDropped(const QList<QUrl> &items, const QPoint &gpos, bool allDay)
2210{
2211 Q_UNUSED(items)
2212 Q_UNUSED(gpos)
2213 Q_UNUSED(allDay)
2214
2215#ifdef AKONADI_PORT_DISABLED // one item -> multiple items, Incidence* -> akonadi item url
2216 // (we might have to fetch the items here first!)
2217 if (gpos.x() < 0 || gpos.y() < 0) {
2218 return;
2219 }
2220
2221 const QDate day = d->mSelectedDates[gpos.x()];
2222 const QTime time = d->mAgenda->gyToTime(gpos.y());
2223 KDateTime newTime(day, time, preferences()->timeSpec());
2224 newTime.setDateOnly(allDay);
2225
2226 Todo::Ptr todo = Akonadi::CalendarUtils4::todo(todoItem);
2227 if (todo && dynamic_cast<Akonadi::ETMCalendar *>(calendar())) {
2228 const Akonadi::Item existingTodoItem = calendar()->itemForIncidence(calendar()->todo(todo->uid()));
2229
2230 if (Todo::Ptr existingTodo = Akonadi::CalendarUtils::todo(existingTodoItem)) {
2231 qCDebug(CALENDARVIEW_LOG) << "Drop existing Todo";
2232 Todo::Ptr oldTodo(existingTodo->clone());
2233 if (changer()) {
2234 existingTodo->setDtDue(newTime);
2235 existingTodo->setAllDay(allDay);
2236 changer()->modifyIncidence(existingTodoItem, oldTodo, this);
2237 } else {
2238 KMessageBox::error(this,
2239 i18n("Unable to modify this to-do, "
2240 "because it cannot be locked."));
2241 }
2242 } else {
2243 qCDebug(CALENDARVIEW_LOG) << "Drop new Todo";
2244 todo->setDtDue(newTime);
2245 todo->setAllDay(allDay);
2246 if (!changer()->addIncidence(todo, this)) {
2247 KMessageBox::error(this, i18n("Unable to save %1 \"%2\".", i18n(todo->type()), todo->summary()));
2248 }
2249 }
2250 }
2251#else
2252 qCDebug(CALENDARVIEW_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO;
2253#endif
2254}
2255
2256static void setDateTime(KCalendarCore::Incidence::Ptr incidence, const QDateTime &dt, bool allDay)
2257{
2258 incidence->setAllDay(allDay);
2259
2260 if (auto todo = CalendarSupport::todo(incidence)) {
2261 // To-dos are displayed on their due date and time. Make sure the todo is displayed
2262 // where it was dropped.
2263 QDateTime dtStart = todo->dtStart();
2264 if (dtStart.isValid()) {
2265 auto duration = todo->dtStart().daysTo(todo->dtDue());
2266 dtStart = dt.addDays(-duration);
2267 dtStart.setTime({0, 0, 0});
2268 }
2269 // Set dtDue before dtStart; see comment in updateEventDates().
2270 todo->setDtDue(dt, true);
2271 todo->setDtStart(dtStart);
2272 } else if (auto event = CalendarSupport::event(incidence)) {
2273 auto duration = event->dtStart().secsTo(event->dtEnd());
2274 if (duration == 0) {
2275 auto defaultDuration = CalendarSupport::KCalPrefs::instance()->defaultDuration().time();
2276 duration = (defaultDuration.hour() * 3600) + (defaultDuration.minute() * 60);
2277 }
2278 event->setDtEnd(dt.addSecs(duration));
2279 event->setDtStart(dt);
2280 } else { // Can't happen, but ...
2281 incidence->setDtStart(dt);
2282 }
2283}
2284
2285void AgendaView::slotIncidencesDropped(const KCalendarCore::Incidence::List &incidences, const QPoint &gpos, bool allDay)
2286{
2287 if (gpos.x() < 0 || gpos.y() < 0) {
2288 return;
2289 }
2290
2291 const QDate day = d->mSelectedDates[gpos.x()];
2292 const QTime time = d->mAgenda->gyToTime(gpos.y());
2293 QDateTime newTime(day, time, QTimeZone::LocalTime);
2294
2295 for (const KCalendarCore::Incidence::Ptr &incidence : incidences) {
2296 const Akonadi::Item existingItem = d->mViewCalendar->item(incidence);
2297 const bool existsInSameCollection = existingItem.isValid();
2298
2299 if (existingItem.isValid() && existsInSameCollection) {
2300 auto newIncidence = existingItem.payload<KCalendarCore::Incidence::Ptr>();
2301
2302 if (newIncidence->dtStart() == newTime && newIncidence->allDay() == allDay) {
2303 // Nothing changed
2304 continue;
2305 }
2306
2307 KCalendarCore::Incidence::Ptr oldIncidence(newIncidence->clone());
2308 setDateTime(newIncidence, newTime, allDay);
2309
2310 (void)changer()->modifyIncidence(existingItem, oldIncidence, this);
2311 } else { // Create a new one
2312 // The drop came from another application. Create a new incidence.
2313 setDateTime(incidence, newTime, allDay);
2314 incidence->setUid(KCalendarCore::CalFormat::createUniqueId());
2315 // Drop into the default collection
2316 const bool added = -1 != changer()->createIncidence(incidence, Akonadi::Collection(), this);
2317
2318 if (added) {
2319 // TODO: make async
2320 if (existingItem.isValid()) { // Dragged from one agenda to another, delete origin
2321 (void)changer()->deleteIncidence(existingItem);
2322 }
2323 }
2324 }
2325 }
2326}
2327
2328void AgendaView::startDrag(const KCalendarCore::Incidence::Ptr &incidence)
2329{
2330 const Akonadi::Item item = d->mViewCalendar->item(incidence);
2331 if (item.isValid()) {
2332 startDrag(item);
2333 }
2334}
2335
2336void AgendaView::startDrag(const Akonadi::Item &incidence)
2337{
2338 if (QDrag *drag = CalendarSupport::createDrag(incidence, this)) {
2339 drag->exec();
2340 }
2341}
2342
2343void AgendaView::readSettings()
2344{
2345 KSharedConfig::Ptr config = KSharedConfig::openConfig();
2346 readSettings(config.data());
2347}
2348
2349void AgendaView::readSettings(const KConfig *config)
2350{
2351 const KConfigGroup group = config->group(QStringLiteral("Views"));
2352
2353 const QList<int> sizes = group.readEntry("Separator AgendaView", QList<int>());
2354
2355 // the size depends on the number of plugins used
2356 // we don't want to read invalid/corrupted settings or else agenda becomes invisible
2357 if (sizes.count() >= 2 && !sizes.contains(0)) {
2358 d->mSplitterAgenda->setSizes(sizes);
2359 updateConfig();
2360 }
2361}
2362
2363void AgendaView::writeSettings(KConfig *config)
2364{
2365 KConfigGroup group = config->group(QStringLiteral("Views"));
2366
2367 QList<int> list = d->mSplitterAgenda->sizes();
2368 group.writeEntry("Separator AgendaView", list);
2369}
2370
2371QList<bool> AgendaView::busyDayMask() const
2372{
2373 if (d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid()) {
2374 return {};
2375 }
2376
2377 QList<bool> busyDayMask;
2378 busyDayMask.resize(d->mSelectedDates.count());
2379
2380 for (int i = 0; i < d->mSelectedDates.count(); ++i) {
2381 busyDayMask[i] = !d->mBusyDays[d->mSelectedDates[i]].isEmpty();
2382 }
2383
2384 return busyDayMask;
2385}
2386
2388{
2389 if (d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid()) {
2390 return;
2391 }
2392
2393 d->mHolidayMask.resize(d->mSelectedDates.count() + 1);
2394
2395 const QList<QDate> workDays = CalendarSupport::workDays(d->mSelectedDates.constFirst().addDays(-1), d->mSelectedDates.last());
2396 for (int i = 0; i < d->mSelectedDates.count(); ++i) {
2397 d->mHolidayMask[i] = !workDays.contains(d->mSelectedDates[i]);
2398 }
2399
2400 // Store the information about the day before the visible area (needed for
2401 // overnight working hours) in the last bit of the mask:
2402 bool showDay = !workDays.contains(d->mSelectedDates[0].addDays(-1));
2403 d->mHolidayMask[d->mSelectedDates.count()] = showDay;
2404
2405 d->mAgenda->setHolidayMask(&d->mHolidayMask);
2406 d->mAllDayAgenda->setHolidayMask(&d->mHolidayMask);
2407}
2408
2410{
2411 d->mAgenda->deselectItem();
2412 d->mAllDayAgenda->deselectItem();
2413}
2414
2416{
2418 d->mTimeSpanInAllDay = true;
2419}
2420
2422{
2423 if (d->mSelectedDates.isEmpty()) {
2424 return;
2425 }
2426
2427 d->mTimeSpanInAllDay = false;
2428
2429 const QDate dayStart = d->mSelectedDates[qBound(0, start.x(), (int)d->mSelectedDates.size() - 1)];
2430 const QDate dayEnd = d->mSelectedDates[qBound(0, end.x(), (int)d->mSelectedDates.size() - 1)];
2431
2432 const QTime timeStart = d->mAgenda->gyToTime(start.y());
2433 const QTime timeEnd = d->mAgenda->gyToTime(end.y() + 1);
2434
2435 d->mTimeSpanBegin = QDateTime(dayStart, timeStart);
2436 d->mTimeSpanEnd = QDateTime(dayEnd, timeEnd);
2437}
2438
2440{
2441 return d->mTimeSpanBegin;
2442}
2443
2445{
2446 return d->mTimeSpanEnd;
2447}
2448
2450{
2451 return d->mTimeSpanInAllDay;
2452}
2453
2455{
2456 d->mTimeSpanBegin.setDate(QDate());
2457 d->mTimeSpanEnd.setDate(QDate());
2458 d->mTimeSpanInAllDay = false;
2459}
2460
2461void AgendaView::removeIncidence(const KCalendarCore::Incidence::Ptr &incidence)
2462{
2463 // Don't wrap this in a if (incidence->isAllDay) because all day
2464 // property might have changed
2465 d->mAllDayAgenda->removeIncidence(incidence);
2466 d->mAgenda->removeIncidence(incidence);
2467
2468 if (!incidence->hasRecurrenceId() && d->mViewCalendar->isValid(incidence->uid())) {
2469 // Deleted incidence is an main incidence
2470 // Delete all exceptions as well
2471 const auto cal = calendar2(incidence->uid());
2472 if (cal) {
2473 const KCalendarCore::Incidence::List exceptions = cal->instances(incidence);
2474 for (const KCalendarCore::Incidence::Ptr &exception : exceptions) {
2475 if (exception->allDay()) {
2476 d->mAllDayAgenda->removeIncidence(exception);
2477 } else {
2478 d->mAgenda->removeIncidence(exception);
2479 }
2480 }
2481 }
2482 }
2483}
2484
2486{
2487 d->mUpdateEventIndicatorsScheduled = false;
2488 d->mMinY = d->mAgenda->minContentsY();
2489 d->mMaxY = d->mAgenda->maxContentsY();
2490
2491 d->mAgenda->checkScrollBoundaries();
2492 updateEventIndicatorTop(d->mAgenda->visibleContentsYMin());
2493 updateEventIndicatorBottom(d->mAgenda->visibleContentsYMax());
2494}
2495
2496void AgendaView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
2497{
2499 d->mAgenda->setIncidenceChanger(changer);
2500 d->mAllDayAgenda->setIncidenceChanger(changer);
2501}
2502
2503void AgendaView::clearTimeSpanSelection()
2504{
2505 d->mAgenda->clearSelection();
2506 d->mAllDayAgenda->clearSelection();
2508}
2509
2510Agenda *AgendaView::agenda() const
2511{
2512 return d->mAgenda;
2513}
2514
2515Agenda *AgendaView::allDayAgenda() const
2516{
2517 return d->mAllDayAgenda;
2518}
2519
2520QSplitter *AgendaView::splitter() const
2521{
2522 return d->mSplitterAgenda;
2523}
2524
2525bool AgendaView::filterByCollectionSelection(const KCalendarCore::Incidence::Ptr &incidence)
2526{
2527 Q_UNUSED(incidence);
2528 return true;
2529 /*
2530 const Akonadi::Item item = d->mViewCalendar->item(incidence);
2531
2532 if (!item.isValid()) {
2533 return true;
2534 }
2535
2536 if (customCollectionSelection()) {
2537 return customCollectionSelection()->contains(item.parentCollection().id());
2538 }
2539
2540 return true;
2541 */
2542}
2543
2544void AgendaView::alignAgendas()
2545{
2546 // resize dummy widget so the allday agenda lines up with the hourly agenda.
2547 if (d->mDummyAllDayLeft) {
2548 d->mDummyAllDayLeft->setFixedWidth(d->mTimeLabelsZone->width() - d->mTimeBarHeaderFrame->width() - SPACING);
2549 }
2550
2551 // Must be async, so they are centered
2552 createDayLabels(true);
2553}
2554
2556{
2557 d->setChanges(changes);
2558}
2559
2560void AgendaView::setTitle(const QString &title)
2561{
2562 d->mTopDayLabelsFrame->setCalendarName(title);
2563}
2564
2565void AgendaView::scheduleUpdateEventIndicators()
2566{
2567 if (!d->mUpdateEventIndicatorsScheduled) {
2568 d->mUpdateEventIndicatorsScheduled = true;
2570 }
2571}
2572
2573#include "agendaview.moc"
2574
2575#include "moc_agendaview.cpp"
static Collection updatedCollection(const QAbstractItemModel *model, qint64 collectionId)
T payload() const
bool isValid() const
void setPayload(const T &p)
This class describes the widgets that represent the various calendar items in the agenda view.
Definition agendaitem.h:61
void setOccurrenceDateTime(const QDateTime &qd)
Update the date of this item's occurrence (not in the event)
AgendaView is the agenda-like view that displays events in a single or multi-day view.
Definition agendaview.h:70
void updateEventIndicators()
Updates the event indicators after a certain incidence was modified or removed.
KCalendarCore::DateList selectedIncidenceDates() const override
returns the currently selected incidence's dates
void clearSelection() override
Clear selection.
void setIncidenceChanger(Akonadi::IncidenceChanger *changer) override
Assign a new incidence change helper object.
int currentDateCount() const override
Returns number of currently shown dates.
QDateTime selectionStart() const override
start-datetime of selection
bool eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const override
return the default start/end date/time for new events
bool selectedIsSingleCell() const
returns if only a single cell is selected, or a range of cells
void deleteSelectedDateTime()
make selected start/end invalid
void setChanges(EventView::Changes) override
Notifies the view that there are pending changes so a redraw is needed.
bool selectedIsAllDay() const
returns true if selection is for whole day
void createDayLabels(bool force)
Create labels for the selected dates.
QDate endDate() const
Last shown day.
void fillAgenda()
Fill agenda using the current set value for the start date.
void updateEventDates(AgendaItem *item, bool addIncidence, Akonadi::Collection::Id collectionId)
Update event belonging to agenda item If the incidence is multi-day, item is the first one.
void showDates(const QDate &start, const QDate &end, const QDate &preferredMonth=QDate()) override
void newTimeSpanSelected(const QPoint &start, const QPoint &end)
Updates data for selected timespan.
void setHolidayMasks()
Set the masks on the agenda widgets indicating, which days are holidays.
virtual KCalendarCore::Calendar::Ptr calendar2(const KCalendarCore::Incidence::Ptr &incidence) const
Return calendar object for a concrete incidence.
QDateTime selectionEnd() const override
end-datetime of selection
QDate startDate() const
First shown day.
void showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date) override
Shows given incidences.
void newTimeSpanSelectedAllDay(const QPoint &start, const QPoint &end)
Updates data for selected timespan for all day event.
void slotIncidencesDropped(const KCalendarCore::Incidence::List &incidences, const QPoint &, bool)
reschedule the todo to the given x- and y- coordinates.
Akonadi::Item::List selectedIncidences() const override
returns the currently selected events
This class provides the interface for a date dependent decoration.
Class for calendar decoration elements.
EventView is the abstract base class from which all other calendar views for event data are derived.
Definition eventview.h:69
void showIncidenceSignal(const Akonadi::Item &)
instructs the receiver to show the incidence in read-only mode.
void newEventSignal()
instructs the receiver to create a new event in given collection.
void deleteIncidenceSignal(const Akonadi::Item &)
instructs the receiver to delete the Incidence in some manner; some possibilities include automatical...
Changes changes() const
Returns if there are pending changes and a redraw is needed.
void editIncidenceSignal(const Akonadi::Item &)
instructs the receiver to begin editing the incidence specified in some manner.
virtual void setIncidenceChanger(Akonadi::IncidenceChanger *changer)
Assign a new incidence change helper object.
static QString createUniqueId()
virtual void calendarIncidenceDeleted(const Incidence::Ptr &incidence, const Calendar *calendar)
KConfigGroup group(const QString &group)
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
static KIconLoader * global()
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
void setText(const QString &text)
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Todo::Ptr todo(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
Type type(const QSqlDatabase &db)
Namespace EventViews provides facilities for displaying incidences, including events,...
Definition agenda.h:33
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & begin()
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QCA_EXPORT void init()
QDate addDays(qint64 ndays) const const
QDate currentDate()
int day() const const
int dayOfWeek() const const
qint64 daysTo(QDate d) const const
bool isValid(int year, int month, int day)
int month() const const
QDateTime addDays(qint64 ndays) const const
QDateTime addDuration(std::chrono::milliseconds msecs) const const
QDateTime addMSecs(qint64 msecs) const const
QDateTime addSecs(qint64 s) const const
QDate date() const const
qint64 daysTo(const QDateTime &other) const const
bool isValid() const const
int offsetFromUtc() const const
qint64 secsTo(const QDateTime &other) const const
void setTime(QTime time)
QTime time() const const
Qt::TimeSpec timeSpec() const const
QTimeZone timeZone() const const
QDateTime toLocalTime() const const
QDateTime toOffsetFromUtc(int offsetSeconds) const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
int pointSize() const const
void setBold(bool enable)
void setPointSize(int pointSize)
bool isRightToLeft()
void addWidget(QWidget *w)
QMargins contentsMargins() const const
virtual void invalidate() override
void setContentsMargins(const QMargins &margins)
virtual void setGeometry(const QRect &r) override
virtual void setSpacing(int)
virtual QSize minimumSize() const const=0
virtual void setGeometry(const QRect &r)=0
virtual QSize sizeHint() const const=0
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
void clear()
bool contains(const AT &value) const const
qsizetype count() const const
T & first()
bool isEmpty() const const
T & last()
void reserve(qsizetype size)
void resize(qsizetype size)
qsizetype size() const const
T takeAt(qsizetype i)
value_type takeFirst()
T value(qsizetype i) const const
QLocale system()
QString toString(QDate date, FormatType format) const const
void clear()
int bottom() const const
int left() const const
int right() const const
int top() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QList< T > findChildren(Qt::FindChildOptions options) const const
void installEventFilter(QObject *filterObj)
QObject * parent() const const
void fill(const QColor &color)
int x() const const
int y() const const
QRect adjusted(int dx1, int dy1, int dx2, int dy2) const const
int height() const const
int left() const const
void setHeight(int height)
void setLeft(int x)
void setTop(int y)
void setWidth(int width)
QSize size() const const
int top() const const
int width() const const
int y() const const
QSharedPointer< T > create(Args &&... args)
T * data() const const
QSharedPointer< X > dynamicCast() const const
void changeSize(int w, int h, QSizePolicy::Policy hPolicy, QSizePolicy::Policy vPolicy)
QString number(double n, char format, int precision)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
SH_ScrollView_FrameOnlyAroundContents
virtual int styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const const=0
AlignCenter
QueuedConnection
FindDirectChildrenOnly
transparent
LeftToRight
Vertical
ElideRight
TimeZone
WA_TransparentForMouseEvents
QTime addMSecs(int ms) const const
QTime addSecs(int s) const const
int hour() const const
int minute() const const
int secsTo(QTime t) const const
QTimeZone systemTimeZone()
QWidget(QWidget *parent, Qt::WindowFlags f)
virtual bool event(QEvent *event) override
void setGeometry(const QRect &)
QLayout * layout() const const
QWidget * parentWidget() const const
void raise()
virtual void resizeEvent(QResizeEvent *event)
void setFixedWidth(int w)
void show()
virtual void showEvent(QShowEvent *event)
QStyle * style() const const
void update()
bool isVisible() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:07:11 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.