File indexing completed on 2025-03-16 04:48:02

0001 /*
0002   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0003   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0004   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0005   SPDX-FileCopyrightText: 2021 Friedrich W. H. Kossebau <kossebau@kde.org>
0006   SPDX-FileContributor: Kevin Krammer <krake@kdab.com>
0007   SPDX-FileContributor: Sergio Martins <sergio@kdab.com>
0008 
0009   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0010 */
0011 #include "agendaview.h"
0012 #include "agenda.h"
0013 #include "agendaitem.h"
0014 #include "alternatelabel.h"
0015 #include "calendardecoration.h"
0016 #include "decorationlabel.h"
0017 #include "prefs.h"
0018 #include "timelabels.h"
0019 #include "timelabelszone.h"
0020 
0021 #include "calendarview_debug.h"
0022 
0023 #include <Akonadi/CalendarUtils>
0024 #include <Akonadi/EntityTreeModel>
0025 #include <Akonadi/IncidenceChanger>
0026 #include <CalendarSupport/CollectionSelection>
0027 #include <CalendarSupport/KCalPrefs>
0028 #include <CalendarSupport/Utils>
0029 
0030 #include <KCalendarCore/CalFilter>
0031 #include <KCalendarCore/CalFormat>
0032 #include <KCalendarCore/OccurrenceIterator>
0033 
0034 #include <KIconLoader> // for SmallIcon()
0035 #include <KMessageBox>
0036 #include <KPluginFactory>
0037 #include <KSqueezedTextLabel>
0038 
0039 #include <KLocalizedString>
0040 #include <QApplication>
0041 #include <QDrag>
0042 #include <QGridLayout>
0043 #include <QLabel>
0044 #include <QPainter>
0045 #include <QScrollBar>
0046 #include <QSplitter>
0047 #include <QStyle>
0048 #include <QTimer>
0049 
0050 #include <chrono>
0051 #include <vector>
0052 
0053 using namespace std::chrono_literals;
0054 
0055 using namespace EventViews;
0056 
0057 enum { SPACING = 2 };
0058 enum {
0059     SHRINKDOWN = 2 // points less for the timezone font
0060 };
0061 
0062 // Layout which places the widgets in equally sized columns,
0063 // matching the calculation of the columns in the agenda.
0064 class AgendaHeaderLayout : public QLayout
0065 {
0066 public:
0067     explicit AgendaHeaderLayout(QWidget *parent);
0068     ~AgendaHeaderLayout() override;
0069 
0070 public: // QLayout API
0071     int count() const override;
0072     QLayoutItem *itemAt(int index) const override;
0073 
0074     void addItem(QLayoutItem *item) override;
0075     QLayoutItem *takeAt(int index) override;
0076 
0077 public: // QLayoutItem API
0078     QSize sizeHint() const override;
0079     QSize minimumSize() const override;
0080 
0081     void invalidate() override;
0082     void setGeometry(const QRect &rect) override;
0083 
0084 private:
0085     void updateCache() const;
0086 
0087 private:
0088     QList<QLayoutItem *> mItems;
0089 
0090     mutable bool mIsDirty = false;
0091     mutable QSize mSizeHint;
0092     mutable QSize mMinSize;
0093 };
0094 
0095 AgendaHeaderLayout::AgendaHeaderLayout(QWidget *parent)
0096     : QLayout(parent)
0097 {
0098 }
0099 
0100 AgendaHeaderLayout::~AgendaHeaderLayout()
0101 {
0102     while (!mItems.isEmpty()) {
0103         delete mItems.takeFirst();
0104     }
0105 }
0106 
0107 void AgendaHeaderLayout::addItem(QLayoutItem *item)
0108 {
0109     mItems.append(item);
0110     invalidate();
0111 }
0112 
0113 int AgendaHeaderLayout::count() const
0114 {
0115     return mItems.size();
0116 }
0117 
0118 QLayoutItem *AgendaHeaderLayout::itemAt(int index) const
0119 {
0120     return mItems.value(index);
0121 }
0122 
0123 QLayoutItem *AgendaHeaderLayout::takeAt(int index)
0124 {
0125     if (index < 0 || index >= mItems.size()) {
0126         return nullptr;
0127     }
0128 
0129     auto item = mItems.takeAt(index);
0130     if (item) {
0131         invalidate();
0132     }
0133     return item;
0134 }
0135 
0136 void AgendaHeaderLayout::invalidate()
0137 {
0138     QLayout::invalidate();
0139     mIsDirty = true;
0140 }
0141 
0142 void AgendaHeaderLayout::setGeometry(const QRect &rect)
0143 {
0144     QLayout::setGeometry(rect);
0145 
0146     if (mItems.isEmpty()) {
0147         return;
0148     }
0149 
0150     const QMargins margins = contentsMargins();
0151 
0152     // same logic as Agenda uses to distribute the width
0153     const int contentWidth = rect.width() - margins.left() - margins.right();
0154     const double agendaGridSpacingX = static_cast<double>(contentWidth) / mItems.size();
0155     int x = margins.left();
0156     const int contentHeight = rect.height() - margins.top() - margins.bottom();
0157     const int y = rect.y() + margins.top();
0158     for (int i = 0; i < mItems.size(); ++i) {
0159         auto item = mItems.at(i);
0160         const int nextX = margins.left() + static_cast<int>((i + 1) * agendaGridSpacingX);
0161         const int width = nextX - x;
0162         item->setGeometry(QRect(x, y, width, contentHeight));
0163         x = nextX;
0164     }
0165 }
0166 
0167 QSize AgendaHeaderLayout::sizeHint() const
0168 {
0169     if (mIsDirty) {
0170         updateCache();
0171     }
0172     return mSizeHint;
0173 }
0174 
0175 QSize AgendaHeaderLayout::minimumSize() const
0176 {
0177     if (mIsDirty) {
0178         updateCache();
0179     }
0180     return mMinSize;
0181 }
0182 
0183 void AgendaHeaderLayout::updateCache() const
0184 {
0185     QSize maxItemSizeHint(0, 0);
0186     QSize maxItemMinSize(0, 0);
0187     for (auto &item : mItems) {
0188         maxItemSizeHint = maxItemSizeHint.expandedTo(item->sizeHint());
0189         maxItemMinSize = maxItemMinSize.expandedTo(item->minimumSize());
0190     }
0191     const QMargins margins = contentsMargins();
0192     const int horizontalMargins = margins.left() + margins.right();
0193     const int verticalMargins = margins.top() + margins.bottom();
0194     mSizeHint = QSize(maxItemSizeHint.width() * mItems.size() + horizontalMargins, maxItemSizeHint.height() + verticalMargins);
0195     mMinSize = QSize(maxItemMinSize.width() * mItems.size() + horizontalMargins, maxItemMinSize.height() + verticalMargins);
0196     mIsDirty = false;
0197 }
0198 
0199 // Header (or footer) for the agenda.
0200 // Optionally has an additional week header, if isSideBySide is set
0201 class AgendaHeader : public QWidget
0202 {
0203     Q_OBJECT
0204 public:
0205     explicit AgendaHeader(bool isSideBySide, QWidget *parent);
0206 
0207     using DecorationList = QList<EventViews::CalendarDecoration::Decoration *>;
0208 
0209 public:
0210     void setCalendarName(const QString &calendarName);
0211     void setAgenda(Agenda *agenda);
0212     bool createDayLabels(const KCalendarCore::DateList &dates, bool withDayLabel, const QStringList &decos, const QStringList &enabledDecos);
0213     void setWeekWidth(int width);
0214     void updateDayLabelSizes();
0215     void updateMargins();
0216 
0217 protected:
0218     void resizeEvent(QResizeEvent *resizeEvent) override;
0219 
0220 private:
0221     static CalendarDecoration::Decoration *loadCalendarDecoration(const QString &name);
0222 
0223     void addDay(const DecorationList &decoList, QDate date, bool withDayLabel);
0224     void clear();
0225     void placeDecorations(const DecorationList &decoList, QDate date, QWidget *labelBox, bool forWeek);
0226     void loadDecorations(const QStringList &decorations, const QStringList &whiteList, DecorationList &decoList);
0227 
0228 private:
0229     const bool mIsSideBySide;
0230 
0231     Agenda *mAgenda = nullptr;
0232     KSqueezedTextLabel *mCalendarNameLabel = nullptr;
0233     QWidget *mDayLabels = nullptr;
0234     AgendaHeaderLayout *mDayLabelsLayout = nullptr;
0235     QWidget *mWeekLabelBox = nullptr;
0236 
0237     QList<AlternateLabel *> mDateDayLabels;
0238 };
0239 
0240 AgendaHeader::AgendaHeader(bool isSideBySide, QWidget *parent)
0241     : QWidget(parent)
0242     , mIsSideBySide(isSideBySide)
0243 {
0244     auto layout = new QVBoxLayout(this);
0245     layout->setContentsMargins(0, 0, 0, 0);
0246 
0247     if (mIsSideBySide) {
0248         mCalendarNameLabel = new KSqueezedTextLabel(this);
0249         mCalendarNameLabel->setAlignment(Qt::AlignCenter);
0250         layout->addWidget(mCalendarNameLabel);
0251     }
0252 
0253     auto *daysWidget = new QWidget(this);
0254     layout->addWidget(daysWidget);
0255 
0256     auto daysLayout = new QHBoxLayout(daysWidget);
0257     daysLayout->setContentsMargins(0, 0, 0, 0);
0258     daysLayout->setSpacing(SPACING);
0259 
0260     if (!mIsSideBySide) {
0261         mWeekLabelBox = new QWidget(daysWidget);
0262         auto weekLabelBoxLayout = new QVBoxLayout(mWeekLabelBox);
0263         weekLabelBoxLayout->setContentsMargins(0, 0, 0, 0);
0264         weekLabelBoxLayout->setSpacing(0);
0265         daysLayout->addWidget(mWeekLabelBox);
0266     }
0267 
0268     mDayLabels = new QWidget(daysWidget);
0269     mDayLabelsLayout = new AgendaHeaderLayout(mDayLabels);
0270     mDayLabelsLayout->setContentsMargins(0, 0, 0, 0);
0271     daysLayout->addWidget(mDayLabels);
0272     daysLayout->setStretchFactor(mDayLabels, 1);
0273 }
0274 
0275 void AgendaHeader::setAgenda(Agenda *agenda)
0276 {
0277     mAgenda = agenda;
0278 }
0279 
0280 void AgendaHeader::setCalendarName(const QString &calendarName)
0281 {
0282     if (mCalendarNameLabel) {
0283         mCalendarNameLabel->setText(calendarName);
0284     }
0285 }
0286 
0287 void AgendaHeader::updateMargins()
0288 {
0289     const int frameWidth = mAgenda ? mAgenda->scrollArea()->frameWidth() : 0;
0290     const int scrollBarWidth = (mIsSideBySide || !mAgenda || !mAgenda->verticalScrollBar()->isVisible()) ? 0 : mAgenda->verticalScrollBar()->width();
0291     const bool isLTR = (layoutDirection() == Qt::LeftToRight);
0292     const int leftSpacing = SPACING + frameWidth;
0293     const int rightSpacing = scrollBarWidth + frameWidth;
0294     mDayLabelsLayout->setContentsMargins(isLTR ? leftSpacing : rightSpacing, 0, isLTR ? rightSpacing : leftSpacing, 0);
0295 }
0296 
0297 void AgendaHeader::updateDayLabelSizes()
0298 {
0299     if (mDateDayLabels.isEmpty()) {
0300         return;
0301     }
0302     // First, calculate the maximum text type that fits for all labels
0303     AlternateLabel::TextType overallType = AlternateLabel::Extensive;
0304     for (auto label : std::as_const(mDateDayLabels)) {
0305         AlternateLabel::TextType type = label->largestFittingTextType();
0306         if (type < overallType) {
0307             overallType = type;
0308         }
0309     }
0310 
0311     // Then, set that maximum text type to all the labels
0312     for (auto label : std::as_const(mDateDayLabels)) {
0313         label->setFixedType(overallType);
0314     }
0315 }
0316 
0317 void AgendaHeader::resizeEvent(QResizeEvent *resizeEvent)
0318 {
0319     QWidget::resizeEvent(resizeEvent);
0320     updateDayLabelSizes();
0321 }
0322 
0323 void AgendaHeader::setWeekWidth(int width)
0324 {
0325     if (!mWeekLabelBox) {
0326         return;
0327     }
0328 
0329     mWeekLabelBox->setFixedWidth(width);
0330 }
0331 
0332 void AgendaHeader::clear()
0333 {
0334     auto childWidgets = mDayLabels->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly);
0335     qDeleteAll(childWidgets);
0336     if (mWeekLabelBox) {
0337         childWidgets = mWeekLabelBox->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly);
0338         qDeleteAll(childWidgets);
0339     }
0340     mDateDayLabels.clear();
0341 }
0342 
0343 bool AgendaHeader::createDayLabels(const KCalendarCore::DateList &dates, bool withDayLabel, const QStringList &decoNames, const QStringList &enabledPlugins)
0344 {
0345     clear();
0346 
0347     QList<CalendarDecoration::Decoration *> decos;
0348     loadDecorations(decoNames, enabledPlugins, decos);
0349     const bool hasDecos = !decos.isEmpty();
0350 
0351     for (const QDate &date : dates) {
0352         addDay(decos, date, withDayLabel);
0353     }
0354 
0355     // Week decoration labels
0356     if (mWeekLabelBox) {
0357         placeDecorations(decos, dates.first(), mWeekLabelBox, true);
0358     }
0359 
0360     qDeleteAll(decos);
0361 
0362     // trigger an update after all layout has been done and the final sizes are known
0363     QTimer::singleShot(0, this, &AgendaHeader::updateDayLabelSizes);
0364 
0365     return hasDecos;
0366 }
0367 
0368 void AgendaHeader::addDay(const DecorationList &decoList, QDate date, bool withDayLabel)
0369 {
0370     auto topDayLabelBox = new QWidget(mDayLabels);
0371     auto topDayLabelBoxLayout = new QVBoxLayout(topDayLabelBox);
0372     topDayLabelBoxLayout->setContentsMargins(0, 0, 0, 0);
0373     topDayLabelBoxLayout->setSpacing(0);
0374 
0375     mDayLabelsLayout->addWidget(topDayLabelBox);
0376 
0377     if (withDayLabel) {
0378         int dW = date.dayOfWeek();
0379         QString veryLongStr = QLocale::system().toString(date, QLocale::LongFormat);
0380         QString longstr = i18nc("short_weekday short_monthname date (e.g. Mon Aug 13)",
0381                                 "%1 %2 %3",
0382                                 QLocale::system().dayName(dW, QLocale::ShortFormat),
0383                                 QLocale::system().monthName(date.month(), QLocale::ShortFormat),
0384                                 date.day());
0385         const QString shortstr = QString::number(date.day());
0386 
0387         auto dayLabel = new AlternateLabel(shortstr, longstr, veryLongStr, topDayLabelBox);
0388         topDayLabelBoxLayout->addWidget(dayLabel);
0389         dayLabel->setAlignment(Qt::AlignHCenter);
0390         if (date == QDate::currentDate()) {
0391             QFont font = dayLabel->font();
0392             font.setBold(true);
0393             dayLabel->setFont(font);
0394         }
0395         mDateDayLabels.append(dayLabel);
0396 
0397         // if a holiday region is selected, show the holiday name
0398         const QStringList texts = CalendarSupport::holiday(date);
0399         for (const QString &text : texts) {
0400             auto label = new KSqueezedTextLabel(text, topDayLabelBox);
0401             label->setTextElideMode(Qt::ElideRight);
0402             topDayLabelBoxLayout->addWidget(label);
0403             label->setAlignment(Qt::AlignCenter);
0404         }
0405     }
0406 
0407     placeDecorations(decoList, date, topDayLabelBox, false);
0408 }
0409 
0410 void AgendaHeader::placeDecorations(const DecorationList &decoList, QDate date, QWidget *labelBox, bool forWeek)
0411 {
0412     for (CalendarDecoration::Decoration *deco : std::as_const(decoList)) {
0413         const CalendarDecoration::Element::List elements = forWeek ? deco->weekElements(date) : deco->dayElements(date);
0414         if (!elements.isEmpty()) {
0415             auto decoHBox = new QWidget(labelBox);
0416             labelBox->layout()->addWidget(decoHBox);
0417             auto layout = new QHBoxLayout(decoHBox);
0418             layout->setSpacing(0);
0419             layout->setContentsMargins(0, 0, 0, 0);
0420             decoHBox->setMinimumWidth(1);
0421 
0422             for (CalendarDecoration::Element *it : elements) {
0423                 auto label = new DecorationLabel(it, decoHBox);
0424                 label->setAlignment(Qt::AlignBottom);
0425                 label->setMinimumWidth(1);
0426                 layout->addWidget(label);
0427             }
0428         }
0429     }
0430 }
0431 
0432 void AgendaHeader::loadDecorations(const QStringList &decorations, const QStringList &whiteList, DecorationList &decoList)
0433 {
0434     for (const QString &decoName : decorations) {
0435         if (whiteList.contains(decoName)) {
0436             CalendarDecoration::Decoration *deco = loadCalendarDecoration(decoName);
0437             if (deco != nullptr) {
0438                 decoList << deco;
0439             }
0440         }
0441     }
0442 }
0443 
0444 CalendarDecoration::Decoration *AgendaHeader::loadCalendarDecoration(const QString &name)
0445 {
0446     auto result = KPluginFactory::instantiatePlugin<CalendarDecoration::Decoration>(KPluginMetaData(QStringLiteral("pim6/korganizer/") + name));
0447 
0448     if (result) {
0449         return result.plugin;
0450     } else {
0451         qCDebug(CALENDARVIEW_LOG) << "Factory creation failed" << result.errorString;
0452         return nullptr;
0453     }
0454 }
0455 
0456 class EventViews::EventIndicatorPrivate
0457 {
0458 public:
0459     EventIndicatorPrivate(EventIndicator *parent, EventIndicator::Location loc)
0460         : mLocation(loc)
0461         , q(parent)
0462     {
0463         mEnabled.resize(mColumns);
0464 
0465         QChar ch;
0466         // Dashed up and down arrow characters
0467         ch = QChar(mLocation == EventIndicator::Top ? 0x21e1 : 0x21e3);
0468         QFont font = q->font();
0469         font.setPointSize(KIconLoader::global()->currentSize(KIconLoader::Dialog));
0470         QFontMetrics fm(font);
0471         QRect rect = fm.boundingRect(ch).adjusted(-2, -2, 2, 2);
0472         mPixmap = QPixmap(rect.size());
0473         mPixmap.fill(Qt::transparent);
0474         QPainter p(&mPixmap);
0475         p.setOpacity(0.33);
0476         p.setFont(font);
0477         p.setPen(q->palette().text().color());
0478         p.drawText(-rect.left(), -rect.top(), ch);
0479     }
0480 
0481     void adjustGeometry()
0482     {
0483         QRect rect;
0484         rect.setWidth(q->parentWidget()->width());
0485         rect.setHeight(q->height());
0486         rect.setLeft(0);
0487         rect.setTop(mLocation == EventIndicator::Top ? 0 : q->parentWidget()->height() - rect.height());
0488         q->setGeometry(rect);
0489     }
0490 
0491 public:
0492     int mColumns = 1;
0493     const EventIndicator::Location mLocation;
0494     QPixmap mPixmap;
0495     QList<bool> mEnabled;
0496 
0497 private:
0498     EventIndicator *const q;
0499 };
0500 
0501 EventIndicator::EventIndicator(Location loc, QWidget *parent)
0502     : QWidget(parent)
0503     , d(new EventIndicatorPrivate(this, loc))
0504 {
0505     setAttribute(Qt::WA_TransparentForMouseEvents);
0506     setFixedHeight(d->mPixmap.height());
0507     parent->installEventFilter(this);
0508 }
0509 
0510 EventIndicator::~EventIndicator() = default;
0511 
0512 void EventIndicator::paintEvent(QPaintEvent *)
0513 {
0514     QPainter painter(this);
0515 
0516     const double cellWidth = static_cast<double>(width()) / d->mColumns;
0517     const bool isRightToLeft = QApplication::isRightToLeft();
0518     const uint pixmapOffset = isRightToLeft ? 0 : (cellWidth - d->mPixmap.width());
0519     for (int i = 0; i < d->mColumns; ++i) {
0520         if (d->mEnabled[i]) {
0521             const int xOffset = (isRightToLeft ? (d->mColumns - 1 - i) : i) * cellWidth;
0522             painter.drawPixmap(xOffset + pixmapOffset, 0, d->mPixmap);
0523         }
0524     }
0525 }
0526 
0527 bool EventIndicator::eventFilter(QObject *, QEvent *event)
0528 {
0529     if (event->type() == QEvent::Resize) {
0530         d->adjustGeometry();
0531     }
0532     return false;
0533 }
0534 
0535 void EventIndicator::changeColumns(int columns)
0536 {
0537     d->mColumns = columns;
0538     d->mEnabled.resize(d->mColumns);
0539 
0540     show();
0541     raise();
0542     update();
0543 }
0544 
0545 void EventIndicator::enableColumn(int column, bool enable)
0546 {
0547     Q_ASSERT(column < d->mEnabled.count());
0548     d->mEnabled[column] = enable;
0549 }
0550 
0551 ////////////////////////////////////////////////////////////////////////////
0552 ////////////////////////////////////////////////////////////////////////////
0553 
0554 class EventViews::AgendaViewPrivate : public KCalendarCore::Calendar::CalendarObserver
0555 {
0556     AgendaView *const q;
0557 
0558 public:
0559     explicit AgendaViewPrivate(AgendaView *parent, bool isInteractive, bool isSideBySide)
0560         : q(parent)
0561         , mUpdateItem(0)
0562         , mIsSideBySide(isSideBySide)
0563         , mIsInteractive(isInteractive)
0564         , mViewCalendar(MultiViewCalendar::Ptr(new MultiViewCalendar()))
0565     {
0566         mViewCalendar->mAgendaView = q;
0567     }
0568 
0569 public:
0570     // view widgets
0571     QVBoxLayout *mMainLayout = nullptr;
0572     AgendaHeader *mTopDayLabelsFrame = nullptr;
0573     AgendaHeader *mBottomDayLabelsFrame = nullptr;
0574     QWidget *mAllDayFrame = nullptr;
0575     QSpacerItem *mAllDayRightSpacer = nullptr;
0576     QWidget *mTimeBarHeaderFrame = nullptr;
0577     QSplitter *mSplitterAgenda = nullptr;
0578     QList<QLabel *> mTimeBarHeaders;
0579 
0580     Agenda *mAllDayAgenda = nullptr;
0581     Agenda *mAgenda = nullptr;
0582 
0583     TimeLabelsZone *mTimeLabelsZone = nullptr;
0584 
0585     KCalendarCore::DateList mSelectedDates; // List of dates to be displayed
0586     KCalendarCore::DateList mSaveSelectedDates; // Save the list of dates between updateViews
0587     int mViewType;
0588     EventIndicator *mEventIndicatorTop = nullptr;
0589     EventIndicator *mEventIndicatorBottom = nullptr;
0590 
0591     QList<int> mMinY;
0592     QList<int> mMaxY;
0593 
0594     QList<bool> mHolidayMask;
0595 
0596     QDateTime mTimeSpanBegin;
0597     QDateTime mTimeSpanEnd;
0598     bool mTimeSpanInAllDay = true;
0599     bool mAllowAgendaUpdate = true;
0600 
0601     Akonadi::Item mUpdateItem;
0602 
0603     const bool mIsSideBySide;
0604 
0605     QWidget *mDummyAllDayLeft = nullptr;
0606     bool mUpdateAllDayAgenda = true;
0607     bool mUpdateAgenda = true;
0608     bool mIsInteractive;
0609     bool mUpdateEventIndicatorsScheduled = false;
0610 
0611     // Contains days that have at least one all-day Event with TRANSP: OPAQUE (busy)
0612     // that has you as organizer or attendee so we can color background with a different
0613     // color
0614     QMap<QDate, KCalendarCore::Event::List> mBusyDays;
0615 
0616     EventViews::MultiViewCalendar::Ptr mViewCalendar;
0617     bool makesWholeDayBusy(const KCalendarCore::Incidence::Ptr &incidence) const;
0618     void clearView();
0619     void setChanges(EventView::Changes changes, const KCalendarCore::Incidence::Ptr &incidence = KCalendarCore::Incidence::Ptr());
0620 
0621     /**
0622         Returns a list of consecutive dates, starting with @p start and ending
0623         with @p end. If either start or end are invalid, a list with
0624         QDate::currentDate() is returned */
0625     static QList<QDate> generateDateList(QDate start, QDate end);
0626 
0627     void changeColumns(int numColumns);
0628 
0629     AgendaItem::List agendaItems(const QString &uid) const;
0630 
0631     // insertAtDateTime is in the view's timezone
0632     void insertIncidence(const KCalendarCore::Incidence::Ptr &, const QDateTime &recurrenceId, const QDateTime &insertAtDateTime, bool createSelected);
0633     void reevaluateIncidence(const KCalendarCore::Incidence::Ptr &incidence);
0634 
0635     bool datesEqual(const KCalendarCore::Incidence::Ptr &one, const KCalendarCore::Incidence::Ptr &two) const;
0636 
0637     /**
0638      * Returns false if the incidence is for sure outside of the visible timespan.
0639      * Returns true if it might be, meaning that to be sure, timezones must be
0640      * taken into account.
0641      * This is a very fast way of discarding incidences that are outside of the
0642      * timespan and only performing expensive timezone operations on the ones
0643      * that might be viisble
0644      */
0645     bool mightBeVisible(const KCalendarCore::Incidence::Ptr &incidence) const;
0646 
0647     void updateAllDayRightSpacer();
0648 
0649 protected:
0650     /* reimplemented from KCalendarCore::Calendar::CalendarObserver */
0651     void calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence) override;
0652     void calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence) override;
0653     void calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar) override;
0654 
0655 private:
0656     // quiet --overloaded-virtual warning
0657     using KCalendarCore::Calendar::CalendarObserver::calendarIncidenceDeleted;
0658 };
0659 
0660 bool AgendaViewPrivate::datesEqual(const KCalendarCore::Incidence::Ptr &one, const KCalendarCore::Incidence::Ptr &two) const
0661 {
0662     const auto start1 = one->dtStart();
0663     const auto start2 = two->dtStart();
0664     const auto end1 = one->dateTime(KCalendarCore::Incidence::RoleDisplayEnd);
0665     const auto end2 = two->dateTime(KCalendarCore::Incidence::RoleDisplayEnd);
0666 
0667     if (start1.isValid() ^ start2.isValid()) {
0668         return false;
0669     }
0670 
0671     if (end1.isValid() ^ end2.isValid()) {
0672         return false;
0673     }
0674 
0675     if (start1.isValid() && start1 != start2) {
0676         return false;
0677     }
0678 
0679     if (end1.isValid() && end1 != end2) {
0680         return false;
0681     }
0682 
0683     return true;
0684 }
0685 
0686 AgendaItem::List AgendaViewPrivate::agendaItems(const QString &uid) const
0687 {
0688     AgendaItem::List allDayAgendaItems = mAllDayAgenda->agendaItems(uid);
0689     return allDayAgendaItems.isEmpty() ? mAgenda->agendaItems(uid) : allDayAgendaItems;
0690 }
0691 
0692 bool AgendaViewPrivate::mightBeVisible(const KCalendarCore::Incidence::Ptr &incidence) const
0693 {
0694     KCalendarCore::Todo::Ptr todo = incidence.dynamicCast<KCalendarCore::Todo>();
0695 
0696     // KDateTime::toTimeSpec() is expensive, so lets first compare only the date,
0697     // to see if the incidence is visible.
0698     // If it's more than 48h of diff, then for sure it won't be visible,
0699     // independently of timezone.
0700     // The largest difference between two timezones is about 24 hours.
0701 
0702     if (todo && todo->isOverdue()) {
0703         // Don't optimize this case. Overdue to-dos have their own rules for displaying themselves
0704         return true;
0705     }
0706 
0707     if (!incidence->recurs()) {
0708         // If DTEND/DTDUE is before the 1st visible column
0709         const QDate tdate = incidence->dateTime(KCalendarCore::Incidence::RoleEnd).date();
0710         if (tdate.daysTo(mSelectedDates.first()) > 2) {
0711             return false;
0712         }
0713 
0714         // if DTSTART is after the last visible column
0715         if (!todo && mSelectedDates.last().daysTo(incidence->dtStart().date()) > 2) {
0716             return false;
0717         }
0718 
0719         // if DTDUE is after the last visible column
0720         if (todo && mSelectedDates.last().daysTo(todo->dtDue().date()) > 2) {
0721             return false;
0722         }
0723     }
0724 
0725     return true;
0726 }
0727 
0728 void AgendaViewPrivate::changeColumns(int numColumns)
0729 {
0730     // mMinY, mMaxY and mEnabled must all have the same size.
0731     // Make sure you preserve this order because mEventIndicatorTop->changeColumns()
0732     // can trigger a lot of stuff, and code will be executed when mMinY wasn't resized yet.
0733     mMinY.resize(numColumns);
0734     mMaxY.resize(numColumns);
0735     mEventIndicatorTop->changeColumns(numColumns);
0736     mEventIndicatorBottom->changeColumns(numColumns);
0737 }
0738 
0739 /** static */
0740 QList<QDate> AgendaViewPrivate::generateDateList(QDate start, QDate end)
0741 {
0742     QList<QDate> list;
0743 
0744     if (start.isValid() && end.isValid() && end >= start && start.daysTo(end) < AgendaView::MAX_DAY_COUNT) {
0745         QDate date = start;
0746         list.reserve(start.daysTo(end) + 1);
0747         while (date <= end) {
0748             list.append(date);
0749             date = date.addDays(1);
0750         }
0751     } else {
0752         list.append(QDate::currentDate());
0753     }
0754 
0755     return list;
0756 }
0757 
0758 void AgendaViewPrivate::reevaluateIncidence(const KCalendarCore::Incidence::Ptr &incidence)
0759 {
0760     if (!incidence || !mViewCalendar->isValid(incidence)) {
0761         qCWarning(CALENDARVIEW_LOG) << "invalid incidence or item not found." << incidence;
0762         return;
0763     }
0764 
0765     q->removeIncidence(incidence);
0766     q->displayIncidence(incidence, false);
0767     mAgenda->checkScrollBoundaries();
0768     q->updateEventIndicators();
0769 }
0770 
0771 void AgendaViewPrivate::calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence)
0772 {
0773     if (!incidence || !mViewCalendar->isValid(incidence)) {
0774         qCCritical(CALENDARVIEW_LOG) << "AgendaViewPrivate::calendarIncidenceAdded() Invalid incidence or item:" << incidence;
0775         Q_ASSERT(false);
0776         return;
0777     }
0778 
0779     if (incidence->hasRecurrenceId()) {
0780         const auto cal = q->calendar2(incidence);
0781         if (cal) {
0782             if (auto mainIncidence = cal->incidence(incidence->uid())) {
0783                 // Reevaluate the main event instead, if it was inserted before this one.
0784                 reevaluateIncidence(mainIncidence);
0785             } else if (q->displayIncidence(incidence, false)) {
0786                 // Display disassociated occurrences because errors sometimes destroy
0787                 // the main recurring incidence.
0788                 mAgenda->checkScrollBoundaries();
0789                 q->scheduleUpdateEventIndicators();
0790             }
0791         }
0792     } else if (incidence->recurs()) {
0793         // Reevaluate recurring incidences to clean up any disassociated
0794         // occurrences that were inserted before it.
0795         reevaluateIncidence(incidence);
0796     } else if (q->displayIncidence(incidence, false)) {
0797         // Ordinary non-recurring non-disassociated instances.
0798         mAgenda->checkScrollBoundaries();
0799         q->scheduleUpdateEventIndicators();
0800     }
0801 }
0802 
0803 void AgendaViewPrivate::calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence)
0804 {
0805     if (!incidence || incidence->uid().isEmpty()) {
0806         qCCritical(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid incidence or empty UID. " << incidence;
0807         Q_ASSERT(false);
0808         return;
0809     }
0810 
0811     AgendaItem::List agendaItems = this->agendaItems(incidence->uid());
0812     if (agendaItems.isEmpty()) {
0813         // Don't warn - it's possible the incidence has been changed in another calendar that we do not display.
0814         // qCWarning(CALENDARVIEW_LOG) << "AgendaView::calendarIncidenceChanged() Invalid agendaItem for incidence " << incidence->uid();
0815         return;
0816     }
0817 
0818     // Optimization: If the dates didn't change, just repaint it.
0819     // This optimization for now because we need to process collisions between agenda items.
0820     if (false && !incidence->recurs() && agendaItems.count() == 1) {
0821         KCalendarCore::Incidence::Ptr originalIncidence = agendaItems.first()->incidence();
0822 
0823         if (datesEqual(originalIncidence, incidence)) {
0824             for (const AgendaItem::QPtr &agendaItem : std::as_const(agendaItems)) {
0825                 agendaItem->setIncidence(KCalendarCore::Incidence::Ptr(incidence->clone()));
0826                 agendaItem->update();
0827             }
0828             return;
0829         }
0830     }
0831 
0832     if (incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence)) {
0833         // Reevaluate the main event instead, if it exists
0834         const auto cal = q->calendar2(incidence);
0835         if (cal) {
0836             KCalendarCore::Incidence::Ptr mainIncidence = cal->incidence(incidence->uid());
0837             reevaluateIncidence(mainIncidence ? mainIncidence : incidence);
0838         }
0839     } else {
0840         reevaluateIncidence(incidence);
0841     }
0842 
0843     // No need to call setChanges(), that triggers a fillAgenda()
0844     // setChanges(q->changes() | IncidencesEdited, incidence);
0845 }
0846 
0847 void AgendaViewPrivate::calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *calendar)
0848 {
0849     Q_UNUSED(calendar)
0850     if (!incidence || incidence->uid().isEmpty()) {
0851         qCWarning(CALENDARVIEW_LOG) << "invalid incidence or empty uid: " << incidence;
0852         Q_ASSERT(false);
0853         return;
0854     }
0855 
0856     q->removeIncidence(incidence);
0857 
0858     if (incidence->hasRecurrenceId()) {
0859         // Reevaluate the main event, if it exists. The exception was removed so the main recurrent series
0860         // will no be bigger.
0861         if (mViewCalendar->isValid(incidence->uid())) {
0862             const auto cal = q->calendar2(incidence->uid());
0863             if (cal) {
0864                 KCalendarCore::Incidence::Ptr mainIncidence = cal->incidence(incidence->uid());
0865                 if (mainIncidence) {
0866                     reevaluateIncidence(mainIncidence);
0867                 }
0868             }
0869         }
0870     } else if (mightBeVisible(incidence)) {
0871         // No need to call setChanges(), that triggers a fillAgenda()
0872         // setChanges(q->changes() | IncidencesDeleted, CalendarSupport::incidence(incidence));
0873         mAgenda->checkScrollBoundaries();
0874         q->scheduleUpdateEventIndicators();
0875     }
0876 }
0877 
0878 void EventViews::AgendaViewPrivate::setChanges(EventView::Changes changes, const KCalendarCore::Incidence::Ptr &incidence)
0879 {
0880     // We could just call EventView::setChanges(...) but we're going to do a little
0881     // optimization. If only an all day item was changed, only all day agenda
0882     // should be updated.
0883 
0884     // all bits = 1
0885     const int ones = ~0;
0886 
0887     const int incidenceOperations = AgendaView::IncidencesAdded | AgendaView::IncidencesEdited | AgendaView::IncidencesDeleted;
0888 
0889     // If changes has a flag turned on, other than incidence operations, then update both agendas
0890     if ((ones ^ incidenceOperations) & changes) {
0891         mUpdateAllDayAgenda = true;
0892         mUpdateAgenda = true;
0893     } else if (incidence) {
0894         mUpdateAllDayAgenda = mUpdateAllDayAgenda | incidence->allDay();
0895         mUpdateAgenda = mUpdateAgenda | !incidence->allDay();
0896     }
0897 
0898     q->EventView::setChanges(changes);
0899 }
0900 
0901 void AgendaViewPrivate::clearView()
0902 {
0903     if (mUpdateAllDayAgenda) {
0904         mAllDayAgenda->clear();
0905     }
0906 
0907     if (mUpdateAgenda) {
0908         mAgenda->clear();
0909     }
0910 
0911     mBusyDays.clear();
0912 }
0913 
0914 void AgendaViewPrivate::insertIncidence(const KCalendarCore::Incidence::Ptr &incidence,
0915                                         const QDateTime &recurrenceId,
0916                                         const QDateTime &insertAtDateTime,
0917                                         bool createSelected)
0918 {
0919     if (!q->filterByCollectionSelection(incidence)) {
0920         return;
0921     }
0922 
0923     KCalendarCore::Event::Ptr event = CalendarSupport::event(incidence);
0924     KCalendarCore::Todo::Ptr todo = CalendarSupport::todo(incidence);
0925 
0926     const QDate insertAtDate = insertAtDateTime.date();
0927 
0928     // In case incidence->dtStart() isn't visible (crosses boundaries)
0929     const int curCol = qMax(mSelectedDates.first().daysTo(insertAtDate), qint64(0));
0930 
0931     // The date for the event is not displayed, just ignore it
0932     if (curCol >= mSelectedDates.count()) {
0933         return;
0934     }
0935 
0936     if (mMinY.count() <= curCol) {
0937         mMinY.resize(mSelectedDates.count());
0938     }
0939 
0940     if (mMaxY.count() <= curCol) {
0941         mMaxY.resize(mSelectedDates.count());
0942     }
0943 
0944     // Default values, which can never be reached
0945     mMinY[curCol] = mAgenda->timeToY(QTime(23, 59)) + 1;
0946     mMaxY[curCol] = mAgenda->timeToY(QTime(0, 0)) - 1;
0947 
0948     int beginX;
0949     int endX;
0950     if (event) {
0951         const QDate firstVisibleDate = mSelectedDates.first();
0952         QDateTime dtEnd = event->dtEnd().toLocalTime();
0953         if (!event->allDay() && dtEnd > event->dtStart()) {
0954             // If dtEnd's time portion is 00:00:00, the event ends on the previous day
0955             // unless it also starts at 00:00:00 (a duration of 0).
0956             dtEnd = dtEnd.addMSecs(-1);
0957         }
0958         const int duration = event->dtStart().toLocalTime().daysTo(dtEnd);
0959         if (insertAtDate < firstVisibleDate) {
0960             beginX = curCol + firstVisibleDate.daysTo(insertAtDate);
0961             endX = beginX + duration;
0962         } else {
0963             beginX = curCol;
0964             endX = beginX + duration;
0965         }
0966     } else if (todo) {
0967         if (!todo->hasDueDate()) {
0968             return; // todo shall not be displayed if it has no date
0969         }
0970         beginX = endX = curCol;
0971     } else {
0972         return;
0973     }
0974 
0975     const QDate today = QDate::currentDate();
0976     if (todo && todo->isOverdue() && today >= insertAtDate) {
0977         mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, curCol, curCol, createSelected);
0978     } else if (incidence->allDay()) {
0979         mAllDayAgenda->insertAllDayItem(incidence, recurrenceId, beginX, endX, createSelected);
0980     } else if (event && event->isMultiDay(QTimeZone::systemTimeZone())) {
0981         // TODO: We need a better isMultiDay(), one that receives the occurrence.
0982 
0983         // In the single-day handling code there's a neat comment on why
0984         // we're calculating the start time this way
0985         const QTime startTime = insertAtDateTime.time();
0986 
0987         // In the single-day handling code there's a neat comment on why we use the
0988         // duration instead of fetching the end time directly
0989         const int durationOfFirstOccurrence = event->dtStart().secsTo(event->dtEnd());
0990         QTime endTime = startTime.addSecs(durationOfFirstOccurrence);
0991 
0992         const int startY = mAgenda->timeToY(startTime);
0993 
0994         if (endTime == QTime(0, 0, 0)) {
0995             endTime = QTime(23, 59, 59);
0996         }
0997         const int endY = mAgenda->timeToY(endTime) - 1;
0998         if ((beginX <= 0 && curCol == 0) || beginX == curCol) {
0999             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 
1081 void 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;
1090     if (q->style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) {
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 
1100 AgendaView::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 
1107 AgendaView::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 
1115 void 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 
1228 AgendaView::~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 
1237 void 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 
1245     auto view = AkonadiViewCalendar::Ptr::create();
1246     view->mCalendar = calendar;
1247     view->mAgendaView = this;
1248     addCalendar(view);
1249 }
1250 
1251 void 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 }
1270 void 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 
1280 bool 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 
1290 KCalendarCore::Calendar::Ptr AgendaView::calendar2(const KCalendarCore::Incidence::Ptr &incidence) const
1291 {
1292     const auto cal = d->mViewCalendar->findCalendar(incidence);
1293     if (cal) {
1294         return cal->getCalendar();
1295     }
1296     return {};
1297 }
1298 
1299 KCalendarCore::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 
1308 void 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 
1320     setChanges(changes);
1321     updateView();
1322 }
1323 
1324 void 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 
1361 void 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 
1369 void 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 
1379 void AgendaView::slotShowIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1380 {
1381     Akonadi::Item item = d->mViewCalendar->item(incidence);
1382     if (item.isValid()) {
1383         Q_EMIT showIncidenceSignal(item);
1384     }
1385 }
1386 
1387 void AgendaView::slotEditIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1388 {
1389     Akonadi::Item item = d->mViewCalendar->item(incidence);
1390     if (item.isValid()) {
1391         Q_EMIT editIncidenceSignal(item);
1392     }
1393 }
1394 
1395 void AgendaView::slotDeleteIncidence(const KCalendarCore::Incidence::Ptr &incidence)
1396 {
1397     Akonadi::Item item = d->mViewCalendar->item(incidence);
1398     if (item.isValid()) {
1399         Q_EMIT deleteIncidenceSignal(item);
1400     }
1401 }
1402 
1403 void 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 
1416 void 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 
1431 void 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 
1465 void 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 
1497 void 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 
1534 void AgendaView::createDayLabels(bool force)
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 
1569 void AgendaView::enableAgendaUpdate(bool enable)
1570 {
1571     d->mAllowAgendaUpdate = enable;
1572 }
1573 
1574 int AgendaView::currentDateCount() const
1575 {
1576     return d->mSelectedDates.count();
1577 }
1578 
1579 Akonadi::Item::List AgendaView::selectedIncidences() const
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 
1596 KCalendarCore::DateList AgendaView::selectedIncidenceDates() const
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 
1614 bool AgendaView::eventDurationHint(QDateTime &startDt, QDateTime &endDt, bool &allDay) const
1615 {
1616     if (selectionStart().isValid()) {
1617         QDateTime start = selectionStart();
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 */
1637 bool AgendaView::selectedIsSingleCell() const
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 
1652 void AgendaView::updateView()
1653 {
1654     fillAgenda();
1655 }
1656 
1657 /*
1658   Update configuration settings for the agenda view. This method is not
1659   complete.
1660 */
1661 void 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();
1671         setHolidayMasks();
1672         createDayLabels(true);
1673         setChanges(changes() | ConfigChanged);
1674         updateView();
1675     }
1676 }
1677 
1678 void 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 
1702 void 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.
1741 static 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 
1756 void 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 {
1881         KCalendarCore::Incidence::Ptr oldIncidence(Akonadi::CalendarUtils::incidence(aitem));
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 
1910 QDate AgendaView::startDate() const
1911 {
1912     if (d->mSelectedDates.isEmpty()) {
1913         return {};
1914     }
1915     return d->mSelectedDates.first();
1916 }
1917 
1918 QDate AgendaView::endDate() const
1919 {
1920     if (d->mSelectedDates.isEmpty()) {
1921         return {};
1922     }
1923     return d->mSelectedDates.last();
1924 }
1925 
1926 void 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 
1946 void 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 
1980 void AgendaView::fillAgenda()
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);
2009         setHolidayMasks();
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();
2033     updateEventIndicators();
2034 
2035     //  mAgenda->viewport()->update();
2036     //  mAllDayAgenda->viewport()->update();
2037 
2038     // make invalid
2039     deleteSelectedDateTime();
2040 
2041     d->mUpdateAgenda = false;
2042     d->mUpdateAllDayAgenda = false;
2043 
2044     if (!somethingReselected) {
2045         Q_EMIT incidenceSelected(Akonadi::Item(), QDate());
2046     }
2047 }
2048 
2049 bool 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 
2181 void 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 
2189 void 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 
2197 void 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 
2244 static 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 
2273 void 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 
2316 void 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 
2324 void AgendaView::startDrag(const Akonadi::Item &incidence)
2325 {
2326     if (QDrag *drag = CalendarSupport::createDrag(incidence, this)) {
2327         drag->exec();
2328     }
2329 }
2330 
2331 void AgendaView::readSettings()
2332 {
2333     KSharedConfig::Ptr config = KSharedConfig::openConfig();
2334     readSettings(config.data());
2335 }
2336 
2337 void 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 
2351 void 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 
2359 QList<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 
2375 void AgendaView::setHolidayMasks()
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 
2397 void AgendaView::clearSelection()
2398 {
2399     d->mAgenda->deselectItem();
2400     d->mAllDayAgenda->deselectItem();
2401 }
2402 
2403 void AgendaView::newTimeSpanSelectedAllDay(const QPoint &start, const QPoint &end)
2404 {
2405     newTimeSpanSelected(start, end);
2406     d->mTimeSpanInAllDay = true;
2407 }
2408 
2409 void AgendaView::newTimeSpanSelected(const QPoint &start, const QPoint &end)
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 
2427 QDateTime AgendaView::selectionStart() const
2428 {
2429     return d->mTimeSpanBegin;
2430 }
2431 
2432 QDateTime AgendaView::selectionEnd() const
2433 {
2434     return d->mTimeSpanEnd;
2435 }
2436 
2437 bool AgendaView::selectedIsAllDay() const
2438 {
2439     return d->mTimeSpanInAllDay;
2440 }
2441 
2442 void AgendaView::deleteSelectedDateTime()
2443 {
2444     d->mTimeSpanBegin.setDate(QDate());
2445     d->mTimeSpanEnd.setDate(QDate());
2446     d->mTimeSpanInAllDay = false;
2447 }
2448 
2449 void 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 
2473 void AgendaView::updateEventIndicators()
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 
2484 void AgendaView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
2485 {
2486     EventView::setIncidenceChanger(changer);
2487     d->mAgenda->setIncidenceChanger(changer);
2488     d->mAllDayAgenda->setIncidenceChanger(changer);
2489 }
2490 
2491 void AgendaView::clearTimeSpanSelection()
2492 {
2493     d->mAgenda->clearSelection();
2494     d->mAllDayAgenda->clearSelection();
2495     deleteSelectedDateTime();
2496 }
2497 
2498 Agenda *AgendaView::agenda() const
2499 {
2500     return d->mAgenda;
2501 }
2502 
2503 Agenda *AgendaView::allDayAgenda() const
2504 {
2505     return d->mAllDayAgenda;
2506 }
2507 
2508 QSplitter *AgendaView::splitter() const
2509 {
2510     return d->mSplitterAgenda;
2511 }
2512 
2513 bool 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 
2531 void 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 
2542 void AgendaView::setChanges(EventView::Changes changes)
2543 {
2544     d->setChanges(changes);
2545 }
2546 
2547 void AgendaView::setTitle(const QString &title)
2548 {
2549     d->mTopDayLabelsFrame->setCalendarName(title);
2550 }
2551 
2552 void AgendaView::scheduleUpdateEventIndicators()
2553 {
2554     if (!d->mUpdateEventIndicatorsScheduled) {
2555         d->mUpdateEventIndicatorsScheduled = true;
2556         QTimer::singleShot(0, this, &AgendaView::updateEventIndicators);
2557     }
2558 }
2559 
2560 #include "agendaview.moc"
2561 
2562 #include "moc_agendaview.cpp"