1/*
2 This file is part of the kcalcore library.
3
4 Copyright (c) 2001-2003 Cornelius Schumacher <schumacher@kde.org>
5 Copyright (C) 2009 Allen Winter <winter@kde.org>
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License as published by the Free Software Foundation; either
10 version 2 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Library General Public License for more details.
16
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to
19 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 Boston, MA 02110-1301, USA.
21*/
22/**
23 @file
24 This file is part of the API for handling calendar data and
25 defines the Todo class.
26
27 @brief
28 Provides a To-do in the sense of RFC2445.
29
30 @author Cornelius Schumacher \<schumacher@kde.org\>
31 @author Allen Winter \<winter@kde.org\>
32*/
33
34#include "todo.h"
35#include "visitor.h"
36#include "recurrence.h"
37
38#include <KDebug>
39
40#include <QTime>
41
42using namespace KCalCore;
43
44/**
45 Private class that helps to provide binary compatibility between releases.
46 @internal
47*/
48//@cond PRIVATE
49class KCalCore::Todo::Private
50{
51public:
52 Private()
53 : mPercentComplete(0)
54 {}
55 Private(const KCalCore::Todo::Private &other)
56 {
57 init(other);
58 }
59
60 void init(const KCalCore::Todo::Private &other);
61
62 KDateTime mDtDue; // to-do due date (if there is one)
63 // ALSO the first occurrence of a recurring to-do
64 KDateTime mDtRecurrence; // next occurrence (for recurring to-dos)
65 KDateTime mCompleted; // to-do completion date (if it has been completed)
66 int mPercentComplete; // to-do percent complete [0,100]
67
68 /**
69 Returns true if the todo got a new date, else false will be returned.
70 */
71 bool recurTodo(Todo *todo);
72};
73
74void KCalCore::Todo::Private::init(const KCalCore::Todo::Private &other)
75{
76 mDtDue = other.mDtDue;
77 mDtRecurrence = other.mDtRecurrence;
78 mCompleted = other.mCompleted;
79 mPercentComplete = other.mPercentComplete;
80}
81
82//@endcond
83
84Todo::Todo()
85 : d(new KCalCore::Todo::Private)
86{
87}
88
89Todo::Todo(const Todo &other)
90 : Incidence(other),
91 d(new KCalCore::Todo::Private(*other.d))
92{
93}
94
95Todo::~Todo()
96{
97 delete d;
98}
99
100Todo *Todo::clone() const
101{
102 return new Todo(*this);
103}
104
105IncidenceBase &Todo::assign(const IncidenceBase &other)
106{
107 if (&other != this) {
108 Incidence::assign(other);
109 const Todo *t = static_cast<const Todo*>(&other);
110 d->init(*(t->d));
111 }
112 return *this;
113}
114
115bool Todo::equals(const IncidenceBase &todo) const
116{
117 if (!Incidence::equals(todo)) {
118 return false;
119 } else {
120 // If they weren't the same type IncidenceBase::equals would had returned false already
121 const Todo *t = static_cast<const Todo*>(&todo);
122 return ((dtDue() == t->dtDue()) ||
123 (!dtDue().isValid() && !t->dtDue().isValid())) &&
124 hasDueDate() == t->hasDueDate() &&
125 hasStartDate() == t->hasStartDate() &&
126 ((completed() == t->completed()) ||
127 (!completed().isValid() && !t->completed().isValid())) &&
128 hasCompletedDate() == t->hasCompletedDate() &&
129 percentComplete() == t->percentComplete();
130 }
131}
132
133Incidence::IncidenceType Todo::type() const
134{
135 return TypeTodo;
136}
137
138QByteArray Todo::typeStr() const
139{
140 return "Todo";
141}
142void Todo::setDtDue(const KDateTime &dtDue, bool first)
143{
144 startUpdates();
145
146 //int diffsecs = d->mDtDue.secsTo(dtDue);
147
148 /*if (mReadOnly) return;
149 const Alarm::List& alarms = alarms();
150 for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) {
151 if (alarm->enabled()) {
152 alarm->setTime(alarm->time().addSecs(diffsecs));
153 }
154 }*/
155
156 if (recurs() && !first) {
157 d->mDtRecurrence = dtDue;
158 } else {
159 d->mDtDue = dtDue;
160 }
161
162 if (recurs() && dtDue.isValid() && (!dtStart().isValid() || dtDue < recurrence()->startDateTime())) {
163 kDebug() << "To-do recurrences are now calculated against DTSTART. Fixing legacy to-do.";
164 setDtStart(dtDue);
165 }
166
167 /*const Alarm::List& alarms = alarms();
168 for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next())
169 alarm->setAlarmStart(d->mDtDue);*/
170 setFieldDirty(FieldDtDue);
171 endUpdates();
172}
173
174KDateTime Todo::dtDue(bool first) const
175{
176 if (!hasDueDate()) {
177 return KDateTime();
178 }
179
180 const KDateTime start = IncidenceBase::dtStart();
181 if (recurs() && !first && d->mDtRecurrence.isValid()) {
182 if (start.isValid()) {
183 // This is the normal case, recurring to-dos have a valid DTSTART.
184 const int duration = start.daysTo(d->mDtDue);
185 KDateTime dt = d->mDtRecurrence.addDays(duration);
186 dt.setTime(d->mDtDue.time());
187 return dt;
188 } else {
189 // This is a legacy case, where recurrence was calculated against DTDUE
190 return d->mDtRecurrence;
191 }
192 }
193
194 return d->mDtDue;
195}
196
197bool Todo::hasDueDate() const
198{
199 return d->mDtDue.isValid();
200}
201
202void Todo::setHasDueDate(bool has)
203{
204 if (mReadOnly) {
205 return;
206 }
207 update();
208 if (!has) {
209 d->mDtDue = KDateTime();
210
211 if (!dtStart().isValid()) {
212 // Recurrence is only calculated against dtdue if dtstart is invalid
213 d->mDtRecurrence = KDateTime();
214 }
215 }
216
217 setFieldDirty(FieldDtDue);
218 updated();
219}
220
221bool Todo::hasStartDate() const
222{
223 return IncidenceBase::dtStart().isValid();
224}
225
226void Todo::setHasStartDate(bool has)
227{
228 if (mReadOnly) {
229 return;
230 }
231
232 update();
233 if (recurs() && !has) {
234 if (!comments().filter(QLatin1String("NoStartDate")).count()) {
235 addComment(QLatin1String("NoStartDate")); //TODO: --> custom flag?
236 }
237 } else {
238 QString s(QLatin1String("NoStartDate"));
239 removeComment(s);
240 }
241
242 if (!has) {
243 if (dtStart().isValid() && d->mDtDue.isValid()) {
244 // If dtstart is invalid then recurrence is calculated against dtdue, so don't clear it.
245 d->mDtRecurrence = KDateTime();
246 }
247 setDtStart(KDateTime());
248 }
249
250 setFieldDirty(FieldDtStart);
251 updated();
252}
253
254KDateTime Todo::dtStart() const
255{
256 return dtStart(/*first=*/false);
257}
258
259KDateTime Todo::dtStart(bool first) const
260{
261 if (!hasStartDate()) {
262 return KDateTime();
263 }
264
265 if (recurs() && !first && d->mDtRecurrence.isValid()) {
266 return d->mDtRecurrence;
267 } else {
268 return IncidenceBase::dtStart();
269 }
270}
271
272void Todo::setDtStart(const KDateTime &dtStart)
273{
274 Incidence::setDtStart(dtStart);
275}
276
277bool Todo::isCompleted() const
278{
279 return d->mPercentComplete == 100;
280}
281
282void Todo::setCompleted(bool completed)
283{
284 update();
285 if (completed) {
286 d->mPercentComplete = 100;
287 } else {
288 d->mPercentComplete = 0;
289 d->mCompleted = KDateTime();
290 }
291 setFieldDirty(FieldCompleted);
292 updated();
293}
294
295KDateTime Todo::completed() const
296{
297 if (hasCompletedDate()) {
298 return d->mCompleted;
299 } else {
300 return KDateTime();
301 }
302}
303
304void Todo::setCompleted(const KDateTime &completed)
305{
306 update();
307 if (!d->recurTodo(this)) {
308 d->mPercentComplete = 100;
309 d->mCompleted = completed.toUtc();
310 setFieldDirty(FieldCompleted);
311 }
312 updated();
313}
314
315bool Todo::hasCompletedDate() const
316{
317 return d->mCompleted.isValid();
318}
319
320int Todo::percentComplete() const
321{
322 return d->mPercentComplete;
323}
324
325void Todo::setPercentComplete(int percent)
326{
327 if (percent > 100) {
328 percent = 100;
329 } else if (percent < 0) {
330 percent = 0;
331 }
332
333 update();
334 d->mPercentComplete = percent;
335 if (percent != 100) {
336 d->mCompleted = KDateTime();
337 }
338 setFieldDirty(FieldPercentComplete);
339 updated();
340}
341
342bool Todo::isInProgress(bool first) const
343{
344 if (isOverdue()) {
345 return false;
346 }
347
348 if (d->mPercentComplete > 0) {
349 return true;
350 }
351
352 if (hasStartDate() && hasDueDate()) {
353 if (allDay()) {
354 QDate currDate = QDate::currentDate();
355 if (dtStart(first).date() <= currDate && currDate < dtDue(first).date()) {
356 return true;
357 }
358 } else {
359 KDateTime currDate = KDateTime::currentUtcDateTime();
360 if (dtStart(first) <= currDate && currDate < dtDue(first)) {
361 return true;
362 }
363 }
364 }
365
366 return false;
367}
368
369bool Todo::isOpenEnded() const
370{
371 if (!hasDueDate() && !isCompleted()) {
372 return true;
373 }
374 return false;
375
376}
377
378bool Todo::isNotStarted(bool first) const
379{
380 if (d->mPercentComplete > 0) {
381 return false;
382 }
383
384 if (!hasStartDate()) {
385 return false;
386 }
387
388 if (allDay()) {
389 if (dtStart(first).date() >= QDate::currentDate()) {
390 return false;
391 }
392 } else {
393 if (dtStart(first) >= KDateTime::currentUtcDateTime()) {
394 return false;
395 }
396 }
397 return true;
398}
399
400void Todo::shiftTimes(const KDateTime::Spec &oldSpec,
401 const KDateTime::Spec &newSpec)
402{
403 Incidence::shiftTimes(oldSpec, newSpec);
404 d->mDtDue = d->mDtDue.toTimeSpec(oldSpec);
405 d->mDtDue.setTimeSpec(newSpec);
406 if (recurs()) {
407 d->mDtRecurrence = d->mDtRecurrence.toTimeSpec(oldSpec);
408 d->mDtRecurrence.setTimeSpec(newSpec);
409 }
410 if (hasCompletedDate()) {
411 d->mCompleted = d->mCompleted.toTimeSpec(oldSpec);
412 d->mCompleted.setTimeSpec(newSpec);
413 }
414}
415
416void Todo::setDtRecurrence(const KDateTime &dt)
417{
418 d->mDtRecurrence = dt;
419 setFieldDirty(FieldRecurrence);
420}
421
422KDateTime Todo::dtRecurrence() const
423{
424 return d->mDtRecurrence.isValid() ? d->mDtRecurrence : d->mDtDue;
425}
426
427bool Todo::recursOn(const QDate &date, const KDateTime::Spec &timeSpec) const
428{
429 QDate today = QDate::currentDate();
430 return
431 Incidence::recursOn(date, timeSpec) &&
432 !(date < today && d->mDtRecurrence.date() < today &&
433 d->mDtRecurrence > recurrence()->startDateTime());
434}
435
436bool Todo::isOverdue() const
437{
438 if (!dtDue().isValid()) {
439 return false; // if it's never due, it can't be overdue
440 }
441
442 const bool inPast = allDay() ? dtDue().date() < QDate::currentDate()
443 : dtDue() < KDateTime::currentUtcDateTime();
444
445 return inPast && !isCompleted();
446}
447
448void Todo::setAllDay(bool allday)
449{
450 if (allday != allDay() && !mReadOnly) {
451 if (hasDueDate()) {
452 setFieldDirty(FieldDtDue);
453 }
454 Incidence::setAllDay(allday);
455 }
456}
457
458//@cond PRIVATE
459bool Todo::Private::recurTodo(Todo *todo)
460{
461 if (todo && todo->recurs()) {
462 Recurrence *r = todo->recurrence();
463 const KDateTime recurrenceEndDateTime = r->endDateTime();
464 KDateTime nextOccurrenceDateTime = r->getNextDateTime(todo->dtStart());
465
466 if ((r->duration() == -1 ||
467 (nextOccurrenceDateTime.isValid() && recurrenceEndDateTime.isValid() &&
468 nextOccurrenceDateTime <= recurrenceEndDateTime))) {
469 // We convert to the same timeSpec so we get the correct .date()
470 const KDateTime rightNow =
471 KDateTime::currentUtcDateTime().toTimeSpec(nextOccurrenceDateTime.timeSpec());
472 const bool isDateOnly = todo->allDay();
473
474 /* Now we search for the occurrence that's _after_ the currentUtcDateTime, or
475 * if it's dateOnly, the occurrrence that's _during or after today_.
476 * The reason we use "<" for date only, but "<=" for ocurrences with time is that
477 * if it's date only, the user can still complete that ocurrence today, so that's
478 * the current ocurrence that needs completing.
479 */
480 while (!todo->recursAt(nextOccurrenceDateTime) ||
481 (!isDateOnly && nextOccurrenceDateTime <= rightNow) ||
482 (isDateOnly && nextOccurrenceDateTime.date() < rightNow.date())) {
483
484 if (!nextOccurrenceDateTime.isValid() ||
485 (nextOccurrenceDateTime > recurrenceEndDateTime && r->duration() != -1)) {
486 return false;
487 }
488 nextOccurrenceDateTime = r->getNextDateTime(nextOccurrenceDateTime);
489 }
490
491 todo->setDtRecurrence(nextOccurrenceDateTime);
492 todo->setCompleted(false);
493 todo->setRevision(todo->revision() + 1);
494
495 return true;
496 }
497 }
498
499 return false;
500}
501//@endcond
502
503bool Todo::accept(Visitor &v, IncidenceBase::Ptr incidence)
504{
505 return v.visit(incidence.staticCast<Todo>());
506}
507
508KDateTime Todo::dateTime(DateTimeRole role) const
509{
510 switch (role) {
511 case RoleAlarmStartOffset:
512 return dtStart();
513 case RoleAlarmEndOffset:
514 return dtDue();
515 case RoleSort:
516 // Sorting to-dos first compares dtDue, then dtStart if
517 // dtDue doesn't exist
518 return hasDueDate() ? dtDue() : dtStart();
519 case RoleCalendarHashing:
520 return dtDue();
521 case RoleStartTimeZone:
522 return dtStart();
523 case RoleEndTimeZone:
524 return dtDue();
525 case RoleEndRecurrenceBase:
526 return dtDue();
527 case RoleDisplayStart:
528 case RoleDisplayEnd:
529 return dtDue().isValid() ? dtDue() : dtStart();
530 case RoleAlarm:
531 if (alarms().isEmpty()) {
532 return KDateTime();
533 } else {
534 Alarm::Ptr alarm = alarms().first();
535 if (alarm->hasStartOffset() && hasStartDate()) {
536 return dtStart();
537 } else if (alarm->hasEndOffset() && hasDueDate()) {
538 return dtDue();
539 } else {
540 // The application shouldn't add alarms on to-dos without dates.
541 return KDateTime();
542 }
543 }
544 case RoleRecurrenceStart:
545 if (dtStart().isValid()) {
546 return dtStart();
547 }
548 return dtDue(); //For the sake of backwards compatibility
549 //where we calculated recurrences based on dtDue
550 case RoleEnd:
551 return dtDue();
552 default:
553 return KDateTime();
554 }
555}
556
557void Todo::setDateTime(const KDateTime &dateTime, DateTimeRole role)
558{
559 switch (role) {
560 case RoleDnD:
561 setDtDue(dateTime);
562 break;
563 case RoleEnd:
564 setDtDue(dateTime, true);
565 break;
566 default:
567 kDebug() << "Unhandled role" << role;
568 }
569}
570
571void Todo::virtual_hook(int id, void *data)
572{
573 switch (static_cast<IncidenceBase::VirtualHook>(id)) {
574 case IncidenceBase::SerializerHook:
575 serialize(*reinterpret_cast<QDataStream*>(data));
576 break;
577 case IncidenceBase::DeserializerHook:
578 deserialize(*reinterpret_cast<QDataStream*>(data));
579 break;
580 default:
581 Q_ASSERT(false);
582 }
583}
584
585QLatin1String Todo::mimeType() const
586{
587 return Todo::todoMimeType();
588}
589
590QLatin1String Todo::todoMimeType()
591{
592 return QLatin1String("application/x-vnd.akonadi.calendar.todo");
593}
594
595QLatin1String Todo::iconName(const KDateTime &recurrenceId) const
596{
597 KDateTime occurrenceDT = recurrenceId;
598
599 if (recurs() && occurrenceDT.isDateOnly()) {
600 occurrenceDT.setTime(QTime(0, 0));
601 }
602
603 const bool usesCompletedTaskPixmap = isCompleted() ||
604 (recurs() && occurrenceDT.isValid() &&
605 occurrenceDT < dtDue(false));
606
607 if (usesCompletedTaskPixmap) {
608 return QLatin1String("task-complete");
609 } else {
610 return QLatin1String("view-calendar-tasks");
611 }
612}
613
614void Todo::serialize(QDataStream &out)
615{
616 Incidence::serialize(out);
617 out << d->mDtDue << d->mDtRecurrence << d->mCompleted << d->mPercentComplete;
618}
619
620void Todo::deserialize(QDataStream &in)
621{
622 Incidence::deserialize(in);
623 in >> d->mDtDue >> d->mDtRecurrence >> d->mCompleted >> d->mPercentComplete;
624}
625