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

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 2001-2003 Cornelius Schumacher <schumacher@kde.org>
0005   SPDX-FileCopyrightText: 2009 Allen Winter <winter@kde.org>
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 Todo class.
0013 
0014   @brief
0015   Provides a To-do in the sense of RFC2445.
0016 
0017   @author Cornelius Schumacher \<schumacher@kde.org\>
0018   @author Allen Winter \<winter@kde.org\>
0019 */
0020 
0021 #include "incidence_p.h"
0022 #include "todo.h"
0023 #include "recurrence.h"
0024 #include "utils_p.h"
0025 #include "visitor.h"
0026 
0027 #include "kcalendarcore_debug.h"
0028 
0029 #include <QTime>
0030 
0031 using namespace KCalendarCore;
0032 
0033 /**
0034   Private class that helps to provide binary compatibility between releases.
0035   @internal
0036 */
0037 //@cond PRIVATE
0038 class KCalendarCore::TodoPrivate : public IncidencePrivate
0039 {
0040     // Due date of the to-do or its first recurrence if it recurs;  invalid() <=> no defined due date.
0041     QDateTime mDtDue;
0042     QDateTime mDtRecurrence; // next occurrence (for recurring to-dos)
0043     QDateTime mCompleted; // to-do completion date (if it has been completed)
0044     int mPercentComplete = 0; // to-do percent complete [0,100]
0045 
0046 public:
0047 
0048     TodoPrivate() = default;
0049     TodoPrivate(const TodoPrivate &other) = default;
0050 
0051     // Copy IncidencePrivate and IncidenceBasePrivate members,
0052     // but default-initialize TodoPrivate members.
0053     TodoPrivate(const Incidence &other)
0054         : IncidencePrivate(other)
0055     {
0056     }
0057 
0058     void init(const TodoPrivate &other);
0059 
0060     void setDtDue(const QDateTime dd);
0061     QDateTime dtDue() const
0062     {
0063         return mDtDue;
0064     }
0065 
0066     void setDtRecurrence(const QDateTime dr);
0067     QDateTime dtRecurrence() const
0068     {
0069         return mDtRecurrence;
0070     }
0071 
0072     void setCompleted(const QDateTime dc);
0073     QDateTime completed() const
0074     {
0075         return mCompleted;
0076     }
0077 
0078     void setPercentComplete(const int pc);
0079     int percentComplete() const
0080     {
0081         return mPercentComplete;
0082     }
0083 
0084     /**
0085       Returns true if the todo got a new date, else false will be returned.
0086     */
0087     bool recurTodo(Todo *todo);
0088 
0089     void deserialize(QDataStream &in);
0090 
0091     bool validStatus(Incidence::Status) override;
0092 };
0093 
0094 void TodoPrivate::setDtDue(const QDateTime dd)
0095 {
0096     if (!identical(dd, mDtDue)) {
0097         mDtDue = dd;
0098         mDirtyFields.insert(IncidenceBase::FieldDtDue);
0099     }
0100 }
0101 
0102 void TodoPrivate::setDtRecurrence(const QDateTime dr)
0103 {
0104     if (!identical(dr, mDtRecurrence)) {
0105         mDtRecurrence = dr;
0106         mDirtyFields.insert(IncidenceBase::FieldRecurrenceId);
0107     }
0108 }
0109 
0110 void TodoPrivate::setCompleted(const QDateTime dc)
0111 {
0112     if (dc != mCompleted) {
0113         mCompleted = dc.toUTC();
0114         mDirtyFields.insert(IncidenceBase::FieldCompleted);
0115     }
0116 }
0117 
0118 void TodoPrivate::setPercentComplete(const int pc)
0119 {
0120     if (pc != mPercentComplete) {
0121         mPercentComplete = pc;
0122         mDirtyFields.insert(IncidenceBase::FieldPercentComplete);
0123     }
0124 }
0125 
0126 void TodoPrivate::init(const TodoPrivate &other)
0127 {
0128     mDtDue = other.mDtDue;
0129     mDtRecurrence = other.mDtRecurrence;
0130     mCompleted = other.mCompleted;
0131     mPercentComplete = other.mPercentComplete;
0132 }
0133 
0134 bool TodoPrivate::validStatus(Incidence::Status status)
0135 {
0136     constexpr unsigned validSet
0137         = 1u << Incidence::StatusNone
0138         | 1u << Incidence::StatusNeedsAction
0139         | 1u << Incidence::StatusCompleted
0140         | 1u << Incidence::StatusInProcess
0141         | 1u << Incidence::StatusCanceled;
0142     return validSet & (1u << status);
0143 }
0144 
0145 //@endcond
0146 
0147 Todo::Todo()
0148     : Incidence(new TodoPrivate())
0149 {
0150 }
0151 
0152 Todo::Todo(const Todo &other)
0153     : Incidence(other, new TodoPrivate(*(other.d_func())))
0154 {
0155 }
0156 
0157 Todo::Todo(const Incidence &other)
0158     : Incidence(other, new TodoPrivate(other))
0159 {
0160 }
0161 
0162 Todo::~Todo() = default;
0163 
0164 Todo *Todo::clone() const
0165 {
0166     return new Todo(*this);
0167 }
0168 
0169 IncidenceBase &Todo::assign(const IncidenceBase &other)
0170 {
0171     Q_D(Todo);
0172     if (&other != this) {
0173         Incidence::assign(other);
0174         const Todo *t = static_cast<const Todo *>(&other);
0175         d->init(*(t->d_func()));
0176     }
0177     return *this;
0178 }
0179 
0180 bool Todo::equals(const IncidenceBase &todo) const
0181 {
0182     if (!Incidence::equals(todo)) {
0183         return false;
0184     } else {
0185         // If they weren't the same type IncidenceBase::equals would had returned false already
0186         const Todo *t = static_cast<const Todo *>(&todo);
0187         return identical(dtDue(), t->dtDue())
0188             && hasDueDate() == t->hasDueDate()
0189             && hasStartDate() == t->hasStartDate() && ((completed() == t->completed()) || (!completed().isValid() && !t->completed().isValid()))
0190             && hasCompletedDate() == t->hasCompletedDate() && percentComplete() == t->percentComplete();
0191     }
0192 }
0193 
0194 Incidence::IncidenceType Todo::type() const
0195 {
0196     return TypeTodo;
0197 }
0198 
0199 QByteArray Todo::typeStr() const
0200 {
0201     return QByteArrayLiteral("Todo");
0202 }
0203 
0204 void Todo::setDtDue(const QDateTime &dtDue, bool first)
0205 {
0206     startUpdates();
0207 
0208     // int diffsecs = d->mDtDue.secsTo(dtDue);
0209 
0210     /*if (mReadOnly) return;
0211     const Alarm::List& alarms = alarms();
0212     for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) {
0213       if (alarm->enabled()) {
0214         alarm->setTime(alarm->time().addSecs(diffsecs));
0215       }
0216     }*/
0217 
0218     Q_D(Todo);
0219     if (recurs() && !first) {
0220         d->setDtRecurrence(dtDue);
0221     } else {
0222         d->setDtDue(dtDue);
0223     }
0224 
0225     if (recurs() && dtDue.isValid() && (!dtStart().isValid() || dtDue < recurrence()->startDateTime())) {
0226         qCDebug(KCALCORE_LOG) << "To-do recurrences are now calculated against DTSTART. Fixing legacy to-do.";
0227         setDtStart(dtDue);
0228     }
0229 
0230     /*const Alarm::List& alarms = alarms();
0231     for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next())
0232       alarm->setAlarmStart(d->mDtDue);*/
0233     endUpdates();
0234 }
0235 
0236 QDateTime Todo::dtDue(bool first) const
0237 {
0238     if (!hasDueDate()) {
0239         return QDateTime();
0240     }
0241 
0242     Q_D(const Todo);
0243     const QDateTime start = IncidenceBase::dtStart();
0244     if (recurs() && !first && d->dtRecurrence().isValid()) {
0245         if (start.isValid()) {
0246             // This is the normal case, recurring to-dos have a valid DTSTART.
0247             const qint64 duration = start.daysTo(d->dtDue());
0248             QDateTime dt = d->dtRecurrence().addDays(duration);
0249             dt.setTime(d->dtDue().time());
0250             return dt;
0251         } else {
0252             // This is a legacy case, where recurrence was calculated against DTDUE
0253             return d->dtRecurrence();
0254         }
0255     }
0256 
0257     return d->dtDue();
0258 }
0259 
0260 bool Todo::hasDueDate() const
0261 {
0262     Q_D(const Todo);
0263     return d->dtDue().isValid();
0264 }
0265 
0266 bool Todo::hasStartDate() const
0267 {
0268     return IncidenceBase::dtStart().isValid();
0269 }
0270 
0271 QDateTime Todo::dtStart() const
0272 {
0273     return dtStart(/*first=*/false);
0274 }
0275 
0276 QDateTime Todo::dtStart(bool first) const
0277 {
0278     if (!hasStartDate()) {
0279         return QDateTime();
0280     }
0281 
0282     Q_D(const Todo);
0283     if (recurs() && !first && d->dtRecurrence().isValid()) {
0284         return d->dtRecurrence();
0285     } else {
0286         return IncidenceBase::dtStart();
0287     }
0288 }
0289 
0290 bool Todo::isCompleted() const
0291 {
0292     Q_D(const Todo);
0293     return d->percentComplete() == 100 || status() == StatusCompleted || hasCompletedDate();
0294 }
0295 
0296 void Todo::setCompleted(bool completed)
0297 {
0298     update();
0299     Q_D(Todo);
0300     if (completed) {
0301         d->setPercentComplete(100);
0302     } else {
0303         d->setPercentComplete(0);
0304         if (hasCompletedDate()) {
0305             d->setCompleted(QDateTime());
0306         }
0307     }
0308     updated();
0309 
0310     setStatus(completed ? StatusCompleted : StatusNone);    // Calls update()/updated().
0311 }
0312 
0313 QDateTime Todo::completed() const
0314 {
0315     Q_D(const Todo);
0316     if (hasCompletedDate()) {
0317         return d->completed();
0318     } else {
0319         return QDateTime();
0320     }
0321 }
0322 
0323 void Todo::setCompleted(const QDateTime &completed)
0324 {
0325     Q_D(Todo);
0326     if (!d->recurTodo(this)) { // May indirectly call update()/updated().
0327         update();
0328         d->setPercentComplete(100);
0329         d->setCompleted(completed);
0330         updated();
0331     }
0332     if (status() != StatusNone) {
0333         setStatus(StatusCompleted); // Calls update()/updated()
0334     }
0335 }
0336 
0337 bool Todo::hasCompletedDate() const
0338 {
0339     Q_D(const Todo);
0340     return d->completed().isValid();
0341 }
0342 
0343 int Todo::percentComplete() const
0344 {
0345     Q_D(const Todo);
0346     return d->percentComplete();
0347 }
0348 
0349 void Todo::setPercentComplete(int percent)
0350 {
0351     if (percent > 100) {
0352         percent = 100;
0353     } else if (percent < 0) {
0354         percent = 0;
0355     }
0356 
0357     update();
0358     Q_D(Todo);
0359     d->setPercentComplete(percent);
0360     if (percent != 100) {
0361         d->setCompleted(QDateTime());
0362     }
0363     updated();
0364     if (percent != 100 && status() == Incidence::StatusCompleted) {
0365         setStatus(Incidence::StatusNone);   // Calls update()/updated().
0366     }
0367 }
0368 
0369 bool Todo::isInProgress(bool first) const
0370 {
0371     if (isOverdue()) {
0372         return false;
0373     }
0374 
0375     Q_D(const Todo);
0376     if (d->percentComplete() > 0) {
0377         return true;
0378     }
0379 
0380     if (hasStartDate() && hasDueDate()) {
0381         if (allDay()) {
0382             QDate currDate = QDate::currentDate();
0383             if (dtStart(first).date() <= currDate && currDate < dtDue(first).date()) {
0384                 return true;
0385             }
0386         } else {
0387             QDateTime currDate = QDateTime::currentDateTimeUtc();
0388             if (dtStart(first) <= currDate && currDate < dtDue(first)) {
0389                 return true;
0390             }
0391         }
0392     }
0393 
0394     return false;
0395 }
0396 
0397 bool Todo::isOpenEnded() const
0398 {
0399     if (!hasDueDate() && !isCompleted()) {
0400         return true;
0401     }
0402     return false;
0403 }
0404 
0405 bool Todo::isNotStarted(bool first) const
0406 {
0407     Q_D(const Todo);
0408     if (d->percentComplete() > 0) {
0409         return false;
0410     }
0411 
0412     if (!hasStartDate()) {
0413         return false;
0414     }
0415 
0416     if (allDay()) {
0417         if (dtStart(first).date() >= QDate::currentDate()) {
0418             return false;
0419         }
0420     } else {
0421         if (dtStart(first) >= QDateTime::currentDateTimeUtc()) {
0422             return false;
0423         }
0424     }
0425     return true;
0426 }
0427 
0428 void Todo::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone)
0429 {
0430     Q_D(Todo);
0431     Incidence::shiftTimes(oldZone, newZone);
0432     auto dt = d->dtDue().toTimeZone(oldZone);
0433     dt.setTimeZone(newZone);
0434     d->setDtDue(dt);
0435     if (recurs()) {
0436         auto dr = d->dtRecurrence().toTimeZone(oldZone);
0437         dr.setTimeZone(newZone);
0438         d->setDtRecurrence(dr);
0439     }
0440     if (hasCompletedDate()) {
0441         auto dc = d->completed().toTimeZone(oldZone);
0442         dc.setTimeZone(newZone);
0443         d->setCompleted(dc);
0444     }
0445 }
0446 
0447 void Todo::setDtRecurrence(const QDateTime &dt)
0448 {
0449     Q_D(Todo);
0450     d->setDtRecurrence(dt);
0451 }
0452 
0453 QDateTime Todo::dtRecurrence() const
0454 {
0455     Q_D(const Todo);
0456     auto dt = d->dtRecurrence();
0457     if (!dt.isValid()) {
0458         dt = IncidenceBase::dtStart();
0459     }
0460     if (!dt.isValid()) {
0461         dt = d->dtDue();
0462     }
0463     return dt;
0464 }
0465 
0466 bool Todo::recursOn(const QDate &date, const QTimeZone &timeZone) const
0467 {
0468     Q_D(const Todo);
0469     QDate today = QDate::currentDate();
0470     return Incidence::recursOn(date, timeZone) && !(date < today && d->dtRecurrence().date() < today && d->dtRecurrence() > recurrence()->startDateTime());
0471 }
0472 
0473 bool Todo::isOverdue() const
0474 {
0475     if (!dtDue().isValid()) {
0476         return false; // if it's never due, it can't be overdue
0477     }
0478 
0479     const bool inPast = allDay() ? dtDue().date() < QDate::currentDate() : dtDue() < QDateTime::currentDateTimeUtc();
0480 
0481     return inPast && !isCompleted();
0482 }
0483 
0484 void Todo::setAllDay(bool allday)
0485 {
0486     if (allday != allDay() && !mReadOnly) {
0487         if (hasDueDate()) {
0488             setFieldDirty(FieldDtDue);
0489         }
0490         Incidence::setAllDay(allday);
0491     }
0492 }
0493 
0494 //@cond PRIVATE
0495 bool TodoPrivate::recurTodo(Todo *todo)
0496 {
0497     if (todo && todo->recurs()) {
0498         Recurrence *r = todo->recurrence();
0499         const QDateTime recurrenceEndDateTime = r->endDateTime();
0500         QDateTime nextOccurrenceDateTime = r->getNextDateTime(todo->dtStart());
0501 
0502         if ((r->duration() == -1 || (nextOccurrenceDateTime.isValid() && recurrenceEndDateTime.isValid() && nextOccurrenceDateTime <= recurrenceEndDateTime))) {
0503             // We convert to the same timeSpec so we get the correct .date()
0504             const auto rightNow = QDateTime::currentDateTimeUtc().toTimeZone(nextOccurrenceDateTime.timeZone());
0505             const bool isDateOnly = todo->allDay();
0506 
0507             /* Now we search for the occurrence that's _after_ the currentUtcDateTime, or
0508              * if it's dateOnly, the occurrrence that's _during or after today_.
0509              * The reason we use "<" for date only, but "<=" for occurrences with time is that
0510              * if it's date only, the user can still complete that occurrence today, so that's
0511              * the current occurrence that needs completing.
0512              */
0513             while (!todo->recursAt(nextOccurrenceDateTime) || (!isDateOnly && nextOccurrenceDateTime <= rightNow)
0514                    || (isDateOnly && nextOccurrenceDateTime.date() < rightNow.date())) {
0515                 if (!nextOccurrenceDateTime.isValid() || (nextOccurrenceDateTime > recurrenceEndDateTime && r->duration() != -1)) {
0516                     return false;
0517                 }
0518                 nextOccurrenceDateTime = r->getNextDateTime(nextOccurrenceDateTime);
0519             }
0520 
0521             todo->setDtRecurrence(nextOccurrenceDateTime);
0522             todo->setCompleted(false);
0523             todo->setRevision(todo->revision() + 1);
0524 
0525             return true;
0526         }
0527     }
0528 
0529     return false;
0530 }
0531 //@endcond
0532 
0533 bool Todo::accept(Visitor &v, const IncidenceBase::Ptr &incidence)
0534 {
0535     return v.visit(incidence.staticCast<Todo>());
0536 }
0537 
0538 QDateTime Todo::dateTime(DateTimeRole role) const
0539 {
0540     switch (role) {
0541     case RoleAlarmStartOffset:
0542         return dtStart();
0543     case RoleAlarmEndOffset:
0544         return dtDue();
0545     case RoleSort:
0546         // Sorting to-dos first compares dtDue, then dtStart if
0547         // dtDue doesn't exist
0548         return hasDueDate() ? dtDue() : dtStart();
0549     case RoleCalendarHashing:
0550         return dtDue();
0551     case RoleStartTimeZone:
0552         return dtStart();
0553     case RoleEndTimeZone:
0554         return dtDue();
0555     case RoleEndRecurrenceBase:
0556         return dtDue();
0557     case RoleDisplayStart:
0558     case RoleDisplayEnd:
0559         return dtDue().isValid() ? dtDue() : dtStart();
0560     case RoleAlarm:
0561         if (alarms().isEmpty()) {
0562             return QDateTime();
0563         } else {
0564             Alarm::Ptr alarm = alarms().at(0);
0565             if (alarm->hasStartOffset() && hasStartDate()) {
0566                 return dtStart();
0567             } else if (alarm->hasEndOffset() && hasDueDate()) {
0568                 return dtDue();
0569             } else {
0570                 // The application shouldn't add alarms on to-dos without dates.
0571                 return QDateTime();
0572             }
0573         }
0574     case RoleRecurrenceStart:
0575         if (dtStart().isValid()) {
0576             return dtStart();
0577         }
0578         return dtDue(); // For the sake of backwards compatibility
0579     // where we calculated recurrences based on dtDue
0580     case RoleEnd:
0581         return dtDue();
0582     default:
0583         return QDateTime();
0584     }
0585 }
0586 
0587 void Todo::setDateTime(const QDateTime &dateTime, DateTimeRole role)
0588 {
0589     switch (role) {
0590     case RoleDnD:
0591         setDtDue(dateTime);
0592         break;
0593     case RoleEnd:
0594         setDtDue(dateTime, true);
0595         break;
0596     default:
0597         qCDebug(KCALCORE_LOG) << "Unhandled role" << role;
0598     }
0599 }
0600 
0601 void Todo::virtual_hook(VirtualHook id, void *data)
0602 {
0603     Q_UNUSED(id);
0604     Q_UNUSED(data);
0605 }
0606 
0607 QLatin1String Todo::mimeType() const
0608 {
0609     return Todo::todoMimeType();
0610 }
0611 
0612 QLatin1String Todo::todoMimeType()
0613 {
0614     return QLatin1String("application/x-vnd.akonadi.calendar.todo");
0615 }
0616 
0617 QLatin1String Todo::iconName(const QDateTime &recurrenceId) const
0618 {
0619     const bool usesCompletedTaskPixmap = isCompleted() || (recurs() && recurrenceId.isValid() && (recurrenceId < dtStart(/*first=*/false)));
0620 
0621     if (usesCompletedTaskPixmap) {
0622         return QLatin1String("task-complete");
0623     } else {
0624         return QLatin1String("view-calendar-tasks");
0625     }
0626 }
0627 
0628 void Todo::serialize(QDataStream &out) const
0629 {
0630     Q_D(const Todo);
0631     Incidence::serialize(out);
0632     serializeQDateTimeAsKDateTime(out, d->dtDue());
0633     serializeQDateTimeAsKDateTime(out, d->dtRecurrence());
0634     serializeQDateTimeAsKDateTime(out, d->completed());
0635     out << d->percentComplete();
0636 }
0637 
0638 void TodoPrivate::deserialize(QDataStream &in)
0639 {
0640     deserializeKDateTimeAsQDateTime(in, mDtDue);
0641     deserializeKDateTimeAsQDateTime(in, mDtRecurrence);
0642     deserializeKDateTimeAsQDateTime(in, mCompleted);
0643     in >> mPercentComplete;
0644 }
0645 
0646 void Todo::deserialize(QDataStream &in)
0647 {
0648     Q_D(Todo);
0649     Incidence::deserialize(in);
0650     d->deserialize(in);
0651 }
0652 
0653 bool Todo::supportsGroupwareCommunication() const
0654 {
0655     return true;
0656 }