File indexing completed on 2024-04-28 15:19:12
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 }