1 /*
2 This file is part of the kcalcore library.
3
4 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
5 SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 /**
10 @file
11 This file is part of the API for handling calendar data and
12 defines the Incidence class.
13
14 @brief
15 Provides the class common to non-FreeBusy (Events, To-dos, Journals)
16 calendar components known as incidences.
17
18 @author Cornelius Schumacher \<schumacher@kde.org\>
19 @author Reinhold Kainhofer \<reinhold@kainhofer.com\>
20 */
21
22 #include "incidence.h"
23 #include "incidence_p.h"
24 #include "calformat.h"
25 #include "kcalendarcore_debug.h"
26 #include "utils_p.h"
27
28 #include <math.h>
29
30 #include <QStringList>
31 #include <QTextDocument> // for .toHtmlEscaped() and Qt::mightBeRichText()
32 #include <QTime>
33 #include <QTimeZone>
34
35 using namespace KCalendarCore;
36
37 IncidencePrivate::IncidencePrivate() = default;
38
IncidencePrivate(const IncidencePrivate & p)39 IncidencePrivate::IncidencePrivate(const IncidencePrivate &p)
40 : mCreated(p.mCreated)
41 , mDescription(p.mDescription)
42 , mSummary(p.mSummary)
43 , mLocation(p.mLocation)
44 , mCategories(p.mCategories)
45 , mResources(p.mResources)
46 , mStatusString(p.mStatusString)
47 , mSchedulingID(p.mSchedulingID)
48 , mRelatedToUid(p.mRelatedToUid)
49 , mRecurrenceId(p.mRecurrenceId)
50 , mConferences(p.mConferences)
51 , mGeoLatitude(p.mGeoLatitude)
52 , mGeoLongitude(p.mGeoLongitude)
53 , mRecurrence(nullptr)
54 , mRevision(p.mRevision)
55 , mPriority(p.mPriority)
56 , mStatus(p.mStatus)
57 , mSecrecy(p.mSecrecy)
58 , mColor(p.mColor)
59 , mDescriptionIsRich(p.mDescriptionIsRich)
60 , mSummaryIsRich(p.mSummaryIsRich)
61 , mLocationIsRich(p.mLocationIsRich)
62 , mThisAndFuture(p.mThisAndFuture)
63 , mLocalOnly(false)
64 {
65 }
66
clear()67 void IncidencePrivate::clear()
68 {
69 mAlarms.clear();
70 mAttachments.clear();
71 delete mRecurrence;
72 mRecurrence = nullptr;
73 }
74
init(Incidence * q,const IncidencePrivate & other)75 void IncidencePrivate::init(Incidence *q, const IncidencePrivate &other)
76 {
77 mRevision = other.mRevision;
78 mCreated = other.mCreated;
79 mDescription = other.mDescription;
80 mDescriptionIsRich = other.mDescriptionIsRich;
81 mSummary = other.mSummary;
82 mSummaryIsRich = other.mSummaryIsRich;
83 mCategories = other.mCategories;
84 mRelatedToUid = other.mRelatedToUid;
85 mResources = other.mResources;
86 mStatusString = other.mStatusString;
87 mStatus = other.mStatus;
88 mSecrecy = other.mSecrecy;
89 mPriority = other.mPriority;
90 mLocation = other.mLocation;
91 mLocationIsRich = other.mLocationIsRich;
92 mGeoLatitude = other.mGeoLatitude;
93 mGeoLongitude = other.mGeoLongitude;
94 mRecurrenceId = other.mRecurrenceId;
95 mConferences = other.mConferences;
96 mThisAndFuture = other.mThisAndFuture;
97 mLocalOnly = other.mLocalOnly;
98 mColor = other.mColor;
99
100 // Alarms and Attachments are stored in ListBase<...>, which is a QValueList<...*>.
101 // We need to really duplicate the objects stored therein, otherwise deleting
102 // i will also delete all attachments from this object (setAutoDelete...)
103 mAlarms.reserve(other.mAlarms.count());
104 for (const Alarm::Ptr &alarm : qAsConst(other.mAlarms)) {
105 Alarm::Ptr b(new Alarm(*alarm.data()));
106 b->setParent(q);
107 mAlarms.append(b);
108 }
109
110 mAttachments = other.mAttachments;
111 if (other.mRecurrence) {
112 mRecurrence = new Recurrence(*(other.mRecurrence));
113 mRecurrence->addObserver(q);
114 } else {
115 mRecurrence = nullptr;
116 }
117 }
118 //@endcond
119
Incidence()120 Incidence::Incidence()
121 : IncidenceBase()
122 , d(new KCalendarCore::IncidencePrivate)
123 {
124 recreate();
125 resetDirtyFields();
126 }
127
Incidence(const Incidence & i)128 Incidence::Incidence(const Incidence &i)
129 : IncidenceBase(i)
130 , Recurrence::RecurrenceObserver()
131 , d(new KCalendarCore::IncidencePrivate(*i.d))
132 {
133 d->init(this, *i.d);
134 resetDirtyFields();
135 }
136
~Incidence()137 Incidence::~Incidence()
138 {
139 // Alarm has a raw incidence pointer, so we must set it to 0
140 // so Alarm doesn't use it after Incidence is destroyed
141 for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) {
142 alarm->setParent(nullptr);
143 }
144 delete d->mRecurrence;
145 delete d;
146 }
147
148 //@cond PRIVATE
149 // A string comparison that considers that null and empty are the same
stringCompare(const QString & s1,const QString & s2)150 static bool stringCompare(const QString &s1, const QString &s2)
151 {
152 return (s1.isEmpty() && s2.isEmpty()) || (s1 == s2);
153 }
154
155 //@endcond
assign(const IncidenceBase & other)156 IncidenceBase &Incidence::assign(const IncidenceBase &other)
157 {
158 if (&other != this) {
159 d->clear();
160 // TODO: should relations be cleared out, as in destructor???
161 IncidenceBase::assign(other);
162 const Incidence *i = static_cast<const Incidence *>(&other);
163 d->init(this, *(i->d));
164 }
165
166 return *this;
167 }
168
equals(const IncidenceBase & incidence) const169 bool Incidence::equals(const IncidenceBase &incidence) const
170 {
171 if (!IncidenceBase::equals(incidence)) {
172 return false;
173 }
174
175 // If they weren't the same type IncidenceBase::equals would had returned false already
176 const Incidence *i2 = static_cast<const Incidence *>(&incidence);
177
178 const Alarm::List alarmList = alarms();
179 const Alarm::List otherAlarmsList = i2->alarms();
180 if (alarmList.count() != otherAlarmsList.count()) {
181 return false;
182 }
183
184 auto matchFunc = [](const Alarm::Ptr &a, const Alarm::Ptr &b) {
185 return *a == *b;
186 };
187
188 const auto [it1, it2] = std::mismatch(alarmList.cbegin(), alarmList.cend(), otherAlarmsList.cbegin(), otherAlarmsList.cend(), matchFunc);
189 // Checking the iterator from one list only, since both lists are the same size
190 if (it1 != alarmList.cend()) {
191 return false;
192 }
193
194 const Attachment::List attachmentList = attachments();
195 const Attachment::List otherAttachmentList = i2->attachments();
196 if (attachmentList.count() != otherAttachmentList.count()) {
197 return false;
198 }
199
200 const auto [at1, at2] = std::mismatch(attachmentList.cbegin(), attachmentList.cend(), otherAttachmentList.cbegin(), otherAttachmentList.cend());
201
202 // Checking the iterator from one list only, since both lists are the same size
203 if (at1 != attachmentList.cend()) {
204 return false;
205 }
206
207 bool recurrenceEqual = (d->mRecurrence == nullptr && i2->d->mRecurrence == nullptr);
208 if (!recurrenceEqual) {
209 recurrence(); // create if doesn't exist
210 i2->recurrence(); // create if doesn't exist
211 recurrenceEqual = d->mRecurrence != nullptr && i2->d->mRecurrence != nullptr && *d->mRecurrence == *i2->d->mRecurrence;
212 }
213
214 if (!qFuzzyCompare(d->mGeoLatitude, i2->d->mGeoLatitude) || !qFuzzyCompare(d->mGeoLongitude, i2->d->mGeoLongitude)) {
215 return false;
216 }
217 // clang-format off
218 return
219 recurrenceEqual
220 && created() == i2->created()
221 && stringCompare(description(), i2->description())
222 && descriptionIsRich() == i2->descriptionIsRich()
223 && stringCompare(summary(), i2->summary())
224 && summaryIsRich() == i2->summaryIsRich()
225 && categories() == i2->categories()
226 && stringCompare(relatedTo(), i2->relatedTo())
227 && resources() == i2->resources()
228 && d->mStatus == i2->d->mStatus
229 && (d->mStatus == StatusNone || stringCompare(d->mStatusString, i2->d->mStatusString))
230 && secrecy() == i2->secrecy()
231 && priority() == i2->priority()
232 && stringCompare(location(), i2->location())
233 && locationIsRich() == i2->locationIsRich()
234 && stringCompare(color(), i2->color())
235 && stringCompare(schedulingID(), i2->schedulingID())
236 && recurrenceId() == i2->recurrenceId()
237 && conferences() == i2->conferences()
238 && thisAndFuture() == i2->thisAndFuture();
239 // clang-format on
240 }
241
instanceIdentifier() const242 QString Incidence::instanceIdentifier() const
243 {
244 if (hasRecurrenceId()) {
245 return uid() + recurrenceId().toString(Qt::ISODate);
246 }
247 return uid();
248 }
249
recreate()250 void Incidence::recreate()
251 {
252 const QDateTime nowUTC = QDateTime::currentDateTimeUtc();
253 setCreated(nowUTC);
254
255 setSchedulingID(QString(), CalFormat::createUniqueId());
256 setRevision(0);
257 setLastModified(nowUTC); // NOLINT false clang-analyzer-optin.cplusplus.VirtualCall
258 }
259
setLastModified(const QDateTime & lm)260 void Incidence::setLastModified(const QDateTime &lm)
261 {
262 if (!d->mLocalOnly) {
263 IncidenceBase::setLastModified(lm);
264 }
265 }
266
setReadOnly(bool readOnly)267 void Incidence::setReadOnly(bool readOnly)
268 {
269 IncidenceBase::setReadOnly(readOnly);
270 if (d->mRecurrence) {
271 d->mRecurrence->setRecurReadOnly(readOnly);
272 }
273 }
274
setLocalOnly(bool localOnly)275 void Incidence::setLocalOnly(bool localOnly)
276 {
277 if (mReadOnly) {
278 return;
279 }
280 d->mLocalOnly = localOnly;
281 }
282
localOnly() const283 bool Incidence::localOnly() const
284 {
285 return d->mLocalOnly;
286 }
287
setAllDay(bool allDay)288 void Incidence::setAllDay(bool allDay)
289 {
290 if (mReadOnly) {
291 return;
292 }
293 if (d->mRecurrence) {
294 d->mRecurrence->setAllDay(allDay);
295 }
296 IncidenceBase::setAllDay(allDay);
297 }
298
setCreated(const QDateTime & created)299 void Incidence::setCreated(const QDateTime &created)
300 {
301 if (mReadOnly || d->mLocalOnly) {
302 return;
303 }
304
305 update();
306 d->mCreated = created.toUTC();
307 const auto ct = d->mCreated.time();
308 // Remove milliseconds
309 d->mCreated.setTime(QTime(ct.hour(), ct.minute(), ct.second()));
310 setFieldDirty(FieldCreated);
311 updated();
312 }
313
created() const314 QDateTime Incidence::created() const
315 {
316 return d->mCreated;
317 }
318
setRevision(int rev)319 void Incidence::setRevision(int rev)
320 {
321 if (mReadOnly || d->mLocalOnly) {
322 return;
323 }
324
325 update();
326 d->mRevision = rev;
327 setFieldDirty(FieldRevision);
328 updated();
329 }
330
revision() const331 int Incidence::revision() const
332 {
333 return d->mRevision;
334 }
335
setDtStart(const QDateTime & dt)336 void Incidence::setDtStart(const QDateTime &dt)
337 {
338 IncidenceBase::setDtStart(dt);
339 if (d->mRecurrence && dirtyFields().contains(FieldDtStart)) {
340 d->mRecurrence->setStartDateTime(dt, allDay());
341 }
342 }
343
shiftTimes(const QTimeZone & oldZone,const QTimeZone & newZone)344 void Incidence::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone)
345 {
346 IncidenceBase::shiftTimes(oldZone, newZone);
347 if (d->mRecurrence) {
348 d->mRecurrence->shiftTimes(oldZone, newZone);
349 }
350 if (d->mAlarms.count() > 0) {
351 update();
352 for (auto alarm : d->mAlarms) {
353 alarm->shiftTimes(oldZone, newZone);
354 }
355 setFieldDirty(FieldAlarms);
356 updated();
357 }
358 }
359
setDescription(const QString & description,bool isRich)360 void Incidence::setDescription(const QString &description, bool isRich)
361 {
362 if (mReadOnly) {
363 return;
364 }
365 update();
366 d->mDescription = description;
367 d->mDescriptionIsRich = isRich;
368 setFieldDirty(FieldDescription);
369 updated();
370 }
371
setDescription(const QString & description)372 void Incidence::setDescription(const QString &description)
373 {
374 setDescription(description, Qt::mightBeRichText(description));
375 }
376
description() const377 QString Incidence::description() const
378 {
379 return d->mDescription;
380 }
381
richDescription() const382 QString Incidence::richDescription() const
383 {
384 if (descriptionIsRich()) {
385 return d->mDescription;
386 } else {
387 return d->mDescription.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
388 }
389 }
390
descriptionIsRich() const391 bool Incidence::descriptionIsRich() const
392 {
393 return d->mDescriptionIsRich;
394 }
395
setSummary(const QString & summary,bool isRich)396 void Incidence::setSummary(const QString &summary, bool isRich)
397 {
398 if (mReadOnly) {
399 return;
400 }
401 if (d->mSummary != summary || d->mSummaryIsRich != isRich) {
402 update();
403 d->mSummary = summary;
404 d->mSummaryIsRich = isRich;
405 setFieldDirty(FieldSummary);
406 updated();
407 }
408 }
409
setSummary(const QString & summary)410 void Incidence::setSummary(const QString &summary)
411 {
412 setSummary(summary, Qt::mightBeRichText(summary));
413 }
414
summary() const415 QString Incidence::summary() const
416 {
417 return d->mSummary;
418 }
419
richSummary() const420 QString Incidence::richSummary() const
421 {
422 if (summaryIsRich()) {
423 return d->mSummary;
424 } else {
425 return d->mSummary.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
426 }
427 }
428
summaryIsRich() const429 bool Incidence::summaryIsRich() const
430 {
431 return d->mSummaryIsRich;
432 }
433
setCategories(const QStringList & categories)434 void Incidence::setCategories(const QStringList &categories)
435 {
436 if (mReadOnly) {
437 return;
438 }
439
440 update();
441 d->mCategories = categories;
442 setFieldDirty(FieldCategories);
443 updated();
444 }
445
setCategories(const QString & catStr)446 void Incidence::setCategories(const QString &catStr)
447 {
448 if (mReadOnly) {
449 return;
450 }
451 update();
452 setFieldDirty(FieldCategories);
453
454 d->mCategories.clear();
455
456 if (catStr.isEmpty()) {
457 updated();
458 return;
459 }
460
461 d->mCategories = catStr.split(QLatin1Char(','));
462
463 for (auto &category : d->mCategories) {
464 category = category.trimmed();
465 }
466
467 updated();
468 }
469
categories() const470 QStringList Incidence::categories() const
471 {
472 return d->mCategories;
473 }
474
categoriesStr() const475 QString Incidence::categoriesStr() const
476 {
477 return d->mCategories.join(QLatin1Char(','));
478 }
479
setRelatedTo(const QString & relatedToUid,RelType relType)480 void Incidence::setRelatedTo(const QString &relatedToUid, RelType relType)
481 {
482 // TODO: RFC says that an incidence can have more than one related-to field
483 // even for the same relType.
484
485 if (d->mRelatedToUid[relType] != relatedToUid) {
486 update();
487 d->mRelatedToUid[relType] = relatedToUid;
488 setFieldDirty(FieldRelatedTo);
489 updated();
490 }
491 }
492
relatedTo(RelType relType) const493 QString Incidence::relatedTo(RelType relType) const
494 {
495 return d->mRelatedToUid.value(relType);
496 }
497
setColor(const QString & colorName)498 void Incidence::setColor(const QString &colorName)
499 {
500 if (mReadOnly) {
501 return;
502 }
503 if (!stringCompare(d->mColor, colorName)) {
504 update();
505 d->mColor = colorName;
506 setFieldDirty(FieldColor);
507 updated();
508 }
509 }
510
color() const511 QString Incidence::color() const
512 {
513 return d->mColor;
514 }
515
516 // %%%%%%%%%%%% Recurrence-related methods %%%%%%%%%%%%%%%%%%%%
517
recurrence() const518 Recurrence *Incidence::recurrence() const
519 {
520 if (!d->mRecurrence) {
521 d->mRecurrence = new Recurrence();
522 d->mRecurrence->setStartDateTime(dateTime(RoleRecurrenceStart), allDay());
523 d->mRecurrence->setAllDay(allDay());
524 d->mRecurrence->setRecurReadOnly(mReadOnly);
525 d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
526 }
527
528 return d->mRecurrence;
529 }
530
clearRecurrence()531 void Incidence::clearRecurrence()
532 {
533 delete d->mRecurrence;
534 d->mRecurrence = nullptr;
535 }
536
recurrenceType() const537 ushort Incidence::recurrenceType() const
538 {
539 if (d->mRecurrence) {
540 return d->mRecurrence->recurrenceType();
541 } else {
542 return Recurrence::rNone;
543 }
544 }
545
recurs() const546 bool Incidence::recurs() const
547 {
548 if (d->mRecurrence) {
549 return d->mRecurrence->recurs();
550 } else {
551 return false;
552 }
553 }
554
recursOn(const QDate & date,const QTimeZone & timeZone) const555 bool Incidence::recursOn(const QDate &date, const QTimeZone &timeZone) const
556 {
557 return d->mRecurrence && d->mRecurrence->recursOn(date, timeZone);
558 }
559
recursAt(const QDateTime & qdt) const560 bool Incidence::recursAt(const QDateTime &qdt) const
561 {
562 return d->mRecurrence && d->mRecurrence->recursAt(qdt);
563 }
564
startDateTimesForDate(const QDate & date,const QTimeZone & timeZone) const565 QList<QDateTime> Incidence::startDateTimesForDate(const QDate &date, const QTimeZone &timeZone) const
566 {
567 QDateTime start = dtStart();
568 QDateTime end = dateTime(RoleEndRecurrenceBase);
569
570 QList<QDateTime> result;
571
572 // TODO_Recurrence: Also work if only due date is given...
573 if (!start.isValid() && !end.isValid()) {
574 return result;
575 }
576
577 // if the incidence doesn't recur,
578 QDateTime kdate(date, {}, timeZone);
579 if (!recurs()) {
580 if (start.date() <= date && end.date() >= date) {
581 result << start;
582 }
583 return result;
584 }
585
586 qint64 days = start.daysTo(end);
587 // Account for possible recurrences going over midnight, while the original event doesn't
588 QDate tmpday(date.addDays(-days - 1));
589 QDateTime tmp;
590 while (tmpday <= date) {
591 if (recurrence()->recursOn(tmpday, timeZone)) {
592 const QList<QTime> times = recurrence()->recurTimesOn(tmpday, timeZone);
593 for (const QTime &time : times) {
594 tmp = QDateTime(tmpday, time, start.timeZone());
595 if (endDateForStart(tmp) >= kdate) {
596 result << tmp;
597 }
598 }
599 }
600 tmpday = tmpday.addDays(1);
601 }
602 return result;
603 }
604
startDateTimesForDateTime(const QDateTime & datetime) const605 QList<QDateTime> Incidence::startDateTimesForDateTime(const QDateTime &datetime) const
606 {
607 QDateTime start = dtStart();
608 QDateTime end = dateTime(RoleEndRecurrenceBase);
609
610 QList<QDateTime> result;
611
612 // TODO_Recurrence: Also work if only due date is given...
613 if (!start.isValid() && !end.isValid()) {
614 return result;
615 }
616
617 // if the incidence doesn't recur,
618 if (!recurs()) {
619 if (!(start > datetime || end < datetime)) {
620 result << start;
621 }
622 return result;
623 }
624
625 qint64 days = start.daysTo(end);
626 // Account for possible recurrences going over midnight, while the original event doesn't
627 QDate tmpday(datetime.date().addDays(-days - 1));
628 QDateTime tmp;
629 while (tmpday <= datetime.date()) {
630 if (recurrence()->recursOn(tmpday, datetime.timeZone())) {
631 // Get the times during the day (in start date's time zone) when recurrences happen
632 const QList<QTime> times = recurrence()->recurTimesOn(tmpday, start.timeZone());
633 for (const QTime &time : times) {
634 tmp = QDateTime(tmpday, time, start.timeZone());
635 if (!(tmp > datetime || endDateForStart(tmp) < datetime)) {
636 result << tmp;
637 }
638 }
639 }
640 tmpday = tmpday.addDays(1);
641 }
642 return result;
643 }
644
endDateForStart(const QDateTime & startDt) const645 QDateTime Incidence::endDateForStart(const QDateTime &startDt) const
646 {
647 QDateTime start = dtStart();
648 QDateTime end = dateTime(RoleEndRecurrenceBase);
649 if (!end.isValid()) {
650 return start;
651 }
652 if (!start.isValid()) {
653 return end;
654 }
655
656 return startDt.addSecs(start.secsTo(end));
657 }
658
addAttachment(const Attachment & attachment)659 void Incidence::addAttachment(const Attachment &attachment)
660 {
661 if (mReadOnly || attachment.isEmpty()) {
662 return;
663 }
664
665 update();
666 d->mAttachments.append(attachment);
667 setFieldDirty(FieldAttachment);
668 updated();
669 }
670
deleteAttachments(const QString & mime)671 void Incidence::deleteAttachments(const QString &mime)
672 {
673 auto it = std::remove_if(d->mAttachments.begin(), d->mAttachments.end(), [&mime](const Attachment &a) {
674 return a.mimeType() == mime;
675 });
676 if (it != d->mAttachments.end()) {
677 update();
678 d->mAttachments.erase(it, d->mAttachments.end());
679 setFieldDirty(FieldAttachment);
680 updated();
681 }
682 }
683
attachments() const684 Attachment::List Incidence::attachments() const
685 {
686 return d->mAttachments;
687 }
688
attachments(const QString & mime) const689 Attachment::List Incidence::attachments(const QString &mime) const
690 {
691 Attachment::List attachments;
692 for (const Attachment &attachment : qAsConst(d->mAttachments)) {
693 if (attachment.mimeType() == mime) {
694 attachments.append(attachment);
695 }
696 }
697 return attachments;
698 }
699
clearAttachments()700 void Incidence::clearAttachments()
701 {
702 update();
703 setFieldDirty(FieldAttachment);
704 d->mAttachments.clear();
705 updated();
706 }
707
setResources(const QStringList & resources)708 void Incidence::setResources(const QStringList &resources)
709 {
710 if (mReadOnly) {
711 return;
712 }
713
714 update();
715 d->mResources = resources;
716 setFieldDirty(FieldResources);
717 updated();
718 }
719
resources() const720 QStringList Incidence::resources() const
721 {
722 return d->mResources;
723 }
724
setPriority(int priority)725 void Incidence::setPriority(int priority)
726 {
727 if (mReadOnly) {
728 return;
729 }
730
731 if (priority < 0 || priority > 9) {
732 qCWarning(KCALCORE_LOG) << "Ignoring invalid priority" << priority;
733 return;
734 }
735
736 update();
737 d->mPriority = priority;
738 setFieldDirty(FieldPriority);
739 updated();
740 }
741
priority() const742 int Incidence::priority() const
743 {
744 return d->mPriority;
745 }
746
setStatus(Incidence::Status status)747 void Incidence::setStatus(Incidence::Status status)
748 {
749 if (mReadOnly || status == StatusX) {
750 return;
751 }
752
753 update();
754 d->mStatus = status;
755 d->mStatusString.clear();
756 setFieldDirty(FieldStatus);
757 updated();
758 }
759
setCustomStatus(const QString & status)760 void Incidence::setCustomStatus(const QString &status)
761 {
762 if (mReadOnly) {
763 return;
764 }
765
766 update();
767 d->mStatus = status.isEmpty() ? StatusNone : StatusX;
768 d->mStatusString = status;
769 setFieldDirty(FieldStatus);
770 updated();
771 }
772
status() const773 Incidence::Status Incidence::status() const
774 {
775 return d->mStatus;
776 }
777
customStatus() const778 QString Incidence::customStatus() const
779 {
780 if (d->mStatus == StatusX) {
781 return d->mStatusString;
782 } else {
783 return QString();
784 }
785 }
786
setSecrecy(Incidence::Secrecy secrecy)787 void Incidence::setSecrecy(Incidence::Secrecy secrecy)
788 {
789 if (mReadOnly) {
790 return;
791 }
792
793 update();
794 d->mSecrecy = secrecy;
795 setFieldDirty(FieldSecrecy);
796 updated();
797 }
798
secrecy() const799 Incidence::Secrecy Incidence::secrecy() const
800 {
801 return d->mSecrecy;
802 }
803
alarms() const804 Alarm::List Incidence::alarms() const
805 {
806 return d->mAlarms;
807 }
808
newAlarm()809 Alarm::Ptr Incidence::newAlarm()
810 {
811 Alarm::Ptr alarm(new Alarm(this));
812 addAlarm(alarm);
813 return alarm;
814 }
815
addAlarm(const Alarm::Ptr & alarm)816 void Incidence::addAlarm(const Alarm::Ptr &alarm)
817 {
818 update();
819 d->mAlarms.append(alarm);
820 setFieldDirty(FieldAlarms);
821 updated();
822 }
823
removeAlarm(const Alarm::Ptr & alarm)824 void Incidence::removeAlarm(const Alarm::Ptr &alarm)
825 {
826 const int index = d->mAlarms.indexOf(alarm);
827 if (index > -1) {
828 update();
829 d->mAlarms.remove(index);
830 setFieldDirty(FieldAlarms);
831 updated();
832 }
833 }
834
clearAlarms()835 void Incidence::clearAlarms()
836 {
837 update();
838 d->mAlarms.clear();
839 setFieldDirty(FieldAlarms);
840 updated();
841 }
842
hasEnabledAlarms() const843 bool Incidence::hasEnabledAlarms() const
844 {
845 return std::any_of(d->mAlarms.cbegin(), d->mAlarms.cend(), [](const Alarm::Ptr &alarm) {
846 return alarm->enabled();
847 });
848 }
849
conferences() const850 Conference::List Incidence::conferences() const
851 {
852 return d->mConferences;
853 }
854
addConference(const Conference & conference)855 void Incidence::addConference(const Conference &conference)
856 {
857 update();
858 d->mConferences.push_back(conference);
859 setFieldDirty(FieldConferences);
860 updated();
861 }
862
setConferences(const Conference::List & conferences)863 void Incidence::setConferences(const Conference::List &conferences)
864 {
865 update();
866 d->mConferences = conferences;
867 setFieldDirty(FieldConferences);
868 updated();
869 }
870
clearConferences()871 void Incidence::clearConferences()
872 {
873 update();
874 d->mConferences.clear();
875 setFieldDirty(FieldConferences);
876 updated();
877 }
878
setLocation(const QString & location,bool isRich)879 void Incidence::setLocation(const QString &location, bool isRich)
880 {
881 if (mReadOnly) {
882 return;
883 }
884
885 if (d->mLocation != location || d->mLocationIsRich != isRich) {
886 update();
887 d->mLocation = location;
888 d->mLocationIsRich = isRich;
889 setFieldDirty(FieldLocation);
890 updated();
891 }
892 }
893
setLocation(const QString & location)894 void Incidence::setLocation(const QString &location)
895 {
896 setLocation(location, Qt::mightBeRichText(location));
897 }
898
location() const899 QString Incidence::location() const
900 {
901 return d->mLocation;
902 }
903
richLocation() const904 QString Incidence::richLocation() const
905 {
906 if (locationIsRich()) {
907 return d->mLocation;
908 } else {
909 return d->mLocation.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br/>"));
910 }
911 }
912
locationIsRich() const913 bool Incidence::locationIsRich() const
914 {
915 return d->mLocationIsRich;
916 }
917
setSchedulingID(const QString & sid,const QString & uid)918 void Incidence::setSchedulingID(const QString &sid, const QString &uid)
919 {
920 if (!uid.isEmpty()) {
921 setUid(uid);
922 }
923 if (sid != d->mSchedulingID) {
924 update();
925 d->mSchedulingID = sid;
926 setFieldDirty(FieldSchedulingId);
927 updated();
928 }
929 }
930
schedulingID() const931 QString Incidence::schedulingID() const
932 {
933 if (d->mSchedulingID.isNull()) {
934 // Nothing set, so use the normal uid
935 return uid();
936 }
937 return d->mSchedulingID;
938 }
939
hasGeo() const940 bool Incidence::hasGeo() const
941 {
942 // For internal consistency, return false if either coordinate is invalid.
943 return d->mGeoLatitude != INVALID_LATLON && d->mGeoLongitude != INVALID_LATLON;
944 }
945
setHasGeo(bool hasGeo)946 void Incidence::setHasGeo(bool hasGeo)
947 {
948 if (mReadOnly) {
949 return;
950 }
951
952 if (!hasGeo) {
953 update();
954 d->mGeoLatitude = INVALID_LATLON;
955 d->mGeoLongitude = INVALID_LATLON;
956 setFieldDirty(FieldGeoLatitude);
957 setFieldDirty(FieldGeoLongitude);
958 updated();
959 }
960 // If hasGeo is true, the caller should set latitude and longitude to legal values..
961 }
962
geoLatitude() const963 float Incidence::geoLatitude() const
964 {
965 // For internal consistency, both coordinates are considered invalid if either is.
966 return (INVALID_LATLON == d->mGeoLongitude) ? INVALID_LATLON : d->mGeoLatitude;
967 }
968
setGeoLatitude(float geolatitude)969 void Incidence::setGeoLatitude(float geolatitude)
970 {
971 if (mReadOnly) {
972 return;
973 }
974
975 if (isnan(geolatitude)) {
976 geolatitude = INVALID_LATLON;
977 }
978 if (geolatitude != INVALID_LATLON && (geolatitude < -90.0 || geolatitude > 90.0)) {
979 qCWarning(KCALCORE_LOG) << "Ignoring invalid latitude" << geolatitude;
980 return;
981 }
982
983 update();
984 d->mGeoLatitude = geolatitude;
985 setFieldDirty(FieldGeoLatitude);
986 updated();
987 }
988
geoLongitude() const989 float Incidence::geoLongitude() const
990 {
991 // For internal consistency, both coordinates are considered invalid if either is.
992 return (INVALID_LATLON == d->mGeoLatitude) ? INVALID_LATLON : d->mGeoLongitude;
993 }
994
setGeoLongitude(float geolongitude)995 void Incidence::setGeoLongitude(float geolongitude)
996 {
997 if (mReadOnly) {
998 return;
999 }
1000
1001 if (isnan(geolongitude)) {
1002 geolongitude = INVALID_LATLON;
1003 }
1004 if (geolongitude != INVALID_LATLON && (geolongitude < -180.0 || geolongitude > 180.0)) {
1005 qCWarning(KCALCORE_LOG) << "Ignoring invalid longitude" << geolongitude;
1006 return;
1007 }
1008
1009 update();
1010 d->mGeoLongitude = geolongitude;
1011 setFieldDirty(FieldGeoLongitude);
1012 updated();
1013 }
1014
hasRecurrenceId() const1015 bool Incidence::hasRecurrenceId() const
1016 {
1017 return (allDay() && d->mRecurrenceId.date().isValid()) || d->mRecurrenceId.isValid();
1018 }
1019
recurrenceId() const1020 QDateTime Incidence::recurrenceId() const
1021 {
1022 return d->mRecurrenceId;
1023 }
1024
setThisAndFuture(bool thisAndFuture)1025 void Incidence::setThisAndFuture(bool thisAndFuture)
1026 {
1027 d->mThisAndFuture = thisAndFuture;
1028 }
1029
thisAndFuture() const1030 bool Incidence::thisAndFuture() const
1031 {
1032 return d->mThisAndFuture;
1033 }
1034
setRecurrenceId(const QDateTime & recurrenceId)1035 void Incidence::setRecurrenceId(const QDateTime &recurrenceId)
1036 {
1037 if (!mReadOnly) {
1038 update();
1039 d->mRecurrenceId = recurrenceId;
1040 setFieldDirty(FieldRecurrenceId);
1041 updated();
1042 }
1043 }
1044
1045 /** Observer interface for the recurrence class. If the recurrence is changed,
1046 this method will be called for the incidence the recurrence object
1047 belongs to. */
recurrenceUpdated(Recurrence * recurrence)1048 void Incidence::recurrenceUpdated(Recurrence *recurrence)
1049 {
1050 if (recurrence == d->mRecurrence) {
1051 update();
1052 setFieldDirty(FieldRecurrence);
1053 updated();
1054 }
1055 }
1056
1057 //@cond PRIVATE
1058 #define ALT_DESC_FIELD "X-ALT-DESC"
1059 #define ALT_DESC_PARAMETERS QStringLiteral("FMTTYPE=text/html")
1060 //@endcond
1061
hasAltDescription() const1062 bool Incidence::hasAltDescription() const
1063 {
1064 const QString value = nonKDECustomProperty(ALT_DESC_FIELD);
1065 const QString parameter = nonKDECustomPropertyParameters(ALT_DESC_FIELD);
1066
1067 return parameter == ALT_DESC_PARAMETERS && !value.isEmpty();
1068 }
1069
setAltDescription(const QString & altdescription)1070 void Incidence::setAltDescription(const QString &altdescription)
1071 {
1072 if (altdescription.isEmpty()) {
1073 removeNonKDECustomProperty(ALT_DESC_FIELD);
1074 } else {
1075 setNonKDECustomProperty(ALT_DESC_FIELD, altdescription, ALT_DESC_PARAMETERS);
1076 }
1077 }
1078
altDescription() const1079 QString Incidence::altDescription() const
1080 {
1081 if (!hasAltDescription()) {
1082 return QString();
1083 } else {
1084 return nonKDECustomProperty(ALT_DESC_FIELD);
1085 }
1086 }
1087
1088 /** static */
mimeTypes()1089 QStringList Incidence::mimeTypes()
1090 {
1091 return QStringList() << QStringLiteral("text/calendar") << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType()
1092 << KCalendarCore::Journal::journalMimeType();
1093 }
1094
serialize(QDataStream & out) const1095 void Incidence::serialize(QDataStream &out) const
1096 {
1097 serializeQDateTimeAsKDateTime(out, d->mCreated);
1098 out << d->mRevision << d->mDescription << d->mDescriptionIsRich << d->mSummary << d->mSummaryIsRich << d->mLocation << d->mLocationIsRich << d->mCategories
1099 << d->mResources << d->mStatusString << d->mPriority << d->mSchedulingID << d->mGeoLatitude << d->mGeoLongitude
1100 << hasGeo(); // No longer used, but serialized/deserialized for compatibility.
1101 serializeQDateTimeAsKDateTime(out, d->mRecurrenceId);
1102 out << d->mThisAndFuture << d->mLocalOnly << d->mStatus << d->mSecrecy << (d->mRecurrence ? true : false) << d->mAttachments.count() << d->mAlarms.count()
1103 << d->mConferences.count() << d->mRelatedToUid;
1104
1105 if (d->mRecurrence) {
1106 out << d->mRecurrence;
1107 }
1108
1109 for (const Attachment &attachment : qAsConst(d->mAttachments)) {
1110 out << attachment;
1111 }
1112
1113 for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) {
1114 out << alarm;
1115 }
1116
1117 for (const Conference &conf : qAsConst(d->mConferences)) {
1118 out << conf;
1119 }
1120 }
1121
deserialize(QDataStream & in)1122 void Incidence::deserialize(QDataStream &in)
1123 {
1124 bool hasGeo; // No longer used, but serialized/deserialized for compatibility.
1125 quint32 status;
1126 quint32 secrecy;
1127 bool hasRecurrence;
1128 int attachmentCount;
1129 int alarmCount;
1130 int conferencesCount;
1131 QMap<int, QString> relatedToUid;
1132 deserializeKDateTimeAsQDateTime(in, d->mCreated);
1133 in >> d->mRevision >> d->mDescription >> d->mDescriptionIsRich >> d->mSummary >> d->mSummaryIsRich >> d->mLocation >> d->mLocationIsRich >> d->mCategories
1134 >> d->mResources >> d->mStatusString >> d->mPriority >> d->mSchedulingID >> d->mGeoLatitude >> d->mGeoLongitude >> hasGeo;
1135 deserializeKDateTimeAsQDateTime(in, d->mRecurrenceId);
1136 in >> d->mThisAndFuture >> d->mLocalOnly >> status >> secrecy >> hasRecurrence >> attachmentCount >> alarmCount >> conferencesCount >> relatedToUid;
1137
1138 if (hasRecurrence) {
1139 d->mRecurrence = new Recurrence();
1140 d->mRecurrence->addObserver(const_cast<KCalendarCore::Incidence *>(this));
1141 in >> d->mRecurrence;
1142 }
1143
1144 d->mAttachments.clear();
1145 d->mAlarms.clear();
1146 d->mConferences.clear();
1147
1148 d->mAttachments.reserve(attachmentCount);
1149 for (int i = 0; i < attachmentCount; ++i) {
1150 Attachment attachment;
1151 in >> attachment;
1152 d->mAttachments.append(attachment);
1153 }
1154
1155 d->mAlarms.reserve(alarmCount);
1156 for (int i = 0; i < alarmCount; ++i) {
1157 Alarm::Ptr alarm = Alarm::Ptr(new Alarm(this));
1158 in >> alarm;
1159 d->mAlarms.append(alarm);
1160 }
1161
1162 d->mConferences.reserve(conferencesCount);
1163 for (int i = 0; i < conferencesCount; ++i) {
1164 Conference conf;
1165 in >> conf;
1166 d->mConferences.push_back(conf);
1167 }
1168
1169 d->mStatus = static_cast<Incidence::Status>(status);
1170 d->mSecrecy = static_cast<Incidence::Secrecy>(secrecy);
1171
1172 d->mRelatedToUid.clear();
1173
1174 auto it = relatedToUid.cbegin();
1175 auto end = relatedToUid.cend();
1176 for (; it != end; ++it) {
1177 d->mRelatedToUid.insert(static_cast<Incidence::RelType>(it.key()), it.value());
1178 }
1179 }
1180
1181 namespace
1182 {
1183 template<typename T>
toVariantList(int size,typename QVector<T>::ConstIterator begin,typename QVector<T>::ConstIterator end)1184 QVariantList toVariantList(int size, typename QVector<T>::ConstIterator begin, typename QVector<T>::ConstIterator end)
1185 {
1186 QVariantList l;
1187 l.reserve(size);
1188 std::transform(begin, end, std::back_inserter(l), [](const T &val) {
1189 return QVariant::fromValue(val);
1190 });
1191 return l;
1192 }
1193
1194 } // namespace
1195
attachmentsVariant() const1196 QVariantList Incidence::attachmentsVariant() const
1197 {
1198 return toVariantList<Attachment>(d->mAttachments.size(), d->mAttachments.cbegin(), d->mAttachments.cend());
1199 }
1200
conferencesVariant() const1201 QVariantList Incidence::conferencesVariant() const
1202 {
1203 return toVariantList<Conference>(d->mConferences.size(), d->mConferences.cbegin(), d->mConferences.cend());
1204 }
1205