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