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

KDE's Doxygen guidelines are available online.