File indexing completed on 2024-11-17 04:42:35

0001 /*
0002   SPDX-FileCopyrightText: 2000, 2001, 2003 Cornelius Schumacher <schumacher@kde.org>
0003   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0004 
0005   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0006 */
0007 #include "agendaitem.h"
0008 #include "eventview.h"
0009 #include "helper.h"
0010 
0011 #include "prefs.h"
0012 #include "prefs_base.h" // for enums
0013 
0014 #include <CalendarSupport/KCalPrefs>
0015 #include <CalendarSupport/Utils>
0016 
0017 #include <Akonadi/TagCache>
0018 
0019 #include <KContacts/VCardDrag>
0020 
0021 #include <KCalUtils/ICalDrag>
0022 #include <KCalUtils/IncidenceFormatter>
0023 #include <KCalUtils/VCalDrag>
0024 
0025 #include <KEmailAddress>
0026 
0027 #include <KLocalizedString>
0028 #include <KMessageBox>
0029 #include <KWordWrap>
0030 
0031 #include <QDragEnterEvent>
0032 #include <QLocale>
0033 #include <QMimeData>
0034 #include <QPainter>
0035 #include <QPainterPath>
0036 #include <QPixmapCache>
0037 #include <QToolTip>
0038 
0039 using namespace KCalendarCore;
0040 using namespace EventViews;
0041 
0042 //-----------------------------------------------------------------------------
0043 
0044 QPixmap *AgendaItem::alarmPxmp = nullptr;
0045 QPixmap *AgendaItem::recurPxmp = nullptr;
0046 QPixmap *AgendaItem::readonlyPxmp = nullptr;
0047 QPixmap *AgendaItem::replyPxmp = nullptr;
0048 QPixmap *AgendaItem::groupPxmp = nullptr;
0049 QPixmap *AgendaItem::groupPxmpTent = nullptr;
0050 QPixmap *AgendaItem::organizerPxmp = nullptr;
0051 QPixmap *AgendaItem::eventPxmp = nullptr;
0052 
0053 //-----------------------------------------------------------------------------
0054 
0055 AgendaItem::AgendaItem(EventView *eventView,
0056                        const MultiViewCalendar::Ptr &calendar,
0057                        const KCalendarCore::Incidence::Ptr &item,
0058                        int itemPos,
0059                        int itemCount,
0060                        const QDateTime &qd,
0061                        bool isSelected,
0062                        QWidget *parent)
0063     : QWidget(parent)
0064     , mEventView(eventView)
0065     , mCalendar(calendar)
0066     , mIncidence(item)
0067     , mOccurrenceDateTime(qd)
0068     , mSelected(isSelected)
0069     , mSpecialEvent(false)
0070 {
0071     if (!mIncidence) {
0072         mValid = false;
0073         return;
0074     }
0075 
0076     mIncidence = Incidence::Ptr(mIncidence->clone());
0077     if (mIncidence->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")
0078         || mIncidence->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) {
0079         const int years = EventViews::yearDiff(mIncidence->dtStart().date(), qd.toLocalTime().date());
0080         if (years > 0) {
0081             mIncidence->setReadOnly(false);
0082             mIncidence->setSummary(i18np("%2 (1 year)", "%2 (%1 years)", years, mIncidence->summary()));
0083             mIncidence->setReadOnly(true);
0084             mCloned = true;
0085         }
0086     }
0087 
0088     mLabelText = mIncidence->summary();
0089     mIconAlarm = false;
0090     mIconRecur = false;
0091     mIconReadonly = false;
0092     mIconReply = false;
0093     mIconGroup = false;
0094     mIconGroupTent = false;
0095     mIconOrganizer = false;
0096     mMultiItemInfo = nullptr;
0097     mStartMoveInfo = nullptr;
0098 
0099     mItemPos = itemPos;
0100     mItemCount = itemCount;
0101 
0102     QPalette pal = palette();
0103     pal.setColor(QPalette::Window, Qt::transparent);
0104     setPalette(pal);
0105 
0106     setCellXY(0, 0, 1);
0107     setCellXRight(0);
0108     setMouseTracking(true);
0109     mResourceColor = QColor();
0110     updateIcons();
0111 
0112     setAcceptDrops(true);
0113 }
0114 
0115 AgendaItem::~AgendaItem() = default;
0116 
0117 void AgendaItem::updateIcons()
0118 {
0119     if (!mValid) {
0120         return;
0121     }
0122     mIconReadonly = mIncidence->isReadOnly();
0123     mIconRecur = mIncidence->recurs() || mIncidence->hasRecurrenceId();
0124     mIconAlarm = mIncidence->hasEnabledAlarms();
0125     if (mIncidence->attendeeCount() > 1) {
0126         if (mEventView->kcalPreferences()->thatIsMe(mIncidence->organizer().email())) {
0127             mIconReply = false;
0128             mIconGroup = false;
0129             mIconGroupTent = false;
0130             mIconOrganizer = true;
0131         } else {
0132             KCalendarCore::Attendee me = mIncidence->attendeeByMails(mEventView->kcalPreferences()->allEmails());
0133 
0134             if (!me.isNull()) {
0135                 if (me.status() == KCalendarCore::Attendee::NeedsAction && me.RSVP()) {
0136                     mIconReply = true;
0137                     mIconGroup = false;
0138                     mIconGroupTent = false;
0139                     mIconOrganizer = false;
0140                 } else if (me.status() == KCalendarCore::Attendee::Tentative) {
0141                     mIconReply = false;
0142                     mIconGroup = false;
0143                     mIconGroupTent = true;
0144                     mIconOrganizer = false;
0145                 } else {
0146                     mIconReply = false;
0147                     mIconGroup = true;
0148                     mIconGroupTent = false;
0149                     mIconOrganizer = false;
0150                 }
0151             } else {
0152                 mIconReply = false;
0153                 mIconGroup = true;
0154                 mIconGroupTent = false;
0155                 mIconOrganizer = false;
0156             }
0157         }
0158     }
0159     update();
0160 }
0161 
0162 void AgendaItem::select(bool selected)
0163 {
0164     if (mSelected != selected) {
0165         mSelected = selected;
0166         update();
0167     }
0168 }
0169 
0170 bool AgendaItem::dissociateFromMultiItem()
0171 {
0172     if (!isMultiItem()) {
0173         return false;
0174     }
0175 
0176     AgendaItem::QPtr firstItem = firstMultiItem();
0177     if (firstItem == this) {
0178         firstItem = nextMultiItem();
0179     }
0180 
0181     AgendaItem::QPtr lastItem = lastMultiItem();
0182     if (lastItem == this) {
0183         lastItem = prevMultiItem();
0184     }
0185 
0186     AgendaItem::QPtr prevItem = prevMultiItem();
0187     AgendaItem::QPtr nextItem = nextMultiItem();
0188 
0189     if (prevItem) {
0190         prevItem->setMultiItem(firstItem, prevItem->prevMultiItem(), nextItem, lastItem);
0191     }
0192     if (nextItem) {
0193         nextItem->setMultiItem(firstItem, prevItem, nextItem->prevMultiItem(), lastItem);
0194     }
0195     delete mMultiItemInfo;
0196     mMultiItemInfo = nullptr;
0197     return true;
0198 }
0199 
0200 void AgendaItem::setIncidence(const KCalendarCore::Incidence::Ptr &incidence)
0201 {
0202     mValid = false;
0203     if (incidence) {
0204         mValid = true;
0205         mIncidence = incidence;
0206         mLabelText = mIncidence->summary();
0207         updateIcons();
0208     }
0209 }
0210 
0211 /*
0212   Return height of item in units of agenda cells
0213 */
0214 int AgendaItem::cellHeight() const
0215 {
0216     return mCellYBottom - mCellYTop + 1;
0217 }
0218 
0219 /*
0220   Return height of item in units of agenda cells
0221 */
0222 int AgendaItem::cellWidth() const
0223 {
0224     return mCellXRight - mCellXLeft + 1;
0225 }
0226 
0227 void AgendaItem::setOccurrenceDateTime(const QDateTime &qd)
0228 {
0229     mOccurrenceDateTime = qd;
0230 }
0231 
0232 QDate AgendaItem::occurrenceDate() const
0233 {
0234     return mOccurrenceDateTime.toLocalTime().date();
0235 }
0236 
0237 void AgendaItem::setCellXY(int X, int YTop, int YBottom)
0238 {
0239     mCellXLeft = X;
0240     mCellYTop = YTop;
0241     mCellYBottom = YBottom;
0242 }
0243 
0244 void AgendaItem::setCellXRight(int XRight)
0245 {
0246     mCellXRight = XRight;
0247 }
0248 
0249 void AgendaItem::setCellX(int XLeft, int XRight)
0250 {
0251     mCellXLeft = XLeft;
0252     mCellXRight = XRight;
0253 }
0254 
0255 void AgendaItem::setCellY(int YTop, int YBottom)
0256 {
0257     mCellYTop = YTop;
0258     mCellYBottom = YBottom;
0259 }
0260 
0261 void AgendaItem::setMultiItem(const AgendaItem::QPtr &first, const AgendaItem::QPtr &prev, const AgendaItem::QPtr &next, const AgendaItem::QPtr &last)
0262 {
0263     if (!mMultiItemInfo) {
0264         mMultiItemInfo = new MultiItemInfo;
0265     }
0266     mMultiItemInfo->mFirstMultiItem = first;
0267     mMultiItemInfo->mPrevMultiItem = prev;
0268     mMultiItemInfo->mNextMultiItem = next;
0269     mMultiItemInfo->mLastMultiItem = last;
0270 }
0271 
0272 bool AgendaItem::isMultiItem() const
0273 {
0274     return mMultiItemInfo;
0275 }
0276 
0277 AgendaItem::QPtr AgendaItem::prependMoveItem(const AgendaItem::QPtr &e)
0278 {
0279     if (!e) {
0280         return nullptr;
0281     }
0282 
0283     AgendaItem::QPtr first = nullptr;
0284     AgendaItem::QPtr last = nullptr;
0285     if (isMultiItem()) {
0286         first = mMultiItemInfo->mFirstMultiItem;
0287         last = mMultiItemInfo->mLastMultiItem;
0288     }
0289     if (!first) {
0290         first = this;
0291     }
0292     if (!last) {
0293         last = this;
0294     }
0295 
0296     e->setMultiItem(nullptr, nullptr, first, last);
0297     first->setMultiItem(e, e, first->nextMultiItem(), first->lastMultiItem());
0298 
0299     AgendaItem::QPtr tmp = first->nextMultiItem();
0300     while (tmp) {
0301         tmp->setMultiItem(e, tmp->prevMultiItem(), tmp->nextMultiItem(), tmp->lastMultiItem());
0302         tmp = tmp->nextMultiItem();
0303     }
0304 
0305     if (mStartMoveInfo && !e->moveInfo()) {
0306         e->mStartMoveInfo = new MultiItemInfo(*mStartMoveInfo);
0307         //    e->moveInfo()->mFirstMultiItem = moveInfo()->mFirstMultiItem;
0308         //    e->moveInfo()->mLastMultiItem = moveInfo()->mLastMultiItem;
0309         e->moveInfo()->mPrevMultiItem = nullptr;
0310         e->moveInfo()->mNextMultiItem = first;
0311     }
0312 
0313     if (first && first->moveInfo()) {
0314         first->moveInfo()->mPrevMultiItem = e;
0315     }
0316     return e;
0317 }
0318 
0319 AgendaItem::QPtr AgendaItem::appendMoveItem(const AgendaItem::QPtr &e)
0320 {
0321     if (!e) {
0322         return nullptr;
0323     }
0324 
0325     AgendaItem::QPtr first = nullptr;
0326     AgendaItem::QPtr last = nullptr;
0327     if (isMultiItem()) {
0328         first = mMultiItemInfo->mFirstMultiItem;
0329         last = mMultiItemInfo->mLastMultiItem;
0330     }
0331     if (!first) {
0332         first = this;
0333     }
0334     if (!last) {
0335         last = this;
0336     }
0337 
0338     e->setMultiItem(first, last, nullptr, nullptr);
0339     AgendaItem::QPtr tmp = first;
0340 
0341     while (tmp) {
0342         tmp->setMultiItem(tmp->firstMultiItem(), tmp->prevMultiItem(), tmp->nextMultiItem(), e);
0343         tmp = tmp->nextMultiItem();
0344     }
0345     last->setMultiItem(last->firstMultiItem(), last->prevMultiItem(), e, e);
0346 
0347     if (mStartMoveInfo && !e->moveInfo()) {
0348         e->mStartMoveInfo = new MultiItemInfo(*mStartMoveInfo);
0349         //    e->moveInfo()->mFirstMultiItem = moveInfo()->mFirstMultiItem;
0350         //    e->moveInfo()->mLastMultiItem = moveInfo()->mLastMultiItem;
0351         e->moveInfo()->mPrevMultiItem = last;
0352         e->moveInfo()->mNextMultiItem = nullptr;
0353     }
0354     if (last && last->moveInfo()) {
0355         last->moveInfo()->mNextMultiItem = e;
0356     }
0357     return e;
0358 }
0359 
0360 AgendaItem::QPtr AgendaItem::removeMoveItem(const AgendaItem::QPtr &e)
0361 {
0362     if (isMultiItem()) {
0363         AgendaItem::QPtr first = mMultiItemInfo->mFirstMultiItem;
0364         AgendaItem::QPtr next;
0365         AgendaItem::QPtr prev;
0366         AgendaItem::QPtr last = mMultiItemInfo->mLastMultiItem;
0367         if (!first) {
0368             first = this;
0369         }
0370         if (!last) {
0371             last = this;
0372         }
0373         if (first == e) {
0374             first = first->nextMultiItem();
0375             first->setMultiItem(nullptr, nullptr, first->nextMultiItem(), first->lastMultiItem());
0376         }
0377         if (last == e) {
0378             last = last->prevMultiItem();
0379             last->setMultiItem(last->firstMultiItem(), last->prevMultiItem(), nullptr, nullptr);
0380         }
0381 
0382         AgendaItem::QPtr tmp = first;
0383         if (first == last) {
0384             delete mMultiItemInfo;
0385             tmp = nullptr;
0386             mMultiItemInfo = nullptr;
0387         }
0388         while (tmp) {
0389             next = tmp->nextMultiItem();
0390             prev = tmp->prevMultiItem();
0391             if (e == next) {
0392                 next = next->nextMultiItem();
0393             }
0394             if (e == prev) {
0395                 prev = prev->prevMultiItem();
0396             }
0397             tmp->setMultiItem((tmp == first) ? nullptr : first, (tmp == prev) ? nullptr : prev, (tmp == next) ? nullptr : next, (tmp == last) ? nullptr : last);
0398             tmp = tmp->nextMultiItem();
0399         }
0400     }
0401 
0402     return e;
0403 }
0404 
0405 void AgendaItem::startMove()
0406 {
0407     AgendaItem::QPtr first = this;
0408     if (isMultiItem() && mMultiItemInfo->mFirstMultiItem) {
0409         first = mMultiItemInfo->mFirstMultiItem;
0410     }
0411     first->startMovePrivate();
0412 }
0413 
0414 void AgendaItem::startMovePrivate()
0415 {
0416     mStartMoveInfo = new MultiItemInfo;
0417     mStartMoveInfo->mStartCellXLeft = mCellXLeft;
0418     mStartMoveInfo->mStartCellXRight = mCellXRight;
0419     mStartMoveInfo->mStartCellYTop = mCellYTop;
0420     mStartMoveInfo->mStartCellYBottom = mCellYBottom;
0421     if (mMultiItemInfo) {
0422         mStartMoveInfo->mFirstMultiItem = mMultiItemInfo->mFirstMultiItem;
0423         mStartMoveInfo->mLastMultiItem = mMultiItemInfo->mLastMultiItem;
0424         mStartMoveInfo->mPrevMultiItem = mMultiItemInfo->mPrevMultiItem;
0425         mStartMoveInfo->mNextMultiItem = mMultiItemInfo->mNextMultiItem;
0426     } else {
0427         mStartMoveInfo->mFirstMultiItem = nullptr;
0428         mStartMoveInfo->mLastMultiItem = nullptr;
0429         mStartMoveInfo->mPrevMultiItem = nullptr;
0430         mStartMoveInfo->mNextMultiItem = nullptr;
0431     }
0432     if (isMultiItem() && mMultiItemInfo->mNextMultiItem) {
0433         mMultiItemInfo->mNextMultiItem->startMovePrivate();
0434     }
0435 }
0436 
0437 void AgendaItem::resetMove()
0438 {
0439     if (mStartMoveInfo) {
0440         if (mStartMoveInfo->mFirstMultiItem) {
0441             mStartMoveInfo->mFirstMultiItem->resetMovePrivate();
0442         } else {
0443             resetMovePrivate();
0444         }
0445     }
0446 }
0447 
0448 void AgendaItem::resetMovePrivate()
0449 {
0450     if (mStartMoveInfo) {
0451         mCellXLeft = mStartMoveInfo->mStartCellXLeft;
0452         mCellXRight = mStartMoveInfo->mStartCellXRight;
0453         mCellYTop = mStartMoveInfo->mStartCellYTop;
0454         mCellYBottom = mStartMoveInfo->mStartCellYBottom;
0455 
0456         // if we don't have mMultiItemInfo, the item didn't span two days before,
0457         // and wasn't moved over midnight, either, so we don't have to reset
0458         // anything. Otherwise, restore from mMoveItemInfo
0459         if (mMultiItemInfo) {
0460             // It was already a multi-day info
0461             mMultiItemInfo->mFirstMultiItem = mStartMoveInfo->mFirstMultiItem;
0462             mMultiItemInfo->mPrevMultiItem = mStartMoveInfo->mPrevMultiItem;
0463             mMultiItemInfo->mNextMultiItem = mStartMoveInfo->mNextMultiItem;
0464             mMultiItemInfo->mLastMultiItem = mStartMoveInfo->mLastMultiItem;
0465 
0466             if (!mStartMoveInfo->mFirstMultiItem) {
0467                 // This was the first multi-item when the move started, delete all previous
0468                 AgendaItem::QPtr toDel = mStartMoveInfo->mPrevMultiItem;
0469                 AgendaItem::QPtr nowDel = nullptr;
0470                 while (toDel) {
0471                     nowDel = toDel;
0472                     if (nowDel->moveInfo()) {
0473                         toDel = nowDel->moveInfo()->mPrevMultiItem;
0474                     }
0475                     Q_EMIT removeAgendaItem(nowDel);
0476                 }
0477                 mMultiItemInfo->mFirstMultiItem = nullptr;
0478                 mMultiItemInfo->mPrevMultiItem = nullptr;
0479             }
0480             if (!mStartMoveInfo->mLastMultiItem) {
0481                 // This was the last multi-item when the move started, delete all next
0482                 AgendaItem::QPtr toDel = mStartMoveInfo->mNextMultiItem;
0483                 AgendaItem::QPtr nowDel = nullptr;
0484                 while (toDel) {
0485                     nowDel = toDel;
0486                     if (nowDel->moveInfo()) {
0487                         toDel = nowDel->moveInfo()->mNextMultiItem;
0488                     }
0489                     Q_EMIT removeAgendaItem(nowDel);
0490                 }
0491                 mMultiItemInfo->mLastMultiItem = nullptr;
0492                 mMultiItemInfo->mNextMultiItem = nullptr;
0493             }
0494 
0495             if (mStartMoveInfo->mFirstMultiItem == nullptr && mStartMoveInfo->mLastMultiItem == nullptr) {
0496                 // it was a single-day event before we started the move.
0497                 delete mMultiItemInfo;
0498                 mMultiItemInfo = nullptr;
0499             }
0500         }
0501         delete mStartMoveInfo;
0502         mStartMoveInfo = nullptr;
0503     }
0504     Q_EMIT showAgendaItem(this);
0505     if (nextMultiItem()) {
0506         nextMultiItem()->resetMovePrivate();
0507     }
0508 }
0509 
0510 void AgendaItem::endMove()
0511 {
0512     AgendaItem::QPtr first = firstMultiItem();
0513     if (!first) {
0514         first = this;
0515     }
0516     first->endMovePrivate();
0517 }
0518 
0519 void AgendaItem::endMovePrivate()
0520 {
0521     if (mStartMoveInfo) {
0522         // if first, delete all previous
0523         if (!firstMultiItem() || firstMultiItem() == this) {
0524             AgendaItem::QPtr toDel = mStartMoveInfo->mPrevMultiItem;
0525             AgendaItem::QPtr nowDel = nullptr;
0526             while (toDel) {
0527                 nowDel = toDel;
0528                 if (nowDel->moveInfo()) {
0529                     toDel = nowDel->moveInfo()->mPrevMultiItem;
0530                 }
0531                 Q_EMIT removeAgendaItem(nowDel);
0532             }
0533         }
0534         // if last, delete all next
0535         if (!lastMultiItem() || lastMultiItem() == this) {
0536             AgendaItem::QPtr toDel = mStartMoveInfo->mNextMultiItem;
0537             AgendaItem::QPtr nowDel = nullptr;
0538             while (toDel) {
0539                 nowDel = toDel;
0540                 if (nowDel->moveInfo()) {
0541                     toDel = nowDel->moveInfo()->mNextMultiItem;
0542                 }
0543                 Q_EMIT removeAgendaItem(nowDel);
0544             }
0545         }
0546         // also delete the moving info
0547         delete mStartMoveInfo;
0548         mStartMoveInfo = nullptr;
0549         if (nextMultiItem()) {
0550             nextMultiItem()->endMovePrivate();
0551         }
0552     }
0553 }
0554 
0555 void AgendaItem::moveRelative(int dx, int dy)
0556 {
0557     int newXLeft = cellXLeft() + dx;
0558     int newXRight = cellXRight() + dx;
0559     int newYTop = cellYTop() + dy;
0560     int newYBottom = cellYBottom() + dy;
0561     setCellXY(newXLeft, newYTop, newYBottom);
0562     setCellXRight(newXRight);
0563 }
0564 
0565 void AgendaItem::expandTop(int dy, const bool allowOverLimit)
0566 {
0567     int newYTop = cellYTop() + dy;
0568     int newYBottom = cellYBottom();
0569     if (newYTop > newYBottom && !allowOverLimit) {
0570         newYTop = newYBottom;
0571     }
0572     setCellY(newYTop, newYBottom);
0573 }
0574 
0575 void AgendaItem::expandBottom(int dy)
0576 {
0577     int newYTop = cellYTop();
0578     int newYBottom = cellYBottom() + dy;
0579     if (newYBottom < newYTop) {
0580         newYBottom = newYTop;
0581     }
0582     setCellY(newYTop, newYBottom);
0583 }
0584 
0585 void AgendaItem::expandLeft(int dx)
0586 {
0587     int newXLeft = cellXLeft() + dx;
0588     int newXRight = cellXRight();
0589     if (newXLeft > newXRight) {
0590         newXLeft = newXRight;
0591     }
0592     setCellX(newXLeft, newXRight);
0593 }
0594 
0595 void AgendaItem::expandRight(int dx)
0596 {
0597     int newXLeft = cellXLeft();
0598     int newXRight = cellXRight() + dx;
0599     if (newXRight < newXLeft) {
0600         newXRight = newXLeft;
0601     }
0602     setCellX(newXLeft, newXRight);
0603 }
0604 
0605 void AgendaItem::dragEnterEvent(QDragEnterEvent *e)
0606 {
0607     const QMimeData *md = e->mimeData();
0608     if (KCalUtils::ICalDrag::canDecode(md) || KCalUtils::VCalDrag::canDecode(md)) {
0609         // TODO: Allow dragging events/todos onto other events to create a relation
0610         e->ignore();
0611         return;
0612     }
0613     if (KContacts::VCardDrag::canDecode(md) || md->hasText()) {
0614         e->accept();
0615     } else {
0616         e->ignore();
0617     }
0618 }
0619 
0620 void AgendaItem::addAttendee(const QString &newAttendee)
0621 {
0622     if (!mValid) {
0623         return;
0624     }
0625 
0626     QString name;
0627     QString email;
0628     KEmailAddress::extractEmailAddressAndName(newAttendee, email, name);
0629     if (!(name.isEmpty() && email.isEmpty())) {
0630         mIncidence->addAttendee(KCalendarCore::Attendee(name, email));
0631         KMessageBox::information(this,
0632                                  i18n("Attendee \"%1\" added to the calendar item \"%2\"", KEmailAddress::normalizedAddress(name, email, QString()), text()),
0633                                  i18nc("@title:window", "Attendee added"),
0634                                  QStringLiteral("AttendeeDroppedAdded"));
0635     }
0636 }
0637 
0638 void AgendaItem::dropEvent(QDropEvent *e)
0639 {
0640     // TODO: Organize this better: First check for attachment
0641     // (not only file, also any other url!), then if it's a vcard,
0642     // otherwise check for attendees, then if the data is binary,
0643     // add a binary attachment.
0644     if (!mValid) {
0645         return;
0646     }
0647 
0648     const QMimeData *md = e->mimeData();
0649 
0650     bool decoded = md->hasText();
0651     QString text = md->text();
0652     if (decoded && text.startsWith(QLatin1StringView("file:"))) {
0653         mIncidence->addAttachment(KCalendarCore::Attachment(text));
0654         return;
0655     }
0656 
0657     KContacts::Addressee::List list;
0658 
0659     if (KContacts::VCardDrag::fromMimeData(md, list)) {
0660         for (const KContacts::Addressee &addressee : std::as_const(list)) {
0661             QString em(addressee.fullEmail());
0662             if (em.isEmpty()) {
0663                 em = addressee.realName();
0664             }
0665             addAttendee(em);
0666         }
0667     }
0668 }
0669 
0670 QList<AgendaItem::QPtr> &AgendaItem::conflictItems()
0671 {
0672     return mConflictItems;
0673 }
0674 
0675 void AgendaItem::setConflictItems(const QList<AgendaItem::QPtr> &ci)
0676 {
0677     mConflictItems = ci;
0678     for (QList<AgendaItem::QPtr>::iterator it = mConflictItems.begin(), end(mConflictItems.end()); it != end; ++it) {
0679         (*it)->addConflictItem(this);
0680     }
0681 }
0682 
0683 void AgendaItem::addConflictItem(const AgendaItem::QPtr &ci)
0684 {
0685     if (!mConflictItems.contains(ci)) {
0686         mConflictItems.append(ci);
0687     }
0688 }
0689 
0690 QString AgendaItem::label() const
0691 {
0692     return mLabelText;
0693 }
0694 
0695 bool AgendaItem::overlaps(CellItem *o) const
0696 {
0697     AgendaItem::QPtr other = static_cast<AgendaItem *>(o);
0698 
0699     if (cellXLeft() <= other->cellXRight() && cellXRight() >= other->cellXLeft()) {
0700         if ((cellYTop() <= other->cellYBottom()) && (cellYBottom() >= other->cellYTop())) {
0701             return true;
0702         }
0703     }
0704 
0705     return false;
0706 }
0707 
0708 static void conditionalPaint(QPainter *p, bool condition, int &x, int y, int ft, const QPixmap &pxmp)
0709 {
0710     if (condition) {
0711         p->drawPixmap(x, y, pxmp);
0712         x += pxmp.width() + ft;
0713     }
0714 }
0715 
0716 void AgendaItem::paintIcon(QPainter *p, int &x, int y, int ft)
0717 {
0718     QString iconName;
0719     if (mIncidence->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) {
0720         mSpecialEvent = true;
0721         iconName = QStringLiteral("view-calendar-wedding-anniversary");
0722     } else if (mIncidence->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")) {
0723         mSpecialEvent = true;
0724         // We don't draw icon. The icon is drawn already, because it's the Akonadi::Collection's icon
0725     }
0726 
0727     conditionalPaint(p, !iconName.isEmpty(), x, y, ft, cachedSmallIcon(iconName));
0728 }
0729 
0730 void AgendaItem::paintIcons(QPainter *p, int &x, int y, int ft)
0731 {
0732     if (!mEventView->preferences()->enableAgendaItemIcons()) {
0733         return;
0734     }
0735 
0736     paintIcon(p, x, y, ft);
0737 
0738     QSet<EventView::ItemIcon> icons = mEventView->preferences()->agendaViewIcons();
0739 
0740     if (icons.contains(EventViews::EventView::CalendarCustomIcon)) {
0741         const QString iconName = mCalendar->iconForIncidence(mIncidence);
0742         if (!iconName.isEmpty() && iconName != QLatin1StringView("view-calendar") && iconName != QLatin1StringView("office-calendar")) {
0743             conditionalPaint(p, true, x, y, ft, QIcon::fromTheme(iconName).pixmap(16, 16));
0744         }
0745     }
0746 
0747     const bool isTodo = mIncidence && mIncidence->type() == Incidence::TypeTodo;
0748 
0749     if (isTodo && icons.contains(EventViews::EventView::TaskIcon)) {
0750         const QString iconName = mIncidence->iconName(mOccurrenceDateTime.toLocalTime());
0751         conditionalPaint(p, !mSpecialEvent, x, y, ft, QIcon::fromTheme(iconName).pixmap(16, 16));
0752     }
0753 
0754     if (icons.contains(EventView::RecurringIcon)) {
0755         conditionalPaint(p, mIconRecur && !mSpecialEvent, x, y, ft, *recurPxmp);
0756     }
0757 
0758     if (icons.contains(EventView::ReminderIcon)) {
0759         conditionalPaint(p, mIconAlarm && !mSpecialEvent, x, y, ft, *alarmPxmp);
0760     }
0761 
0762     if (icons.contains(EventView::ReadOnlyIcon)) {
0763         conditionalPaint(p, mIconReadonly && !mSpecialEvent, x, y, ft, *readonlyPxmp);
0764     }
0765 
0766     if (icons.contains(EventView::ReplyIcon)) {
0767         conditionalPaint(p, mIconReply, x, y, ft, *replyPxmp);
0768     }
0769 
0770     if (icons.contains(EventView::AttendingIcon)) {
0771         conditionalPaint(p, mIconGroup, x, y, ft, *groupPxmp);
0772     }
0773 
0774     if (icons.contains(EventView::TentativeIcon)) {
0775         conditionalPaint(p, mIconGroupTent, x, y, ft, *groupPxmpTent);
0776     }
0777 
0778     if (icons.contains(EventView::OrganizerIcon)) {
0779         conditionalPaint(p, mIconOrganizer, x, y, ft, *organizerPxmp);
0780     }
0781 }
0782 
0783 void AgendaItem::paintEvent(QPaintEvent *ev)
0784 {
0785     if (!mValid) {
0786         return;
0787     }
0788 
0789     QRect visRect = visibleRegion().boundingRect();
0790     // when scrolling horizontally in the side-by-side view, the repainted area is clipped
0791     // to the newly visible area, which is a problem since the content changes when visRect
0792     // changes, so repaint the full item in that case
0793     if (ev->rect() != visRect && visRect.isValid() && ev->rect().isValid()) {
0794         update(visRect);
0795         return;
0796     }
0797 
0798     QPainter p(this);
0799     p.setRenderHint(QPainter::Antialiasing);
0800     const int fmargin = 0; // frame margin
0801     const int ft = 1; // frame thickness for layout, see drawRoundedRect(),
0802     // keep multiple of 2
0803     const int margin = 5 + ft + fmargin; // frame + space between frame and content
0804 
0805     // General idea is to always show the icons (even in the all-day events).
0806     // This creates a consistent feeling for the user when the view mode
0807     // changes and therefore the available width changes.
0808     // Also look at #17984
0809 
0810     if (!alarmPxmp) {
0811         alarmPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("task-reminder")).pixmap(16, 16));
0812         recurPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("appointment-recurring")).pixmap(16, 16));
0813         readonlyPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("object-locked")).pixmap(16, 16));
0814         replyPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-reply-sender")).pixmap(16, 16));
0815         groupPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-attending")).pixmap(16, 16));
0816         groupPxmpTent = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-attending-tentative")).pixmap(16, 16));
0817         organizerPxmp = new QPixmap(QIcon::fromTheme(QStringLiteral("meeting-organizer")).pixmap(16, 16));
0818     }
0819 
0820     const auto categoryColor = getCategoryColor();
0821     const auto resourceColor = mResourceColor.isValid() ? mResourceColor : categoryColor;
0822     const auto frameColor = getFrameColor(resourceColor, categoryColor);
0823     const auto bgBaseColor = getBackgroundColor(resourceColor, categoryColor);
0824     const auto bgColor = mSelected ? bgBaseColor.lighter(EventView::BRIGHTNESS_FACTOR) : bgBaseColor;
0825     const auto textColor = EventViews::getTextColor(bgColor);
0826 
0827     p.setPen(textColor);
0828 
0829     p.setFont(mEventView->preferences()->agendaViewFont());
0830     QFontMetrics fm = p.fontMetrics();
0831 
0832     const int singleLineHeight = fm.boundingRect(mLabelText).height();
0833 
0834     const bool roundTop = !prevMultiItem();
0835     const bool roundBottom = !nextMultiItem();
0836 
0837     drawRoundedRect(&p,
0838                     QRect(fmargin, fmargin, width() - fmargin * 2, height() - fmargin * 2),
0839                     mSelected,
0840                     bgColor,
0841                     frameColor,
0842                     true,
0843                     ft,
0844                     roundTop,
0845                     roundBottom);
0846 
0847     // calculate the height of the full version (case 4) to test whether it is
0848     // possible
0849 
0850     QString shortH;
0851     QString longH;
0852     if (!isMultiItem()) {
0853         shortH = QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleDisplayStart).toLocalTime().time(), QLocale::ShortFormat);
0854 
0855         if (CalendarSupport::hasEvent(mIncidence)) {
0856             longH =
0857                 i18n("%1 - %2", shortH, QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().time(), QLocale::ShortFormat));
0858         } else {
0859             longH = shortH;
0860         }
0861     } else if (!mMultiItemInfo->mFirstMultiItem) {
0862         shortH = QLocale().toString(mIncidence->dtStart().toLocalTime().time(), QLocale::ShortFormat);
0863         longH = shortH;
0864     } else {
0865         shortH = QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().time(), QLocale::ShortFormat);
0866         longH = i18n("- %1", shortH);
0867     }
0868 
0869     KWordWrap ww = KWordWrap::formatText(fm, QRect(0, 0, width() - (2 * margin), -1), 0, mLabelText);
0870     int th = ww.boundingRect().height();
0871 
0872     int hlHeight =
0873         qMax(fm.boundingRect(longH).height(),
0874              qMax(alarmPxmp->height(),
0875                   qMax(recurPxmp->height(), qMax(readonlyPxmp->height(), qMax(replyPxmp->height(), qMax(groupPxmp->height(), organizerPxmp->height()))))));
0876 
0877     const bool completelyRenderable = th < (height() - 2 * ft - 2 - hlHeight);
0878 
0879     // case 1: do not draw text when not even a single line fits
0880     // Don't do this any more, always try to print out the text.
0881     // Even if it's just a few pixel, one can still guess the whole
0882     // text from just four pixels' height!
0883     if ( //( singleLineHeight > height() - 4 ) ||
0884         (width() < 16)) {
0885         int x = qRound((width() - 16) / 2.0);
0886         paintIcon(&p, x /*by-ref*/, margin, ft);
0887         return;
0888     }
0889 
0890     // case 2: draw a single line when no more space
0891     if ((2 * singleLineHeight) > (height() - 2 * margin)) {
0892         int x = margin;
0893         int txtWidth;
0894 
0895         if (mIncidence->allDay()) {
0896             x += visRect.left();
0897             const int y = qRound((height() - 16) / 2.0);
0898             paintIcons(&p, x, y, ft);
0899             txtWidth = visRect.right() - margin - x;
0900         } else {
0901             const int y = qRound((height() - 16) / 2.0);
0902             paintIcons(&p, x, y, ft);
0903             txtWidth = width() - margin - x;
0904         }
0905 
0906         const int y = ((height() - singleLineHeight) / 2) + fm.ascent();
0907         KWordWrap::drawFadeoutText(&p, x, y, txtWidth, mLabelText);
0908         return;
0909     }
0910 
0911     // case 3: enough for 2-5 lines, but not for the header.
0912     //         Also used for the middle days in multi-events
0913     if (((!completelyRenderable) && ((height() - (2 * margin)) <= (5 * singleLineHeight)))
0914         || (isMultiItem() && mMultiItemInfo->mNextMultiItem && mMultiItemInfo->mFirstMultiItem)) {
0915         int x = margin;
0916         int txtWidth;
0917 
0918         if (mIncidence->allDay()) {
0919             x += visRect.left();
0920             paintIcons(&p, x, margin, ft);
0921             txtWidth = visRect.right() - margin - x;
0922         } else {
0923             paintIcons(&p, x, margin, ft);
0924             txtWidth = width() - margin - x;
0925         }
0926 
0927         ww = KWordWrap::formatText(fm, QRect(0, 0, txtWidth, (height() - (2 * margin))), 0, mLabelText);
0928 
0929         ww.drawText(&p, x, margin, Qt::AlignHCenter | KWordWrap::FadeOut);
0930         return;
0931     }
0932 
0933     // case 4: paint everything, with header:
0934     // consists of (vertically) ft + headline&icons + ft + text + margin
0935     int y = 2 * ft + hlHeight;
0936     if (completelyRenderable) {
0937         y += (height() - (2 * ft) - margin - hlHeight - th) / 2;
0938     }
0939 
0940     int x = margin;
0941     int txtWidth;
0942     int hTxtWidth;
0943     int eventX;
0944 
0945     if (mIncidence->allDay()) {
0946         shortH.clear();
0947         longH.clear();
0948 
0949         if (const KCalendarCore::Event::Ptr event = CalendarSupport::event(mIncidence)) {
0950             if (event->isMultiDay(QTimeZone::systemTimeZone())) {
0951                 // multi-day, all-day event
0952                 shortH = i18n("%1 - %2",
0953                               QLocale().toString(mIncidence->dtStart().toLocalTime().date()),
0954                               QLocale().toString(mIncidence->dateTime(KCalendarCore::Incidence::RoleEnd).toLocalTime().date()));
0955                 longH = shortH;
0956 
0957                 // paint headline
0958                 drawRoundedRect(&p,
0959                                 QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
0960                                 mSelected,
0961                                 frameColor,
0962                                 frameColor,
0963                                 false,
0964                                 ft,
0965                                 roundTop,
0966                                 false);
0967             } else {
0968                 // single-day, all-day event
0969 
0970                 // paint headline
0971                 drawRoundedRect(&p,
0972                                 QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
0973                                 mSelected,
0974                                 frameColor,
0975                                 frameColor,
0976                                 false,
0977                                 ft,
0978                                 roundTop,
0979                                 false);
0980             }
0981         } else {
0982             // to-do
0983 
0984             // paint headline
0985             drawRoundedRect(&p,
0986                             QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
0987                             mSelected,
0988                             frameColor,
0989                             frameColor,
0990                             false,
0991                             ft,
0992                             roundTop,
0993                             false);
0994         }
0995 
0996         x += visRect.left();
0997         eventX = x;
0998         txtWidth = visRect.right() - margin - x;
0999         paintIcons(&p, x, margin / 2, ft);
1000         hTxtWidth = visRect.right() - margin - x;
1001     } else {
1002         // paint headline
1003         drawRoundedRect(&p,
1004                         QRect(fmargin, fmargin, width() - fmargin * 2, -fmargin * 2 + margin + hlHeight),
1005                         mSelected,
1006                         frameColor,
1007                         frameColor,
1008                         false,
1009                         ft,
1010                         roundTop,
1011                         false);
1012 
1013         txtWidth = width() - margin - x;
1014         eventX = x;
1015         paintIcons(&p, x, margin / 2, ft);
1016         hTxtWidth = width() - margin - x;
1017     }
1018 
1019     QString headline;
1020     int hw = fm.boundingRect(longH).width();
1021     if (hw > hTxtWidth) {
1022         headline = shortH;
1023         hw = fm.boundingRect(shortH).width();
1024         if (hw < txtWidth) {
1025             x += (hTxtWidth - hw) / 2;
1026         }
1027     } else {
1028         headline = longH;
1029         x += (hTxtWidth - hw) / 2;
1030     }
1031     p.setBackground(QBrush(frameColor));
1032     p.setPen(EventViews::getTextColor(frameColor));
1033     KWordWrap::drawFadeoutText(&p, x, (margin + hlHeight + fm.ascent()) / 2 - 2, hTxtWidth, headline);
1034 
1035     // draw event text
1036     ww = KWordWrap::formatText(fm, QRect(0, 0, txtWidth, height() - margin - y), 0, mLabelText);
1037 
1038     p.setBackground(QBrush(bgColor));
1039     p.setPen(textColor);
1040     QString ws = ww.wrappedString();
1041     if (QStringView(ws).left(ws.length() - 1).indexOf(QLatin1Char('\n')) >= 0) {
1042         ww.drawText(&p, eventX, y, Qt::AlignLeft | KWordWrap::FadeOut);
1043     } else {
1044         ww.drawText(&p, eventX + (txtWidth - ww.boundingRect().width() - 2 * margin) / 2, y, Qt::AlignHCenter | KWordWrap::FadeOut);
1045     }
1046 }
1047 
1048 void AgendaItem::drawRoundedRect(QPainter *p,
1049                                  QRect rect,
1050                                  bool selected,
1051                                  const QColor &bgColor,
1052                                  const QColor &frameColor,
1053                                  bool frame,
1054                                  int ft,
1055                                  bool roundTop,
1056                                  bool roundBottom)
1057 {
1058     Q_UNUSED(ft)
1059     if (!mValid) {
1060         return;
1061     }
1062 
1063     QPainterPath path;
1064 
1065     const int RECT_MARGIN = 2;
1066     const int RADIUS = 2; // absolute radius
1067 
1068     const QRect rectWithMargin(rect.x() + RECT_MARGIN, rect.y() + RECT_MARGIN, rect.width() - 2 * RECT_MARGIN, rect.height() - 2 * RECT_MARGIN);
1069 
1070     const QPoint pointLeftTop(rectWithMargin.x(), rectWithMargin.y());
1071     const QPoint pointRightTop(rectWithMargin.x() + rectWithMargin.width(), rectWithMargin.y());
1072     const QPoint pointLeftBottom(rectWithMargin.x(), rectWithMargin.y() + rectWithMargin.height());
1073     const QPoint pointRightBottom(rectWithMargin.x() + rectWithMargin.width(), rectWithMargin.y() + rectWithMargin.height());
1074 
1075     if (!roundTop && !roundBottom) {
1076         path.addRect(rectWithMargin);
1077     } else if (roundTop && roundBottom) {
1078         path.addRoundedRect(rectWithMargin, RADIUS, RADIUS, Qt::AbsoluteSize);
1079     } else if (roundTop) {
1080         path.moveTo(pointRightBottom);
1081         path.lineTo(pointLeftBottom);
1082         path.lineTo(QPoint(pointLeftTop.x(), pointLeftTop.y() + RADIUS));
1083         path.quadTo(pointLeftTop, QPoint(pointLeftTop.x() + RADIUS, pointLeftTop.y()));
1084         path.lineTo(QPoint(pointRightTop.x() - RADIUS, pointRightTop.y()));
1085         path.quadTo(pointRightTop, QPoint(pointRightTop.x(), pointRightTop.y() + RADIUS));
1086         path.lineTo(pointRightBottom);
1087     } else if (roundBottom) {
1088         path.moveTo(pointRightTop);
1089         path.lineTo(QPoint(pointRightBottom.x(), pointRightBottom.y() - RADIUS));
1090         path.quadTo(pointRightBottom, QPoint(pointRightBottom.x() - RADIUS, pointRightBottom.y()));
1091         path.lineTo(QPoint(pointLeftBottom.x() + RADIUS, pointLeftBottom.y()));
1092         path.quadTo(pointLeftBottom, QPoint(pointLeftBottom.x(), pointLeftBottom.y() - RADIUS));
1093         path.lineTo(pointLeftTop);
1094         path.lineTo(pointRightTop);
1095     }
1096 
1097     path.closeSubpath();
1098     p->save();
1099     p->setRenderHint(QPainter::Antialiasing, false);
1100     const QPen border(frameColor, 1.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
1101     p->setPen(border);
1102 
1103     // header
1104     if (!frame) {
1105         QBrush brushSolid(Qt::SolidPattern);
1106 
1107         QColor top = bgColor.darker(250);
1108         top.setAlpha(selected ? 40 : 60);
1109         brushSolid.setColor(top);
1110 
1111         p->setBrush(bgColor);
1112         p->drawPath(path);
1113 
1114         p->setBrush(brushSolid);
1115         p->drawPath(path);
1116         p->restore();
1117 
1118         return;
1119     }
1120 
1121     p->setBrush(bgColor);
1122     p->drawPath(path);
1123     p->restore();
1124 }
1125 
1126 QColor AgendaItem::getCategoryColor() const
1127 {
1128     const QStringList &categories = mIncidence->categories();
1129     if (categories.isEmpty() || !Akonadi::TagCache::instance()->tagColor(categories.first()).isValid()) {
1130         const auto colorPreference = mEventView->preferences()->agendaViewColors();
1131         if (colorPreference == PrefsBase::CategoryOnly || !mResourceColor.isValid()) {
1132             return CalendarSupport::KCalPrefs::instance()->unsetCategoryColor();
1133         }
1134         return mResourceColor;
1135     }
1136     return Akonadi::TagCache::instance()->tagColor(categories.first());
1137 }
1138 
1139 QColor AgendaItem::getFrameColor(const QColor &resourceColor, const QColor &categoryColor) const
1140 {
1141     const auto colorPreference = mEventView->preferences()->agendaViewColors();
1142     const bool frameDisplaysCategory = (colorPreference == PrefsBase::CategoryOnly || colorPreference == PrefsBase::ResourceInsideCategoryOutside);
1143     return frameDisplaysCategory ? categoryColor : resourceColor;
1144 }
1145 
1146 QColor AgendaItem::getBackgroundColor(const QColor &resourceColor, const QColor &categoryColor) const
1147 {
1148     if (CalendarSupport::hasTodo(mIncidence) && !mEventView->preferences()->todosUseCategoryColors()) {
1149         Todo::Ptr todo = CalendarSupport::todo(mIncidence);
1150         Q_ASSERT(todo);
1151         const QDate dueDate = todo->dtDue().toLocalTime().date();
1152         const QDate today = QDate::currentDate();
1153         const QDate occurrenceDate = this->occurrenceDate();
1154         if (todo->isOverdue() && today >= occurrenceDate) {
1155             return mEventView->preferences()->todoOverdueColor();
1156         } else if (dueDate == today && dueDate == occurrenceDate && !todo->isCompleted()) {
1157             return mEventView->preferences()->todoDueTodayColor();
1158         }
1159     }
1160     const auto colorPreference = mEventView->preferences()->agendaViewColors();
1161     const bool bgDisplaysCategory = (colorPreference == PrefsBase::CategoryOnly || colorPreference == PrefsBase::CategoryInsideResourceOutside);
1162     return bgDisplaysCategory ? categoryColor : resourceColor;
1163 }
1164 
1165 bool AgendaItem::eventFilter(QObject *obj, QEvent *event)
1166 {
1167     if (event->type() == QEvent::Paint) {
1168         return mValid;
1169     } else {
1170         // standard event processing
1171         return QObject::eventFilter(obj, event);
1172     }
1173 }
1174 
1175 bool AgendaItem::event(QEvent *event)
1176 {
1177     if (event->type() == QEvent::ToolTip) {
1178         if (!mEventView->preferences()->enableToolTips()) {
1179             return true;
1180         } else if (mValid) {
1181             auto helpEvent = static_cast<QHelpEvent *>(event);
1182             QToolTip::showText(helpEvent->globalPos(),
1183                                KCalUtils::IncidenceFormatter::toolTipStr(mCalendar->displayName(mIncidence), mIncidence, occurrenceDate(), true),
1184                                this);
1185         }
1186     }
1187     return QWidget::event(event);
1188 }
1189 
1190 #include "moc_agendaitem.cpp"