kde-playground/kdepimlibs/kcalcore/todo.cpp

631 lines
16 KiB
C++
Raw Normal View History

/*
This file is part of the kcalcore library.
Copyright (c) 2001-2003 Cornelius Schumacher <schumacher@kde.org>
Copyright (C) 2009 Allen Winter <winter@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
/**
@file
This file is part of the API for handling calendar data and
defines the Todo class.
@brief
Provides a To-do in the sense of RFC2445.
@author Cornelius Schumacher \<schumacher@kde.org\>
@author Allen Winter \<winter@kde.org\>
*/
#include "todo.h"
#include "visitor.h"
#include "recurrence.h"
#include <KDebug>
#include <QTime>
using namespace KCalCore;
/**
Private class that helps to provide binary compatibility between releases.
@internal
*/
//@cond PRIVATE
class KCalCore::Todo::Private
{
public:
Private()
: mPercentComplete(0)
{}
Private(const KCalCore::Todo::Private &other)
{
init(other);
}
void init(const KCalCore::Todo::Private &other);
KDateTime mDtDue; // to-do due date (if there is one)
// ALSO the first occurrence of a recurring to-do
KDateTime mDtRecurrence; // next occurrence (for recurring to-dos)
KDateTime mCompleted; // to-do completion date (if it has been completed)
int mPercentComplete; // to-do percent complete [0,100]
/**
Returns true if the todo got a new date, else false will be returned.
*/
bool recurTodo(Todo *todo);
};
void KCalCore::Todo::Private::init(const KCalCore::Todo::Private &other)
{
mDtDue = other.mDtDue;
mDtRecurrence = other.mDtRecurrence;
mCompleted = other.mCompleted;
mPercentComplete = other.mPercentComplete;
}
//@endcond
Todo::Todo()
: d(new KCalCore::Todo::Private)
{
}
Todo::Todo(const Todo &other)
: Incidence(other),
d(new KCalCore::Todo::Private(*other.d))
{
}
Todo::Todo(const Incidence &other)
: Incidence(other)
, d(new KCalCore::Todo::Private)
{
}
Todo::~Todo()
{
delete d;
}
Todo *Todo::clone() const
{
return new Todo(*this);
}
IncidenceBase &Todo::assign(const IncidenceBase &other)
{
if (&other != this) {
Incidence::assign(other);
const Todo *t = static_cast<const Todo*>(&other);
d->init(*(t->d));
}
return *this;
}
bool Todo::equals(const IncidenceBase &todo) const
{
if (!Incidence::equals(todo)) {
return false;
} else {
// If they weren't the same type IncidenceBase::equals would had returned false already
const Todo *t = static_cast<const Todo*>(&todo);
return ((dtDue() == t->dtDue()) ||
(!dtDue().isValid() && !t->dtDue().isValid())) &&
hasDueDate() == t->hasDueDate() &&
hasStartDate() == t->hasStartDate() &&
((completed() == t->completed()) ||
(!completed().isValid() && !t->completed().isValid())) &&
hasCompletedDate() == t->hasCompletedDate() &&
percentComplete() == t->percentComplete();
}
}
Incidence::IncidenceType Todo::type() const
{
return TypeTodo;
}
QByteArray Todo::typeStr() const
{
return "Todo";
}
void Todo::setDtDue(const KDateTime &dtDue, bool first)
{
startUpdates();
//int diffsecs = d->mDtDue.secsTo(dtDue);
/*if (mReadOnly) return;
const Alarm::List& alarms = alarms();
for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) {
if (alarm->enabled()) {
alarm->setTime(alarm->time().addSecs(diffsecs));
}
}*/
if (recurs() && !first) {
d->mDtRecurrence = dtDue;
} else {
d->mDtDue = dtDue;
}
if (recurs() && dtDue.isValid() && (!dtStart().isValid() || dtDue < recurrence()->startDateTime())) {
kDebug() << "To-do recurrences are now calculated against DTSTART. Fixing legacy to-do.";
setDtStart(dtDue);
}
/*const Alarm::List& alarms = alarms();
for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next())
alarm->setAlarmStart(d->mDtDue);*/
setFieldDirty(FieldDtDue);
endUpdates();
}
KDateTime Todo::dtDue(bool first) const
{
if (!hasDueDate()) {
return KDateTime();
}
const KDateTime start = IncidenceBase::dtStart();
if (recurs() && !first && d->mDtRecurrence.isValid()) {
if (start.isValid()) {
// This is the normal case, recurring to-dos have a valid DTSTART.
const int duration = start.daysTo(d->mDtDue);
KDateTime dt = d->mDtRecurrence.addDays(duration);
dt.setTime(d->mDtDue.time());
return dt;
} else {
// This is a legacy case, where recurrence was calculated against DTDUE
return d->mDtRecurrence;
}
}
return d->mDtDue;
}
bool Todo::hasDueDate() const
{
return d->mDtDue.isValid();
}
void Todo::setHasDueDate(bool has)
{
if (mReadOnly) {
return;
}
update();
if (!has) {
d->mDtDue = KDateTime();
if (!dtStart().isValid()) {
// Recurrence is only calculated against dtdue if dtstart is invalid
d->mDtRecurrence = KDateTime();
}
}
setFieldDirty(FieldDtDue);
updated();
}
bool Todo::hasStartDate() const
{
return IncidenceBase::dtStart().isValid();
}
void Todo::setHasStartDate(bool has)
{
if (mReadOnly) {
return;
}
update();
if (recurs() && !has) {
if (!comments().filter(QLatin1String("NoStartDate")).count()) {
addComment(QLatin1String("NoStartDate")); //TODO: --> custom flag?
}
} else {
QString s(QLatin1String("NoStartDate"));
removeComment(s);
}
if (!has) {
if (dtStart().isValid() && d->mDtDue.isValid()) {
// If dtstart is invalid then recurrence is calculated against dtdue, so don't clear it.
d->mDtRecurrence = KDateTime();
}
setDtStart(KDateTime());
}
setFieldDirty(FieldDtStart);
updated();
}
KDateTime Todo::dtStart() const
{
return dtStart(/*first=*/false);
}
KDateTime Todo::dtStart(bool first) const
{
if (!hasStartDate()) {
return KDateTime();
}
if (recurs() && !first && d->mDtRecurrence.isValid()) {
return d->mDtRecurrence;
} else {
return IncidenceBase::dtStart();
}
}
void Todo::setDtStart(const KDateTime &dtStart)
{
Incidence::setDtStart(dtStart);
}
bool Todo::isCompleted() const
{
return d->mPercentComplete == 100;
}
void Todo::setCompleted(bool completed)
{
update();
if (completed) {
d->mPercentComplete = 100;
} else {
d->mPercentComplete = 0;
d->mCompleted = KDateTime();
}
setFieldDirty(FieldCompleted);
updated();
}
KDateTime Todo::completed() const
{
if (hasCompletedDate()) {
return d->mCompleted;
} else {
return KDateTime();
}
}
void Todo::setCompleted(const KDateTime &completed)
{
update();
if (!d->recurTodo(this)) {
d->mPercentComplete = 100;
d->mCompleted = completed.toUtc();
setFieldDirty(FieldCompleted);
}
updated();
}
bool Todo::hasCompletedDate() const
{
return d->mCompleted.isValid();
}
int Todo::percentComplete() const
{
return d->mPercentComplete;
}
void Todo::setPercentComplete(int percent)
{
if (percent > 100) {
percent = 100;
} else if (percent < 0) {
percent = 0;
}
update();
d->mPercentComplete = percent;
if (percent != 100) {
d->mCompleted = KDateTime();
}
setFieldDirty(FieldPercentComplete);
updated();
}
bool Todo::isInProgress(bool first) const
{
if (isOverdue()) {
return false;
}
if (d->mPercentComplete > 0) {
return true;
}
if (hasStartDate() && hasDueDate()) {
if (allDay()) {
QDate currDate = QDate::currentDate();
if (dtStart(first).date() <= currDate && currDate < dtDue(first).date()) {
return true;
}
} else {
KDateTime currDate = KDateTime::currentUtcDateTime();
if (dtStart(first) <= currDate && currDate < dtDue(first)) {
return true;
}
}
}
return false;
}
bool Todo::isOpenEnded() const
{
if (!hasDueDate() && !isCompleted()) {
return true;
}
return false;
}
bool Todo::isNotStarted(bool first) const
{
if (d->mPercentComplete > 0) {
return false;
}
if (!hasStartDate()) {
return false;
}
if (allDay()) {
if (dtStart(first).date() >= QDate::currentDate()) {
return false;
}
} else {
if (dtStart(first) >= KDateTime::currentUtcDateTime()) {
return false;
}
}
return true;
}
void Todo::shiftTimes(const KDateTime::Spec &oldSpec,
const KDateTime::Spec &newSpec)
{
Incidence::shiftTimes(oldSpec, newSpec);
d->mDtDue = d->mDtDue.toTimeSpec(oldSpec);
d->mDtDue.setTimeSpec(newSpec);
if (recurs()) {
d->mDtRecurrence = d->mDtRecurrence.toTimeSpec(oldSpec);
d->mDtRecurrence.setTimeSpec(newSpec);
}
if (hasCompletedDate()) {
d->mCompleted = d->mCompleted.toTimeSpec(oldSpec);
d->mCompleted.setTimeSpec(newSpec);
}
}
void Todo::setDtRecurrence(const KDateTime &dt)
{
d->mDtRecurrence = dt;
setFieldDirty(FieldRecurrence);
}
KDateTime Todo::dtRecurrence() const
{
return d->mDtRecurrence.isValid() ? d->mDtRecurrence : d->mDtDue;
}
bool Todo::recursOn(const QDate &date, const KDateTime::Spec &timeSpec) const
{
QDate today = QDate::currentDate();
return
Incidence::recursOn(date, timeSpec) &&
!(date < today && d->mDtRecurrence.date() < today &&
d->mDtRecurrence > recurrence()->startDateTime());
}
bool Todo::isOverdue() const
{
if (!dtDue().isValid()) {
return false; // if it's never due, it can't be overdue
}
const bool inPast = allDay() ? dtDue().date() < QDate::currentDate()
: dtDue() < KDateTime::currentUtcDateTime();
return inPast && !isCompleted();
}
void Todo::setAllDay(bool allday)
{
if (allday != allDay() && !mReadOnly) {
if (hasDueDate()) {
setFieldDirty(FieldDtDue);
}
Incidence::setAllDay(allday);
}
}
//@cond PRIVATE
bool Todo::Private::recurTodo(Todo *todo)
{
if (todo && todo->recurs()) {
Recurrence *r = todo->recurrence();
const KDateTime recurrenceEndDateTime = r->endDateTime();
KDateTime nextOccurrenceDateTime = r->getNextDateTime(todo->dtStart());
if ((r->duration() == -1 ||
(nextOccurrenceDateTime.isValid() && recurrenceEndDateTime.isValid() &&
nextOccurrenceDateTime <= recurrenceEndDateTime))) {
// We convert to the same timeSpec so we get the correct .date()
const KDateTime rightNow =
KDateTime::currentUtcDateTime().toTimeSpec(nextOccurrenceDateTime.timeSpec());
const bool isDateOnly = todo->allDay();
/* Now we search for the occurrence that's _after_ the currentUtcDateTime, or
* if it's dateOnly, the occurrrence that's _during or after today_.
* The reason we use "<" for date only, but "<=" for ocurrences with time is that
* if it's date only, the user can still complete that ocurrence today, so that's
* the current ocurrence that needs completing.
*/
while (!todo->recursAt(nextOccurrenceDateTime) ||
(!isDateOnly && nextOccurrenceDateTime <= rightNow) ||
(isDateOnly && nextOccurrenceDateTime.date() < rightNow.date())) {
if (!nextOccurrenceDateTime.isValid() ||
(nextOccurrenceDateTime > recurrenceEndDateTime && r->duration() != -1)) {
return false;
}
nextOccurrenceDateTime = r->getNextDateTime(nextOccurrenceDateTime);
}
todo->setDtRecurrence(nextOccurrenceDateTime);
todo->setCompleted(false);
todo->setRevision(todo->revision() + 1);
return true;
}
}
return false;
}
//@endcond
bool Todo::accept(Visitor &v, IncidenceBase::Ptr incidence)
{
return v.visit(incidence.staticCast<Todo>());
}
KDateTime Todo::dateTime(DateTimeRole role) const
{
switch (role) {
case RoleAlarmStartOffset:
return dtStart();
case RoleAlarmEndOffset:
return dtDue();
case RoleSort:
// Sorting to-dos first compares dtDue, then dtStart if
// dtDue doesn't exist
return hasDueDate() ? dtDue() : dtStart();
case RoleCalendarHashing:
return dtDue();
case RoleStartTimeZone:
return dtStart();
case RoleEndTimeZone:
return dtDue();
case RoleEndRecurrenceBase:
return dtDue();
case RoleDisplayStart:
case RoleDisplayEnd:
return dtDue().isValid() ? dtDue() : dtStart();
case RoleAlarm:
if (alarms().isEmpty()) {
return KDateTime();
} else {
Alarm::Ptr alarm = alarms().first();
if (alarm->hasStartOffset() && hasStartDate()) {
return dtStart();
} else if (alarm->hasEndOffset() && hasDueDate()) {
return dtDue();
} else {
// The application shouldn't add alarms on to-dos without dates.
return KDateTime();
}
}
case RoleRecurrenceStart:
if (dtStart().isValid()) {
return dtStart();
}
return dtDue(); //For the sake of backwards compatibility
//where we calculated recurrences based on dtDue
case RoleEnd:
return dtDue();
default:
return KDateTime();
}
}
void Todo::setDateTime(const KDateTime &dateTime, DateTimeRole role)
{
switch (role) {
case RoleDnD:
setDtDue(dateTime);
break;
case RoleEnd:
setDtDue(dateTime, true);
break;
default:
kDebug() << "Unhandled role" << role;
}
}
void Todo::virtual_hook(int id, void *data)
{
switch (static_cast<IncidenceBase::VirtualHook>(id)) {
case IncidenceBase::SerializerHook:
serialize(*reinterpret_cast<QDataStream*>(data));
break;
case IncidenceBase::DeserializerHook:
deserialize(*reinterpret_cast<QDataStream*>(data));
break;
default:
Q_ASSERT(false);
}
}
QLatin1String Todo::mimeType() const
{
return Todo::todoMimeType();
}
QLatin1String Todo::todoMimeType()
{
return QLatin1String("application/x-vnd.akonadi.calendar.todo");
}
QLatin1String Todo::iconName(const KDateTime &recurrenceId) const
{
KDateTime occurrenceDT = recurrenceId;
if (recurs() && occurrenceDT.isDateOnly()) {
occurrenceDT.setTime(QTime(0, 0));
}
const bool usesCompletedTaskPixmap = isCompleted() ||
(recurs() && occurrenceDT.isValid() &&
occurrenceDT < dtDue(false));
if (usesCompletedTaskPixmap) {
return QLatin1String("task-complete");
} else {
return QLatin1String("view-calendar-tasks");
}
}
void Todo::serialize(QDataStream &out)
{
Incidence::serialize(out);
out << d->mDtDue << d->mDtRecurrence << d->mCompleted << d->mPercentComplete;
}
void Todo::deserialize(QDataStream &in)
{
Incidence::deserialize(in);
in >> d->mDtDue >> d->mDtRecurrence >> d->mCompleted >> d->mPercentComplete;
}