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 | |
42 | using namespace KCalCore; |
43 | |
44 | /** |
45 | Private class that helps to provide binary compatibility between releases. |
46 | @internal |
47 | */ |
48 | //@cond PRIVATE |
49 | class KCalCore::Todo::Private |
50 | { |
51 | public: |
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 | |
74 | void 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 | |
84 | Todo::Todo() |
85 | : d(new KCalCore::Todo::Private) |
86 | { |
87 | } |
88 | |
89 | Todo::Todo(const Todo &other) |
90 | : Incidence(other), |
91 | d(new KCalCore::Todo::Private(*other.d)) |
92 | { |
93 | } |
94 | |
95 | Todo::~Todo() |
96 | { |
97 | delete d; |
98 | } |
99 | |
100 | Todo *Todo::clone() const |
101 | { |
102 | return new Todo(*this); |
103 | } |
104 | |
105 | IncidenceBase &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 | |
115 | bool 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 | |
133 | Incidence::IncidenceType Todo::type() const |
134 | { |
135 | return TypeTodo; |
136 | } |
137 | |
138 | QByteArray Todo::typeStr() const |
139 | { |
140 | return "Todo" ; |
141 | } |
142 | void 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 | |
174 | KDateTime 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 | |
197 | bool Todo::hasDueDate() const |
198 | { |
199 | return d->mDtDue.isValid(); |
200 | } |
201 | |
202 | void 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 | |
221 | bool Todo::hasStartDate() const |
222 | { |
223 | return IncidenceBase::dtStart().isValid(); |
224 | } |
225 | |
226 | void 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 | |
254 | KDateTime Todo::dtStart() const |
255 | { |
256 | return dtStart(/*first=*/false); |
257 | } |
258 | |
259 | KDateTime 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 | |
272 | void Todo::setDtStart(const KDateTime &dtStart) |
273 | { |
274 | Incidence::setDtStart(dtStart); |
275 | } |
276 | |
277 | bool Todo::isCompleted() const |
278 | { |
279 | return d->mPercentComplete == 100; |
280 | } |
281 | |
282 | void 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 | |
295 | KDateTime Todo::completed() const |
296 | { |
297 | if (hasCompletedDate()) { |
298 | return d->mCompleted; |
299 | } else { |
300 | return KDateTime(); |
301 | } |
302 | } |
303 | |
304 | void 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 | |
315 | bool Todo::hasCompletedDate() const |
316 | { |
317 | return d->mCompleted.isValid(); |
318 | } |
319 | |
320 | int Todo::percentComplete() const |
321 | { |
322 | return d->mPercentComplete; |
323 | } |
324 | |
325 | void 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 | |
342 | bool 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 | |
369 | bool Todo::isOpenEnded() const |
370 | { |
371 | if (!hasDueDate() && !isCompleted()) { |
372 | return true; |
373 | } |
374 | return false; |
375 | |
376 | } |
377 | |
378 | bool 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 | |
400 | void 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 | |
416 | void Todo::setDtRecurrence(const KDateTime &dt) |
417 | { |
418 | d->mDtRecurrence = dt; |
419 | setFieldDirty(FieldRecurrence); |
420 | } |
421 | |
422 | KDateTime Todo::dtRecurrence() const |
423 | { |
424 | return d->mDtRecurrence.isValid() ? d->mDtRecurrence : d->mDtDue; |
425 | } |
426 | |
427 | bool 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 | |
436 | bool 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 | |
448 | void 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 |
459 | bool 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 | |
503 | bool Todo::accept(Visitor &v, IncidenceBase::Ptr incidence) |
504 | { |
505 | return v.visit(incidence.staticCast<Todo>()); |
506 | } |
507 | |
508 | KDateTime 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 | |
557 | void 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 | |
571 | void 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 | |
585 | QLatin1String Todo::mimeType() const |
586 | { |
587 | return Todo::todoMimeType(); |
588 | } |
589 | |
590 | QLatin1String Todo::todoMimeType() |
591 | { |
592 | return QLatin1String("application/x-vnd.akonadi.calendar.todo" ); |
593 | } |
594 | |
595 | QLatin1String 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 | |
614 | void Todo::serialize(QDataStream &out) |
615 | { |
616 | Incidence::serialize(out); |
617 | out << d->mDtDue << d->mDtRecurrence << d->mCompleted << d->mPercentComplete; |
618 | } |
619 | |
620 | void Todo::deserialize(QDataStream &in) |
621 | { |
622 | Incidence::deserialize(in); |
623 | in >> d->mDtDue >> d->mDtRecurrence >> d->mCompleted >> d->mPercentComplete; |
624 | } |
625 | |