diff --git a/akonadi/calendar/calendarbase.cpp b/akonadi/calendar/calendarbase.cpp index a0edb142c..0da54cde5 100644 --- a/akonadi/calendar/calendarbase.cpp +++ b/akonadi/calendar/calendarbase.cpp @@ -1,723 +1,734 @@ /* Copyright (C) 2011 Sérgio Martins Copyright (C) 2012 Sérgio Martins 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. */ #include "calendarbase.h" #include "calendarbase_p.h" #include "incidencechanger.h" #include "utils_p.h" #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; MultiCalendar::MultiCalendar(const KTimeZone &tz) : KCalCore::MemoryCalendar(tz) { } void MultiCalendar::addParentInformation(const KCalCore::Incidence::Ptr &inc, const KCalCore::Incidence::Ptr &parentIncidence) { mChildrenByIncidence[parentIncidence].append(inc); mParentByIncidence.insert(inc, parentIncidence); } void MultiCalendar::cleanupParentInformation(const KCalCore::Incidence::Ptr &inc) { if (KCalCore::Incidence::Ptr parentIncidence = mParentByIncidence.value(inc)) { mChildrenByIncidence[parentIncidence].remove(mChildrenByIncidence[parentIncidence].indexOf(inc)); mParentByIncidence.remove(inc); } } void MultiCalendar::incidenceUpdated(KCalCore::IncidenceBase *ptr) { Incidence::Ptr inc = KCalCore::MemoryCalendar::incidence(ptr); if (inc) { if (!inc->relatedTo().isEmpty()) { //Check if current parent still corresponds to relatedTo KCalCore::Incidence::Ptr parentIncidence = mParentByIncidence.value(inc); if (parentIncidence && parentIncidence->uid() != inc->relatedTo()) { cleanupParentInformation(inc); addParentInformation(inc, incidence(calendar(inc), inc->relatedTo())); } } } MemoryCalendar::incidenceUpdated(ptr); } bool MultiCalendar::addIncidenceToCalendar(const QString &calendar, const KCalCore::Incidence::Ptr &inc) { if (!mIncidenceByCalendar.values(calendar).contains(inc)) { mIncidenceByCalendar.insert(calendar, inc); mCalendarByIncidence.insert(inc, calendar); if (!inc->relatedTo().isEmpty()) { KCalCore::Incidence::Ptr parentIncidence = incidence(calendar, inc->relatedTo()); if (parentIncidence) { addParentInformation(inc, parentIncidence); } } MemoryCalendar::addIncidence(inc); } return true; } bool MultiCalendar::deleteIncidenceFromCalendar(const QString &calendar, const KCalCore::Incidence::Ptr &inc) { MemoryCalendar::deleteIncidence(inc); mIncidenceByCalendar.remove(calendar, inc); mCalendarByIncidence.remove(inc); cleanupParentInformation(inc); return true; } QString MultiCalendar::realUid(const KCalCore::Incidence::Ptr &incidence) const { return calendar(incidence) + incidence->uid(); } QString MultiCalendar::realIdentifier(const KCalCore::Incidence::Ptr &incidence) const { return calendar(incidence) + incidence->instanceIdentifier(); } KCalCore::Incidence::List MultiCalendar::incidencesFromCalendar(const QString &calendar) const { return mIncidenceByCalendar.values(calendar).toVector(); } KCalCore::Incidence::Ptr MultiCalendar::incidence(const QString &calendar, const QString &uid, const KDateTime &recurrenceId) const { Q_FOREACH (const KCalCore::Incidence::Ptr &inc, mIncidenceByCalendar.values(calendar)) { if (inc->uid() == uid && ((recurrenceId.isNull() && !inc->recurrenceId().isValid()) || recurrenceId == inc->recurrenceId())) { return inc; } } return KCalCore::Incidence::Ptr(); } KCalCore::Incidence::List MultiCalendar::incidenceExceptions(const KCalCore::Incidence::Ptr &incidence) const { KCalCore::Incidence::List instances; Q_FOREACH (const KCalCore::Incidence::Ptr &inc, mIncidenceByCalendar.values(calendar(incidence))) { if (inc->uid() == incidence->uid() && inc->recurrenceId().isValid()) { instances << inc; } } return instances; } QStringList MultiCalendar::calendars(const QString &uid, const KDateTime &recurrenceId) const { QStringList calendars; Q_FOREACH (const QString &cal, mIncidenceByCalendar.uniqueKeys()) { if (incidence(cal, uid, recurrenceId)) { calendars << cal; } } return calendars; } QString MultiCalendar::calendar(const KCalCore::Incidence::Ptr &incidence) const { Q_ASSERT(mCalendarByIncidence.contains(incidence)); return mCalendarByIncidence.value(incidence); } QString MultiCalendar::uniqueInstanceIdentifier(const KCalCore::Incidence::Ptr &incidence) const { return calendar(incidence) + incidence->instanceIdentifier(); } KCalCore::Incidence::List MultiCalendar::childIncidences(const KCalCore::Incidence::Ptr &incidence) const { // Q_ASSERT(mChildrenByIncidence.contains(incidence)); return mChildrenByIncidence.value(incidence); } static QString itemToString(const Akonadi::Item &item) { const KCalCore::Incidence::Ptr &incidence = CalendarUtils::incidence(item); QString str; QTextStream stream(&str); stream << item.id() << "; summary=" << incidence->summary() << "; uid=" << incidence->uid() << "; type=" << incidence->type() << "; recurs=" << incidence->recurs() << "; recurrenceId=" << incidence->recurrenceId().toString() << "; dtStart=" << incidence->dtStart().toString() << "; dtEnd=" << incidence->dateTime(Incidence::RoleEnd).toString() << "; parentCollection=" << item.storageCollectionId() << item.parentCollection().displayName(); return str; } CalendarBasePrivate::CalendarBasePrivate(CalendarBase *qq) : QObject() , mIncidenceChanger(new IncidenceChanger()) , mBatchInsertionCancelled(false) , mListensForNewItems(false) , mLastCreationCancelled(false) , q(qq) { connect(mIncidenceChanger, SIGNAL(createFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(slotCreateFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString))); connect(mIncidenceChanger, SIGNAL(deleteFinished(int,QVector,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(slotDeleteFinished(int,QVector,Akonadi::IncidenceChanger::ResultCode,QString))); connect(mIncidenceChanger, SIGNAL(modifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(slotModifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString))); mIncidenceChanger->setDestinationPolicy(IncidenceChanger::DestinationPolicyAsk); mIncidenceChanger->setGroupwareCommunication(false); mIncidenceChanger->setHistoryEnabled(false); } CalendarBasePrivate::~CalendarBasePrivate() { delete mIncidenceChanger; mIncidenceChanger = 0; } void CalendarBasePrivate::internalInsert(const Akonadi::Item &item) { Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); KCalCore::Incidence::Ptr incidence = CalendarUtils::incidence(item); Q_ASSERT(item.storageCollectionId() > 0); if (!incidence) { kError() << "Incidence is null. id=" << item.id() << "; hasPayload()=" << item.hasPayload() << "; has incidence=" << item.hasPayload() << "; mime type=" << item.mimeType(); Q_ASSERT(false); return; } //kDebug() << "Inserting incidence in calendar. id=" << item.id() << "uid=" << incidence->uid(); if (incidence->uid().isEmpty()) { // This code path should never happen kError() << "Incidence has empty UID. id=" << item.id() << "; summary=" << incidence->summary() << "Please fix it. Ignoring this incidence."; return; } - const QString calendar = QString::number(item.storageCollectionId()); + const QString calendar = QString::number(item.parentCollection().id()); const QString uniqueInstanceIdentifier = calendar + incidence->instanceIdentifier(); if (mItemIdByUniqueInstanceIdentifier.contains(uniqueInstanceIdentifier) && mItemIdByUniqueInstanceIdentifier[uniqueInstanceIdentifier] != item.id()) { // We only allow duplicate UIDs if they have the same item id, for example // when using virtual folders. kWarning() << "Discarding duplicate incidence with instanceIdentifier=" << uniqueInstanceIdentifier << "and summary " << incidence->summary() << "; recurrenceId() =" << incidence->recurrenceId() << "; new id=" << item.id() << "; existing id=" << mItemIdByUniqueInstanceIdentifier[uniqueInstanceIdentifier]; return; } if (incidence->type() == KCalCore::Incidence::TypeEvent && !incidence->dtStart().isValid()) { // TODO: make the parser discard them would also be a good idea kWarning() << "Discarding event with invalid DTSTART. identifier=" << incidence->instanceIdentifier() << "; summary=" << incidence->summary(); return; } Akonadi::Collection collection = item.parentCollection(); if (collection.isValid()) { // Some items don't have collection set if (item.storageCollectionId() != collection.id() && item.storageCollectionId() > -1) { if (mCollections.contains(item.storageCollectionId())) { collection = mCollections.value(item.storageCollectionId()); incidence->setReadOnly(!(collection.rights() & Akonadi::Collection::CanChangeItem)); } else if (!mCollectionJobs.key(item.storageCollectionId())) { collection = Akonadi::Collection(item.storageCollectionId()); Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Base, this); connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); mCollectionJobs.insert(job, collection.id()); } } else { mCollections.insert(collection.id(), collection); incidence->setReadOnly(!(collection.rights() & Akonadi::Collection::CanChangeItem)); } } mItemById.insert(item.id(), item); mItemIdByUniqueInstanceIdentifier.insert(uniqueInstanceIdentifier, item.id()); mItemsByCollection.insert(item.storageCollectionId(), item); + // we need item in storageCollection, to find exceptions + if (item.parentCollection().id() != item.storageCollectionId()) { + mItemsByCollection.insert(item.parentCollection().id(), item); + } + incidence->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(item.id())); // Must be the last one due to re-entrancy const bool result = q->MultiCalendar::addIncidenceToCalendar(calendar, incidence); if (!result) { kError() << "Error adding incidence " << itemToString(item); Q_ASSERT(false); } } void CalendarBasePrivate::collectionFetchResult(KJob* job) { Akonadi::Collection::Id colid = mCollectionJobs.take(job); if ( job->error() ) { qDebug() << "Error occurred"; return; } Akonadi::CollectionFetchJob *fetchJob = qobject_cast( job ); const Akonadi::Collection collection = fetchJob->collections().first(); if (collection.id() != colid) { kError() << "Fetched the wrong collection, should fetch: " << colid << "fetched: " << collection.id(); } bool isReadOnly = !(collection.rights() & Akonadi::Collection::CanChangeItem); foreach (const Akonadi::Item &item, mItemsByCollection.values(collection.id())) { KCalCore::Incidence::Ptr incidence = CalendarUtils::incidence(item); incidence->setReadOnly(isReadOnly); } mCollections.insert(collection.id(), collection); if (mCollectionJobs.count() == 0) { emit fetchFinished(); } } void CalendarBasePrivate::internalRemove(const Akonadi::Item &item) { Q_ASSERT(item.isValid()); Incidence::Ptr tmp = CalendarUtils::incidence(item); if (!tmp) { kError() << "CalendarBase::internalRemove1: incidence is null, item.id=" << item.id(); return; } Q_ASSERT(item.storageCollectionId() > 0); - const QString calendar = QString::number(item.storageCollectionId()); + const QString calendar = QString::number(item.parentCollection().id()); const QString uniqueInstanceIdentifier = calendar + tmp->instanceIdentifier(); // We want the one stored in the calendar Incidence::Ptr incidence = q->incidence(calendar, tmp->uid(), tmp->recurrenceId()); // Null incidence means it was deleted via CalendarBase::deleteIncidence(), but then // the ETMCalendar received the monitor notification and tried to delete it again. if (incidence) { - mItemById.remove(item.id()); // kDebug() << "Deleting incidence from calendar .id=" << item.id() << "uid=" << incidence->uid(); mItemIdByUniqueInstanceIdentifier.remove(uniqueInstanceIdentifier); - mItemsByCollection.remove(item.storageCollectionId(), item); + mItemsByCollection.remove(item.parentCollection().id(), item); // Must be the last one due to re-entrancy // This will trigger the removal of all exceptions via deleteIncidence const bool result = q->MultiCalendar::deleteIncidenceFromCalendar(calendar, incidence); if (!result) { kError() << "Error removing incidence " << itemToString(item); Q_ASSERT(false); } + + // only remove ItemById entry if all incidences are deleted + // we need item in storageCollection, to find exceptions + if (mItemIdByUniqueInstanceIdentifier.key(item.id(), QString()).isEmpty()) { + mItemById.remove(item.id()); + mItemsByCollection.remove(item.storageCollectionId(), item); + } } else { kWarning() << "CalendarBase::internalRemove2: incidence is null, item.id=" << itemToString(item); } } void CalendarBasePrivate::deleteAllIncidencesOfType(const QString &mimeType) { kWarning() << "Refusing to delete your Incidences."; Q_UNUSED(mimeType); /* QHash::iterator i; Item::List incidences; for ( i = mItemById.begin(); i != mItemById.end(); ++i ) { if ( i.value().payload()->mimeType() == mimeType ) incidences.append( i.value() ); } mIncidenceChanger->deleteIncidences( incidences ); */ } void CalendarBasePrivate::slotDeleteFinished(int changeId, const QVector &itemIds, IncidenceChanger::ResultCode resultCode, const QString &errorMessage) { Q_UNUSED(changeId); if (resultCode == IncidenceChanger::ResultCodeSuccess) { foreach(const Akonadi::Item::Id &id, itemIds) { if (mItemById.contains(id)) internalRemove(mItemById.value(id)); } } emit q->deleteFinished(resultCode == IncidenceChanger::ResultCodeSuccess, errorMessage); } void CalendarBasePrivate::slotCreateFinished(int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorMessage) { Q_UNUSED(changeId); Q_UNUSED(item); if (resultCode == IncidenceChanger::ResultCodeSuccess && !mListensForNewItems) { Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); - //FIXME fetch, otherwise we don't have the storageCOllectionId + //FIXME fetch, otherwise we don't have the storageCollectionId internalInsert(item); } mLastCreationCancelled = (resultCode == IncidenceChanger::ResultCodeUserCanceled); emit q->createFinished(resultCode == IncidenceChanger::ResultCodeSuccess, errorMessage); } void CalendarBasePrivate::slotModifyFinished(int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorMessage) { Q_UNUSED(changeId); Q_UNUSED(item); QString message = errorMessage; if (resultCode == IncidenceChanger::ResultCodeSuccess) { KCalCore::Incidence::Ptr incidence = CalendarUtils::incidence(item); Q_ASSERT(incidence); Q_ASSERT(item.storageCollectionId() > 0); - const QString calendar = QString::number(item.storageCollectionId()); + const QString calendar = QString::number(item.parentCollection().id()); // We want the one stored in the calendar KCalCore::Incidence::Ptr localIncidence = q->incidence(calendar, incidence->uid(), incidence->recurrenceId()); if (localIncidence) { //update our local one *(static_cast(localIncidence.data())) = *(incidence.data()); } else { // This shouldn't happen, unless the incidence gets deleted between event loops kWarning() << "CalendarBasePrivate::slotModifyFinished() Incidence was deleted already probably? id=" << item.id(); message = i18n("Could not find incidence to update, it probably was deleted recently."); resultCode = IncidenceChanger::ResultCodeAlreadyDeleted; } } emit q->modifyFinished(resultCode == IncidenceChanger::ResultCodeSuccess, message); } void CalendarBasePrivate::handleUidChange(const Akonadi::Item &oldItem, const Akonadi::Item &newItem) { Q_ASSERT(oldItem.isValid()); Incidence::Ptr newIncidence = CalendarUtils::incidence(newItem); Q_ASSERT(newIncidence); Incidence::Ptr oldIncidence = CalendarUtils::incidence(oldItem); Q_ASSERT(oldIncidence); - const QString calendar = QString::number(newItem.storageCollectionId()); + const QString calendar = QString::number(newItem.parentCollection().id()); const QString uniqueInstanceIdentifier = calendar + newIncidence->instanceIdentifier(); if (mItemIdByUniqueInstanceIdentifier.contains(uniqueInstanceIdentifier)) { Incidence::Ptr oldIncidence = CalendarUtils::incidence(oldItem); kWarning() << "New uid shouldn't be known: " << uniqueInstanceIdentifier << "; id=" << newItem.id() << "; oldItem.id=" << mItemIdByUniqueInstanceIdentifier.value(uniqueInstanceIdentifier) << "; new summary= " << newIncidence->summary() << "; new recurrenceId=" << newIncidence->recurrenceId() << "; oldIncidence" << oldIncidence; if (oldIncidence) { kWarning() << "; oldIncidence uid=" << oldIncidence->uid() << "; oldIncidence recurrenceId = " << oldIncidence->recurrenceId() << "; oldIncidence summary = " << oldIncidence->summary(); } Q_ASSERT(false); return; } mItemIdByUniqueInstanceIdentifier.insert(uniqueInstanceIdentifier, newItem.id()); // Get the real pointer oldIncidence = q->MultiCalendar::incidence(calendar, oldIncidence->uid(), oldIncidence->recurrenceId()); if (!oldIncidence) { // How can this happen ? kWarning() << "Couldn't find old incidence"; Q_ASSERT(false); return; } if (q->realIdentifier(newIncidence) == q->realIdentifier(oldIncidence)) { kWarning() << "New uid=" << newIncidence->uid() << "; old uid=" << oldIncidence->uid() << "; new recurrenceId=" << newIncidence->recurrenceId() << "; old recurrenceId=" << oldIncidence->recurrenceId() << "; new summary = " << newIncidence->summary() << "; old summary = " << oldIncidence->summary() << "; id = " << newItem.id(); Q_ASSERT(false); // The reason we're here in the first place return; } - const QString oldCalendar = QString::number(oldItem.storageCollectionId()); + const QString oldCalendar = QString::number(oldItem.parentCollection().id()); const QString oldUniqueInstanceIdentifier = oldCalendar + oldIncidence->instanceIdentifier(); mItemIdByUniqueInstanceIdentifier.remove(oldUniqueInstanceIdentifier); const QString oldUid = oldIncidence->uid(); // Update internal maps of the base class, MemoryCalendar q->setObserversEnabled(false); q->MultiCalendar::deleteIncidenceFromCalendar(calendar, oldIncidence); q->MultiCalendar::addIncidenceToCalendar(calendar, newIncidence); const QString newUid = newIncidence->uid(); newIncidence->setUid(oldUid); // We set and unset just to notify observers of a change. q->setObserversEnabled(true); newIncidence->setUid(newUid); } CalendarBase::CalendarBase(QObject *parent) : MultiCalendar(KSystemTimeZones::local()) , d_ptr(new CalendarBasePrivate(this)) { setParent(parent); setDeletionTracking(false); } CalendarBase::CalendarBase(CalendarBasePrivate *const dd, QObject *parent) : MultiCalendar(KSystemTimeZones::local()) , d_ptr(dd) { setParent(parent); setDeletionTracking(false); } CalendarBase::~CalendarBase() { } Akonadi::Item CalendarBase::item(Akonadi::Item::Id id) const { Q_D(const CalendarBase); Akonadi::Item i; if (d->mItemById.contains(id)) { i = d->mItemById[id]; } else { kDebug() << "Can't find any item with id " << id; } return i; } Akonadi::Item CalendarBasePrivate::item(const QString &uniqueInstanceIdentifier) const { Akonadi::Item i; if (uniqueInstanceIdentifier.isEmpty()) return i; if (mItemIdByUniqueInstanceIdentifier.contains(uniqueInstanceIdentifier)) { const Akonadi::Item::Id id = mItemIdByUniqueInstanceIdentifier.value(uniqueInstanceIdentifier); if (!mItemById.contains(id)) { kError() << "Item with id " << id << "(uid=" << uniqueInstanceIdentifier << ") not found, but in uid map"; Q_ASSERT_X(false, "CalendarBase::item", "not in mItemById"); } i = mItemById[id]; } else { kDebug() << "Can't find any incidence with uid " << uniqueInstanceIdentifier; } return i; } Item CalendarBase::item(const Incidence::Ptr &incidence) const { Q_D(const CalendarBase); const QString cal = calendar(incidence); Q_ASSERT(!cal.isEmpty()); return incidence ? d->item(cal + incidence->instanceIdentifier()) : Item(); } Akonadi::Item::List CalendarBase::items() const { Q_D(const CalendarBase); return d->mItemById.values(); } Akonadi::Item::List CalendarBase::items(Akonadi::Collection::Id id) const { Q_D(const CalendarBase); return d->mItemsByCollection.values(id); } Akonadi::Item::List CalendarBase::itemList(const KCalCore::Incidence::List &incidences) const { Akonadi::Item::List items; foreach(const KCalCore::Incidence::Ptr &incidence, incidences) { if (incidence) { items << item(incidence); } else { items << Akonadi::Item(); } } return items; } KCalCore::Incidence::List CalendarBase::childIncidences(const Akonadi::Item::Id &parentId) const { Q_D(const CalendarBase); KCalCore::Incidence::List childs; if (d->mItemById.contains(parentId)) { const Akonadi::Item item = d->mItemById.value(parentId); Q_ASSERT(item.isValid()); KCalCore::Incidence::Ptr parent = CalendarUtils::incidence(item); const QString calendar = QString::number(item.storageCollectionId()); if (parent) { childs = MultiCalendar::childIncidences(incidence(calendar, parent->uid(), parent->recurrenceId())); } else { Q_ASSERT(false); } } return childs; } Akonadi::Item::List CalendarBase::childItems(const Akonadi::Item::Id &parentId) const { Q_D(const CalendarBase); if (!d->mItemById.contains(parentId)) { kWarning() << "invalid parent " << parentId; Q_ASSERT(false); return Akonadi::Item::List(); } const Akonadi::Item item = d->mItemById.value(parentId); Q_ASSERT(item.isValid()); const QString calendar = QString::number(item.storageCollectionId()); Akonadi::Item::List children; KCalCore::Incidence::List incidences = childIncidences(parentId); Q_FOREACH (const KCalCore::Incidence::Ptr &inc, incidences) { children << d->mItemById.value(d->mItemIdByUniqueInstanceIdentifier.value(calendar + inc->instanceIdentifier())); } return children; } Akonadi::Item::List CalendarBase::childItems(const Incidence::Ptr &incidence) const { Q_D(const CalendarBase); return childItems(d->mItemIdByUniqueInstanceIdentifier.value(calendar(incidence) + incidence->instanceIdentifier())); } bool CalendarBase::addIncidence(const KCalCore::Incidence::Ptr &incidence) { //Let the incidencechanger decide what collection to use return addIncidence("-1", incidence); } bool CalendarBase::addIncidence(const QString &calendar, const KCalCore::Incidence::Ptr &incidence) { //TODO: Parent for dialogs Q_D(CalendarBase); // User canceled on the collection selection dialog if (batchAdding() && d->mBatchInsertionCancelled) { return false; } d->mLastCreationCancelled = false; Akonadi::Collection collection = Akonadi::Collection(calendar.toLongLong()); const int changeId = d->mIncidenceChanger->createIncidence(incidence, collection); if (batchAdding()) { const Akonadi::Collection lastCollection = d->mIncidenceChanger->lastCollectionUsed(); if (changeId != -1 && !lastCollection.isValid()) { d->mBatchInsertionCancelled = true; } } return changeId != -1; } bool CalendarBase::deleteIncidence(const KCalCore::Incidence::Ptr &incidence) { Q_D(CalendarBase); Q_ASSERT(incidence); Akonadi::Item item_ = item(incidence); return -1 != d->mIncidenceChanger->deleteIncidence(item_); //TODO delete exceptions in incidencechanger? } bool CalendarBase::deleteIncidenceInstances(const KCalCore::Incidence::Ptr &incidence) { //We have to avoid that we actually delete the incidences and make sure only the MemoryCalendar implementation is called foreach (const KCalCore::Incidence::Ptr &exception, incidenceExceptions(incidence)) { MemoryCalendar::deleteIncidence(exception); } return true; } bool CalendarBase::modifyIncidence(const KCalCore::Incidence::Ptr &oldIncidence, const KCalCore::Incidence::Ptr &newIncidence) { Q_D(CalendarBase); Q_ASSERT(oldIncidence); Q_ASSERT(newIncidence); Akonadi::Item item_ = item(oldIncidence); item_.setPayload(newIncidence); return -1 != d->mIncidenceChanger->modifyIncidence(item_); } void CalendarBase::setWeakPointer(const QWeakPointer &pointer) { Q_D(CalendarBase); d->mWeakPointer = pointer; } QWeakPointer CalendarBase::weakPointer() const { Q_D(const CalendarBase); return d->mWeakPointer; } IncidenceChanger* CalendarBase::incidenceChanger() const { Q_D(const CalendarBase); return d->mIncidenceChanger; } void CalendarBase::startBatchAdding() { KCalCore::MemoryCalendar::startBatchAdding(); } void CalendarBase::endBatchAdding() { Q_D(CalendarBase); d->mBatchInsertionCancelled = false; KCalCore::MemoryCalendar::endBatchAdding(); } #include "moc_calendarbase.cpp" #include "moc_calendarbase_p.cpp" diff --git a/akonadi/calendar/etmcalendar.cpp b/akonadi/calendar/etmcalendar.cpp index d61fc5df9..0a4593f9c 100644 --- a/akonadi/calendar/etmcalendar.cpp +++ b/akonadi/calendar/etmcalendar.cpp @@ -1,669 +1,669 @@ /* Copyright (C) 2011-2013 Sérgio Martins 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. */ #include "etmcalendar.h" #include "etmcalendar_p.h" #include "blockalarmsattribute.h" #include "calendarmodel_p.h" #include "kcolumnfilterproxymodel_p.h" #include "calfilterproxymodel_p.h" #include "calfilterpartstatusproxymodel_p.h" #include "utils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; class CollectionFilter : public QSortFilterProxyModel { public: explicit CollectionFilter(QObject *parent = 0): QSortFilterProxyModel (parent) {}; virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { const Collection collection = sourceModel()->index(sourceRow, 0, sourceParent).data(Akonadi::EntityTreeModel::CollectionRole).value(); return collection.isValid(); } }; //TODO: implement batchAdding ETMCalendarPrivate::ETMCalendarPrivate(ETMCalendar *qq) : CalendarBasePrivate(qq) , mETM(0) , mFilteredETM(0) , mCheckableProxyModel(0) , mCollectionProxyModel(0) , mCalFilterProxyModel(0) , mCalFilterPartStatusProxyModel(0) , mSelectionProxy(0) , mCollectionFilteringEnabled(true) , q(qq) { mListensForNewItems = true; } void ETMCalendarPrivate::init() { if (!mETM) { Akonadi::Session *session = new Akonadi::Session("ETMCalendar", q); Akonadi::ChangeRecorder *monitor = new Akonadi::ChangeRecorder(q); connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(onCollectionChanged(Akonadi::Collection,QSet))); Akonadi::ItemFetchScope scope; scope.fetchFullPayload(true); scope.fetchAttribute(); monitor->setSession(session); monitor->setCollectionMonitored(Akonadi::Collection::root()); monitor->fetchCollection(true); monitor->setItemFetchScope(scope); monitor->setAllMonitored(true); QStringList allMimeTypes; allMimeTypes << KCalCore::Event::eventMimeType() << KCalCore::Todo::todoMimeType() << KCalCore::Journal::journalMimeType(); foreach(const QString &mimetype, allMimeTypes) { monitor->setMimeTypeMonitored(mimetype, mMimeTypes.isEmpty() || mMimeTypes.contains(mimetype)); } mETM = CalendarModel::create(monitor); mETM->setObjectName("ETM"); mETM->setListFilter(Akonadi::CollectionFetchScope::Display); } setupFilteredETM(); connect(q, SIGNAL(filterChanged()), SLOT(onFilterChanged())); connect(mETM.data(), SIGNAL(collectionPopulated(Akonadi::Collection::Id)), SLOT(onCollectionPopulated(Akonadi::Collection::Id))); connect(mETM.data(), SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(onRowsInserted(QModelIndex,int,int))); connect(mETM.data(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(onDataChanged(QModelIndex,QModelIndex))); connect(mETM.data(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int))); connect(mETM.data(), SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(onRowsRemoved(QModelIndex,int,int))); connect(mFilteredETM, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(onDataChangedInFilteredModel(QModelIndex,QModelIndex))); connect(mFilteredETM, SIGNAL(layoutChanged()), SLOT(onLayoutChangedInFilteredModel())); connect(mFilteredETM, SIGNAL(modelReset()), SLOT(onModelResetInFilteredModel())); connect(mFilteredETM, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(onRowsInsertedInFilteredModel(QModelIndex,int,int))); connect(mFilteredETM, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SLOT(onRowsAboutToBeRemovedInFilteredModel(QModelIndex,int,int))); loadFromETM(); } void ETMCalendarPrivate::onCollectionChanged(const Akonadi::Collection &collection, const QSet &attributeNames) { Q_ASSERT(collection.isValid()); // Is the collection changed to read-only, we update all Incidences if (attributeNames.contains("AccessRights")) { Akonadi::Item::List items = q->items(); foreach(const Akonadi::Item &item, items) { if (item.storageCollectionId() == collection.id()) { KCalCore::Incidence::Ptr incidence = CalendarUtils::incidence(item); if (incidence) incidence->setReadOnly(!(collection.rights() & Akonadi::Collection::CanChangeItem)); } } } emit q->collectionChanged(collection, attributeNames); } void ETMCalendarPrivate::setupFilteredETM() { // We're only interested in the CollectionTitle column KColumnFilterProxyModel *columnFilterProxy = new KColumnFilterProxyModel(this); columnFilterProxy->setSourceModel(mETM.data()); columnFilterProxy->setVisibleColumn(CalendarModel::CollectionTitle); columnFilterProxy->setObjectName("Remove columns"); CollectionFilter *mCollectionProxyModel = new CollectionFilter(this); mCollectionProxyModel->setObjectName("Only show collections"); mCollectionProxyModel->setDynamicSortFilter(true); mCollectionProxyModel->setSourceModel(columnFilterProxy); // Keep track of selected items. QItemSelectionModel* selectionModel = new QItemSelectionModel(mCollectionProxyModel); selectionModel->setObjectName("Calendar Selection Model"); // Make item selection work by means of checkboxes. mCheckableProxyModel = new CheckableProxyModel(this); mCheckableProxyModel->setSelectionModel(selectionModel); //FIXME this should be based on the available calendars mCheckableProxyModel->setSourceModel(mCollectionProxyModel); mCheckableProxyModel->setObjectName("Add checkboxes"); mSelectionProxy = new KSelectionProxyModel(selectionModel, /**parent=*/this); mSelectionProxy->setObjectName("Only show items of selected collection"); mSelectionProxy->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection); mSelectionProxy->setSourceModel(mETM.data()); mCalFilterProxyModel = new CalFilterProxyModel(this); mCalFilterProxyModel->setFilter(q->filter()); mCalFilterProxyModel->setSourceModel(mSelectionProxy); mCalFilterProxyModel->setObjectName("KCalCore::CalFilter filtering"); mCalFilterPartStatusProxyModel = new CalFilterPartStatusProxyModel(this); mCalFilterPartStatusProxyModel->setFilterVirtual(false); QList blockedStatusList; blockedStatusList << KCalCore::Attendee::NeedsAction; blockedStatusList << KCalCore::Attendee::Declined; mCalFilterPartStatusProxyModel->setDynamicSortFilter(true); mCalFilterPartStatusProxyModel->setBlockedStatusList(blockedStatusList); mCalFilterPartStatusProxyModel->setSourceModel(mCalFilterProxyModel); mCalFilterPartStatusProxyModel->setObjectName("PartStatus filtering"); mFilteredETM = new Akonadi::EntityMimeTypeFilterModel(this); mFilteredETM->setSourceModel(mCalFilterPartStatusProxyModel); mFilteredETM->setHeaderGroup(Akonadi::EntityTreeModel::ItemListHeaders); mFilteredETM->setSortRole(CalendarModel::SortRole); mFilteredETM->setObjectName("Show headers"); #ifdef AKONADI_CALENDAR_DEBUG_MODEL QTreeView *view = new QTreeView; view->setModel(mFilteredETM); view->show(); #endif } ETMCalendarPrivate::~ETMCalendarPrivate() { } void ETMCalendarPrivate::loadFromETM() { itemsAdded(itemsFromModel(mFilteredETM)); } void ETMCalendarPrivate::clear() { mCollectionMap.clear(); mItemsByCollection.clear(); itemsRemoved(mItemById.values()); if (!mItemById.isEmpty()) { // This never happens kDebug() << "This shouldnt happen: !mItemById.isEmpty()"; foreach(Akonadi::Item::Id id, mItemById.keys()) { kDebug() << "Id = " << id; } mItemById.clear(); //Q_ASSERT(false); // TODO: discover why this happens } if (!mItemIdByUniqueInstanceIdentifier.isEmpty()) { // This never happens kDebug() << "This shouldnt happen: !mItemIdByUid.isEmpty()"; foreach(const QString &uid, mItemIdByUniqueInstanceIdentifier.keys()) { kDebug() << "uid: " << uid; } mItemIdByUniqueInstanceIdentifier.clear(); //Q_ASSERT(false); } } Akonadi::Item::List ETMCalendarPrivate::itemsFromModel(const QAbstractItemModel *model, const QModelIndex &parentIndex, int start, int end) { const int endRow = end >= 0 ? end : model->rowCount(parentIndex) - 1; Akonadi::Item::List items; int row = start; QModelIndex i = model->index(row, 0, parentIndex); while (row <= endRow) { const Akonadi::Item item = itemFromIndex(i); if (item.hasPayload()) { items << item; } else { const QModelIndex childIndex = i.child(0, 0); if (childIndex.isValid()) { items << itemsFromModel(model, i); } } ++row; i = i.sibling(row, 0); } return items; } Akonadi::Collection::List ETMCalendarPrivate::collectionsFromModel(const QAbstractItemModel *model, const QModelIndex &parentIndex, int start, int end) { const int endRow = end >= 0 ? end : model->rowCount(parentIndex) - 1; Akonadi::Collection::List collections; int row = start; QModelIndex i = model->index(row, 0, parentIndex); while (row <= endRow) { const Akonadi::Collection collection = collectionFromIndex(i); if (collection.isValid()) { collections << collection; QModelIndex childIndex = i.child(0, 0); if (childIndex.isValid()) { collections << collectionsFromModel(model, i); } } ++row; i = i.sibling(row, 0); } return collections; } Akonadi::Item ETMCalendarPrivate::itemFromIndex(const QModelIndex &idx) { Akonadi::Item item = idx.data(Akonadi::EntityTreeModel::ItemRole).value(); item.setParentCollection( idx.data(Akonadi::EntityTreeModel::ParentCollectionRole).value()); return item; } void ETMCalendarPrivate::itemsAdded(const Akonadi::Item::List &items) { if (!items.isEmpty()) { foreach(const Akonadi::Item &item, items) { internalInsert(item); } Akonadi::Collection::Id id = items.first().storageCollectionId(); if (mPopulatedCollectionIds.contains(id)) { // If the collection isn't populated yet, it will be sent later // Saves some cpu cycles emit q->calendarChanged(); } } } void ETMCalendarPrivate::itemsRemoved(const Akonadi::Item::List &items) { foreach(const Akonadi::Item &item, items) { internalRemove(item); } emit q->calendarChanged(); } Akonadi::Collection ETMCalendarPrivate::collectionFromIndex(const QModelIndex &index) { return index.data(Akonadi::EntityTreeModel::CollectionRole).value(); } void ETMCalendarPrivate::onRowsInserted(const QModelIndex &index, int start, int end) { Akonadi::Collection::List collections = collectionsFromModel(mETM.data(), index, start, end); foreach(const Akonadi::Collection &collection, collections) { mCollectionMap[collection.id()] = collection; } if (!collections.isEmpty()) emit q->collectionsAdded(collections); } void ETMCalendarPrivate::onCollectionPopulated(Akonadi::Collection::Id id) { mPopulatedCollectionIds.insert(id); emit q->calendarChanged(); } void ETMCalendarPrivate::onRowsRemoved(const QModelIndex &index, int start, int end) { Akonadi::Collection::List collections = collectionsFromModel(mETM.data(), index, start, end); foreach(const Akonadi::Collection &collection, collections) { mCollectionMap.remove(collection.id()); } if (!collections.isEmpty()) emit q->collectionsRemoved(collections); } void ETMCalendarPrivate::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { // We only update collections, because items are handled in the filtered model Q_ASSERT(topLeft.row() <= bottomRight.row()); const int endRow = bottomRight.row(); for (int row = topLeft.row(); row <= endRow; row++) { const Akonadi::Collection col = collectionFromIndex(topLeft.sibling(row, 0)); if (col.isValid()) { // Attributes might have changed, store the new collection and discard the old one mCollectionMap.insert(col.id(), col); } ++row; } } void ETMCalendarPrivate::onRowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow) { //TODO Q_UNUSED(sourceParent); Q_UNUSED(sourceStart); Q_UNUSED(sourceEnd); Q_UNUSED(destinationParent); Q_UNUSED(destinationRow); } void ETMCalendarPrivate::onLayoutChangedInFilteredModel() { clear(); loadFromETM(); } void ETMCalendarPrivate::onModelResetInFilteredModel() { clear(); loadFromETM(); } void ETMCalendarPrivate::onDataChangedInFilteredModel(const QModelIndex &topLeft, const QModelIndex &bottomRight) { Q_ASSERT(topLeft.row() <= bottomRight.row()); const int endRow = bottomRight.row(); QModelIndex i(topLeft); int row = i.row(); while (row <= endRow) { const Akonadi::Item item = itemFromIndex(i); if (item.isValid() && item.hasPayload()) updateItem(item); ++row; i = i.sibling(row, topLeft.column()); } emit q->calendarChanged(); } void ETMCalendarPrivate::updateItem(const Akonadi::Item &item) { Incidence::Ptr newIncidence = CalendarUtils::incidence(item); Q_ASSERT(newIncidence); Q_ASSERT(!newIncidence->uid().isEmpty()); newIncidence->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(item.id())); Q_ASSERT(item.storageCollectionId() >= 0); - IncidenceBase::Ptr existingIncidence = q->incidence(QString::number(item.storageCollectionId()), newIncidence->uid(), newIncidence->recurrenceId()); + IncidenceBase::Ptr existingIncidence = q->incidence(QString::number(item.parentCollection().id()), newIncidence->uid(), newIncidence->recurrenceId()); if (!existingIncidence && !mItemById.contains(item.id())) { // We don't know about this one because it was discarded, for example because of not having DTSTART return; } - mItemsByCollection.insert(item.storageCollectionId(), item); + mItemsByCollection.insert(item.parentCollection().id(), item); Akonadi::Item oldItem = mItemById.value(item.id()); if (existingIncidence) { // We set the payload so that the internal incidence pointer and the one in mItemById stay the same Akonadi::Item updatedItem = item; updatedItem.setPayload(existingIncidence.staticCast()); mItemById.insert(item.id(), updatedItem); // The item needs updating too, revision changed. *(existingIncidence.data()) = *(newIncidence.data()); } else { mItemById.insert(item.id(), item); // The item needs updating too, revision changed. // The item changed it's UID, update our maps, the Google resource changes the UID when we create incidences. handleUidChange(oldItem, item); } } void ETMCalendarPrivate::onRowsInsertedInFilteredModel(const QModelIndex &index, int start, int end) { itemsAdded(itemsFromModel(mFilteredETM, index, start, end)); } void ETMCalendarPrivate::onRowsAboutToBeRemovedInFilteredModel(const QModelIndex &index, int start, int end) { itemsRemoved(itemsFromModel(mFilteredETM, index, start, end)); } void ETMCalendarPrivate::onFilterChanged() { mCalFilterProxyModel->setFilter(q->filter()); } ETMCalendar::ETMCalendar(QObject *parent) : CalendarBase(new ETMCalendarPrivate(this), parent) { Q_D(ETMCalendar); d->init(); } ETMCalendar::ETMCalendar(const QStringList &mimeTypes, QObject *parent) : CalendarBase(new ETMCalendarPrivate(this), parent) { Q_D(ETMCalendar); d->mMimeTypes = mimeTypes; d->init(); } ETMCalendar::ETMCalendar(ETMCalendar *other, QObject *parent) : CalendarBase(new ETMCalendarPrivate(this), parent) { Q_D(ETMCalendar); CalendarModel *model = qobject_cast(other->entityTreeModel()); if (model) { d->mETM = model->weakPointer().toStrongRef(); } d->init(); } ETMCalendar::ETMCalendar(ChangeRecorder *monitor, QObject *parent) : CalendarBase(new ETMCalendarPrivate(this), parent) { Q_D(ETMCalendar); if (monitor) { connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), d, SLOT(onCollectionChanged(Akonadi::Collection,QSet))); d->mETM = CalendarModel::create(monitor); d->mETM->setObjectName("ETM"); d->mETM->setListFilter(Akonadi::CollectionFetchScope::Display); } d->init(); } ETMCalendar::~ETMCalendar() { } //TODO: move this up? Akonadi::Collection ETMCalendar::collection(Akonadi::Collection::Id id) const { Q_D(const ETMCalendar); return d->mCollectionMap.value(id); } bool ETMCalendar::hasRight(const KCalCore::Incidence::Ptr &inc, Akonadi::Collection::Right right) const { return hasRight(item(inc), right); } bool ETMCalendar::hasRight(const Akonadi::Item &item, Akonadi::Collection::Right right) const { // if the users changes the rights, item.parentCollection() // can still have the old rights, so we use call collection() // which returns the updated one const Akonadi::Collection col = collection(item.storageCollectionId()); return col.rights() & right; } QAbstractItemModel *ETMCalendar::model() const { Q_D(const ETMCalendar); return d->mFilteredETM; } KCheckableProxyModel *ETMCalendar::checkableProxyModel() const { Q_D(const ETMCalendar); return d->mCheckableProxyModel; } KCalCore::Alarm::List ETMCalendar::alarms(const KDateTime &from, const KDateTime &to, bool excludeBlockedAlarms) const { Q_D(const ETMCalendar); KCalCore::Alarm::List alarmList; QHashIterator i(d->mItemById); while (i.hasNext()) { const Akonadi::Item item = i.next().value(); BlockAlarmsAttribute *blockedAttr = 0; if (excludeBlockedAlarms) { // take the collection from m_collectionMap, because we need the up-to-date collection attrs Akonadi::Collection parentCollection = d->mCollectionMap.value(item.storageCollectionId()); if (parentCollection.isValid() && parentCollection.hasAttribute()) { blockedAttr = parentCollection.attribute(); if (blockedAttr->isEverythingBlocked()) { continue; } } } KCalCore::Incidence::Ptr incidence; if (item.isValid() && item.hasPayload()) { incidence = KCalCore::Incidence::Ptr(item.payload()->clone()); } else { continue; } if (!incidence) { continue; } if (blockedAttr) { // Remove all blocked types of alarms Q_FOREACH(const KCalCore::Alarm::Ptr &alarm, incidence->alarms()) { if (blockedAttr->isAlarmTypeBlocked(alarm->type())) { incidence->removeAlarm(alarm); } } } if (incidence->alarms().isEmpty()) { continue; } Alarm::List tmpList; if (incidence->recurs()) { appendRecurringAlarms(tmpList, incidence, from, to); } else { appendAlarms(tmpList, incidence, from, to); } // We need to tag them with the incidence uid in case // the caller will need it, because when we get out of // this scope the incidence will be destroyed. QVectorIterator a(tmpList); while (a.hasNext()) { a.next()->setCustomProperty("ETMCalendar", "parentUid", incidence->uid()); } alarmList += tmpList; } return alarmList; } Akonadi::EntityTreeModel *ETMCalendar::entityTreeModel() const { Q_D(const ETMCalendar); return d->mETM.data(); } void ETMCalendar::setCollectionFilteringEnabled(bool enable) { Q_D(ETMCalendar); if (d->mCollectionFilteringEnabled != enable) { d->mCollectionFilteringEnabled = enable; if (enable) { d->mSelectionProxy->setSourceModel(d->mETM.data()); QAbstractItemModel *oldModel = d->mCalFilterProxyModel->sourceModel(); d->mCalFilterProxyModel->setSourceModel(d->mSelectionProxy); delete qobject_cast(oldModel); } else { KDescendantsProxyModel *flatner = new KDescendantsProxyModel(this); flatner->setSourceModel(d->mETM.data()); d->mCalFilterProxyModel->setSourceModel(flatner); } } } bool ETMCalendar::collectionFilteringEnabled() const { Q_D(const ETMCalendar); return d->mCollectionFilteringEnabled; } bool ETMCalendar::isLoaded() const { Q_D(const ETMCalendar); if (!entityTreeModel()->isCollectionTreeFetched()) return false; Akonadi::Collection::List collections = d->mCollectionMap.values(); foreach (const Akonadi::Collection &collection, collections) { if (!entityTreeModel()->isCollectionPopulated(collection.id())) return false; } return true; } #include "moc_etmcalendar.cpp" #include "moc_etmcalendar_p.cpp"