kde-playground/kdepimlibs/akonadi/calendar/etmcalendar.cpp
2015-04-14 21:49:29 +00:00

631 lines
21 KiB
C++

/*
Copyright (C) 2011-2013 Sérgio Martins <iamsergio@gmail.com>
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 "utils_p.h"
#include <akonadi/item.h>
#include <akonadi/session.h>
#include <akonadi/collection.h>
#include <akonadi/changerecorder.h>
#include <akonadi/itemfetchscope.h>
#include <akonadi/entitydisplayattribute.h>
#include <akonadi/entitymimetypefiltermodel.h>
#include <akonadi/collectionfilterproxymodel.h>
#include <KSelectionProxyModel>
#include <KDescendantsProxyModel>
#include <QItemSelectionModel>
#include <QTreeView>
using namespace Akonadi;
using namespace KCalCore;
//TODO: implement batchAdding
ETMCalendarPrivate::ETMCalendarPrivate(ETMCalendar *qq) : CalendarBasePrivate(qq)
, mETM(0)
, mFilteredETM(0)
, mCheckableProxyModel(0)
, mCollectionProxyModel(0)
, mCalFilterProxyModel(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<QByteArray>)),
SLOT(onCollectionChanged(Akonadi::Collection,QSet<QByteArray>)));
Akonadi::ItemFetchScope scope;
scope.fetchFullPayload(true);
scope.fetchAttribute<Akonadi::EntityDisplayAttribute>();
monitor->setSession(session);
monitor->setCollectionMonitored(Akonadi::Collection::root());
monitor->fetchCollection(true);
monitor->setItemFetchScope(scope);
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");
}
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<QByteArray> &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");
mCollectionProxyModel = new Akonadi::CollectionFilterProxyModel(this);
mCollectionProxyModel->setObjectName("Only show collections");
mCollectionProxyModel->setDynamicSortFilter(true);
mCollectionProxyModel->addMimeTypeFilter(QString::fromLatin1("text/calendar"));
mCollectionProxyModel->setExcludeVirtualCollections(true);
mCollectionProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
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);
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");
mFilteredETM = new Akonadi::EntityMimeTypeFilterModel(this);
mFilteredETM->setSourceModel(mCalFilterProxyModel);
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 (!mItemIdByUid.isEmpty()) {
// This never happens
kDebug() << "This shouldnt happen: !mItemIdByUid.isEmpty()";
foreach(const QString &uid, mItemIdByUid.keys()) {
kDebug() << "uid: " << uid;
}
mItemIdByUid.clear();
//Q_ASSERT(false);
}
mParentUidToChildrenUid.clear();
//m_virtualItems.clear();
}
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<KCalCore::Incidence::Ptr>()) {
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<Akonadi::Item>();
item.setParentCollection(
idx.data(Akonadi::EntityTreeModel::ParentCollectionRole).value<Akonadi::Collection>());
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<Akonadi::Collection>();
}
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();
QModelIndex i(topLeft);
int row = i.row();
while (row <= endRow) {
const Akonadi::Collection col = collectionFromIndex(i);
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<KCalCore::Incidence::Ptr>())
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()));
IncidenceBase::Ptr existingIncidence = q->incidence(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);
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<KCalCore::Incidence::Ptr>(existingIncidence.staticCast<KCalCore::Incidence>());
mItemById.insert(item.id(), updatedItem); // The item needs updating too, revision changed.
// Check if RELATED-TO changed, updating parenting information
handleParentChanged(newIncidence);
*(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, newIncidence->instanceIdentifier());
}
}
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<Akonadi::CalendarModel*>(other->entityTreeModel());
if (model) {
d->mETM = model->weakPointer().toStrongRef();
}
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 QString &uid, Akonadi::Collection::Right right) const
{
return hasRight(item(uid), 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<Akonadi::Item::Id, Akonadi::Item> 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<BlockAlarmsAttribute>()) {
blockedAttr = parentCollection.attribute<BlockAlarmsAttribute>();
if (blockedAttr->isAlarmTypeBlocked(Alarm::Audio) && blockedAttr->isAlarmTypeBlocked(Alarm::Display)
&& blockedAttr->isAlarmTypeBlocked(Alarm::Email) && blockedAttr->isAlarmTypeBlocked(Alarm::Procedure))
{
continue;
}
}
}
KCalCore::Incidence::Ptr incidence;
if (item.isValid() && item.hasPayload<KCalCore::Incidence::Ptr>()) {
incidence = KCalCore::Incidence::Ptr(item.payload<KCalCore::Incidence::Ptr>()->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<Alarm::Ptr> 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<KDescendantsProxyModel *>(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"