kde-playground/kdepim/kalarm/akonadimodel.cpp

1932 lines
73 KiB
C++
Raw Normal View History

/*
* akonadimodel.cpp - KAlarm calendar file access using Akonadi
* Program: kalarm
* Copyright © 2007-2014 by David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "akonadimodel.h"
#include "alarmtime.h"
#include "autoqpointer.h"
#include "calendarmigrator.h"
#include "mainwindow.h"
#include "messagebox.h"
#include "preferences.h"
#include "synchtimer.h"
#include "kalarmsettings.h"
#include "kalarmdirsettings.h"
#include <kalarmcal/alarmtext.h>
#include <kalarmcal/collectionattribute.h>
#include <kalarmcal/compatibilityattribute.h>
#include <kalarmcal/eventattribute.h>
#include <akonadi/agentfilterproxymodel.h>
#include <akonadi/agentinstancecreatejob.h>
#include <akonadi/agentmanager.h>
#include <akonadi/agenttype.h>
#include <akonadi/agenttypedialog.h>
#include <akonadi/attributefactory.h>
#include <akonadi/changerecorder.h>
#include <akonadi/collectiondeletejob.h>
#include <akonadi/collectionmodifyjob.h>
#include <akonadi/entitydisplayattribute.h>
#include <akonadi/item.h>
#include <akonadi/itemcreatejob.h>
#include <akonadi/itemmodifyjob.h>
#include <akonadi/itemdeletejob.h>
#include <akonadi/itemfetchscope.h>
#include <klocale.h>
#include <kcolorutils.h>
#include <QApplication>
#include <QFileInfo>
#include <QTimer>
using namespace Akonadi;
using namespace KAlarmCal;
static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;
//static bool checkItem_true(const Item&) { return true; }
/*=============================================================================
= Class: AkonadiModel
=============================================================================*/
AkonadiModel* AkonadiModel::mInstance = 0;
QPixmap* AkonadiModel::mTextIcon = 0;
QPixmap* AkonadiModel::mFileIcon = 0;
QPixmap* AkonadiModel::mCommandIcon = 0;
QPixmap* AkonadiModel::mEmailIcon = 0;
QPixmap* AkonadiModel::mAudioIcon = 0;
QSize AkonadiModel::mIconSize;
int AkonadiModel::mTimeHourPos = -2;
/******************************************************************************
* Construct and return the singleton.
*/
AkonadiModel* AkonadiModel::instance()
{
if (!mInstance)
mInstance = new AkonadiModel(new ChangeRecorder(qApp), qApp);
return mInstance;
}
/******************************************************************************
* Constructor.
*/
AkonadiModel::AkonadiModel(ChangeRecorder* monitor, QObject* parent)
: EntityTreeModel(monitor, parent),
mMonitor(monitor),
mResourcesChecked(false),
mMigrating(false)
{
// Set lazy population to enable the contents of unselected collections to be ignored
setItemPopulationStrategy(LazyPopulation);
// Restrict monitoring to collections containing the KAlarm mime types
monitor->setCollectionMonitored(Collection::root());
monitor->setResourceMonitored("akonadi_kalarm_resource");
monitor->setResourceMonitored("akonadi_kalarm_dir_resource");
monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE);
monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED);
monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE);
monitor->itemFetchScope().fetchFullPayload();
monitor->itemFetchScope().fetchAttribute<EventAttribute>();
AttributeFactory::registerAttribute<CollectionAttribute>();
AttributeFactory::registerAttribute<CompatibilityAttribute>();
AttributeFactory::registerAttribute<EventAttribute>();
if (!mTextIcon)
{
mTextIcon = new QPixmap(SmallIcon(QLatin1String("dialog-information")));
mFileIcon = new QPixmap(SmallIcon(QLatin1String("document-open")));
mCommandIcon = new QPixmap(SmallIcon(QLatin1String("system-run")));
mEmailIcon = new QPixmap(SmallIcon(QLatin1String("mail-message-unread")));
mAudioIcon = new QPixmap(SmallIcon(QLatin1String("audio-x-generic")));
mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size());
}
#ifdef __GNUC__
#warning Only want to monitor collection properties, not content, when this becomes possible
#endif
connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet<QByteArray>)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet<QByteArray>)));
connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), SLOT(slotCollectionRemoved(Akonadi::Collection)));
initCalendarMigrator();
MinuteTimer::connect(this, SLOT(slotUpdateTimeTo()));
Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor)));
Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor)));
Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays()));
Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours()));
connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotRowsInserted(QModelIndex,int,int)));
connect(this, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)));
connect(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)), SLOT(slotMonitoredItemChanged(Akonadi::Item,QSet<QByteArray>)));
connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)),
SLOT(checkResources(Akonadi::ServerManager::State)));
checkResources(ServerManager::state());
}
AkonadiModel::~AkonadiModel()
{
if (mInstance == this)
mInstance = 0;
}
/******************************************************************************
* Called when the server manager changes state.
* If it is now running, i.e. the agent manager knows about
* all existing resources.
* Once it is running, i.e. the agent manager knows about
* all existing resources, if necessary migrate any KResources alarm calendars from
* pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources
* if any are missing.
*/
void AkonadiModel::checkResources(ServerManager::State state)
{
switch (state)
{
case ServerManager::Running:
if (!mResourcesChecked)
{
kDebug() << "Server running";
mResourcesChecked = true;
mMigrating = true;
CalendarMigrator::execute();
}
break;
case ServerManager::NotRunning:
kDebug() << "Server stopped";
mResourcesChecked = false;
mMigrating = false;
mCollectionAlarmTypes.clear();
mCollectionRights.clear();
mCollectionEnabled.clear();
initCalendarMigrator();
emit serverStopped();
break;
default:
break;
}
}
/******************************************************************************
* Initialise the calendar migrator so that it can be run (either for the first
* time, or again).
*/
void AkonadiModel::initCalendarMigrator()
{
CalendarMigrator::reset();
connect(CalendarMigrator::instance(), SIGNAL(creating(QString,Akonadi::Collection::Id,bool)),
SLOT(slotCollectionBeingCreated(QString,Akonadi::Collection::Id,bool)));
connect(CalendarMigrator::instance(), SIGNAL(destroyed(QObject*)), SLOT(slotMigrationCompleted()));
}
/******************************************************************************
* Return whether calendar migration has completed.
*/
bool AkonadiModel::isMigrationCompleted() const
{
return mResourcesChecked && !mMigrating;
}
/******************************************************************************
* Return the data for a given role, for a specified item.
*/
QVariant AkonadiModel::data(const QModelIndex& index, int role) const
{
// First check that it's a role we're interested in - if not, use the base method
switch (role)
{
case Qt::BackgroundRole:
case Qt::ForegroundRole:
case Qt::DisplayRole:
case Qt::TextAlignmentRole:
case Qt::DecorationRole:
case Qt::SizeHintRole:
case Qt::AccessibleTextRole:
case Qt::ToolTipRole:
case Qt::CheckStateRole:
case SortRole:
case ValueRole:
case StatusRole:
case AlarmActionsRole:
case AlarmSubActionRole:
case EnabledRole:
case EnabledTypesRole:
case CommandErrorRole:
case BaseColourRole:
case AlarmTypeRole:
case IsStandardRole:
break;
default:
return EntityTreeModel::data(index, role);
}
const Collection collection = index.data(CollectionRole).value<Collection>();
if (collection.isValid())
{
// This is a Collection row
switch (role)
{
case Qt::DisplayRole:
return collection.displayName();
case EnabledTypesRole:
if (!collection.hasAttribute<CollectionAttribute>())
return 0;
return static_cast<int>(collection.attribute<CollectionAttribute>()->enabled());
case BaseColourRole:
role = Qt::BackgroundRole;
break;
case Qt::BackgroundRole:
{
const QColor colour = backgroundColor_p(collection);
if (colour.isValid())
return colour;
break;
}
case Qt::ForegroundRole:
return foregroundColor(collection, collection.contentMimeTypes());
case Qt::ToolTipRole:
return tooltip(collection, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE);
case AlarmTypeRole:
return static_cast<int>(types(collection));
case IsStandardRole:
if (!collection.hasAttribute<CollectionAttribute>()
|| !isCompatible(collection))
return 0;
return static_cast<int>(collection.attribute<CollectionAttribute>()->standard());
case KeepFormatRole:
if (!collection.hasAttribute<CollectionAttribute>())
return false;
return collection.attribute<CollectionAttribute>()->keepFormat();
default:
break;
}
}
else
{
const Item item = index.data(ItemRole).value<Item>();
if (item.isValid())
{
// This is an Item row
const QString mime = item.mimeType();
if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE)
|| !item.hasPayload<KAEvent>())
return QVariant();
switch (role)
{
case StatusRole:
// Mime type has a one-to-one relationship to event's category()
if (mime == KAlarmCal::MIME_ACTIVE)
return CalEvent::ACTIVE;
if (mime == KAlarmCal::MIME_ARCHIVED)
return CalEvent::ARCHIVED;
if (mime == KAlarmCal::MIME_TEMPLATE)
return CalEvent::TEMPLATE;
return QVariant();
case CommandErrorRole:
if (!item.hasAttribute<EventAttribute>())
return KAEvent::CMD_NO_ERROR;
return item.attribute<EventAttribute>()->commandError();
default:
break;
}
const int column = index.column();
if (role == Qt::WhatsThisRole)
return whatsThisText(column);
const KAEvent event(this->event(item));
if (!event.isValid())
return QVariant();
if (role == AlarmActionsRole)
return event.actionTypes();
if (role == AlarmSubActionRole)
return event.actionSubType();
bool calendarColour = false;
switch (column)
{
case TimeColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DisplayRole:
if (event.expired())
return AlarmTime::alarmTimeText(event.startDateTime());
return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER));
case SortRole:
{
DateTime due;
if (event.expired())
due = event.startDateTime();
else
due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER);
return due.isValid() ? due.effectiveKDateTime().toUtc().dateTime()
: QDateTime(QDate(9999,12,31), QTime(0,0,0));
}
default:
break;
}
break;
case TimeToColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DisplayRole:
if (event.expired())
return QString();
return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER));
case SortRole:
{
if (event.expired())
return -1;
const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER);
const KDateTime now = KDateTime::currentUtcDateTime();
if (due.isDateOnly())
return now.date().daysTo(due.date()) * 1440;
return (now.secsTo(due.effectiveKDateTime()) + 59) / 60;
}
}
break;
case RepeatColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DisplayRole:
return repeatText(event);
case Qt::TextAlignmentRole:
return Qt::AlignHCenter;
case SortRole:
return repeatOrder(event);
}
break;
case ColourColumn:
switch (role)
{
case Qt::BackgroundRole:
{
const KAEvent::Actions type = event.actionTypes();
if (type & KAEvent::ACT_DISPLAY)
return event.bgColour();
if (type == KAEvent::ACT_COMMAND)
{
if (event.commandError() != KAEvent::CMD_NO_ERROR)
return Qt::red;
}
break;
}
case Qt::ForegroundRole:
if (event.commandError() != KAEvent::CMD_NO_ERROR)
{
if (event.actionTypes() == KAEvent::ACT_COMMAND)
return Qt::white;
QColor colour = Qt::red;
int r, g, b;
event.bgColour().getRgb(&r, &g, &b);
if (r > 128 && g <= 128 && b <= 128)
colour = Qt::white;
return colour;
}
break;
case Qt::DisplayRole:
if (event.commandError() != KAEvent::CMD_NO_ERROR)
return QString::fromLatin1("!");
break;
case SortRole:
{
const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY)
? event.bgColour().rgb() : 0;
return QString::fromLatin1("%1").arg(i, 6, 10, QLatin1Char('0'));
}
default:
break;
}
break;
case TypeColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DecorationRole:
{
QVariant v;
v.setValue(*eventIcon(event));
return v;
}
case Qt::TextAlignmentRole:
return Qt::AlignHCenter;
case Qt::SizeHintRole:
return mIconSize;
case Qt::AccessibleTextRole:
#ifdef __GNUC__
#warning Implement accessibility
#endif
return QString();
case ValueRole:
return static_cast<int>(event.actionSubType());
case SortRole:
return QString::fromLatin1("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0'));
}
break;
case TextColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DisplayRole:
case SortRole:
return AlarmText::summary(event, 1);
case Qt::ToolTipRole:
return AlarmText::summary(event, 10);
default:
break;
}
break;
case TemplateNameColumn:
switch (role)
{
case Qt::BackgroundRole:
calendarColour = true;
break;
case Qt::DisplayRole:
return event.templateName();
case SortRole:
return event.templateName().toUpper();
}
break;
default:
break;
}
switch (role)
{
case Qt::ForegroundRole:
if (!event.enabled())
return Preferences::disabledColour();
if (event.expired())
return Preferences::archivedColour();
break; // use the default for normal active alarms
case Qt::ToolTipRole:
// Show the last command execution error message
switch (event.commandError())
{
case KAEvent::CMD_ERROR:
return i18nc("@info:tooltip", "Command execution failed");
case KAEvent::CMD_ERROR_PRE:
return i18nc("@info:tooltip", "Pre-alarm action execution failed");
case KAEvent::CMD_ERROR_POST:
return i18nc("@info:tooltip", "Post-alarm action execution failed");
case KAEvent::CMD_ERROR_PRE_POST:
return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed");
default:
case KAEvent::CMD_NO_ERROR:
break;
}
break;
case EnabledRole:
return event.enabled();
default:
break;
}
if (calendarColour)
{
Collection parent = item.parentCollection();
const QColor colour = backgroundColor(parent);
if (colour.isValid())
return colour;
}
}
}
return EntityTreeModel::data(index, role);
}
/******************************************************************************
* Set the font to use for all items, or the checked state of one item.
* The font must always be set at initialisation.
*/
bool AkonadiModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid())
return false;
// NOTE: need to emit dataChanged() whenever something is updated (except via a job).
Collection collection = index.data(CollectionRole).value<Collection>();
if (collection.isValid())
{
// This is a Collection row
bool updateCollection = false;
CollectionAttribute* attr = 0;
switch (role)
{
case Qt::BackgroundRole:
{
const QColor colour = value.value<QColor>();
attr = collection.attribute<CollectionAttribute>(Entity::AddIfMissing);
if (attr->backgroundColor() == colour)
return true; // no change
attr->setBackgroundColor(colour);
updateCollection = true;
break;
}
case EnabledTypesRole:
{
const CalEvent::Types types = static_cast<CalEvent::Types>(value.value<int>());
attr = collection.attribute<CollectionAttribute>(Entity::AddIfMissing);
if (attr->enabled() == types)
return true; // no change
kDebug() << "Set enabled:" << types << ", was=" << attr->enabled();
attr->setEnabled(types);
updateCollection = true;
break;
}
case IsStandardRole:
if (collection.hasAttribute<CollectionAttribute>()
&& isCompatible(collection))
{
const CalEvent::Types types = static_cast<CalEvent::Types>(value.value<int>());
attr = collection.attribute<CollectionAttribute>(Entity::AddIfMissing);
kDebug()<<"Set standard:"<<types<<", was="<<attr->standard();
attr->setStandard(types);
updateCollection = true;
}
break;
case KeepFormatRole:
{
const bool keepFormat = value.value<bool>();
attr = collection.attribute<CollectionAttribute>(Entity::AddIfMissing);
if (attr->keepFormat() == keepFormat)
return true; // no change
attr->setKeepFormat(keepFormat);
updateCollection = true;
break;
}
default:
break;
}
if (updateCollection)
{
// Update the CollectionAttribute value.
// Note that we can't supply 'collection' to CollectionModifyJob since
// that also contains the CompatibilityAttribute value, which is read-only
// for applications. So create a new Collection instance and only set a
// value for CollectionAttribute.
Collection c(collection.id());
CollectionAttribute* att = c.attribute<CollectionAttribute>(Entity::AddIfMissing);
*att = *attr;
CollectionModifyJob* job = new CollectionModifyJob(c, this);
connect(job, SIGNAL(result(KJob*)), this, SLOT(modifyCollectionJobDone(KJob*)));
return true;
}
}
else
{
Item item = index.data(ItemRole).value<Item>();
if (item.isValid())
{
bool updateItem = false;
switch (role)
{
case CommandErrorRole:
{
const KAEvent::CmdErrType err = static_cast<KAEvent::CmdErrType>(value.toInt());
switch (err)
{
case KAEvent::CMD_NO_ERROR:
case KAEvent::CMD_ERROR:
case KAEvent::CMD_ERROR_PRE:
case KAEvent::CMD_ERROR_POST:
case KAEvent::CMD_ERROR_PRE_POST:
{
if (err == KAEvent::CMD_NO_ERROR && !item.hasAttribute<EventAttribute>())
return true; // no change
EventAttribute* attr = item.attribute<EventAttribute>(Entity::AddIfMissing);
if (attr->commandError() == err)
return true; // no change
attr->setCommandError(err);
updateItem = true;
kDebug()<<"Item:"<<item.id()<<" CommandErrorRole ->"<<err;
break;
}
default:
return false;
}
break;
}
default:
kDebug()<<"Item: passing to EntityTreeModel::setData("<<role<<")";
break;
}
if (updateItem)
{
queueItemModifyJob(item);
return true;
}
}
}
return EntityTreeModel::setData(index, value, role);
}
/******************************************************************************
* Return the number of columns for either a collection or an item.
*/
int AkonadiModel::entityColumnCount(HeaderGroup group) const
{
switch (group)
{
case CollectionTreeHeaders:
return 1;
case ItemListHeaders:
return ColumnCount;
default:
return EntityTreeModel::entityColumnCount(group);
}
}
/******************************************************************************
* Return data for a column heading.
*/
QVariant AkonadiModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup group) const
{
if (orientation == Qt::Horizontal)
{
switch (group)
{
case CollectionTreeHeaders:
if (section != 0)
return QVariant();
if (role == Qt::DisplayRole)
return i18nc("@title:column", "Calendars");
break;
case ItemListHeaders:
if (section < 0 || section >= ColumnCount)
return QVariant();
if (role == Qt::DisplayRole)
{
switch (section)
{
case TimeColumn:
return i18nc("@title:column", "Time");
case TimeToColumn:
return i18nc("@title:column", "Time To");
case RepeatColumn:
return i18nc("@title:column", "Repeat");
case ColourColumn:
return QString();
case TypeColumn:
return QString();
case TextColumn:
return i18nc("@title:column", "Message, File or Command");
case TemplateNameColumn:
return i18nc("@title:column Template name", "Name");
}
}
else if (role == Qt::WhatsThisRole)
return whatsThisText(section);
break;
default:
break;
}
}
return EntityTreeModel::entityHeaderData(section, orientation, role, group);
}
/******************************************************************************
* Recursive function to emit the dataChanged() signal for all items in a
* specified column range.
*/
void AkonadiModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent)
{
int start = -1;
int end = -1;
for (int row = 0, count = rowCount(parent); row < count; ++row)
{
const QModelIndex ix = index(row, 0, parent);
const Item item = data(ix, ItemRole).value<Item>();
const bool isItem = item.isValid();
if (isItem)
{
if ((*checkFunc)(item))
{
// For efficiency, emit a single signal for each group of
// consecutive items, rather than a separate signal for each item.
if (start < 0)
start = row;
end = row;
continue;
}
}
if (start >= 0)
emit dataChanged(index(start, startColumn, parent), index(end, endColumn, parent));
start = -1;
if (!isItem)
signalDataChanged(checkFunc, startColumn, endColumn, ix);
}
if (start >= 0)
emit dataChanged(index(start, startColumn, parent), index(end, endColumn, parent));
}
/******************************************************************************
* Signal every minute that the time-to-alarm values have changed.
*/
static bool checkItem_isActive(const Item& item)
{ return item.mimeType() == KAlarmCal::MIME_ACTIVE; }
void AkonadiModel::slotUpdateTimeTo()
{
signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex());
}
/******************************************************************************
* Called when the colour used to display archived alarms has changed.
*/
static bool checkItem_isArchived(const Item& item)
{ return item.mimeType() == KAlarmCal::MIME_ARCHIVED; }
void AkonadiModel::slotUpdateArchivedColour(const QColor&)
{
kDebug();
signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex());
}
/******************************************************************************
* Called when the colour used to display disabled alarms has changed.
*/
static bool checkItem_isDisabled(const Item& item)
{
if (item.hasPayload<KAEvent>())
{
const KAEvent event = item.payload<KAEvent>();
if (event.isValid())
return !event.enabled();
}
return false;
}
void AkonadiModel::slotUpdateDisabledColour(const QColor&)
{
kDebug();
signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex());
}
/******************************************************************************
* Called when the definition of holidays has changed.
*/
static bool checkItem_excludesHolidays(const Item& item)
{
if (item.hasPayload<KAEvent>())
{
const KAEvent event = item.payload<KAEvent>();
if (event.isValid() && event.holidaysExcluded())
return true;
}
return false;
}
void AkonadiModel::slotUpdateHolidays()
{
kDebug();
Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns
signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex());
}
/******************************************************************************
* Called when the definition of working hours has changed.
*/
static bool checkItem_workTimeOnly(const Item& item)
{
if (item.hasPayload<KAEvent>())
{
const KAEvent event = item.payload<KAEvent>();
if (event.isValid() && event.workTimeOnly())
return true;
}
return false;
}
void AkonadiModel::slotUpdateWorkingHours()
{
kDebug();
Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns
signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex());
}
/******************************************************************************
* Called when the command error status of an alarm has changed, to save the new
* status and update the visual command error indication.
*/
void AkonadiModel::updateCommandError(const KAEvent& event)
{
const QModelIndex ix = itemIndex(event.itemId());
if (ix.isValid())
setData(ix, QVariant(static_cast<int>(event.commandError())), CommandErrorRole);
}
/******************************************************************************
* Return the foreground color for displaying a collection, based on the
* supplied mime types which it contains, and on whether it is fully writable.
*/
QColor AkonadiModel::foregroundColor(const Akonadi::Collection& collection, const QStringList& mimeTypes)
{
QColor colour;
if (mimeTypes.contains(KAlarmCal::MIME_ACTIVE))
colour = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color();
else if (mimeTypes.contains(KAlarmCal::MIME_ARCHIVED))
colour = Preferences::archivedColour();
else if (mimeTypes.contains(KAlarmCal::MIME_TEMPLATE))
colour = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color();
if (colour.isValid() && isWritable(collection) <= 0)
return KColorUtils::lighten(colour, 0.2);
return colour;
}
/******************************************************************************
* Set the background color for displaying the collection and its alarms.
*/
void AkonadiModel::setBackgroundColor(Collection& collection, const QColor& colour)
{
const QModelIndex ix = modelIndexForCollection(this, collection);
if (ix.isValid())
setData(ix, QVariant(colour), Qt::BackgroundRole);
}
/******************************************************************************
* Return the background color for displaying the collection and its alarms,
* after updating the collection from the Akonadi database.
*/
QColor AkonadiModel::backgroundColor(Akonadi::Collection& collection) const
{
if (!collection.isValid())
return QColor();
refresh(collection);
return backgroundColor_p(collection);
}
/******************************************************************************
* Return the background color for displaying the collection and its alarms.
*/
QColor AkonadiModel::backgroundColor_p(const Akonadi::Collection& collection) const
{
if (!collection.isValid() || !collection.hasAttribute<CollectionAttribute>())
return QColor();
return collection.attribute<CollectionAttribute>()->backgroundColor();
}
/******************************************************************************
* Return the display name for the collection, after updating the collection
* from the Akonadi database.
*/
QString AkonadiModel::displayName(Akonadi::Collection& collection) const
{
if (!collection.isValid())
return QString();
refresh(collection);
return collection.displayName();
}
/******************************************************************************
* Return the storage type (file, directory, etc.) for the collection.
*/
QString AkonadiModel::storageType(const Akonadi::Collection& collection) const
{
const KUrl url = collection.remoteId();
return !url.isLocalFile() ? i18nc("@info/plain", "URL")
: QFileInfo(url.toLocalFile()).isDir() ? i18nc("@info/plain Directory in filesystem", "Directory")
: i18nc("@info/plain", "File");
}
/******************************************************************************
* Return a collection's tooltip text. The collection's enabled status is
* evaluated for specified alarm types.
*/
QString AkonadiModel::tooltip(const Collection& collection, CalEvent::Types types) const
{
const QString name = QLatin1Char('@') + collection.displayName(); // insert markers for stripping out name
const KUrl url = collection.remoteId();
const QString type = QLatin1Char('@') + storageType(collection); // file/directory/URL etc.
const QString locn = url.pathOrUrl();
const bool inactive = !collection.hasAttribute<CollectionAttribute>()
|| !(collection.attribute<CollectionAttribute>()->enabled() & types);
const QString disabled = i18nc("@info/plain", "Disabled");
const QString readonly = readOnlyTooltip(collection);
const bool writable = readonly.isEmpty();
if (inactive && !writable)
return i18nc("@info:tooltip",
"%1"
"<nl/>%2: <filename>%3</filename>"
"<nl/>%4, %5",
name, type, locn, disabled, readonly);
if (inactive || !writable)
return i18nc("@info:tooltip",
"%1"
"<nl/>%2: <filename>%3</filename>"
"<nl/>%4",
name, type, locn, (inactive ? disabled : readonly));
return i18nc("@info:tooltip",
"%1"
"<nl/>%2: <filename>%3</filename>",
name, type, locn);
}
/******************************************************************************
* Return the read-only status tooltip for a collection.
* A null string is returned if the collection is fully writable.
*/
QString AkonadiModel::readOnlyTooltip(const Collection& collection)
{
KACalendar::Compat compat;
switch (AkonadiModel::isWritable(collection, compat))
{
case 1:
return QString();
case 0:
return i18nc("@info/plain", "Read-only (old format)");
default:
if (compat == KACalendar::Current)
return i18nc("@info/plain", "Read-only");
return i18nc("@info/plain", "Read-only (other format)");
}
}
/******************************************************************************
* Return the repetition text.
*/
QString AkonadiModel::repeatText(const KAEvent& event) const
{
QString repeatText = event.recurrenceText(true);
if (repeatText.isEmpty())
repeatText = event.repetitionText(true);
return repeatText;
}
/******************************************************************************
* Return a string for sorting the repetition column.
*/
QString AkonadiModel::repeatOrder(const KAEvent& event) const
{
int repeatOrder = 0;
int repeatInterval = 0;
if (event.repeatAtLogin())
repeatOrder = 1;
else
{
repeatInterval = event.recurInterval();
switch (event.recurType())
{
case KARecurrence::MINUTELY:
repeatOrder = 2;
break;
case KARecurrence::DAILY:
repeatOrder = 3;
break;
case KARecurrence::WEEKLY:
repeatOrder = 4;
break;
case KARecurrence::MONTHLY_DAY:
case KARecurrence::MONTHLY_POS:
repeatOrder = 5;
break;
case KARecurrence::ANNUAL_DATE:
case KARecurrence::ANNUAL_POS:
repeatOrder = 6;
break;
case KARecurrence::NO_RECUR:
default:
break;
}
}
return QString::fromLatin1("%1%2").arg(static_cast<char>('0' + repeatOrder)).arg(repeatInterval, 8, 10, QLatin1Char('0'));
}
/******************************************************************************
* Return the icon associated with the event's action.
*/
QPixmap* AkonadiModel::eventIcon(const KAEvent& event) const
{
switch (event.actionTypes())
{
case KAEvent::ACT_EMAIL:
return mEmailIcon;
case KAEvent::ACT_AUDIO:
return mAudioIcon;
case KAEvent::ACT_COMMAND:
return mCommandIcon;
case KAEvent::ACT_DISPLAY:
if (event.actionSubType() == KAEvent::FILE)
return mFileIcon;
// fall through to ACT_DISPLAY_COMMAND
case KAEvent::ACT_DISPLAY_COMMAND:
default:
return mTextIcon;
}
}
/******************************************************************************
* Returns the QWhatsThis text for a specified column.
*/
QString AkonadiModel::whatsThisText(int column) const
{
switch (column)
{
case TimeColumn:
return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm");
case TimeToColumn:
return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm");
case RepeatColumn:
return i18nc("@info:whatsthis", "How often the alarm recurs");
case ColourColumn:
return i18nc("@info:whatsthis", "Background color of alarm message");
case TypeColumn:
return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)");
case TextColumn:
return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line");
case TemplateNameColumn:
return i18nc("@info:whatsthis", "Name of the alarm template");
default:
return QString();
}
}
/******************************************************************************
* Remove a collection from Akonadi. The calendar file is not removed.
*/
bool AkonadiModel::removeCollection(const Akonadi::Collection& collection)
{
if (!collection.isValid())
return false;
kDebug() << collection.id();
Collection col = collection;
mCollectionsDeleting << collection.id();
// Note: CollectionDeleteJob deletes the backend storage also.
AgentManager* agentManager = AgentManager::self();
const AgentInstance instance = agentManager->instance(collection.resource());
if (instance.isValid())
agentManager->removeInstance(instance);
#if 0
CollectionDeleteJob* job = new CollectionDeleteJob(col);
connect(job, SIGNAL(result(KJob*)), SLOT(deleteCollectionJobDone(KJob*)));
mPendingCollectionJobs[job] = CollJobData(col.id(), displayName(col));
job->start();
#endif
return true;
}
/******************************************************************************
* Return whether a collection is currently being deleted.
*/
bool AkonadiModel::isCollectionBeingDeleted(Collection::Id id) const
{
return mCollectionsDeleting.contains(id);
}
#if 0
/******************************************************************************
* Called when a collection deletion job has completed.
* Checks for any error.
*/
void AkonadiModel::deleteCollectionJobDone(KJob* j)
{
QMap<KJob*, CollJobData>::iterator it = mPendingCollectionJobs.find(j);
CollJobData jobData;
if (it != mPendingCollectionJobs.end())
{
jobData = it.value();
mPendingCollectionJobs.erase(it);
}
if (j->error())
{
emit collectionDeleted(jobData.id, false);
const QString errMsg = i18nc("@info", "Failed to remove calendar <resource>%1</resource>.", jobData.displayName);
kError() << errMsg << ":" << j->errorString();
KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1<nl/>(%2)", errMsg, j->errorString()));
}
else
emit collectionDeleted(jobData.id, true);
}
#endif
/******************************************************************************
* Reload a collection from Akonadi storage. The backend data is not reloaded.
*/
bool AkonadiModel::reloadCollection(const Akonadi::Collection& collection)
{
if (!collection.isValid())
return false;
kDebug() << collection.id();
mMonitor->setCollectionMonitored(collection, false);
mMonitor->setCollectionMonitored(collection, true);
return true;
}
/******************************************************************************
* Reload a collection from Akonadi storage. The backend data is not reloaded.
*/
void AkonadiModel::reload()
{
kDebug();
const Collection::List collections = mMonitor->collectionsMonitored();
foreach (const Collection& collection, collections)
{
mMonitor->setCollectionMonitored(collection, false);
mMonitor->setCollectionMonitored(collection, true);
}
}
/******************************************************************************
* Called when a collection modification job has completed.
* Checks for any error.
*/
void AkonadiModel::modifyCollectionJobDone(KJob* j)
{
Collection collection = static_cast<CollectionModifyJob*>(j)->collection();
const Collection::Id id = collection.id();
if (j->error())
{
emit collectionModified(id, false);
if (mCollectionsDeleted.contains(id))
mCollectionsDeleted.removeAll(id);
else
{
const QString errMsg = i18nc("@info", "Failed to update calendar <resource>%1</resource>.", displayName(collection));
kError() << "Id:" << collection.id() << errMsg << ":" << j->errorString();
KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1<nl/>(%2)", errMsg, j->errorString()));
}
}
else
emit collectionModified(id, true);
}
/******************************************************************************
* Returns the index to a specified event.
*/
QModelIndex AkonadiModel::eventIndex(const KAEvent& event)
{
return itemIndex(event.itemId());
}
/******************************************************************************
* Search for an event's item ID. This method ignores any itemId() value
* contained in the KAEvent. The collectionId() is used if available.
*/
Item::Id AkonadiModel::findItemId(const KAEvent& event)
{
Collection::Id colId = event.collectionId();
QModelIndex start = (colId < 0) ? index(0, 0) : collectionIndex(Collection(colId));
Qt::MatchFlags flags = (colId < 0) ? Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive | Qt::MatchWrap
: Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive;
const QModelIndexList indexes = match(start, RemoteIdRole, event.id(), -1, flags);
foreach (const QModelIndex& ix, indexes)
{
if (ix.isValid())
{
Item::Id id = ix.data(ItemIdRole).toLongLong();
if (id >= 0)
{
if (colId < 0
|| ix.data(ParentCollectionRole).value<Collection>().id() == colId)
return id;
}
}
}
return -1;
}
#if 0
/******************************************************************************
* Return all events of a given type belonging to a collection.
*/
KAEvent::List AkonadiModel::events(Akonadi::Collection& collection, CalEvent::Type type) const
{
KAEvent::List list;
const QModelIndex ix = modelIndexForCollection(this, collection);
if (ix.isValid())
getChildEvents(ix, type, list);
return list;
}
/******************************************************************************
* Recursive function to append all child Events with a given mime type.
*/
void AkonadiModel::getChildEvents(const QModelIndex& parent, CalEvent::Type type, KAEvent::List& events) const
{
for (int row = 0, count = rowCount(parent); row < count; ++row)
{
const QModelIndex ix = index(row, 0, parent);
const Item item = data(ix, ItemRole).value<Item>();
if (item.isValid())
{
if (item.hasPayload<KAEvent>())
{
KAEvent event = item.payload<KAEvent>();
if (event.isValid() && event.category() == type)
events += event;
}
}
else
{
const Collection c = ix.data(CollectionRole).value<Collection>();
if (c.isValid())
getChildEvents(ix, type, events);
}
}
}
#endif
KAEvent AkonadiModel::event(Item::Id itemId) const
{
const QModelIndex ix = itemIndex(itemId);
if (!ix.isValid())
return KAEvent();
return event(ix.data(ItemRole).value<Item>(), ix, 0);
}
KAEvent AkonadiModel::event(const QModelIndex& index) const
{
return event(index.data(ItemRole).value<Item>(), index, 0);
}
KAEvent AkonadiModel::event(const Item& item, const QModelIndex& index, Collection* collection) const
{
if (!item.isValid() || !item.hasPayload<KAEvent>())
return KAEvent();
const QModelIndex ix = index.isValid() ? index : itemIndex(item.id());
if (!ix.isValid())
return KAEvent();
KAEvent e = item.payload<KAEvent>();
if (e.isValid())
{
Collection c = data(ix, ParentCollectionRole).value<Collection>();
// Set collection ID using a const method, to avoid unnecessary copying of KAEvent
e.setCollectionId_const(c.id());
if (collection)
*collection = c;
}
return e;
}
#if 0
/******************************************************************************
* Add an event to the default or a user-selected Collection.
*/
AkonadiModel::Result AkonadiModel::addEvent(KAEvent* event, CalEvent::Type type, QWidget* promptParent, bool noPrompt)
{
kDebug() << event->id();
// Determine parent collection - prompt or use default
bool cancelled;
const Collection collection = destination(type, Collection::CanCreateItem, promptParent, noPrompt, &cancelled);
if (!collection.isValid())
{
delete event;
if (cancelled)
return Cancelled;
kDebug() << "No collection";
return Failed;
}
if (!addEvent(event, collection))
{
kDebug() << "Failed";
return Failed; // event was deleted by addEvent()
}
return Success;
}
#endif
/******************************************************************************
* Add events to a specified Collection.
* Events which are scheduled to be added to the collection are updated with
* their Akonadi item ID.
* The caller must connect to the itemDone() signal to check whether events
* have been added successfully. Note that the first signal may be emitted
* before this function returns.
* Reply = true if item creation has been scheduled for all events,
* = false if at least one item creation failed to be scheduled.
*/
bool AkonadiModel::addEvents(const KAEvent::List& events, Collection& collection)
{
bool ok = true;
for (int i = 0, count = events.count(); i < count; ++i)
ok = ok && addEvent(*events[i], collection);
return ok;
}
/******************************************************************************
* Add an event to a specified Collection.
* If the event is scheduled to be added to the collection, it is updated with
* its Akonadi item ID.
* The event's 'updated' flag is cleared.
* The caller must connect to the itemDone() signal to check whether events
* have been added successfully.
* Reply = true if item creation has been scheduled.
*/
bool AkonadiModel::addEvent(KAEvent& event, Collection& collection)
{
kDebug() << "ID:" << event.id();
Item item;
if (!event.setItemPayload(item, collection.contentMimeTypes()))
{
kWarning() << "Invalid mime type for collection";
return false;
}
event.setItemId(item.id());
kDebug()<<"-> item id="<<item.id();
ItemCreateJob* job = new ItemCreateJob(item, collection);
connect(job, SIGNAL(result(KJob*)), SLOT(itemJobDone(KJob*)));
mPendingItemJobs[job] = item.id();
job->start();
kDebug()<<"...exiting";
return true;
}
/******************************************************************************
* Update an event in its collection.
* The event retains its existing Akonadi item ID.
* The event's 'updated' flag is cleared.
* The caller must connect to the itemDone() signal to check whether the event
* has been updated successfully.
* Reply = true if item update has been scheduled.
*/
bool AkonadiModel::updateEvent(KAEvent& event)
{
kDebug() << "ID:" << event.id();
return updateEvent(event.itemId(), event);
}
bool AkonadiModel::updateEvent(Akonadi::Entity::Id itemId, KAEvent& newEvent)
{
kDebug()<<"item id="<<itemId;
const QModelIndex ix = itemIndex(itemId);
if (!ix.isValid())
return false;
const Collection collection = ix.data(ParentCollectionRole).value<Collection>();
Item item = ix.data(ItemRole).value<Item>();
kDebug()<<"item id="<<item.id()<<", revision="<<item.revision();
if (!newEvent.setItemPayload(item, collection.contentMimeTypes()))
{
kWarning() << "Invalid mime type for collection";
return false;
}
// setData(ix, QVariant::fromValue(item), ItemRole);
queueItemModifyJob(item);
return true;
}
/******************************************************************************
* Delete an event from its collection.
*/
bool AkonadiModel::deleteEvent(const KAEvent& event)
{
return deleteEvent(event.itemId());
}
bool AkonadiModel::deleteEvent(Akonadi::Entity::Id itemId)
{
kDebug() << itemId;
const QModelIndex ix = itemIndex(itemId);
if (!ix.isValid())
return false;
if (mCollectionsDeleting.contains(ix.data(ParentCollectionRole).value<Collection>().id()))
{
kDebug() << "Collection being deleted";
return true; // the event's collection is being deleted
}
const Item item = ix.data(ItemRole).value<Item>();
ItemDeleteJob* job = new ItemDeleteJob(item);
connect(job, SIGNAL(result(KJob*)), SLOT(itemJobDone(KJob*)));
mPendingItemJobs[job] = itemId;
job->start();
return true;
}
/******************************************************************************
* Queue an ItemModifyJob for execution. Ensure that only one job is
* simultaneously active for any one Item.
*
* This is necessary because we can't call two ItemModifyJobs for the same Item
* at the same time; otherwise Akonadi will detect a conflict and require manual
* intervention to resolve it.
*/
void AkonadiModel::queueItemModifyJob(const Item& item)
{
kDebug() << item.id();
QMap<Item::Id, Item>::Iterator it = mItemModifyJobQueue.find(item.id());
if (it != mItemModifyJobQueue.end())
{
// A job is already queued for this item. Replace the queued item value with the new one.
kDebug() << "Replacing previously queued job";
it.value() = item;
}
else
{
// There is no job already queued for this item
if (mItemsBeingCreated.contains(item.id()))
{
kDebug() << "Waiting for item initialisation";
mItemModifyJobQueue[item.id()] = item; // wait for item initialisation to complete
}
else
{
Item newItem = item;
const Item current = itemById(item.id()); // fetch the up-to-date item
if (current.isValid())
newItem.setRevision(current.revision());
mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing
ItemModifyJob* job = new ItemModifyJob(newItem);
job->disableRevisionCheck();
connect(job, SIGNAL(result(KJob*)), SLOT(itemJobDone(KJob*)));
mPendingItemJobs[job] = item.id();
kDebug() << "Executing Modify job for item" << item.id() << ", revision=" << newItem.revision();
}
}
}
/******************************************************************************
* Called when an item job has completed.
* Checks for any error.
* Note that for an ItemModifyJob, the item revision number may not be updated
* to the post-modification value. The next queued ItemModifyJob is therefore
* not kicked off from here, but instead from the slot attached to the
* itemChanged() signal, which has the revision updated.
*/
void AkonadiModel::itemJobDone(KJob* j)
{
const QMap<KJob*, Entity::Id>::iterator it = mPendingItemJobs.find(j);
Entity::Id itemId = -1;
if (it != mPendingItemJobs.end())
{
itemId = it.value();
mPendingItemJobs.erase(it);
}
const QByteArray jobClass = j->metaObject()->className();
kDebug() << jobClass;
if (j->error())
{
QString errMsg;
if (jobClass == "Akonadi::ItemCreateJob")
errMsg = i18nc("@info/plain", "Failed to create alarm.");
else if (jobClass == "Akonadi::ItemModifyJob")
errMsg = i18nc("@info/plain", "Failed to update alarm.");
else if (jobClass == "Akonadi::ItemDeleteJob")
errMsg = i18nc("@info/plain", "Failed to delete alarm.");
else
Q_ASSERT(0);
kError() << errMsg << itemId << ":" << j->errorString();
emit itemDone(itemId, false);
if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob")
{
// Execute the next queued job for this item
const Item current = itemById(itemId); // fetch the up-to-date item
checkQueuedItemModifyJob(current);
}
KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1<nl/>(%2)", errMsg, j->errorString()));
}
else
{
if (jobClass == "Akonadi::ItemCreateJob")
{
// Prevent modification of the item until it is fully initialised.
// Either slotMonitoredItemChanged() or slotRowsInserted(), or both,
// will be called when the item is done.
kDebug() << "item id=" << static_cast<ItemCreateJob*>(j)->item().id();
mItemsBeingCreated << static_cast<ItemCreateJob*>(j)->item().id();
}
emit itemDone(itemId);
}
/* if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob")
{
const QMap<Item::Id, Item>::iterator it = mItemModifyJobQueue.find(itemId);
if (it != mItemModifyJobQueue.end())
{
if (!it.value().isValid())
mItemModifyJobQueue.erase(it); // there are no more jobs queued for the item
}
}*/
}
/******************************************************************************
* Check whether there are any ItemModifyJobs waiting for a specified item, and
* if so execute the first one provided its creation has completed. This
* prevents clashes in Akonadi conflicts between simultaneous ItemModifyJobs for
* the same item.
*
* Note that when an item is newly created (e.g. via addEvent()), the KAlarm
* resource itemAdded() function creates an ItemModifyJob to give it a remote
* ID. Until that job is complete, any other ItemModifyJob for the item will
* cause a conflict.
*/
void AkonadiModel::checkQueuedItemModifyJob(const Item& item)
{
if (mItemsBeingCreated.contains(item.id()))
{kDebug()<<"Still being created";
return; // the item hasn't been fully initialised yet
}
const QMap<Item::Id, Item>::iterator it = mItemModifyJobQueue.find(item.id());
if (it == mItemModifyJobQueue.end())
{kDebug()<<"No jobs queued";
return; // there are no jobs queued for the item
}
Item qitem = it.value();
if (!qitem.isValid())
{
// There is no further job queued for the item, so remove the item from the list
kDebug()<<"No more jobs queued";
mItemModifyJobQueue.erase(it);
}
else
{
// Queue the next job for the Item, after updating the Item's
// revision number to match that set by the job just completed.
qitem.setRevision(item.revision());
mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing
ItemModifyJob* job = new ItemModifyJob(qitem);
job->disableRevisionCheck();
connect(job, SIGNAL(result(KJob*)), SLOT(itemJobDone(KJob*)));
mPendingItemJobs[job] = qitem.id();
kDebug() << "Executing queued Modify job for item" << qitem.id() << ", revision=" << qitem.revision();
}
}
/******************************************************************************
* Called when rows have been inserted into the model.
*/
void AkonadiModel::slotRowsInserted(const QModelIndex& parent, int start, int end)
{
kDebug() << start << "-" << end << "(parent =" << parent << ")";
for (int row = start; row <= end; ++row)
{
const QModelIndex ix = index(row, 0, parent);
const Collection collection = ix.data(CollectionRole).value<Collection>();
if (collection.isValid())
{
// A collection has been inserted.
// Ignore it if it isn't owned by a valid resource.
kDebug() << "Collection" << collection.id() << collection.name();
if (AgentManager::self()->instance(collection.resource()).isValid())
{
QSet<QByteArray> attrs;
attrs += CollectionAttribute::name();
setCollectionChanged(collection, attrs, true);
emit collectionAdded(collection);
if (!mCollectionsBeingCreated.contains(collection.remoteId())
&& (collection.rights() & writableRights) == writableRights)
{
// Update to current KAlarm format if necessary, and if the user agrees
CalendarMigrator::updateToCurrentFormat(collection, false, MainWindow::mainMainWindow());
}
}
}
else
{
// An item has been inserted
const Item item = ix.data(ItemRole).value<Item>();
if (item.isValid())
{
kDebug() << "item id=" << item.id() << ", revision=" << item.revision();
if (mItemsBeingCreated.removeAll(item.id())) // the new item has now been initialised
checkQueuedItemModifyJob(item); // execute the next job queued for the item
}
}
}
const EventList events = eventList(parent, start, end);
if (!events.isEmpty())
emit eventsAdded(events);
}
/******************************************************************************
* Called when rows are about to be removed from the model.
*/
void AkonadiModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
{
kDebug() << start << "-" << end << "(parent =" << parent << ")";
const EventList events = eventList(parent, start, end);
if (!events.isEmpty())
{
foreach (const Event& event, events)
kDebug() << "Collection:" << event.collection.id() << ", Event ID:" << event.event.id();
emit eventsToBeRemoved(events);
}
}
/******************************************************************************
* Return a list of KAEvent/Collection pairs for a given range of rows.
*/
AkonadiModel::EventList AkonadiModel::eventList(const QModelIndex& parent, int start, int end)
{
EventList events;
for (int row = start; row <= end; ++row)
{
Collection c;
const QModelIndex ix = index(row, 0, parent);
const KAEvent evnt = event(ix.data(ItemRole).value<Item>(), ix, &c);
if (evnt.isValid())
events += Event(evnt, c);
}
return events;
}
/******************************************************************************
* Called when a monitored collection's properties or content have changed.
* Optionally emits a signal if properties of interest have changed.
*/
void AkonadiModel::setCollectionChanged(const Collection& collection, const QSet<QByteArray>& attributeNames, bool rowInserted)
{
// Check for a read/write permission change
const Collection::Rights oldRights = mCollectionRights.value(collection.id(), Collection::AllRights);
const Collection::Rights newRights = collection.rights() & writableRights;
if (newRights != oldRights)
{
kDebug() << "Collection" << collection.id() << ": rights ->" << newRights;
mCollectionRights[collection.id()] = newRights;
emit collectionStatusChanged(collection, ReadOnly, (newRights != writableRights), rowInserted);
}
// Check for a change in content mime types
// (e.g. when a collection is first created at startup).
const CalEvent::Types oldAlarmTypes = mCollectionAlarmTypes.value(collection.id(), CalEvent::EMPTY);
const CalEvent::Types newAlarmTypes = CalEvent::types(collection.contentMimeTypes());
if (newAlarmTypes != oldAlarmTypes)
{
kDebug() << "Collection" << collection.id() << ": alarm types ->" << newAlarmTypes;
mCollectionAlarmTypes[collection.id()] = newAlarmTypes;
emit collectionStatusChanged(collection, AlarmTypes, static_cast<int>(newAlarmTypes), rowInserted);
}
// Check for the collection being enabled/disabled
if (attributeNames.contains(CollectionAttribute::name()))
{
static bool firstEnabled = true;
const CalEvent::Types oldEnabled = mCollectionEnabled.value(collection.id(), CalEvent::EMPTY);
const CalEvent::Types newEnabled = collection.hasAttribute<CollectionAttribute>() ? collection.attribute<CollectionAttribute>()->enabled() : CalEvent::EMPTY;
if (firstEnabled || newEnabled != oldEnabled)
{
kDebug() << "Collection" << collection.id() << ": enabled ->" << newEnabled;
firstEnabled = false;
mCollectionEnabled[collection.id()] = newEnabled;
emit collectionStatusChanged(collection, Enabled, static_cast<int>(newEnabled), rowInserted);
}
}
// Check for the backend calendar format changing
if (attributeNames.contains(CompatibilityAttribute::name()))
{
// Update to current KAlarm format if necessary, and if the user agrees
kDebug() << "CompatibilityAttribute";
Collection col(collection);
refresh(col);
CalendarMigrator::updateToCurrentFormat(col, false, MainWindow::mainMainWindow());
}
if (mMigrating)
{
mCollectionIdsBeingCreated.removeAll(collection.id());
if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()
&& CalendarMigrator::completed())
{
kDebug() << "Migration completed";
mMigrating = false;
emit migrationCompleted();
}
}
}
/******************************************************************************
* Called when a monitored collection is removed.
*/
void AkonadiModel::slotCollectionRemoved(const Collection& collection)
{
const Collection::Id id = collection.id();
kDebug() << id;
mCollectionRights.remove(id);
mCollectionsDeleting.removeAll(id);
while (mCollectionsDeleted.count() > 20) // don't let list grow indefinitely
mCollectionsDeleted.removeFirst();
mCollectionsDeleted << id;
}
/******************************************************************************
* Called when a collection creation is about to start, or has completed.
*/
void AkonadiModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished)
{
if (finished)
{
mCollectionsBeingCreated.removeAll(path);
mCollectionIdsBeingCreated << id;
}
else
mCollectionsBeingCreated << path;
}
/******************************************************************************
* Called when calendar migration has completed.
*/
void AkonadiModel::slotMigrationCompleted()
{
if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty())
{
kDebug() << "Migration completed";
mMigrating = false;
emit migrationCompleted();
}
}
/******************************************************************************
* Called when an item in the monitored collections has changed.
*/
void AkonadiModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet<QByteArray>&)
{
kDebug() << "item id=" << item.id() << ", revision=" << item.revision();
mItemsBeingCreated.removeAll(item.id()); // the new item has now been initialised
checkQueuedItemModifyJob(item); // execute the next job queued for the item
KAEvent evnt = event(item);
if (!evnt.isValid())
return;
const QModelIndexList indexes = modelIndexesForItem(this, item);
foreach (const QModelIndex& index, indexes)
{
if (index.isValid())
{
// Wait to ensure that the base EntityTreeModel has processed the
// itemChanged() signal first, before we emit eventChanged().
Collection c = data(index, ParentCollectionRole).value<Collection>();
evnt.setCollectionId(c.id());
mPendingEventChanges.enqueue(Event(evnt, c));
QTimer::singleShot(0, this, SLOT(slotEmitEventChanged()));
break;
}
}
}
/******************************************************************************
* Called to emit a signal when an event in the monitored collections has
* changed.
*/
void AkonadiModel::slotEmitEventChanged()
{
while (!mPendingEventChanges.isEmpty())
{
emit eventChanged(mPendingEventChanges.dequeue());
}
}
/******************************************************************************
* Refresh the specified Collection with up to date data.
* Return: true if successful, false if collection not found.
*/
bool AkonadiModel::refresh(Akonadi::Collection& collection) const
{
const QModelIndex ix = modelIndexForCollection(this, collection);
if (!ix.isValid())
return false;
collection = ix.data(CollectionRole).value<Collection>();
return true;
}
/******************************************************************************
* Refresh the specified Item with up to date data.
* Return: true if successful, false if item not found.
*/
bool AkonadiModel::refresh(Akonadi::Item& item) const
{
const QModelIndexList ixs = modelIndexesForItem(this, item);
if (ixs.isEmpty() || !ixs[0].isValid())
return false;
item = ixs[0].data(ItemRole).value<Item>();
return true;
}
/******************************************************************************
* Find the QModelIndex of a collection.
*/
QModelIndex AkonadiModel::collectionIndex(const Collection& collection) const
{
const QModelIndex ix = modelIndexForCollection(this, collection);
if (!ix.isValid())
return QModelIndex();
return ix;
}
/******************************************************************************
* Return the up to date collection with the specified Akonadi ID.
*/
Collection AkonadiModel::collectionById(Collection::Id id) const
{
const QModelIndex ix = modelIndexForCollection(this, Collection(id));
if (!ix.isValid())
return Collection();
return ix.data(CollectionRole).value<Collection>();
}
/******************************************************************************
* Find the QModelIndex of an item.
*/
QModelIndex AkonadiModel::itemIndex(const Item& item) const
{
const QModelIndexList ixs = modelIndexesForItem(this, item);
if (ixs.isEmpty() || !ixs[0].isValid())
return QModelIndex();
return ixs[0];
}
/******************************************************************************
* Return the up to date item with the specified Akonadi ID.
*/
Item AkonadiModel::itemById(Item::Id id) const
{
const QModelIndexList ixs = modelIndexesForItem(this, Item(id));
if (ixs.isEmpty() || !ixs[0].isValid())
return Item();
return ixs[0].data(ItemRole).value<Item>();
}
/******************************************************************************
* Find the collection containing the specified Akonadi item ID.
*/
Collection AkonadiModel::collectionForItem(Item::Id id) const
{
const QModelIndex ix = itemIndex(id);
if (!ix.isValid())
return Collection();
return ix.data(ParentCollectionRole).value<Collection>();
}
bool AkonadiModel::isCompatible(const Collection& collection)
{
return collection.hasAttribute<CompatibilityAttribute>()
&& collection.attribute<CompatibilityAttribute>()->compatibility() == KACalendar::Current;
}
/******************************************************************************
* Return whether a collection is fully writable.
*/
int AkonadiModel::isWritable(const Akonadi::Collection& collection)
{
KACalendar::Compat format;
return isWritable(collection, format);
}
int AkonadiModel::isWritable(const Akonadi::Collection& collection, KACalendar::Compat& format)
{
format = KACalendar::Incompatible;
if (!collection.isValid())
return -1;
Collection col = collection;
instance()->refresh(col); // update with latest data
if ((col.rights() & writableRights) != writableRights)
{
format = KACalendar::Current;
return -1;
}
if (!col.hasAttribute<CompatibilityAttribute>())
return -1;
format = col.attribute<CompatibilityAttribute>()->compatibility();
switch (format)
{
case KACalendar::Current:
return 1;
case KACalendar::Converted:
case KACalendar::Convertible:
return 0;
default:
return -1;
}
}
CalEvent::Types AkonadiModel::types(const Collection& collection)
{
return CalEvent::types(collection.contentMimeTypes());
}
// vim: et sw=4: