File indexing completed on 2024-05-05 12:10:45

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0005   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0006 
0007   SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 /**
0010   @file
0011   This file is part of the API for handling calendar data and
0012   defines the Incidence class.
0013 
0014   @brief
0015   Provides the class common to non-FreeBusy (Events, To-dos, Journals)
0016   calendar components known as incidences.
0017 
0018   @author Cornelius Schumacher \<schumacher@kde.org\>
0019   @author Reinhold Kainhofer \<reinhold@kainhofer.com\>
0020 */
0021 
0022 #include "incidence.h"
0023 #include "incidence_p.h"
0024 #include "calformat.h"
0025 #include "kcalendarcore_debug.h"
0026 #include "utils_p.h"
0027 
0028 #include <math.h>
0029 
0030 #include <QStringList>
0031 #include <QTextDocument> // for .toHtmlEscaped() and Qt::mightBeRichText()
0032 #include <QTime>
0033 #include <QTimeZone>
0034 
0035 using namespace KCalendarCore;
0036 
0037 IncidencePrivate::IncidencePrivate() = default;
0038 
0039 IncidencePrivate::IncidencePrivate(const IncidencePrivate &p)
0040     : IncidenceBasePrivate(p)
0041     , mCreated(p.mCreated)
0042     , mDescription(p.mDescription)
0043     , mSummary(p.mSummary)
0044     , mLocation(p.mLocation)
0045     , mCategories(p.mCategories)
0046     , mResources(p.mResources)
0047     , mStatusString(p.mStatusString)
0048     , mSchedulingID(p.mSchedulingID)
0049     , mRelatedToUid(p.mRelatedToUid)
0050     , mRecurrenceId(p.mRecurrenceId)
0051     , mConferences(p.mConferences)
0052     , mGeoLatitude(p.mGeoLatitude)
0053     , mGeoLongitude(p.mGeoLongitude)
0054     , mRecurrence(nullptr)
0055     , mRevision(p.mRevision)
0056     , mPriority(p.mPriority)
0057     , mStatus(p.mStatus)
0058     , mSecrecy(p.mSecrecy)
0059     , mColor(p.mColor)
0060     , mDescriptionIsRich(p.mDescriptionIsRich)
0061     , mSummaryIsRich(p.mSummaryIsRich)
0062     , mLocationIsRich(p.mLocationIsRich)
0063     , mThisAndFuture(p.mThisAndFuture)
0064     , mLocalOnly(false)
0065 {
0066 }
0067 
0068 IncidencePrivate::IncidencePrivate(const Incidence &other)
0069     : IncidencePrivate(*other.d_func())
0070 {
0071 }
0072 
0073 void IncidencePrivate::clear()
0074 {
0075     mAlarms.clear();
0076     mAttachments.clear();
0077     delete mRecurrence;
0078     mRecurrence = nullptr;
0079 }
0080 
0081 void IncidencePrivate::init(Incidence *q, const IncidencePrivate &other)
0082 {
0083     mRevision = other.mRevision;
0084     mCreated = other.mCreated;
0085     mDescription = other.mDescription;
0086     mDescriptionIsRich = other.mDescriptionIsRich;
0087     mSummary = other.mSummary;
0088     mSummaryIsRich = other.mSummaryIsRich;
0089     mCategories = other.mCategories;
0090     mRelatedToUid = other.mRelatedToUid;
0091     mResources = other.mResources;
0092     mStatusString = other.mStatusString;
0093     mStatus = other.mStatus;
0094     mSecrecy = other.mSecrecy;
0095     mPriority = other.mPriority;
0096     mLocation = other.mLocation;
0097     mLocationIsRich = other.mLocationIsRich;
0098     mGeoLatitude = other.mGeoLatitude;
0099     mGeoLongitude = other.mGeoLongitude;
0100     mRecurrenceId = other.mRecurrenceId;
0101     mConferences = other.mConferences;
0102     mThisAndFuture = other.mThisAndFuture;
0103     mLocalOnly = other.mLocalOnly;
0104     mColor = other.mColor;
0105 
0106     // Alarms and Attachments are stored in ListBase<...>, which is a QValueList<...*>.
0107     // We need to really duplicate the objects stored therein, otherwise deleting
0108     // i will also delete all attachments from this object (setAutoDelete...)
0109     mAlarms.reserve(other.mAlarms.count());
0110     for (const Alarm::Ptr &alarm : std::as_const(other.mAlarms)) {
0111         Alarm::Ptr b(new Alarm(*alarm.data()));
0112         b->setParent(q);
0113         mAlarms.append(b);
0114     }
0115 
0116     mAttachments = other.mAttachments;
0117     if (other.mRecurrence) {
0118         mRecurrence = new Recurrence(*(other.mRecurrence));
0119         mRecurrence->addObserver(q);
0120     } else {
0121         mRecurrence = nullptr;
0122     }
0123 }
0124 
0125 bool IncidencePrivate::validStatus(Incidence::Status status)
0126 {
0127     return status == Incidence::StatusNone;
0128 }
0129 //@endcond
0130 
0131 #if KCALENDARCORE_BUILD_DEPRECATED_SINCE(5, 91)
0132 Incidence::Incidence()
0133     : IncidenceBase(new IncidencePrivate)
0134 {
0135     recreate();
0136     resetDirtyFields();
0137 }
0138 
0139 Incidence::Incidence(const Incidence &i)
0140     : IncidenceBase(i, new IncidencePrivate(*(i.d_func())))
0141     , Recurrence::RecurrenceObserver()
0142 {
0143     Q_D(Incidence);
0144     d->init(this, *(i.d_func()));
0145     resetDirtyFields();
0146 }
0147 #endif
0148 
0149 Incidence::Incidence(IncidencePrivate *p)
0150     : IncidenceBase(p)
0151 {
0152     recreate();
0153     resetDirtyFields();
0154 }
0155 
0156 Incidence::Incidence(const Incidence &other, IncidencePrivate *p)
0157     : IncidenceBase(other, p)
0158     , Recurrence::RecurrenceObserver()
0159 {
0160     Q_D(Incidence);
0161     d->init(this, *(other.d_func()));
0162     resetDirtyFields();
0163 }
0164 
0165 Incidence::~Incidence()
0166 {
0167     // Alarm has a raw incidence pointer, so we must set it to 0
0168     // so Alarm doesn't use it after Incidence is destroyed
0169     Q_D(const Incidence);
0170     for (const Alarm::Ptr &alarm : std::as_const(d->mAlarms)) {
0171         alarm->setParent(nullptr);
0172     }
0173     delete d->mRecurrence;
0174 }
0175 
0176 //@cond PRIVATE
0177 // A string comparison that considers that null and empty are the same
0178 static bool stringCompare(const QString &s1, const QString &s2)
0179 {
0180     return (s1.isEmpty() && s2.isEmpty()) || (s1 == s2);
0181 }
0182 
0183 //@endcond
0184 IncidenceBase &Incidence::assign(const IncidenceBase &other)
0185 {
0186     Q_D(Incidence);
0187     if (&other != this) {
0188         d->clear();
0189         // TODO: should relations be cleared out, as in destructor???
0190         IncidenceBase::assign(other);
0191         const Incidence *i = static_cast<const Incidence *>(&other);
0192         d->init(this, *(i->d_func()));
0193     }
0194 
0195     return *this;
0196 }
0197 
0198 bool Incidence::equals(const IncidenceBase &incidence) const
0199 {
0200     if (!IncidenceBase::equals(incidence)) {
0201         return false;
0202     }
0203 
0204     // If they weren't the same type IncidenceBase::equals would had returned false already
0205     const Incidence *i2 = static_cast<const Incidence *>(&incidence);
0206 
0207     const Alarm::List alarmList = alarms();
0208     const Alarm::List otherAlarmsList = i2->alarms();
0209     if (alarmList.count() != otherAlarmsList.count()) {
0210         return false;
0211     }
0212 
0213     auto matchFunc = [](const Alarm::Ptr &a, const Alarm::Ptr &b) {
0214         return *a == *b;
0215     };
0216 
0217     const auto [it1, it2] = std::mismatch(alarmList.cbegin(), alarmList.cend(), otherAlarmsList.cbegin(), otherAlarmsList.cend(), matchFunc);
0218     // Checking the iterator from one list only, since both lists are the same size
0219     if (it1 != alarmList.cend()) {
0220         return false;
0221     }
0222 
0223     const Attachment::List attachmentList = attachments();
0224     const Attachment::List otherAttachmentList = i2->attachments();
0225     if (attachmentList.count() != otherAttachmentList.count()) {
0226         return false;
0227     }
0228 
0229     const auto [at1, at2] = std::mismatch(attachmentList.cbegin(), attachmentList.cend(), otherAttachmentList.cbegin(), otherAttachmentList.cend());
0230 
0231     // Checking the iterator from one list only, since both lists are the same size
0232     if (at1 != attachmentList.cend()) {
0233         return false;
0234     }
0235 
0236     Q_D(const Incidence);
0237     bool recurrenceEqual = (d->mRecurrence == nullptr && i2->d_func()->mRecurrence == nullptr);
0238     if (!recurrenceEqual) {
0239         recurrence(); // create if doesn't exist
0240         i2->recurrence(); // create if doesn't exist
0241         recurrenceEqual = d->mRecurrence != nullptr && i2->d_func()->mRecurrence != nullptr
0242             && *d->mRecurrence == *i2->d_func()->mRecurrence;
0243     }
0244 
0245     if (!qFuzzyCompare(d->mGeoLatitude, i2->d_func()->mGeoLatitude) || !qFuzzyCompare(d->mGeoLongitude, i2->d_func()->mGeoLongitude)) {
0246         return false;
0247     }
0248     // clang-format off
0249     return
0250         recurrenceEqual
0251         && created() == i2->created()
0252         && stringCompare(description(), i2->description())
0253         && descriptionIsRich() == i2->descriptionIsRich()
0254         && stringCompare(summary(), i2->summary())
0255         && summaryIsRich() == i2->summaryIsRich()
0256         && categories() == i2->categories()
0257         && stringCompare(relatedTo(), i2->relatedTo())
0258         && resources() == i2->resources()
0259         && d->mStatus == i2->d_func()->mStatus
0260         && (d->mStatus == StatusNone || stringCompare(d->mStatusString, i2->d_func()->mStatusString))
0261         && secrecy() == i2->secrecy()
0262         && priority() == i2->priority()
0263         && stringCompare(location(), i2->location())
0264         && locationIsRich() == i2->locationIsRich()
0265         && stringCompare(color(), i2->color())
0266         && stringCompare(schedulingID(), i2->schedulingID())
0267         && recurrenceId() == i2->recurrenceId()
0268         && conferences() == i2->conferences()
0269         && thisAndFuture() == i2->thisAndFuture();
0270     // clang-format on
0271 }
0272 
0273 QString Incidence::instanceIdentifier() const
0274 {
0275     if (hasRecurrenceId()) {
0276         return uid() + recurrenceId().toString(Qt::ISODate);
0277     }
0278     return uid();
0279 }
0280 
0281 void Incidence::recreate()
0282 {
0283     const QDateTime nowUTC = QDateTime::currentDateTimeUtc();
0284     setCreated(nowUTC);
0285 
0286     setSchedulingID(QString(), CalFormat::createUniqueId());
0287     setRevision(0);
0288     setLastModified(nowUTC); // NOLINT false clang-analyzer-optin.cplusplus.VirtualCall
0289 }
0290 
0291 void Incidence::setLastModified(const QDateTime &lm)
0292 {
0293     Q_D(const Incidence);
0294     if (!d->mLocalOnly) {
0295         IncidenceBase::setLastModified(lm);
0296     }
0297 }
0298 
0299 void Incidence::setReadOnly(bool readOnly)
0300 {
0301     Q_D(const Incidence);
0302     IncidenceBase::setReadOnly(readOnly);
0303     if (d->mRecurrence) {
0304         d->mRecurrence->setRecurReadOnly(readOnly);
0305     }
0306 }
0307 
0308 void Incidence::setLocalOnly(bool localOnly)
0309 {
0310     if (mReadOnly) {
0311         return;
0312     }
0313     Q_D(Incidence);
0314     d->mLocalOnly = localOnly;
0315 }
0316 
0317 bool Incidence::localOnly() const
0318 {
0319     Q_D(const Incidence);
0320     return d->mLocalOnly;
0321 }
0322 
0323 void Incidence::setAllDay(bool allDay)
0324 {
0325     if (mReadOnly) {
0326         return;
0327     }
0328     Q_D(const Incidence);
0329     if (d->mRecurrence) {
0330         d->mRecurrence->setAllDay(allDay);
0331     }
0332     IncidenceBase::setAllDay(allDay);
0333 }
0334 
0335 void Incidence::setCreated(const QDateTime &created)
0336 {
0337     Q_D(Incidence);
0338     if (mReadOnly || d->mLocalOnly) {
0339         return;
0340     }
0341 
0342     update();
0343     d->mCreated = created.toUTC();
0344     const auto ct = d->mCreated.time();
0345     // Remove milliseconds
0346     d->mCreated.setTime(QTime(ct.hour(), ct.minute(), ct.second()));
0347     setFieldDirty(FieldCreated);
0348     updated();
0349 }
0350 
0351 QDateTime Incidence::created() const
0352 {
0353     Q_D(const Incidence);
0354     return d->mCreated;
0355 }
0356 
0357 void Incidence::setRevision(int rev)
0358 {
0359     Q_D(Incidence);
0360     if (mReadOnly || d->mLocalOnly) {
0361         return;
0362     }
0363 
0364     update();
0365     d->mRevision = rev;
0366     setFieldDirty(FieldRevision);
0367     updated();
0368 }
0369 
0370 int Incidence::revision() const
0371 {
0372     Q_D(const Incidence);
0373     return d->mRevision;
0374 }
0375 
0376 void Incidence::setDtStart(const QDateTime &dt)
0377 {
0378     Q_D(const Incidence);
0379     IncidenceBase::setDtStart(dt);
0380     if (d->mRecurrence && dirtyFields().contains(FieldDtStart)) {
0381         d->mRecurrence->setStartDateTime(dt, allDay());
0382     }
0383 }
0384 
0385 void Incidence::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone)
0386 {
0387     Q_D(const Incidence);
0388     IncidenceBase::shiftTimes(oldZone, newZone);
0389     if (d->mRecurrence) {
0390         d->mRecurrence->shiftTimes(oldZone, newZone);
0391     }
0392     if (d->mAlarms.count() > 0) {
0393         update();
0394         for (auto alarm : d->mAlarms) {
0395             alarm->shiftTimes(oldZone, newZone);
0396         }
0397         setFieldDirty(FieldAlarms);
0398         updated();
0399     }
0400 }
0401 
0402 void Incidence::setDescription(const QString &description, bool isRich)
0403 {
0404     if (mReadOnly) {
0405         return;
0406     }
0407     update();
0408     Q_D(Incidence);
0409     d->mDescription = description;
0410     d->mDescriptionIsRich = isRich;
0411     setFieldDirty(FieldDescription);
0412     updated();
0413 }
0414 
0415 void Incidence::setDescription(const QString &description)
0416 {
0417     setDescription(description, Qt::mightBeRichText(description));
0418 }
0419 
0420 QString Incidence::description() const
0421 {
0422     Q_D(const Incidence);
0423     return d->mDescription;
0424 }
0425 
0426 QString Incidence::richDescription() const
0427 {
0428     Q_D(const Incidence);
0429     if (descriptionIsRich()) {
0430         return d->mDescription;
0431     } else {
0432         return d->mDescription.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
0433     }
0434 }
0435 
0436 bool Incidence::descriptionIsRich() const
0437 {
0438     Q_D(const Incidence);
0439     return d->mDescriptionIsRich;
0440 }
0441 
0442 void Incidence::setSummary(const QString &summary, bool isRich)
0443 {
0444     if (mReadOnly) {
0445         return;
0446     }
0447     Q_D(Incidence);
0448     if (d->mSummary != summary || d->mSummaryIsRich != isRich) {
0449         update();
0450         d->mSummary = summary;
0451         d->mSummaryIsRich = isRich;
0452         setFieldDirty(FieldSummary);
0453         updated();
0454     }
0455 }
0456 
0457 void Incidence::setSummary(const QString &summary)
0458 {
0459     setSummary(summary, Qt::mightBeRichText(summary));
0460 }
0461 
0462 QString Incidence::summary() const
0463 {
0464     Q_D(const Incidence);
0465     return d->mSummary;
0466 }
0467 
0468 QString Incidence::richSummary() const
0469 {
0470     Q_D(const Incidence);
0471     if (summaryIsRich()) {
0472         return d->mSummary;
0473     } else {
0474         return d->mSummary.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
0475     }
0476 }
0477 
0478 bool Incidence::summaryIsRich() const
0479 {
0480     Q_D(const Incidence);
0481     return d->mSummaryIsRich;
0482 }
0483 
0484 void Incidence::setCategories(const QStringList &categories)
0485 {
0486     if (mReadOnly) {
0487         return;
0488     }
0489 
0490     Q_D(Incidence);
0491     update();
0492     d->mCategories = categories;
0493     setFieldDirty(FieldCategories);
0494     updated();
0495 }
0496 
0497 void Incidence::setCategories(const QString &catStr)
0498 {
0499     if (mReadOnly) {
0500         return;
0501     }
0502     update();
0503     setFieldDirty(FieldCategories);
0504 
0505     Q_D(Incidence);
0506     d->mCategories.clear();
0507 
0508     if (catStr.isEmpty()) {
0509         updated();
0510         return;
0511     }
0512 
0513     d->mCategories = catStr.split(QLatin1Char(','));
0514 
0515     for (auto &category : d->mCategories) {
0516         category = category.trimmed();
0517     }
0518 
0519     updated();
0520 }
0521 
0522 QStringList Incidence::categories() const
0523 {
0524     Q_D(const Incidence);
0525     return d->mCategories;
0526 }
0527 
0528 QString Incidence::categoriesStr() const
0529 {
0530     Q_D(const Incidence);
0531     return d->mCategories.join(QLatin1Char(','));
0532 }
0533 
0534 void Incidence::setRelatedTo(const QString &relatedToUid, RelType relType)
0535 {
0536     // TODO: RFC says that an incidence can have more than one related-to field
0537     // even for the same relType.
0538 
0539     Q_D(Incidence);
0540     if (d->mRelatedToUid[relType] != relatedToUid) {
0541         update();
0542         d->mRelatedToUid[relType] = relatedToUid;
0543         setFieldDirty(FieldRelatedTo);
0544         updated();
0545     }
0546 }
0547 
0548 QString Incidence::relatedTo(RelType relType) const
0549 {
0550     Q_D(const Incidence);
0551     return d->mRelatedToUid.value(relType);
0552 }
0553 
0554 void Incidence::setColor(const QString &colorName)
0555 {
0556     if (mReadOnly) {
0557         return;
0558     }
0559     Q_D(Incidence);
0560     if (!stringCompare(d->mColor, colorName)) {
0561         update();
0562         d->mColor = colorName;
0563         setFieldDirty(FieldColor);
0564         updated();
0565     }
0566 }
0567 
0568 QString Incidence::color() const
0569 {
0570     Q_D(const Incidence);
0571     return d->mColor;
0572 }
0573 
0574 // %%%%%%%%%%%%  Recurrence-related methods %%%%%%%%%%%%%%%%%%%%
0575 
0576 Recurrence *Incidence::recurrence() const
0577 {
0578     Q_D(const Incidence);
0579     if (!d->mRecurrence) {
0580         d->mRecurrence = new Recurrence();
0581         d->mRecurrence->setStartDateTime(dateTime(RoleRecurrenceStart), allDay());
0582         d->mRecurrence->setAllDay(allDay());
0583         d->mRecurrence->setRecurReadOnly(mReadOnly);
0584         d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
0585     }
0586 
0587     return d->mRecurrence;
0588 }
0589 
0590 void Incidence::clearRecurrence()
0591 {
0592     Q_D(const Incidence);
0593     delete d->mRecurrence;
0594     d->mRecurrence = nullptr;
0595 }
0596 
0597 ushort Incidence::recurrenceType() const
0598 {
0599     Q_D(const Incidence);
0600     if (d->mRecurrence) {
0601         return d->mRecurrence->recurrenceType();
0602     } else {
0603         return Recurrence::rNone;
0604     }
0605 }
0606 
0607 bool Incidence::recurs() const
0608 {
0609     Q_D(const Incidence);
0610     if (d->mRecurrence) {
0611         return d->mRecurrence->recurs();
0612     } else {
0613         return false;
0614     }
0615 }
0616 
0617 bool Incidence::recursOn(const QDate &date, const QTimeZone &timeZone) const
0618 {
0619     Q_D(const Incidence);
0620     return d->mRecurrence && d->mRecurrence->recursOn(date, timeZone);
0621 }
0622 
0623 bool Incidence::recursAt(const QDateTime &qdt) const
0624 {
0625     Q_D(const Incidence);
0626     return d->mRecurrence && d->mRecurrence->recursAt(qdt);
0627 }
0628 
0629 QList<QDateTime> Incidence::startDateTimesForDate(const QDate &date, const QTimeZone &timeZone) const
0630 {
0631     QList<QDateTime> result;
0632     if (!date.isValid()) {
0633         qCWarning(KCALCORE_LOG) << "Invalid date encountered";
0634         return result;
0635     }
0636 
0637     QDateTime start = dtStart();
0638     QDateTime end = dateTime(RoleEndRecurrenceBase);
0639 
0640     // TODO_Recurrence: Also work if only due date is given...
0641     if (!start.isValid() && !end.isValid()) {
0642         return result;
0643     }
0644 
0645     // if the incidence doesn't recur,
0646     QDateTime kdate(date, {}, timeZone);
0647     if (!recurs()) {
0648         if (start.date() <= date && end.date() >= date) {
0649             result << start;
0650         }
0651         return result;
0652     }
0653 
0654     qint64 days = start.daysTo(end);
0655     // Account for possible recurrences going over midnight, while the original event doesn't
0656     QDate tmpday(date.addDays(-days - 1));
0657     QDateTime tmp;
0658     while (tmpday <= date) {
0659         if (recurrence()->recursOn(tmpday, timeZone)) {
0660             const QList<QTime> times = recurrence()->recurTimesOn(tmpday, timeZone);
0661             for (const QTime &time : times) {
0662                 tmp = QDateTime(tmpday, time, start.timeZone());
0663                 if (endDateForStart(tmp) >= kdate) {
0664                     result << tmp;
0665                 }
0666             }
0667         }
0668         tmpday = tmpday.addDays(1);
0669     }
0670     return result;
0671 }
0672 
0673 QList<QDateTime> Incidence::startDateTimesForDateTime(const QDateTime &datetime) const
0674 {
0675     QList<QDateTime> result;
0676     if (!datetime.isValid()) {
0677         qCWarning(KCALCORE_LOG) << "Invalid datetime encountered";
0678         return result;
0679     }
0680 
0681     QDateTime start = dtStart();
0682     QDateTime end = dateTime(RoleEndRecurrenceBase);
0683 
0684     // TODO_Recurrence: Also work if only due date is given...
0685     if (!start.isValid() && !end.isValid()) {
0686         return result;
0687     }
0688 
0689     // if the incidence doesn't recur,
0690     if (!recurs()) {
0691         if (!(start > datetime || end < datetime)) {
0692             result << start;
0693         }
0694         return result;
0695     }
0696 
0697     qint64 days = start.daysTo(end);
0698     // Account for possible recurrences going over midnight, while the original event doesn't
0699     QDate tmpday(datetime.date().addDays(-days - 1));
0700     QDateTime tmp;
0701     while (tmpday <= datetime.date()) {
0702         if (recurrence()->recursOn(tmpday, datetime.timeZone())) {
0703             // Get the times during the day (in start date's time zone) when recurrences happen
0704             const QList<QTime> times = recurrence()->recurTimesOn(tmpday, start.timeZone());
0705             for (const QTime &time : times) {
0706                 tmp = QDateTime(tmpday, time, start.timeZone());
0707                 if (!(tmp > datetime || endDateForStart(tmp) < datetime)) {
0708                     result << tmp;
0709                 }
0710             }
0711         }
0712         tmpday = tmpday.addDays(1);
0713     }
0714     return result;
0715 }
0716 
0717 QDateTime Incidence::endDateForStart(const QDateTime &startDt) const
0718 {
0719     QDateTime start = dtStart();
0720     QDateTime end = dateTime(RoleEndRecurrenceBase);
0721     if (!end.isValid()) {
0722         return start;
0723     }
0724     if (!start.isValid()) {
0725         return end;
0726     }
0727 
0728     return startDt.addSecs(start.secsTo(end));
0729 }
0730 
0731 void Incidence::addAttachment(const Attachment &attachment)
0732 {
0733     if (mReadOnly || attachment.isEmpty()) {
0734         return;
0735     }
0736 
0737     Q_D(Incidence);
0738     update();
0739     d->mAttachments.append(attachment);
0740     setFieldDirty(FieldAttachment);
0741     updated();
0742 }
0743 
0744 void Incidence::deleteAttachments(const QString &mime)
0745 {
0746     Q_D(Incidence);
0747     auto it = std::remove_if(d->mAttachments.begin(), d->mAttachments.end(), [&mime](const Attachment &a) {
0748         return a.mimeType() == mime;
0749     });
0750     if (it != d->mAttachments.end()) {
0751         update();
0752         d->mAttachments.erase(it, d->mAttachments.end());
0753         setFieldDirty(FieldAttachment);
0754         updated();
0755     }
0756 }
0757 
0758 Attachment::List Incidence::attachments() const
0759 {
0760     Q_D(const Incidence);
0761     return d->mAttachments;
0762 }
0763 
0764 Attachment::List Incidence::attachments(const QString &mime) const
0765 {
0766     Q_D(const Incidence);
0767     Attachment::List attachments;
0768     for (const Attachment &attachment : std::as_const(d->mAttachments)) {
0769         if (attachment.mimeType() == mime) {
0770             attachments.append(attachment);
0771         }
0772     }
0773     return attachments;
0774 }
0775 
0776 void Incidence::clearAttachments()
0777 {
0778     Q_D(Incidence);
0779     update();
0780     setFieldDirty(FieldAttachment);
0781     d->mAttachments.clear();
0782     updated();
0783 }
0784 
0785 void Incidence::setResources(const QStringList &resources)
0786 {
0787     if (mReadOnly) {
0788         return;
0789     }
0790 
0791     update();
0792     Q_D(Incidence);
0793     d->mResources = resources;
0794     setFieldDirty(FieldResources);
0795     updated();
0796 }
0797 
0798 QStringList Incidence::resources() const
0799 {
0800     Q_D(const Incidence);
0801     return d->mResources;
0802 }
0803 
0804 void Incidence::setPriority(int priority)
0805 {
0806     if (mReadOnly) {
0807         return;
0808     }
0809 
0810     if (priority < 0 || priority > 9) {
0811         qCWarning(KCALCORE_LOG)  << "Ignoring invalid priority" << priority;
0812         return;
0813     }
0814 
0815     update();
0816     Q_D(Incidence);
0817     d->mPriority = priority;
0818     setFieldDirty(FieldPriority);
0819     updated();
0820 }
0821 
0822 int Incidence::priority() const
0823 {
0824     Q_D(const Incidence);
0825     return d->mPriority;
0826 }
0827 
0828 void Incidence::setStatus(Incidence::Status status)
0829 {
0830     if (mReadOnly) {
0831         qCWarning(KCALCORE_LOG)  << "Attempt to set status of read-only incidence";
0832         return;
0833     }
0834 
0835     Q_D(Incidence);
0836     if (d->validStatus(status)) {
0837         update();
0838         d->mStatus = status;
0839         d->mStatusString.clear();
0840         setFieldDirty(FieldStatus);
0841         updated();
0842     } else {
0843         qCWarning(KCALCORE_LOG)  << "Ignoring invalid status" << status << "for" << typeStr();
0844     }
0845 }
0846 
0847 void Incidence::setCustomStatus(const QString &status)
0848 {
0849     if (mReadOnly) {
0850         return;
0851     }
0852 
0853     update();
0854     Q_D(Incidence);
0855     d->mStatus = status.isEmpty() ? StatusNone : StatusX;
0856     d->mStatusString = status;
0857     setFieldDirty(FieldStatus);
0858     updated();
0859 }
0860 
0861 Incidence::Status Incidence::status() const
0862 {
0863     Q_D(const Incidence);
0864     return d->mStatus;
0865 }
0866 
0867 QString Incidence::customStatus() const
0868 {
0869     Q_D(const Incidence);
0870     if (d->mStatus == StatusX) {
0871         return d->mStatusString;
0872     } else {
0873         return QString();
0874     }
0875 }
0876 
0877 void Incidence::setSecrecy(Incidence::Secrecy secrecy)
0878 {
0879     if (mReadOnly) {
0880         return;
0881     }
0882 
0883     update();
0884     Q_D(Incidence);
0885     d->mSecrecy = secrecy;
0886     setFieldDirty(FieldSecrecy);
0887     updated();
0888 }
0889 
0890 Incidence::Secrecy Incidence::secrecy() const
0891 {
0892     Q_D(const Incidence);
0893     return d->mSecrecy;
0894 }
0895 
0896 Alarm::List Incidence::alarms() const
0897 {
0898     Q_D(const Incidence);
0899     return d->mAlarms;
0900 }
0901 
0902 Alarm::Ptr Incidence::newAlarm()
0903 {
0904     Q_D(Incidence);
0905     Alarm::Ptr alarm(new Alarm(this));
0906     addAlarm(alarm);
0907     return alarm;
0908 }
0909 
0910 void Incidence::addAlarm(const Alarm::Ptr &alarm)
0911 {
0912     Q_D(Incidence);
0913     update();
0914     d->mAlarms.append(alarm);
0915     setFieldDirty(FieldAlarms);
0916     updated();
0917 }
0918 
0919 void Incidence::removeAlarm(const Alarm::Ptr &alarm)
0920 {
0921     Q_D(Incidence);
0922     const int index = d->mAlarms.indexOf(alarm);
0923     if (index > -1) {
0924         update();
0925         d->mAlarms.remove(index);
0926         setFieldDirty(FieldAlarms);
0927         updated();
0928     }
0929 }
0930 
0931 void Incidence::clearAlarms()
0932 {
0933     update();
0934     Q_D(Incidence);
0935     d->mAlarms.clear();
0936     setFieldDirty(FieldAlarms);
0937     updated();
0938 }
0939 
0940 bool Incidence::hasEnabledAlarms() const
0941 {
0942     Q_D(const Incidence);
0943     return std::any_of(d->mAlarms.cbegin(), d->mAlarms.cend(), [](const Alarm::Ptr &alarm) {
0944         return alarm->enabled();
0945     });
0946 }
0947 
0948 Conference::List Incidence::conferences() const
0949 {
0950     Q_D(const Incidence);
0951     return d->mConferences;
0952 }
0953 
0954 void Incidence::addConference(const Conference &conference)
0955 {
0956     update();
0957     Q_D(Incidence);
0958     d->mConferences.push_back(conference);
0959     setFieldDirty(FieldConferences);
0960     updated();
0961 }
0962 
0963 void Incidence::setConferences(const Conference::List &conferences)
0964 {
0965     update();
0966     Q_D(Incidence);
0967     d->mConferences = conferences;
0968     setFieldDirty(FieldConferences);
0969     updated();
0970 }
0971 
0972 void Incidence::clearConferences()
0973 {
0974     update();
0975     Q_D(Incidence);
0976     d->mConferences.clear();
0977     setFieldDirty(FieldConferences);
0978     updated();
0979 }
0980 
0981 void Incidence::setLocation(const QString &location, bool isRich)
0982 {
0983     if (mReadOnly) {
0984         return;
0985     }
0986 
0987     Q_D(Incidence);
0988     if (d->mLocation != location || d->mLocationIsRich != isRich) {
0989         update();
0990         d->mLocation = location;
0991         d->mLocationIsRich = isRich;
0992         setFieldDirty(FieldLocation);
0993         updated();
0994     }
0995 }
0996 
0997 void Incidence::setLocation(const QString &location)
0998 {
0999     setLocation(location, Qt::mightBeRichText(location));
1000 }
1001 
1002 QString Incidence::location() const
1003 {
1004     Q_D(const Incidence);
1005     return d->mLocation;
1006 }
1007 
1008 QString Incidence::richLocation() const
1009 {
1010     Q_D(const Incidence);
1011     if (locationIsRich()) {
1012         return d->mLocation;
1013     } else {
1014         return d->mLocation.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
1015     }
1016 }
1017 
1018 bool Incidence::locationIsRich() const
1019 {
1020     Q_D(const Incidence);
1021     return d->mLocationIsRich;
1022 }
1023 
1024 void Incidence::setSchedulingID(const QString &sid, const QString &uid)
1025 {
1026     if (!uid.isEmpty()) {
1027         setUid(uid);
1028     }
1029     Q_D(Incidence);
1030     if (sid != d->mSchedulingID) {
1031         update();
1032         d->mSchedulingID = sid;
1033         setFieldDirty(FieldSchedulingId);
1034         updated();
1035     }
1036 }
1037 
1038 QString Incidence::schedulingID() const
1039 {
1040     Q_D(const Incidence);
1041     if (d->mSchedulingID.isNull()) {
1042         // Nothing set, so use the normal uid
1043         return uid();
1044     }
1045     return d->mSchedulingID;
1046 }
1047 
1048 bool Incidence::hasGeo() const
1049 {
1050     Q_D(const Incidence);
1051     // For internal consistency, return false if either coordinate is invalid.
1052     return d->mGeoLatitude != INVALID_LATLON && d->mGeoLongitude != INVALID_LATLON;
1053 }
1054 #if KCALENDARCORE_BUILD_DEPRECATED_SINCE(5, 89)
1055 void Incidence::setHasGeo(bool hasGeo)
1056 {
1057     if (mReadOnly) {
1058         return;
1059     }
1060 
1061     Q_D(Incidence);
1062     if (!hasGeo) {
1063         update();
1064         d->mGeoLatitude = INVALID_LATLON;
1065         d->mGeoLongitude = INVALID_LATLON;
1066         setFieldDirty(FieldGeoLatitude);
1067         setFieldDirty(FieldGeoLongitude);
1068         updated();
1069     }
1070     // If hasGeo is true, the caller should set latitude and longitude to legal values..
1071 }
1072 #endif
1073 float Incidence::geoLatitude() const
1074 {
1075     Q_D(const Incidence);
1076     // For internal consistency, both coordinates are considered invalid if either is.
1077     return (INVALID_LATLON == d->mGeoLongitude) ? INVALID_LATLON : d->mGeoLatitude;
1078 }
1079 
1080 void Incidence::setGeoLatitude(float geolatitude)
1081 {
1082     if (mReadOnly) {
1083         return;
1084     }
1085 
1086     if (isnan(geolatitude)) {
1087         geolatitude = INVALID_LATLON;
1088     }
1089     if (geolatitude != INVALID_LATLON && (geolatitude < -90.0 || geolatitude > 90.0)) {
1090         qCWarning(KCALCORE_LOG) << "Ignoring invalid  latitude" << geolatitude;
1091         return;
1092     }
1093 
1094     Q_D(Incidence);
1095     update();
1096     d->mGeoLatitude = geolatitude;
1097     setFieldDirty(FieldGeoLatitude);
1098     updated();
1099 }
1100 
1101 float Incidence::geoLongitude() const
1102 {
1103     Q_D(const Incidence);
1104     // For internal consistency, both coordinates are considered invalid if either is.
1105     return (INVALID_LATLON == d->mGeoLatitude) ? INVALID_LATLON : d->mGeoLongitude;
1106 }
1107 
1108 void Incidence::setGeoLongitude(float geolongitude)
1109 {
1110     if (mReadOnly) {
1111         return;
1112     }
1113 
1114     if (isnan(geolongitude)) {
1115         geolongitude = INVALID_LATLON;
1116     }
1117     if (geolongitude != INVALID_LATLON && (geolongitude < -180.0 || geolongitude > 180.0)) {
1118         qCWarning(KCALCORE_LOG) << "Ignoring invalid  longitude" << geolongitude;
1119         return;
1120     }
1121 
1122     Q_D(Incidence);
1123     update();
1124     d->mGeoLongitude = geolongitude;
1125     setFieldDirty(FieldGeoLongitude);
1126     updated();
1127 }
1128 
1129 bool Incidence::hasRecurrenceId() const
1130 {
1131     Q_D(const Incidence);
1132     return (allDay() && d->mRecurrenceId.date().isValid()) || d->mRecurrenceId.isValid();
1133 }
1134 
1135 QDateTime Incidence::recurrenceId() const
1136 {
1137     Q_D(const Incidence);
1138     return d->mRecurrenceId;
1139 }
1140 
1141 void Incidence::setThisAndFuture(bool thisAndFuture)
1142 {
1143     Q_D(Incidence);
1144     d->mThisAndFuture = thisAndFuture;
1145 }
1146 
1147 bool Incidence::thisAndFuture() const
1148 {
1149     Q_D(const Incidence);
1150     return d->mThisAndFuture;
1151 }
1152 
1153 void Incidence::setRecurrenceId(const QDateTime &recurrenceId)
1154 {
1155     if (!mReadOnly) {
1156         update();
1157         Q_D(Incidence);
1158         d->mRecurrenceId = recurrenceId;
1159         setFieldDirty(FieldRecurrenceId);
1160         updated();
1161     }
1162 }
1163 
1164 /** Observer interface for the recurrence class. If the recurrence is changed,
1165     this method will be called for the incidence the recurrence object
1166     belongs to. */
1167 void Incidence::recurrenceUpdated(Recurrence *recurrence)
1168 {
1169     Q_D(const Incidence);
1170     if (recurrence == d->mRecurrence) {
1171         update();
1172         setFieldDirty(FieldRecurrence);
1173         updated();
1174     }
1175 }
1176 
1177 //@cond PRIVATE
1178 #define ALT_DESC_FIELD "X-ALT-DESC"
1179 #define ALT_DESC_PARAMETERS QStringLiteral("FMTTYPE=text/html")
1180 //@endcond
1181 
1182 bool Incidence::hasAltDescription() const
1183 {
1184     const QString value = nonKDECustomProperty(ALT_DESC_FIELD);
1185     const QString parameter = nonKDECustomPropertyParameters(ALT_DESC_FIELD);
1186 
1187     return parameter == ALT_DESC_PARAMETERS && !value.isEmpty();
1188 }
1189 
1190 void Incidence::setAltDescription(const QString &altdescription)
1191 {
1192     if (altdescription.isEmpty()) {
1193         removeNonKDECustomProperty(ALT_DESC_FIELD);
1194     } else {
1195         setNonKDECustomProperty(ALT_DESC_FIELD, altdescription, ALT_DESC_PARAMETERS);
1196     }
1197 }
1198 
1199 QString Incidence::altDescription() const
1200 {
1201     if (!hasAltDescription()) {
1202         return QString();
1203     } else {
1204         return nonKDECustomProperty(ALT_DESC_FIELD);
1205     }
1206 }
1207 
1208 /** static */
1209 QStringList Incidence::mimeTypes()
1210 {
1211     return QStringList() << QStringLiteral("text/calendar") << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType()
1212                          << KCalendarCore::Journal::journalMimeType();
1213 }
1214 
1215 void Incidence::serialize(QDataStream &out) const
1216 {
1217     Q_D(const Incidence);
1218     serializeQDateTimeAsKDateTime(out, d->mCreated);
1219     out << d->mRevision << d->mDescription << d->mDescriptionIsRich << d->mSummary << d->mSummaryIsRich << d->mLocation << d->mLocationIsRich << d->mCategories
1220         << d->mResources << d->mStatusString << d->mPriority << d->mSchedulingID << d->mGeoLatitude << d->mGeoLongitude
1221         << hasGeo();    // No longer used, but serialized/deserialized for compatibility.
1222     serializeQDateTimeAsKDateTime(out, d->mRecurrenceId);
1223     out << d->mThisAndFuture << d->mLocalOnly << d->mStatus << d->mSecrecy << (d->mRecurrence ? true : false) << (qint32)d->mAttachments.count()
1224         << (qint32)d->mAlarms.count() << (qint32)d->mConferences.count() << d->mRelatedToUid;
1225 
1226     if (d->mRecurrence) {
1227         out << d->mRecurrence;
1228     }
1229 
1230     for (const Attachment &attachment : std::as_const(d->mAttachments)) {
1231         out << attachment;
1232     }
1233 
1234     for (const Alarm::Ptr &alarm : std::as_const(d->mAlarms)) {
1235         out << alarm;
1236     }
1237 
1238     for (const Conference &conf : std::as_const(d->mConferences)) {
1239         out << conf;
1240     }
1241 }
1242 
1243 void Incidence::deserialize(QDataStream &in)
1244 {
1245     bool hasGeo;    // No longer used, but serialized/deserialized for compatibility.
1246     quint32 status;
1247     quint32 secrecy;
1248     bool hasRecurrence;
1249     int attachmentCount;
1250     int alarmCount;
1251     int conferencesCount;
1252     QMap<int, QString> relatedToUid;
1253     Q_D(Incidence);
1254     deserializeKDateTimeAsQDateTime(in, d->mCreated);
1255     in >> d->mRevision >> d->mDescription >> d->mDescriptionIsRich >> d->mSummary >> d->mSummaryIsRich >> d->mLocation >> d->mLocationIsRich >> d->mCategories
1256         >> d->mResources >> d->mStatusString >> d->mPriority >> d->mSchedulingID >> d->mGeoLatitude >> d->mGeoLongitude >> hasGeo;
1257     deserializeKDateTimeAsQDateTime(in, d->mRecurrenceId);
1258     in >> d->mThisAndFuture >> d->mLocalOnly >> status >> secrecy >> hasRecurrence >> attachmentCount >> alarmCount >> conferencesCount >> relatedToUid;
1259 
1260     if (hasRecurrence) {
1261         d->mRecurrence = new Recurrence();
1262         d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
1263         in >> d->mRecurrence;
1264     }
1265 
1266     d->mAttachments.clear();
1267     d->mAlarms.clear();
1268     d->mConferences.clear();
1269 
1270     d->mAttachments.reserve(attachmentCount);
1271     for (int i = 0; i < attachmentCount; ++i) {
1272         Attachment attachment;
1273         in >> attachment;
1274         d->mAttachments.append(attachment);
1275     }
1276 
1277     d->mAlarms.reserve(alarmCount);
1278     for (int i = 0; i < alarmCount; ++i) {
1279         Alarm::Ptr alarm = Alarm::Ptr(new Alarm(this));
1280         in >> alarm;
1281         d->mAlarms.append(alarm);
1282     }
1283 
1284     d->mConferences.reserve(conferencesCount);
1285     for (int i = 0; i < conferencesCount; ++i) {
1286         Conference conf;
1287         in >> conf;
1288         d->mConferences.push_back(conf);
1289     }
1290 
1291     d->mStatus = static_cast<Incidence::Status>(status);
1292     d->mSecrecy = static_cast<Incidence::Secrecy>(secrecy);
1293 
1294     d->mRelatedToUid.clear();
1295 
1296     auto it = relatedToUid.cbegin();
1297     auto end = relatedToUid.cend();
1298     for (; it != end; ++it) {
1299         d->mRelatedToUid.insert(static_cast<Incidence::RelType>(it.key()), it.value());
1300     }
1301 }
1302 
1303 namespace
1304 {
1305 template<typename T>
1306 QVariantList toVariantList(int size, typename QVector<T>::ConstIterator begin, typename QVector<T>::ConstIterator end)
1307 {
1308     QVariantList l;
1309     l.reserve(size);
1310     std::transform(begin, end, std::back_inserter(l), [](const T &val) {
1311         return QVariant::fromValue(val);
1312     });
1313     return l;
1314 }
1315 
1316 } // namespace
1317 
1318 QVariantList Incidence::attachmentsVariant() const
1319 {
1320     Q_D(const Incidence);
1321     return toVariantList<Attachment>(d->mAttachments.size(), d->mAttachments.cbegin(), d->mAttachments.cend());
1322 }
1323 
1324 QVariantList Incidence::conferencesVariant() const
1325 {
1326     Q_D(const Incidence);
1327     return toVariantList<Conference>(d->mConferences.size(), d->mConferences.cbegin(), d->mConferences.cend());
1328 }
1329 
1330 #include "moc_incidence.cpp"