1/*
2 This file is part of the kcal 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 "incidenceformatter.h"
36
37#include <kglobal.h>
38#include <klocale.h>
39#include <klocalizedstring.h>
40#include <kdebug.h>
41#include <ksystemtimezone.h>
42
43using namespace KCal;
44
45/**
46 Private class that helps to provide binary compatibility between releases.
47 @internal
48*/
49//@cond PRIVATE
50class KCal::Todo::Private
51{
52 public:
53 Private()
54 : mPercentComplete( 0 ),
55 mHasDueDate( false ),
56 mHasStartDate( false ),
57 mHasCompletedDate( false )
58 {}
59 Private( const KCal::Todo::Private &other )
60 { init( other ); }
61
62 void init( const KCal::Todo::Private &other );
63
64 KDateTime mDtDue; // to-do due date (if there is one)
65 // ALSO the first occurrence of a recurring to-do
66 KDateTime mDtRecurrence; // next occurrence (for recurring to-dos)
67 KDateTime mCompleted; // to-do completion date (if it has been completed)
68 int mPercentComplete; // to-do percent complete [0,100]
69 bool mHasDueDate; // true if the to-do has a due date
70 bool mHasStartDate; // true if the to-do has a starting date
71 bool mHasCompletedDate; // true if the to-do has a completion date
72
73 /**
74 Returns true if the todo got a new date, else false will be returned.
75 */
76 bool recurTodo( Todo *todo );
77};
78
79void KCal::Todo::Private::init( const KCal::Todo::Private &other )
80{
81 mDtDue = other.mDtDue;
82 mDtRecurrence = other.mDtRecurrence;
83 mCompleted = other.mCompleted;
84 mPercentComplete = other.mPercentComplete;
85 mHasDueDate = other.mHasDueDate;
86 mHasStartDate = other.mHasStartDate;
87 mHasCompletedDate = other.mHasCompletedDate;
88}
89
90//@endcond
91
92Todo::Todo()
93 : d( new KCal::Todo::Private )
94{
95}
96
97Todo::Todo( const Todo &other )
98 : Incidence( other ),
99 d( new KCal::Todo::Private( *other.d ) )
100{
101}
102
103Todo::~Todo()
104{
105 delete d;
106}
107
108Todo *Todo::clone()
109{
110 return new Todo( *this );
111}
112
113Todo &Todo::operator=( const Todo &other )
114{
115 // check for self assignment
116 if ( &other == this ) {
117 return *this;
118 }
119
120 Incidence::operator=( other );
121 d->init( *other.d );
122 return *this;
123}
124
125bool Todo::operator==( const Todo &todo ) const
126{
127 return
128 Incidence::operator==( todo ) &&
129 dtDue() == todo.dtDue() &&
130 hasDueDate() == todo.hasDueDate() &&
131 hasStartDate() == todo.hasStartDate() &&
132 completed() == todo.completed() &&
133 hasCompletedDate() == todo.hasCompletedDate() &&
134 percentComplete() == todo.percentComplete();
135}
136
137QByteArray Todo::type() const
138{
139 return "Todo";
140}
141
142//KDE5:
143//QString Todo::typeStr() const
144//{
145// return i18nc( "incidence type is to-do/task", "to-do" );
146//}
147
148void Todo::setDtDue( const KDateTime &dtDue, bool first )
149{
150 //int diffsecs = d->mDtDue.secsTo(dtDue);
151
152 /*if (mReadOnly) return;
153 const Alarm::List& alarms = alarms();
154 for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) {
155 if (alarm->enabled()) {
156 alarm->setTime(alarm->time().addSecs(diffsecs));
157 }
158 }*/
159
160 d->mHasDueDate = true;
161 if ( recurs() && !first ) {
162 d->mDtRecurrence = dtDue;
163 } else {
164 d->mDtDue = dtDue;
165 // TODO: This doesn't seem right...
166 recurrence()->setStartDateTime( dtDue );
167 recurrence()->setAllDay( allDay() );
168 }
169
170 if ( recurs() && dtDue < recurrence()->startDateTime() ) {
171 setDtStart( dtDue );
172 }
173
174 /*const Alarm::List& alarms = alarms();
175 for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next())
176 alarm->setAlarmStart(d->mDtDue);*/
177
178 updated();
179}
180
181KDateTime Todo::dtDue( bool first ) const
182{
183 if ( !hasDueDate() ) {
184 return KDateTime();
185 }
186 if ( recurs() && !first && d->mDtRecurrence.isValid() ) {
187 return d->mDtRecurrence;
188 }
189
190 return d->mDtDue;
191}
192
193QString Todo::dtDueTimeStr( bool shortfmt, const KDateTime::Spec &spec ) const
194{
195 if ( spec.isValid() ) {
196
197 QString timeZone;
198 if ( spec.timeZone() != KSystemTimeZones::local() ) {
199 timeZone = ' ' + spec.timeZone().name();
200 }
201
202 return KGlobal::locale()->formatTime(
203 dtDue( !recurs() ).toTimeSpec( spec ).time(), !shortfmt ) + timeZone;
204 } else {
205 return KGlobal::locale()->formatTime(
206 dtDue( !recurs() ).time(), !shortfmt );
207 }
208}
209
210QString Todo::dtDueDateStr( bool shortfmt, const KDateTime::Spec &spec ) const
211{
212 if ( spec.isValid() ) {
213
214 QString timeZone;
215 if ( spec.timeZone() != KSystemTimeZones::local() ) {
216 timeZone = ' ' + spec.timeZone().name();
217 }
218
219 return KGlobal::locale()->formatDate(
220 dtDue( !recurs() ).toTimeSpec( spec ).date(),
221 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone;
222 } else {
223 return KGlobal::locale()->formatDate(
224 dtDue( !recurs() ).date(),
225 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
226 }
227}
228
229QString Todo::dtDueStr( bool shortfmt, const KDateTime::Spec &spec ) const
230{
231 if ( allDay() ) {
232 return IncidenceFormatter::dateToString( dtDue(), shortfmt, spec );
233 }
234
235 if ( spec.isValid() ) {
236
237 QString timeZone;
238 if ( spec.timeZone() != KSystemTimeZones::local() ) {
239 timeZone = ' ' + spec.timeZone().name();
240 }
241
242 return KGlobal::locale()->formatDateTime(
243 dtDue( !recurs() ).toTimeSpec( spec ).dateTime(),
244 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone;
245 } else {
246 return KGlobal::locale()->formatDateTime(
247 dtDue( !recurs() ).dateTime(),
248 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
249 }
250}
251
252bool Todo::hasDueDate() const
253{
254 return d->mHasDueDate;
255}
256
257void Todo::setHasDueDate( bool f )
258{
259 if ( mReadOnly ) {
260 return;
261 }
262 d->mHasDueDate = f;
263 updated();
264}
265
266bool Todo::hasStartDate() const
267{
268 return d->mHasStartDate;
269}
270
271void Todo::setHasStartDate( bool f )
272{
273 if ( mReadOnly ) {
274 return;
275 }
276
277 if ( recurs() && !f ) {
278 if ( !comments().filter( "NoStartDate" ).count() ) {
279 addComment( "NoStartDate" ); //TODO: --> custom flag?
280 }
281 } else {
282 QString s( "NoStartDate" );
283 removeComment( s );
284 }
285 d->mHasStartDate = f;
286 updated();
287}
288
289KDateTime Todo::dtStart() const
290{
291 return dtStart( false );
292}
293
294KDateTime Todo::dtStart( bool first ) const
295{
296 if ( !hasStartDate() ) {
297 return KDateTime();
298 }
299 if ( recurs() && !first ) {
300 KDateTime dt = d->mDtRecurrence.addDays( dtDue( true ).daysTo( IncidenceBase::dtStart() ) );
301 dt.setTime( IncidenceBase::dtStart().time() );
302 return dt;
303 } else {
304 return IncidenceBase::dtStart();
305 }
306}
307
308void Todo::setDtStart( const KDateTime &dtStart )
309{
310 // TODO: This doesn't seem right (rfc 2445/6 says, recurrence is calculated from the dtstart...)
311 if ( recurs() ) {
312 recurrence()->setStartDateTime( d->mDtDue );
313 recurrence()->setAllDay( allDay() );
314 }
315 d->mHasStartDate = true;
316 IncidenceBase::setDtStart( dtStart );
317}
318
319QString Todo::dtStartTimeStr( bool shortfmt, bool first, const KDateTime::Spec &spec ) const
320{
321 if ( spec.isValid() ) {
322
323 QString timeZone;
324 if ( spec.timeZone() != KSystemTimeZones::local() ) {
325 timeZone = ' ' + spec.timeZone().name();
326 }
327
328 return KGlobal::locale()->formatTime(
329 dtStart( first ).toTimeSpec( spec ).time(), !shortfmt ) + timeZone;
330 } else {
331 return KGlobal::locale()->formatTime(
332 dtStart( first ).time(), !shortfmt );
333 }
334}
335
336QString Todo::dtStartTimeStr( bool shortfmt, const KDateTime::Spec &spec ) const
337{
338 return IncidenceFormatter::timeToString( dtStart(), shortfmt, spec );
339}
340
341QString Todo::dtStartDateStr( bool shortfmt, bool first, const KDateTime::Spec &spec ) const
342{
343 if ( spec.isValid() ) {
344
345 QString timeZone;
346 if ( spec.timeZone() != KSystemTimeZones::local() ) {
347 timeZone = ' ' + spec.timeZone().name();
348 }
349
350 return KGlobal::locale()->formatDate(
351 dtStart( first ).toTimeSpec( spec ).date(),
352 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone;
353 } else {
354 return KGlobal::locale()->formatDate(
355 dtStart( first ).date(),
356 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
357 }
358}
359
360QString Todo::dtStartDateStr( bool shortfmt, const KDateTime::Spec &spec ) const
361{
362 return IncidenceFormatter::dateToString( dtStart(), shortfmt, spec );
363}
364
365QString Todo::dtStartStr( bool shortfmt, bool first, const KDateTime::Spec &spec ) const
366{
367 if ( allDay() ) {
368 return IncidenceFormatter::dateToString( dtStart(), shortfmt, spec );
369 }
370
371 if ( spec.isValid() ) {
372
373 QString timeZone;
374 if ( spec.timeZone() != KSystemTimeZones::local() ) {
375 timeZone = ' ' + spec.timeZone().name();
376 }
377
378 return KGlobal::locale()->formatDateTime(
379 dtStart( first ).toTimeSpec( spec ).dateTime(),
380 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone;
381 } else {
382 return KGlobal::locale()->formatDateTime(
383 dtStart( first ).dateTime(),
384 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
385 }
386}
387
388QString Todo::dtStartStr( bool shortfmt, const KDateTime::Spec &spec ) const
389{
390 if ( allDay() ) {
391 return IncidenceFormatter::dateToString( dtStart(), shortfmt, spec );
392 }
393
394 if ( spec.isValid() ) {
395
396 QString timeZone;
397 if ( spec.timeZone() != KSystemTimeZones::local() ) {
398 timeZone = ' ' + spec.timeZone().name();
399 }
400
401 return KGlobal::locale()->formatDateTime(
402 dtStart().toTimeSpec( spec ).dateTime(),
403 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone;
404 } else {
405 return KGlobal::locale()->formatDateTime(
406 dtStart().dateTime(),
407 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
408 }
409}
410
411bool Todo::isCompleted() const
412{
413 if ( d->mPercentComplete == 100 ) {
414 return true;
415 } else {
416 return false;
417 }
418}
419
420void Todo::setCompleted( bool completed )
421{
422 if ( completed ) {
423 d->mPercentComplete = 100;
424 } else {
425 d->mPercentComplete = 0;
426 d->mHasCompletedDate = false;
427 d->mCompleted = KDateTime();
428 }
429 updated();
430}
431
432KDateTime Todo::completed() const
433{
434 if ( hasCompletedDate() ) {
435 return d->mCompleted;
436 } else {
437 return KDateTime();
438 }
439}
440
441QString Todo::completedStr( bool shortfmt ) const
442{
443 return
444 KGlobal::locale()->formatDateTime( d->mCompleted.dateTime(),
445 ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) );
446}
447
448void Todo::setCompleted( const KDateTime &completed )
449{
450 if ( !d->recurTodo( this ) ) {
451 d->mHasCompletedDate = true;
452 d->mPercentComplete = 100;
453 d->mCompleted = completed.toUtc();
454 }
455 updated();
456}
457
458bool Todo::hasCompletedDate() const
459{
460 return d->mHasCompletedDate;
461}
462
463int Todo::percentComplete() const
464{
465 return d->mPercentComplete;
466}
467
468void Todo::setPercentComplete( int percent )
469{
470 //TODO: (?) assert percent between 0 and 100, inclusive
471 d->mPercentComplete = percent;
472 if ( percent != 100 ) {
473 d->mHasCompletedDate = false;
474 }
475 updated();
476}
477
478bool Todo::isInProgress( bool first ) const
479{
480 if ( isOverdue() ) {
481 return false;
482 }
483
484 if ( d->mPercentComplete > 0 ) {
485 return true;
486 }
487
488 if ( d->mHasStartDate && d->mHasDueDate ) {
489 if ( allDay() ) {
490 QDate currDate = QDate::currentDate();
491 if ( dtStart( first ).date() <= currDate && currDate < dtDue( first ).date() ) {
492 return true;
493 }
494 } else {
495 KDateTime currDate = KDateTime::currentUtcDateTime();
496 if ( dtStart( first ) <= currDate && currDate < dtDue( first ) ) {
497 return true;
498 }
499 }
500 }
501
502 return false;
503}
504
505bool Todo::isOpenEnded() const
506{
507 if ( !d->mHasDueDate && !isCompleted() ) {
508 return true;
509 }
510 return false;
511
512}
513
514bool Todo::isNotStarted( bool first ) const
515{
516 if ( d->mPercentComplete > 0 ) {
517 return false;
518 }
519
520 if ( !d->mHasStartDate ) {
521 return false;
522 }
523
524 if ( allDay() ) {
525 if ( dtStart( first ).date() >= QDate::currentDate() ) {
526 return false;
527 }
528 } else {
529 if ( dtStart( first ) >= KDateTime::currentUtcDateTime() ) {
530 return false;
531 }
532 }
533 return true;
534}
535
536void Todo::shiftTimes( const KDateTime::Spec &oldSpec,
537 const KDateTime::Spec &newSpec )
538{
539 Incidence::shiftTimes( oldSpec, newSpec );
540 d->mDtDue = d->mDtDue.toTimeSpec( oldSpec );
541 d->mDtDue.setTimeSpec( newSpec );
542 if ( recurs() ) {
543 d->mDtRecurrence = d->mDtRecurrence.toTimeSpec( oldSpec );
544 d->mDtRecurrence.setTimeSpec( newSpec );
545 }
546 if ( d->mHasCompletedDate ) {
547 d->mCompleted = d->mCompleted.toTimeSpec( oldSpec );
548 d->mCompleted.setTimeSpec( newSpec );
549 }
550}
551
552void Todo::setDtRecurrence( const KDateTime &dt )
553{
554 d->mDtRecurrence = dt;
555}
556
557KDateTime Todo::dtRecurrence() const
558{
559 return d->mDtRecurrence.isValid() ? d->mDtRecurrence : d->mDtDue;
560}
561
562bool Todo::recursOn( const QDate &date, const KDateTime::Spec &timeSpec ) const
563{
564 QDate today = QDate::currentDate();
565 return
566 Incidence::recursOn( date, timeSpec ) &&
567 !( date < today && d->mDtRecurrence.date() < today &&
568 d->mDtRecurrence > recurrence()->startDateTime() );
569}
570
571bool Todo::isOverdue() const
572{
573 if ( !dtDue().isValid() ) {
574 return false; // if it's never due, it can't be overdue
575 }
576
577 bool inPast = allDay() ?
578 dtDue().date() < QDate::currentDate() :
579 dtDue() < KDateTime::currentUtcDateTime();
580 return inPast && !isCompleted();
581}
582
583KDateTime Todo::endDateRecurrenceBase() const
584{
585 return dtDue();
586}
587
588//@cond PRIVATE
589bool Todo::Private::recurTodo( Todo *todo )
590{
591 if ( todo->recurs() ) {
592 Recurrence *r = todo->recurrence();
593 KDateTime endDateTime = r->endDateTime();
594 KDateTime nextDate = r->getNextDateTime( todo->dtDue() );
595
596 if ( ( r->duration() == -1 ||
597 ( nextDate.isValid() && endDateTime.isValid() &&
598 nextDate <= endDateTime ) ) ) {
599
600 while ( !todo->recursAt( nextDate ) ||
601 nextDate <= KDateTime::currentUtcDateTime() ) {
602
603 if ( !nextDate.isValid() ||
604 ( nextDate > endDateTime && r->duration() != -1 ) ) {
605
606 return false;
607 }
608
609 nextDate = r->getNextDateTime( nextDate );
610 }
611
612 todo->setDtDue( nextDate );
613 todo->setCompleted( false );
614 todo->setRevision( todo->revision() + 1 );
615
616 return true;
617 }
618 }
619
620 return false;
621}
622//@endcond
623