1 /*
2   SPDX-FileCopyrightText: 2000, 2001, 2003 Cornelius Schumacher <schumacher@kde.org>
3   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
4 
5   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
6 */
7 #include "agendaitem.h"
8 #include "eventview.h"
9 #include "helper.h"
10 
11 #include "prefs.h"
12 #include "prefs_base.h" // for enums
13 
14 #include <CalendarSupport/KCalPrefs>
15 #include <CalendarSupport/Utils>
16 
17 #include <KContacts/VCardDrag>
18 
19 #include <KCalUtils/ICalDrag>
20 #include <KCalUtils/IncidenceFormatter>
21 #include <KCalUtils/VCalDrag>
22 
23 #include <KEmailAddress>
24 
25 #include <KLocalizedString>
26 #include <KMessageBox>
27 #include <KWordWrap>
28 
29 #include <QDragEnterEvent>
30 #include <QLocale>
31 #include <QMimeData>
32 #include <QPainter>
33 #include <QPainterPath>
34 #include <QPixmapCache>
35 #include <QToolTip>
36 
37 using namespace KCalendarCore;
38 using namespace EventViews;
39 
40 //-----------------------------------------------------------------------------
41 
42 QPixmap *AgendaItem::alarmPxmp = nullptr;
43 QPixmap *AgendaItem::recurPxmp = nullptr;
44 QPixmap *AgendaItem::readonlyPxmp = nullptr;
45 QPixmap *AgendaItem::replyPxmp = nullptr;
46 QPixmap *AgendaItem::groupPxmp = nullptr;
47 QPixmap *AgendaItem::groupPxmpTent = nullptr;
48 QPixmap *AgendaItem::organizerPxmp = nullptr;
49 QPixmap *AgendaItem::eventPxmp = nullptr;
50 
51 //-----------------------------------------------------------------------------
52 
AgendaItem(EventView * eventView,const MultiViewCalendar::Ptr & calendar,const KCalendarCore::Incidence::Ptr & item,int itemPos,int itemCount,const QDateTime & qd,bool isSelected,QWidget * parent)53 AgendaItem::AgendaItem(EventView *eventView,
54                        const MultiViewCalendar::Ptr &calendar,
55                        const KCalendarCore::Incidence::Ptr &item,
56                        int itemPos,
57                        int itemCount,
58                        const QDateTime &qd,
59                        bool isSelected,
60                        QWidget *parent)
61     : QWidget(parent)
62     , mEventView(eventView)
63     , mCalendar(calendar)
64     , mIncidence(item)
65     , mOccurrenceDateTime(qd)
66     , mSelected(isSelected)
67     , mSpecialEvent(false)
68 {
69     if (!mIncidence) {
70         mValid = false;
71         return;
72     }
73 
74     mIncidence = Incidence::Ptr(mIncidence->clone());
75     if (mIncidence->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES") || mIncidence->customProperty("KABC", "ANNIVERSARY") == QLatin1String("YES")) {
76         const int years = EventViews::yearDiff(mIncidence->dtStart().date(), qd.toLocalTime().date());
77         if (years > 0) {
78             mIncidence->setReadOnly(false);
79             mIncidence->setSummary(i18np("%2 (1 year)", "%2 (%1 years)", years, mIncidence->summary()));
80             mIncidence->setReadOnly(true);
81             mCloned = true;
82         }
83     }
84 
85     mLabelText = mIncidence->summary();
86     mIconAlarm = false;
87     mIconRecur = false;
88     mIconReadonly = false;
89     mIconReply = false;
90     mIconGroup = false;
91     mIconGroupTent = false;
92     mIconOrganizer = false;
93     mMultiItemInfo = nullptr;
94     mStartMoveInfo = nullptr;
95 
96     mItemPos = itemPos;
97     mItemCount = itemCount;
98 
99     QPalette pal = palette();
100     pal.setColor(QPalette::Window, Qt::transparent);
101     setPalette(pal);
102 
103     setCellXY(0, 0, 1);
104     setCellXRight(0);
105     setMouseTracking(true);
106     mResourceColor = QColor();
107     updateIcons();
108 
109     setAcceptDrops(true);
110 }
111 
~AgendaItem()112 AgendaItem::~AgendaItem()
113 {
114 }
115 
updateIcons()116 void AgendaItem::updateIcons()
117 {
118     if (!mValid) {
119         return;
120     }
121     mIconReadonly = mIncidence->isReadOnly();
122     mIconRecur = mIncidence->recurs() || mIncidence->hasRecurrenceId();
123     mIconAlarm = mIncidence->hasEnabledAlarms();
124     if (mIncidence->attendeeCount() > 1) {
125         if (mEventView->kcalPreferences()->thatIsMe(mIncidence->organizer().email())) {
126             mIconReply = false;
127             mIconGroup = false;
128             mIconGroupTent = false;
129             mIconOrganizer = true;
130         } else {
131             KCalendarCore::Attendee me = mIncidence->attendeeByMails(mEventView->kcalPreferences()->allEmails());
132 
133             if (!me.isNull()) {
134                 if (me.status() == KCalendarCore::Attendee::NeedsAction && me.RSVP()) {
135                     mIconReply = true;
136                     mIconGroup = false;
137                     mIconGroupTent = false;
138                     mIconOrganizer = false;
139                 } else if (me.status() == KCalendarCore::Attendee::Tentative) {
140                     mIconReply = false;
141                     mIconGroup = false;
142                     mIconGroupTent = true;
143                     mIconOrganizer = false;
144                 } else {
145                     mIconReply = false;
146                     mIconGroup = true;
147                     mIconGroupTent = false;
148                     mIconOrganizer = false;
149                 }
150             } else {
151                 mIconReply = false;
152                 mIconGroup = true;
153                 mIconGroupTent = false;
154                 mIconOrganizer = false;
155             }
156         }
157     }
158     update();
159 }
160 
select(bool selected)161 void AgendaItem::select(bool selected)
162 {
163     if (mSelected != selected) {
164         mSelected = selected;
165         update();
166     }
167 }
168 
dissociateFromMultiItem()169 bool AgendaItem::dissociateFromMultiItem()
170 {
171     if (!isMultiItem()) {
172         return false;
173     }
174 
175     AgendaItem::QPtr firstItem = firstMultiItem();
176     if (firstItem == this) {
177         firstItem = nextMultiItem();
178     }
179 
180     AgendaItem::QPtr lastItem = lastMultiItem();
181     if (lastItem == this) {
182         lastItem = prevMultiItem();
183     }
184 
185     AgendaItem::QPtr prevItem = prevMultiItem();
186     AgendaItem::QPtr nextItem = nextMultiItem();
187 
188     if (prevItem) {
189         prevItem->setMultiItem(firstItem, prevItem->prevMultiItem(), nextItem, lastItem);
190     }
191     if (nextItem) {
192         nextItem->setMultiItem(firstItem, prevItem, nextItem->prevMultiItem(), lastItem);
193     }
194     delete mMultiItemInfo;
195     mMultiItemInfo = nullptr;
196     return true;
197 }
198 
setIncidence(const KCalendarCore::Incidence::Ptr & incidence)199 void AgendaItem::setIncidence(const KCalendarCore::Incidence::Ptr &incidence)
200 {
201     mValid = false;
202     if (incidence) {
203         mValid = true;
204         mIncidence = incidence;
205         mLabelText = mIncidence->summary();
206         updateIcons();
207     }
208 }
209 
210 /*
211   Return height of item in units of agenda cells
212 */
cellHeight() const213 int AgendaItem::cellHeight() const
214 {
215     return mCellYBottom - mCellYTop + 1;
216 }
217 
218 /*
219   Return height of item in units of agenda cells
220 */
cellWidth() const221 int AgendaItem::cellWidth() const
222 {
223     return mCellXRight - mCellXLeft + 1;
224 }
225 
setOccurrenceDateTime(const QDateTime & qd)226 void AgendaItem::setOccurrenceDateTime(const QDateTime &qd)
227 {
228     mOccurrenceDateTime = qd;
229 }
230 
occurrenceDate() const231 QDate AgendaItem::occurrenceDate() const
232 {
233     return mOccurrenceDateTime.toLocalTime().date();
234 }
235 
setCellXY(int X,int YTop,int YBottom)236 void AgendaItem::setCellXY(int X, int YTop, int YBottom)
237 {
238     mCellXLeft = X;
239     mCellYTop = YTop;
240     mCellYBottom = YBottom;
241 }
242 
setCellXRight(int XRight)243 void AgendaItem::setCellXRight(int XRight)
244 {
245     mCellXRight = XRight;
246 }
247 
setCellX(int XLeft,int XRight)248 void AgendaItem::setCellX(int XLeft, int XRight)
249 {
250     mCellXLeft = XLeft;
251     mCellXRight = XRight;
252 }
253 
setCellY(int YTop,int YBottom)254 void AgendaItem::setCellY(int YTop, int YBottom)
255 {
256     mCellYTop = YTop;
257     mCellYBottom = YBottom;
258 }
259 
setMultiItem(const AgendaItem::QPtr & first,const AgendaItem::QPtr & prev,const AgendaItem::QPtr & next,const AgendaItem::QPtr & last)260 void AgendaItem::setMultiItem(const AgendaItem::QPtr &first, const AgendaItem::QPtr &prev, const AgendaItem::QPtr &next, const AgendaItem::QPtr &last)
261 {
262     if (!mMultiItemInfo) {
263         mMultiItemInfo = new MultiItemInfo;
264     }
265     mMultiItemInfo->mFirstMultiItem = first;
266     mMultiItemInfo->mPrevMultiItem = prev;
267     mMultiItemInfo->mNextMultiItem = next;
268     mMultiItemInfo->mLastMultiItem = last;
269 }
270 
isMultiItem() const271 bool AgendaItem::isMultiItem() const
272 {
273     return mMultiItemInfo;
274 }
275 
prependMoveItem(const AgendaItem::QPtr & e)276 AgendaItem::QPtr AgendaItem::prependMoveItem(const AgendaItem::QPtr &e)
277 {
278     if (!e) {
279         return nullptr;
280     }
281 
282     AgendaItem::QPtr first = nullptr;
283     AgendaItem::QPtr last = nullptr;
284     if (isMultiItem()) {
285         first = mMultiItemInfo->mFirstMultiItem;
286         last = mMultiItemInfo->mLastMultiItem;
287     }
288     if (!first) {
289         first = this;
290     }
291     if (!last) {
292         last = this;
293     }
294 
295     e->setMultiItem(nullptr, nullptr, first, last);
296     first->setMultiItem(e, e, first->nextMultiItem(), first->lastMultiItem());
297 
298     AgendaItem::QPtr tmp = first->nextMultiItem();
299     while (tmp) {
300         tmp->setMultiItem(e, tmp->prevMultiItem(), tmp->nextMultiItem(), tmp->lastMultiItem());
301         tmp = tmp->nextMultiItem();
302     }
303 
304     if (mStartMoveInfo && !e->moveInfo()) {
305         e->mStartMoveInfo = new MultiItemInfo(*mStartMoveInfo);
306         //    e->moveInfo()->mFirstMultiItem = moveInfo()->mFirstMultiItem;
307         //    e->moveInfo()->mLastMultiItem = moveInfo()->mLastMultiItem;
308         e->moveInfo()->mPrevMultiItem = nullptr;
309         e->moveInfo()->mNextMultiItem = first;
310     }
311 
312     if (first && first->moveInfo()) {
313         first->moveInfo()->mPrevMultiItem = e;
314     }
315     return e;
316 }
317 
appendMoveItem(const AgendaItem::QPtr & e)318 AgendaItem::QPtr AgendaItem::appendMoveItem(const AgendaItem::QPtr &e)
319 {
320     if (!e) {
321         return nullptr;
322     }
323 
324     AgendaItem::QPtr first = nullptr;
325     AgendaItem::QPtr last = nullptr;
326     if (isMultiItem()) {
327         first = mMultiItemInfo->mFirstMultiItem;
328         last = mMultiItemInfo->mLastMultiItem;
329     }
330     if (!first) {
331         first = this;
332     }
333     if (!last) {
334         last = this;
335     }
336 
337     e->setMultiItem(first, last, nullptr, nullptr);
338     AgendaItem::QPtr tmp = first;
339 
340     while (tmp) {
341         tmp->setMultiItem(tmp->firstMultiItem(), tmp->prevMultiItem(), tmp->nextMultiItem(), e);
342         tmp = tmp->nextMultiItem();
343     }
344     last->setMultiItem(last->firstMultiItem(), last->prevMultiItem(), e, e);
345 
346     if (mStartMoveInfo && !e->moveInfo()) {
347         e->mStartMoveInfo = new MultiItemInfo(*mStartMoveInfo);
348         //    e->moveInfo()->mFirstMultiItem = moveInfo()->mFirstMultiItem;
349         //    e->moveInfo()->mLastMultiItem = moveInfo()->mLastMultiItem;
350         e->moveInfo()->mPrevMultiItem = last;
351         e->moveInfo()->mNextMultiItem = nullptr;
352     }
353     if (last && last->moveInfo()) {
354         last->moveInfo()->mNextMultiItem = e;
355     }
356     return e;
357 }
358 
removeMoveItem(const AgendaItem::QPtr & e)359 AgendaItem::QPtr AgendaItem::removeMoveItem(const AgendaItem::QPtr &e)
360 {
361     if (isMultiItem()) {
362         AgendaItem::QPtr first = mMultiItemInfo->mFirstMultiItem;
363         AgendaItem::QPtr next;
364         AgendaItem::QPtr prev;
365         AgendaItem::QPtr last = mMultiItemInfo->mLastMultiItem;
366         if (!first) {
367             first = this;
368         }
369         if (!last) {
370             last = this;
371         }
372         if (first == e) {
373             first = first->nextMultiItem();
374             first->setMultiItem(nullptr, nullptr, first->nextMultiItem(), first->lastMultiItem());
375         }
376         if (last == e) {
377             last = last->prevMultiItem();
378             last->setMultiItem(last->firstMultiItem(), last->prevMultiItem(), nullptr, nullptr);
379         }
380 
381         AgendaItem::QPtr tmp = first;
382         if (first == last) {
383             delete mMultiItemInfo;
384             tmp = nullptr;
385             mMultiItemInfo = nullptr;
386         }
387         while (tmp) {
388             next = tmp->nextMultiItem();
389             prev = tmp->prevMultiItem();
390             if (e == next) {
391                 next = next->nextMultiItem();
392             }
393             if (e == prev) {
394                 prev = prev->prevMultiItem();
395             }
396             tmp->setMultiItem((tmp == first) ? nullptr : first, (tmp == prev) ? nullptr : prev, (tmp == next) ? nullptr : next, (tmp == last) ? nullptr : last);
397             tmp = tmp->nextMultiItem();
398         }
399     }
400 
401     return e;
402 }
403 
startMove()404 void AgendaItem::startMove()
405 {
406     AgendaItem::QPtr first = this;
407     if (isMultiItem() && mMultiItemInfo->mFirstMultiItem) {
408         first = mMultiItemInfo->mFirstMultiItem;
409     }
410     first->startMovePrivate();
411 }
412 
startMovePrivate()413 void AgendaItem::startMovePrivate()
414 {
415     mStartMoveInfo = new MultiItemInfo;
416     mStartMoveInfo->mStartCellXLeft = mCellXLeft;
417     mStartMoveInfo->mStartCellXRight = mCellXRight;
418     mStartMoveInfo->mStartCellYTop = mCellYTop;
419     mStartMoveInfo->mStartCellYBottom = mCellYBottom;
420     if (mMultiItemInfo) {
421         mStartMoveInfo->mFirstMultiItem = mMultiItemInfo->mFirstMultiItem;
422         mStartMoveInfo->mLastMultiItem = mMultiItemInfo->mLastMultiItem;
423         mStartMoveInfo->mPrevMultiItem = mMultiItemInfo->mPrevMultiItem;
424         mStartMoveInfo->mNextMultiItem = mMultiItemInfo->mNextMultiItem;
425     } else {
426         mStartMoveInfo->mFirstMultiItem = nullptr;
427         mStartMoveInfo->mLastMultiItem = nullptr;
428         mStartMoveInfo->mPrevMultiItem = nullptr;
429         mStartMoveInfo->mNextMultiItem = nullptr;
430     }
431     if (isMultiItem() && mMultiItemInfo->mNextMultiItem) {
432         mMultiItemInfo->mNextMultiItem->startMovePrivate();
433     }
434 }
435 
resetMove()436 void AgendaItem::resetMove()
437 {
438     if (mStartMoveInfo) {
439         if (mStartMoveInfo->mFirstMultiItem) {
440             mStartMoveInfo->mFirstMultiItem->resetMovePrivate();
441         } else {
442             resetMovePrivate();
443         }
444     }
445 }
446 
resetMovePrivate()447 void AgendaItem::resetMovePrivate()
448 {
449     if (mStartMoveInfo) {
450         mCellXLeft = mStartMoveInfo->mStartCellXLeft;
451         mCellXRight = mStartMoveInfo->mStartCellXRight;
452         mCellYTop = mStartMoveInfo->mStartCellYTop;
453         mCellYBottom = mStartMoveInfo->mStartCellYBottom;
454 
455         // if we don't have mMultiItemInfo, the item didn't span two days before,
456         // and wasn't moved over midnight, either, so we don't have to reset
457         // anything. Otherwise, restore from mMoveItemInfo
458         if (mMultiItemInfo) {
459             // It was already a multi-day info
460             mMultiItemInfo->mFirstMultiItem = mStartMoveInfo->mFirstMultiItem;
461             mMultiItemInfo->mPrevMultiItem = mStartMoveInfo->mPrevMultiItem;
462             mMultiItemInfo->mNextMultiItem = mStartMoveInfo->mNextMultiItem;
463             mMultiItemInfo->mLastMultiItem = mStartMoveInfo->mLastMultiItem;
464 
465             if (!mStartMoveInfo->mFirstMultiItem) {
466                 // This was the first multi-item when the move started, delete all previous
467                 AgendaItem::QPtr toDel = mStartMoveInfo->mPrevMultiItem;
468                 AgendaItem::QPtr nowDel = nullptr;
469                 while (toDel) {
470                     nowDel = toDel;
471                     if (nowDel->moveInfo()) {
472                         toDel = nowDel->moveInfo()->mPrevMultiItem;
473                     }
474                     Q_EMIT removeAgendaItem(nowDel);
475                 }
476                 mMultiItemInfo->mFirstMultiItem = nullptr;
477                 mMultiItemInfo->mPrevMultiItem = nullptr;
478             }
479             if (!mStartMoveInfo->mLastMultiItem) {
480                 // This was the last multi-item when the move started, delete all next
481                 AgendaItem::QPtr toDel = mStartMoveInfo->mNextMultiItem;
482                 AgendaItem::QPtr nowDel = nullptr;
483                 while (toDel) {
484                     nowDel = toDel;
485                     if (nowDel->moveInfo()) {
486                         toDel = nowDel->moveInfo()->mNextMultiItem;
487                     }
488                     Q_EMIT removeAgendaItem(nowDel);
489                 }
490                 mMultiItemInfo->mLastMultiItem = nullptr;
491                 mMultiItemInfo->mNextMultiItem = nullptr;
492             }
493 
494             if (mStartMoveInfo->mFirstMultiItem == nullptr && mStartMoveInfo->mLastMultiItem == nullptr) {
495                 // it was a single-day event before we started the move.
496                 delete mMultiItemInfo;
497                 mMultiItemInfo = nullptr;
498             }
499         }
500         delete mStartMoveInfo;
501         mStartMoveInfo = nullptr;
502     }
503     Q_EMIT showAgendaItem(this);
504     if (nextMultiItem()) {
505         nextMultiItem()->resetMovePrivate();
506     }
507 }
508 
endMove()509 void AgendaItem::endMove()
510 {
511     AgendaItem::QPtr first = firstMultiItem();
512     if (!first) {
513         first = this;
514     }
515     first->endMovePrivate();
516 }
517 
endMovePrivate()518 void AgendaItem::endMovePrivate()
519 {
520     if (mStartMoveInfo) {
521         // if first, delete all previous
522         if (!firstMultiItem() || firstMultiItem() == this) {
523             AgendaItem::QPtr toDel = mStartMoveInfo->mPrevMultiItem;
524             AgendaItem::QPtr nowDel = nullptr;
525             while (toDel) {
526                 nowDel = toDel;
527                 if (nowDel->moveInfo()) {
528                     toDel = nowDel->moveInfo()->mPrevMultiItem;
529                 }
530                 Q_EMIT removeAgendaItem(nowDel);
531             }
532         }
533         // if last, delete all next
534         if (!lastMultiItem() || lastMultiItem() == this) {
535             AgendaItem::QPtr toDel = mStartMoveInfo->mNextMultiItem;
536             AgendaItem::QPtr nowDel = nullptr;
537             while (toDel) {
538                 nowDel = toDel;
539                 if (nowDel->moveInfo()) {
540                     toDel = nowDel->moveInfo()->mNextMultiItem;
541                 }
542                 Q_EMIT removeAgendaItem(nowDel);
543             }
544         }
545         // also delete the moving info
546         delete mStartMoveInfo;
547         mStartMoveInfo = nullptr;
548         if (nextMultiItem()) {
549             nextMultiItem()->endMovePrivate();
550         }
551     }
552 }
553 
moveRelative(int dx,int dy)554 void AgendaItem::moveRelative(int dx, int dy)
555 {
556     int newXLeft = cellXLeft() + dx;
557     int newXRight = cellXRight() + dx;
558     int newYTop = cellYTop() + dy;
559     int newYBottom = cellYBottom() + dy;
560     setCellXY(newXLeft, newYTop, newYBottom);
561     setCellXRight(newXRight);
562 }
563 
expandTop(int dy,const bool allowOverLimit)564 void AgendaItem::expandTop(int dy, const bool allowOverLimit)
565 {
566     int newYTop = cellYTop() + dy;
567     int newYBottom = cellYBottom();
568     if (newYTop > newYBottom && !allowOverLimit) {
569         newYTop = newYBottom;
570     }
571     setCellY(newYTop, newYBottom);
572 }
573 
expandBottom(int dy)574 void AgendaItem::expandBottom(int dy)
575 {
576     int newYTop = cellYTop();
577     int newYBottom = cellYBottom() + dy;
578     if (newYBottom < newYTop) {
579         newYBottom = newYTop;
580     }
581     setCellY(newYTop, newYBottom);
582 }
583 
expandLeft(int dx)584 void AgendaItem::expandLeft(int dx)
585 {
586     int newXLeft = cellXLeft() + dx;
587     int newXRight = cellXRight();
588     if (newXLeft > newXRight) {
589         newXLeft = newXRight;
590     }
591     setCellX(newXLeft, newXRight);
592 }
593 
expandRight(int dx)594 void AgendaItem::expandRight(int dx)
595 {
596     int newXLeft = cellXLeft();
597     int newXRight = cellXRight() + dx;
598     if (newXRight < newXLeft) {
599         newXRight = newXLeft;
600     }
601     setCellX(newXLeft, newXRight);
602 }
603 
dragEnterEvent(QDragEnterEvent * e)604 void AgendaItem::dragEnterEvent(QDragEnterEvent *e)
605 {
606     const QMimeData *md = e->mimeData();
607     if (KCalUtils::ICalDrag::canDecode(md) || KCalUtils::VCalDrag::canDecode(md)) {
608         // TODO: Allow dragging events/todos onto other events to create a relation
609         e->ignore();
610         return;
611     }
612     if (KContacts::VCardDrag::canDecode(md) || md->hasText()) {
613         e->accept();
614     } else {
615         e->ignore();
616     }
617 }
618 
addAttendee(const QString & newAttendee)619 void AgendaItem::addAttendee(const QString &newAttendee)
620 {
621     if (!mValid) {
622         return;
623     }
624 
625     QString name;
626     QString email;
627     KEmailAddress::extractEmailAddressAndName(newAttendee, email, name);
628     if (!(name.isEmpty() && email.isEmpty())) {
629         mIncidence->addAttendee(KCalendarCore::Attendee(name, email));
630         KMessageBox::information(this,
631                                  i18n("Attendee \"%1\" added to the calendar item \"%2\"", KEmailAddress::normalizedAddress(name, email, QString()), text()),
632                                  i18n("Attendee added"),
633                                  QStringLiteral("AttendeeDroppedAdded"));
634     }
635 }
636 
dropEvent(QDropEvent * e)637 void AgendaItem::dropEvent(QDropEvent *e)
638 {
639     // TODO: Organize this better: First check for attachment
640     // (not only file, also any other url!), then if it's a vcard,
641     // otherwise check for attendees, then if the data is binary,
642     // add a binary attachment.
643     if (!mValid) {
644         return;
645     }
646 
647     const QMimeData *md = e->mimeData();
648 
649     bool decoded = md->hasText();
650     QString text = md->text();
651     if (decoded && text.startsWith(QLatin1String("file:"))) {
652         mIncidence->addAttachment(KCalendarCore::Attachment(text));
653         return;
654     }
655 
656     KContacts::Addressee::List list;
657 
658     if (KContacts::VCardDrag::fromMimeData(md, list)) {
659         for (const KContacts::Addressee &addressee : std::as_const(list)) {
660             QString em(addressee.fullEmail());
661             if (em.isEmpty()) {
662                 em = addressee.realName();
663             }
664             addAttendee(em);
665         }
666     }
667 }
668 
conflictItems()669 QList<AgendaItem::QPtr> &AgendaItem::conflictItems()
670 {
671     return mConflictItems;
672 }
673 
setConflictItems(const QList<AgendaItem::QPtr> & ci)674 void AgendaItem::setConflictItems(const QList<AgendaItem::QPtr> &ci)
675 {
676     mConflictItems = ci;
677     for (QList<AgendaItem::QPtr>::iterator it = mConflictItems.begin(), end(mConflictItems.end()); it != end; ++it) {
678         (*it)->addConflictItem(this);
679     }
680 }
681 
addConflictItem(const AgendaItem::QPtr & ci)682 void AgendaItem::addConflictItem(const AgendaItem::QPtr &ci)
683 {
684     if (!mConflictItems.contains(ci)) {
685         mConflictItems.append(ci);
686     }
687 }
688 
label() const689 QString AgendaItem::label() const
690 {
691     return mLabelText;
692 }
693 
overlaps(CellItem * o) const694 bool AgendaItem::overlaps(CellItem *o) const
695 {
696     AgendaItem::QPtr other = static_cast<AgendaItem *>(o);
697 
698     if (cellXLeft() <= other->cellXRight() && cellXRight() >= other->cellXLeft()) {
699         if ((cellYTop() <= other->cellYBottom()) && (cellYBottom() >= other->cellYTop())) {
700             return true;
701         }
702     }
703 
704     return false;
705 }
706 
conditionalPaint(QPainter * p,bool condition,int & x,int y,int ft,const QPixmap & pxmp)707 static void conditionalPaint(QPainter *p, bool condition, int &x, int y, int ft, const QPixmap &pxmp)
708 {
709     if (condition) {
710         p->drawPixmap(x, y, pxmp);
711         x += pxmp.width() + ft;
712     }
713 }
714 
paintIcon(QPainter * p,int & x,int y,int ft)715 void AgendaItem::paintIcon(QPainter *p, int &x, int y, int ft)
716 {
717     QString iconName;
718     if (mIncidence->customProperty("KABC", "ANNIVERSARY") == QLatin1String("YES")) {
719         mSpecialEvent = true;
720         iconName = QStringLiteral("view-calendar-wedding-anniversary");
721     } else if (mIncidence->customProperty("KABC", "BIRTHDAY") == QLatin1String("YES")) {
722         mSpecialEvent = true;
723         // We don't draw icon. The icon is drawn already, because it's the Akonadi::Collection's icon
724     }
725 
726     conditionalPaint(p, !iconName.isEmpty(), x, y, ft, cachedSmallIcon(iconName));
727 }
728 
paintIcons(QPainter * p,int & x,int y,int ft)729 void AgendaItem::paintIcons(QPainter *p, int &x, int y, int ft)
730 {
731     if (!mEventView->preferences()->enableAgendaItemIcons()) {
732         return;
733     }
734 
735     paintIcon(p, x, y, ft);
736 
737     QSet<EventView::ItemIcon> icons = mEventView->preferences()->agendaViewIcons();
738 
739     if (icons.contains(EventViews::EventView::CalendarCustomIcon)) {
740         const QString iconName = mCalendar->iconForIncidence(mIncidence);
741         if (!iconName.isEmpty() && iconName != QLatin1String("view-calendar") && iconName != QLatin1String("office-calendar")) {
742             conditionalPaint(p, true, x, y, ft, QIcon::fromTheme(iconName).pixmap(16, 16));
743         }
744     }
745 
746     const bool isTodo = mIncidence && mIncidence->type() == Incidence::TypeTodo;
747 
748     if (isTodo && icons.contains(EventViews::EventView::TaskIcon)) {
749         const QString iconName = mIncidence->iconName(mOccurrenceDateTime.toLocalTime());
750         conditionalPaint(p, !mSpecialEvent, x, y, ft, QIcon::fromTheme(iconName).pixmap(16, 16));
751     }
752 
753     if (icons.contains(EventView::RecurringIcon)) {
754         conditionalPaint(p, mIconRecur && !mSpecialEvent, x, y, ft, *recurPxmp);
755     }
756 
757     if (icons.contains(EventView::ReminderIcon)) {
758         conditionalPaint(p, mIconAlarm && !mSpecialEvent, x, y, ft, *alarmPxmp);
759     }
760 
761     if (icons.contains(EventView::ReadOnlyIcon)) {
762         conditionalPaint(p, mIconReadonly && !mSpecialEvent, x, y, ft, *readonlyPxmp);
763     }
764 
765     if (icons.contains(EventView::ReplyIcon)) {
766         conditionalPaint(p, mIconReply, x, y, ft, *replyPxmp);
767     }
768 
769     if (icons.contains(EventView::AttendingIcon)) {
770         conditionalPaint(p, mIconGroup, x, y, ft, *groupPxmp);
771     }
772 
773     if (icons.contains(EventView::TentativeIcon)) {
774         conditionalPaint(p, mIconGroupTent, x, y, ft, *groupPxmpTent);
775     }
776 
777     if (icons.contains(EventView::OrganizerIcon)) {
778         conditionalPaint(p, mIconOrganizer, x, y, ft, *organizerPxmp);
779     }
780 }
781 
paintEvent(QPaintEvent * ev)782 void AgendaItem::paintEvent(QPaintEvent *ev)
783 {
784     if (!mValid) {
785         return;
786     }
787 
788     QRect visRect = visibleRegion().boundingRect();
789     // when scrolling horizontally in the side-by-side view, the repainted area is clipped
790     // to the newly visible area, which is a problem since the content changes when visRect
791     // changes, so repaint the full item in that case
792     if (ev->rect() != visRect && visRect.isValid() && ev->rect().isValid()) {
793         update(visRect);
794         return;
795     }
796 
797     QPainter p(this);
798     p.setRenderHint(QPainter::Antialiasing);
799     const int fmargin = 0; // frame margin
800     const int ft = 1; // frame thickness for layout, see drawRoundedRect(),
801     // keep multiple of 2
802     const int margin = 5 + ft + fmargin; // frame + space between frame and content
803 
804     // General idea is to always show the icons (even in the all-day events).
805     // This creates a consistent feeling for the user when the view mode
806     // changes and therefore the available width changes.
807     // Also look at #17984
808 
809     if (!alarmPxmp) {
810         alarmPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("task-reminder")).pixmap(16, 16));
811         recurPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("appointment-recurring")).pixmap(16, 16));
812         readonlyPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("object-locked")).pixmap(16, 16));
813         replyPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-reply-sender")).pixmap(16, 16));
814         groupPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-attending")).pixmap(16, 16));
815         groupPxmpTent = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-attending-tentative")).pixmap(16, 16));
816         organizerPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-organizer")).pixmap(16, 16));
817     }
818 
819     const auto categoryColor = getCategoryColor();
820     const auto resourceColor = mResourceColor.isValid() ? mResourceColor : categoryColor;
821     const auto frameColor = getFrameColor(resourceColor, categoryColor);
822     const auto bgBaseColor = getBackgroundColor(resourceColor, categoryColor);
823     const auto bgColor = mSelected ? bgBaseColor.lighter(EventView::BRIGHTNESS_FACTOR) : bgBaseColor;
824     const auto textColor = EventViews::getTextColor(bgColor);
825 
826     p.setPen(textColor);
827 
828     p.setFont(mEventView->preferences()->agendaViewFont());
829     QFontMetrics fm = p.fontMetrics();
830 
831     const int singleLineHeight = fm.boundingRect(mLabelText).height();
832 
833     const bool roundTop = !prevMultiItem();
834     const bool roundBottom = !nextMultiItem();
835 
836     drawRoundedRect(&p, QRect(fmargin, fmargin, width() - fmargin * 2, height() - fmargin * 2), mSelected, bgColor, frameColor, true, ft, roundTop, roundBottom);
837 
838     // calculate the height of the full version (case 4) to test whether it is
839     // possible
840 
841     QString shortH;
842     QString longH;
843     if (!isMultiItem()) {
844         shortH = QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleDisplayStart).toLocalTime().time(), QLocale::ShortFormat);
845 
846         if (CalendarSupport::hasEvent(mIncidence)) {
847             longH =
848                 i18n("%1 - %2", shortH, QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().time(), QLocale::ShortFormat));
849         } else {
850             longH = shortH;
851         }
852     } else if (!mMultiItemInfo->mFirstMultiItem) {
853         shortH = QLocale().toString(mIncidence->dtStart().toLocalTime().time(), QLocale::ShortFormat);
854         longH = shortH;
855     } else {
856         shortH = QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().time(), QLocale::ShortFormat);
857         longH = i18n("- %1", shortH);
858     }
859 
860     KWordWrap ww = KWordWrap::formatText(fm, QRect(0, 0, width() - (2 * margin), -1), 0, mLabelText);
861     int th = ww.boundingRect().height();
862 
863     int hlHeight =
864         qMax(fm.boundingRect(longH).height(),
865              qMax(alarmPxmp->height(),
866                   qMax(recurPxmp->height(), qMax(readonlyPxmp->height(), qMax(replyPxmp->height(), qMax(groupPxmp->height(), organizerPxmp->height()))))));
867 
868     const bool completelyRenderable = th < (height() - 2 * ft - 2 - hlHeight);
869 
870     // case 1: do not draw text when not even a single line fits
871     // Don't do this any more, always try to print out the text.
872     // Even if it's just a few pixel, one can still guess the whole
873     // text from just four pixels' height!
874     if ( //( singleLineHeight > height() - 4 ) ||
875         (width() < 16)) {
876         int x = qRound((width() - 16) / 2.0);
877         paintIcon(&p, x /*by-ref*/, margin, ft);
878         return;
879     }
880 
881     // case 2: draw a single line when no more space
882     if ((2 * singleLineHeight) > (height() - 2 * margin)) {
883         int x = margin;
884         int txtWidth;
885 
886         if (mIncidence->allDay()) {
887             x += visRect.left();
888             const int y = qRound((height() - 16) / 2.0);
889             paintIcons(&p, x, y, ft);
890             txtWidth = visRect.right() - margin - x;
891         } else {
892             const int y = qRound((height() - 16) / 2.0);
893             paintIcons(&p, x, y, ft);
894             txtWidth = width() - margin - x;
895         }
896 
897         const int y = ((height() - singleLineHeight) / 2) + fm.ascent();
898         KWordWrap::drawFadeoutText(&p, x, y, txtWidth, mLabelText);
899         return;
900     }
901 
902     // case 3: enough for 2-5 lines, but not for the header.
903     //         Also used for the middle days in multi-events
904     if (((!completelyRenderable) && ((height() - (2 * margin)) <= (5 * singleLineHeight)))
905         || (isMultiItem() && mMultiItemInfo->mNextMultiItem && mMultiItemInfo->mFirstMultiItem)) {
906         int x = margin;
907         int txtWidth;
908 
909         if (mIncidence->allDay()) {
910             x += visRect.left();
911             paintIcons(&p, x, margin, ft);
912             txtWidth = visRect.right() - margin - x;
913         } else {
914             paintIcons(&p, x, margin, ft);
915             txtWidth = width() - margin - x;
916         }
917 
918         ww = KWordWrap::formatText(fm, QRect(0, 0, txtWidth, (height() - (2 * margin))), 0, mLabelText);
919 
920         ww.drawText(&p, x, margin, Qt::AlignHCenter | KWordWrap::FadeOut);
921         return;
922     }
923 
924     // case 4: paint everything, with header:
925     // consists of (vertically) ft + headline&icons + ft + text + margin
926     int y = 2 * ft + hlHeight;
927     if (completelyRenderable) {
928         y += (height() - (2 * ft) - margin - hlHeight - th) / 2;
929     }
930 
931     int x = margin;
932     int txtWidth;
933     int hTxtWidth;
934     int eventX;
935 
936     if (mIncidence->allDay()) {
937         shortH.clear();
938         longH.clear();
939 
940         if (const KCalendarCore::Event::Ptr event = CalendarSupport::event(mIncidence)) {
941             if (event->isMultiDay(QTimeZone::systemTimeZone())) {
942                 // multi-day, all-day event
943                 shortH = i18n("%1 - %2",
944                               QLocale().toString(mIncidence->dtStart().toLocalTime().date()),
945                               QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().date()));
946                 longH = shortH;
947 
948                 // paint headline
949                 drawRoundedRect(&p,
950                                 QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
951                                 mSelected,
952                                 frameColor,
953                                 frameColor,
954                                 false,
955                                 ft,
956                                 roundTop,
957                                 false);
958             } else {
959                 // single-day, all-day event
960 
961                 // paint headline
962                 drawRoundedRect(&p,
963                                 QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
964                                 mSelected,
965                                 frameColor,
966                                 frameColor,
967                                 false,
968                                 ft,
969                                 roundTop,
970                                 false);
971             }
972         } else {
973             // to-do
974 
975             // paint headline
976             drawRoundedRect(&p,
977                             QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
978                             mSelected,
979                             frameColor,
980                             frameColor,
981                             false,
982                             ft,
983                             roundTop,
984                             false);
985         }
986 
987         x += visRect.left();
988         eventX = x;
989         txtWidth = visRect.right() - margin - x;
990         paintIcons(&p, x, margin / 2, ft);
991         hTxtWidth = visRect.right() - margin - x;
992     } else {
993         // paint headline
994         drawRoundedRect(&p,
995                         QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
996                         mSelected,
997                         frameColor,
998                         frameColor,
999                         false,
1000                         ft,
1001                         roundTop,
1002                         false);
1003 
1004         txtWidth = width() - margin - x;
1005         eventX = x;
1006         paintIcons(&p, x, margin / 2, ft);
1007         hTxtWidth = width() - margin - x;
1008     }
1009 
1010     QString headline;
1011     int hw = fm.boundingRect(longH).width();
1012     if (hw > hTxtWidth) {
1013         headline = shortH;
1014         hw = fm.boundingRect(shortH).width();
1015         if (hw < txtWidth) {
1016             x += (hTxtWidth - hw) / 2;
1017         }
1018     } else {
1019         headline = longH;
1020         x += (hTxtWidth - hw) / 2;
1021     }
1022     p.setBackground(QBrush(frameColor));
1023     p.setPen(EventViews::getTextColor(frameColor));
1024     KWordWrap::drawFadeoutText(&p, x, (margin + hlHeight + fm.ascent()) / 2 - 2, hTxtWidth, headline);
1025 
1026     // draw event text
1027     ww = KWordWrap::formatText(fm, QRect(0, 0, txtWidth, height() - margin - y), 0, mLabelText);
1028 
1029     p.setBackground(QBrush(bgColor));
1030     p.setPen(textColor);
1031     QString ws = ww.wrappedString();
1032     if (ws.leftRef(ws.length() - 1).indexOf(QLatin1Char('\n')) >= 0) {
1033         ww.drawText(&p, eventX, y, Qt::AlignLeft | KWordWrap::FadeOut);
1034     } else {
1035         ww.drawText(&p, eventX + (txtWidth - ww.boundingRect().width() - 2 * margin) / 2, y, Qt::AlignHCenter | KWordWrap::FadeOut);
1036     }
1037 }
1038 
drawRoundedRect(QPainter * p,QRect rect,bool selected,const QColor & bgColor,const QColor & frameColor,bool frame,int ft,bool roundTop,bool roundBottom)1039 void AgendaItem::drawRoundedRect(QPainter *p, QRect rect, bool selected, const QColor &bgColor, const QColor &frameColor, bool frame, int ft, bool roundTop, bool roundBottom)
1040 {
1041     Q_UNUSED(ft)
1042     if (!mValid) {
1043         return;
1044     }
1045 
1046     QPainterPath path;
1047 
1048     const int RECT_MARGIN = 2;
1049     const int RADIUS = 2; // absolute radius
1050 
1051     const QRect rectWithMargin(rect.x() + RECT_MARGIN, rect.y() + RECT_MARGIN, rect.width() - 2 * RECT_MARGIN, rect.height() - 2 * RECT_MARGIN);
1052 
1053     const QPoint pointLeftTop(rectWithMargin.x(), rectWithMargin.y());
1054     const QPoint pointRightTop(rectWithMargin.x() + rectWithMargin.width(), rectWithMargin.y());
1055     const QPoint pointLeftBottom(rectWithMargin.x(), rectWithMargin.y() + rectWithMargin.height());
1056     const QPoint pointRightBottom(rectWithMargin.x() + rectWithMargin.width(), rectWithMargin.y() + rectWithMargin.height());
1057 
1058     if (!roundTop && !roundBottom) {
1059         path.addRect(rectWithMargin);
1060     } else if (roundTop && roundBottom) {
1061         path.addRoundedRect(rectWithMargin, RADIUS, RADIUS, Qt::AbsoluteSize);
1062     } else if (roundTop) {
1063         path.moveTo(pointRightBottom);
1064         path.lineTo(pointLeftBottom);
1065         path.lineTo(QPoint(pointLeftTop.x(), pointLeftTop.y() + RADIUS));
1066         path.quadTo(pointLeftTop, QPoint(pointLeftTop.x() + RADIUS, pointLeftTop.y()));
1067         path.lineTo(QPoint(pointRightTop.x() - RADIUS, pointRightTop.y()));
1068         path.quadTo(pointRightTop, QPoint(pointRightTop.x(), pointRightTop.y() + RADIUS));
1069         path.lineTo(pointRightBottom);
1070     } else if (roundBottom) {
1071         path.moveTo(pointRightTop);
1072         path.lineTo(QPoint(pointRightBottom.x(), pointRightBottom.y() - RADIUS));
1073         path.quadTo(pointRightBottom, QPoint(pointRightBottom.x() - RADIUS, pointRightBottom.y()));
1074         path.lineTo(QPoint(pointLeftBottom.x() + RADIUS, pointLeftBottom.y()));
1075         path.quadTo(pointLeftBottom, QPoint(pointLeftBottom.x(), pointLeftBottom.y() - RADIUS));
1076         path.lineTo(pointLeftTop);
1077         path.lineTo(pointRightTop);
1078     }
1079 
1080     path.closeSubpath();
1081     p->save();
1082     p->setRenderHint(QPainter::Antialiasing, false);
1083     const QPen border(frameColor, 1.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
1084     p->setPen(border);
1085 
1086     // header
1087     if (!frame) {
1088         QBrush brushSolid(Qt::SolidPattern);
1089 
1090         QColor top = bgColor.darker(250);
1091         top.setAlpha(selected ? 40 : 60);
1092         brushSolid.setColor(top);
1093 
1094         p->setBrush(bgColor);
1095         p->drawPath(path);
1096 
1097         p->setBrush(brushSolid);
1098         p->drawPath(path);
1099         p->restore();
1100 
1101         return;
1102     }
1103 
1104     p->setBrush(bgColor);
1105     p->drawPath(path);
1106     p->restore();
1107 }
1108 
getCategoryColor() const1109 QColor AgendaItem::getCategoryColor() const
1110 {
1111     const QStringList &categories = mIncidence->categories();
1112     if (categories.isEmpty() || !CalendarSupport::KCalPrefs::instance()->hasCategoryColor(categories.first())) {
1113         const auto colorPreference = mEventView->preferences()->agendaViewColors();
1114         if (colorPreference == PrefsBase::CategoryOnly || !mResourceColor.isValid()) {
1115             return CalendarSupport::KCalPrefs::instance()->unsetCategoryColor();
1116         }
1117         return mResourceColor;
1118     }
1119     return CalendarSupport::KCalPrefs::instance()->categoryColor(categories.first());
1120 }
1121 
getFrameColor(const QColor & resourceColor,const QColor & categoryColor) const1122 QColor AgendaItem::getFrameColor(const QColor &resourceColor, const QColor &categoryColor) const
1123 {
1124     const auto colorPreference = mEventView->preferences()->agendaViewColors();
1125     const bool frameDisplaysCategory = (colorPreference == PrefsBase::CategoryOnly || colorPreference == PrefsBase::ResourceInsideCategoryOutside);
1126     return frameDisplaysCategory ? categoryColor : resourceColor;
1127 }
1128 
getBackgroundColor(const QColor & resourceColor,const QColor & categoryColor) const1129 QColor AgendaItem::getBackgroundColor(const QColor &resourceColor, const QColor &categoryColor) const
1130 {
1131     if (CalendarSupport::hasTodo(mIncidence) && !mEventView->preferences()->todosUseCategoryColors()) {
1132         Todo::Ptr todo = CalendarSupport::todo(mIncidence);
1133         Q_ASSERT(todo);
1134         const QDate dueDate = todo->dtDue().toLocalTime().date();
1135         const QDate today = QDate::currentDate();
1136         const QDate occurrenceDate = this->occurrenceDate();
1137         if (todo->isOverdue() && today >= occurrenceDate) {
1138             return mEventView->preferences()->todoOverdueColor();
1139         } else if (dueDate == today && dueDate == occurrenceDate && !todo->isCompleted()) {
1140             return mEventView->preferences()->todoDueTodayColor();
1141         }
1142     }
1143     const auto colorPreference = mEventView->preferences()->agendaViewColors();
1144     const bool bgDisplaysCategory = (colorPreference == PrefsBase::CategoryOnly || colorPreference == PrefsBase::CategoryInsideResourceOutside);
1145     return bgDisplaysCategory ? categoryColor : resourceColor;
1146 }
1147 
eventFilter(QObject * obj,QEvent * event)1148 bool AgendaItem::eventFilter(QObject *obj, QEvent *event)
1149 {
1150     if (event->type() == QEvent::Paint) {
1151         return mValid;
1152     } else {
1153         // standard event processing
1154         return QObject::eventFilter(obj, event);
1155     }
1156 }
1157 
event(QEvent * event)1158 bool AgendaItem::event(QEvent *event)
1159 {
1160     if (event->type() == QEvent::ToolTip) {
1161         if (!mEventView->preferences()->enableToolTips()) {
1162             return true;
1163         } else if (mValid) {
1164             auto helpEvent = static_cast<QHelpEvent *>(event);
1165             QToolTip::showText(helpEvent->globalPos(),
1166                                KCalUtils::IncidenceFormatter::toolTipStr(mCalendar->displayName(mIncidence), mIncidence, occurrenceDate(), true),
1167                                this);
1168         }
1169     }
1170     return QWidget::event(event);
1171 }
1172