File indexing completed on 2024-04-21 03:52:50

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