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"