Fix dnd email
[kdepim.git] / kalarm / akonadimodel.cpp
blob8ae9ea35707d5ad1543efd3df85b80e666fdbd70
1 /*
2 * akonadimodel.cpp - KAlarm calendar file access using Akonadi
3 * Program: kalarm
4 * Copyright © 2007-2014 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "akonadimodel.h"
22 #include "alarmtime.h"
23 #include "autoqpointer.h"
24 #include "calendarmigrator.h"
25 #include "mainwindow.h"
26 #include "messagebox.h"
27 #include "preferences.h"
28 #include "synchtimer.h"
29 #include "kalarmsettings.h"
30 #include "kalarmdirsettings.h"
32 #include <kalarmcal/alarmtext.h>
33 #include <kalarmcal/collectionattribute.h>
34 #include <kalarmcal/compatibilityattribute.h>
35 #include <kalarmcal/eventattribute.h>
37 #include <AkonadiCore/agentfilterproxymodel.h>
38 #include <AkonadiCore/agentinstancecreatejob.h>
39 #include <AkonadiCore/agentmanager.h>
40 #include <AkonadiCore/agenttype.h>
41 #include <AkonadiCore/attributefactory.h>
42 #include <AkonadiCore/changerecorder.h>
43 #include <AkonadiCore/collectiondeletejob.h>
44 #include <AkonadiCore/collectionmodifyjob.h>
45 #include <AkonadiCore/entitydisplayattribute.h>
46 #include <AkonadiCore/item.h>
47 #include <AkonadiCore/itemcreatejob.h>
48 #include <AkonadiCore/itemmodifyjob.h>
49 #include <AkonadiCore/itemdeletejob.h>
50 #include <AkonadiCore/itemfetchscope.h>
51 #include <AkonadiWidgets/agenttypedialog.h>
53 #include <KLocalizedString>
54 #include <kcolorutils.h>
55 #include <KIconLoader>
57 #include <QUrl>
58 #include <QApplication>
59 #include <QFileInfo>
60 #include <QTimer>
61 #include "kalarm_debug.h"
63 using namespace Akonadi;
64 using namespace KAlarmCal;
66 static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;
68 //static bool checkItem_true(const Item&) { return true; }
70 /*=============================================================================
71 = Class: AkonadiModel
72 =============================================================================*/
74 AkonadiModel* AkonadiModel::mInstance = Q_NULLPTR;
75 QPixmap* AkonadiModel::mTextIcon = Q_NULLPTR;
76 QPixmap* AkonadiModel::mFileIcon = Q_NULLPTR;
77 QPixmap* AkonadiModel::mCommandIcon = Q_NULLPTR;
78 QPixmap* AkonadiModel::mEmailIcon = Q_NULLPTR;
79 QPixmap* AkonadiModel::mAudioIcon = Q_NULLPTR;
80 QSize AkonadiModel::mIconSize;
81 int AkonadiModel::mTimeHourPos = -2;
83 /******************************************************************************
84 * Construct and return the singleton.
86 AkonadiModel* AkonadiModel::instance()
88 if (!mInstance)
89 mInstance = new AkonadiModel(new ChangeRecorder(qApp), qApp);
90 return mInstance;
93 /******************************************************************************
94 * Constructor.
96 AkonadiModel::AkonadiModel(ChangeRecorder* monitor, QObject* parent)
97 : EntityTreeModel(monitor, parent),
98 mMonitor(monitor),
99 mResourcesChecked(false),
100 mMigrating(false)
102 // Set lazy population to enable the contents of unselected collections to be ignored
103 setItemPopulationStrategy(LazyPopulation);
105 // Restrict monitoring to collections containing the KAlarm mime types
106 monitor->setCollectionMonitored(Collection::root());
107 monitor->setResourceMonitored("akonadi_kalarm_resource");
108 monitor->setResourceMonitored("akonadi_kalarm_dir_resource");
109 monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE);
110 monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED);
111 monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE);
112 monitor->itemFetchScope().fetchFullPayload();
113 monitor->itemFetchScope().fetchAttribute<EventAttribute>();
115 AttributeFactory::registerAttribute<CollectionAttribute>();
116 AttributeFactory::registerAttribute<CompatibilityAttribute>();
117 AttributeFactory::registerAttribute<EventAttribute>();
119 if (!mTextIcon)
121 mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16));
122 mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16));
123 mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16));
124 mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-message-unread")).pixmap(16, 16));
125 mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16));
126 mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size());
129 #ifdef __GNUC__
130 #warning Only want to monitor collection properties, not content, when this becomes possible
131 #endif
132 connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet<QByteArray>)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet<QByteArray>)));
133 connect(monitor, &Monitor::collectionRemoved, this, &AkonadiModel::slotCollectionRemoved);
134 initCalendarMigrator();
135 MinuteTimer::connect(this, SLOT(slotUpdateTimeTo()));
136 Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor)));
137 Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor)));
138 Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays()));
139 Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours()));
141 connect(this, &AkonadiModel::rowsInserted, this, &AkonadiModel::slotRowsInserted);
142 connect(this, &AkonadiModel::rowsAboutToBeRemoved, this, &AkonadiModel::slotRowsAboutToBeRemoved);
143 connect(monitor, &Monitor::itemChanged, this, &AkonadiModel::slotMonitoredItemChanged);
145 connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiModel::checkResources);
146 checkResources(ServerManager::state());
149 AkonadiModel::~AkonadiModel()
151 if (mInstance == this)
152 mInstance = Q_NULLPTR;
155 /******************************************************************************
156 * Called when the server manager changes state.
157 * If it is now running, i.e. the agent manager knows about
158 * all existing resources.
159 * Once it is running, i.e. the agent manager knows about
160 * all existing resources, if necessary migrate any KResources alarm calendars from
161 * pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources
162 * if any are missing.
164 void AkonadiModel::checkResources(ServerManager::State state)
166 switch (state)
168 case ServerManager::Running:
169 if (!mResourcesChecked)
171 qCDebug(KALARM_LOG) << "Server running";
172 mResourcesChecked = true;
173 mMigrating = true;
174 CalendarMigrator::execute();
176 break;
177 case ServerManager::NotRunning:
178 qCDebug(KALARM_LOG) << "Server stopped";
179 mResourcesChecked = false;
180 mMigrating = false;
181 mCollectionAlarmTypes.clear();
182 mCollectionRights.clear();
183 mCollectionEnabled.clear();
184 initCalendarMigrator();
185 Q_EMIT serverStopped();
186 break;
187 default:
188 break;
192 /******************************************************************************
193 * Initialise the calendar migrator so that it can be run (either for the first
194 * time, or again).
196 void AkonadiModel::initCalendarMigrator()
198 CalendarMigrator::reset();
199 connect(CalendarMigrator::instance(), &CalendarMigrator::creating,
200 this, &AkonadiModel::slotCollectionBeingCreated);
201 connect(CalendarMigrator::instance(), &QObject::destroyed, this, &AkonadiModel::slotMigrationCompleted);
204 /******************************************************************************
205 * Return whether calendar migration has completed.
207 bool AkonadiModel::isMigrationCompleted() const
209 return mResourcesChecked && !mMigrating;
212 /******************************************************************************
213 * Return the data for a given role, for a specified item.
215 QVariant AkonadiModel::data(const QModelIndex& index, int role) const
217 // First check that it's a role we're interested in - if not, use the base method
218 switch (role)
220 case Qt::BackgroundRole:
221 case Qt::ForegroundRole:
222 case Qt::DisplayRole:
223 case Qt::TextAlignmentRole:
224 case Qt::DecorationRole:
225 case Qt::SizeHintRole:
226 case Qt::AccessibleTextRole:
227 case Qt::ToolTipRole:
228 case Qt::CheckStateRole:
229 case SortRole:
230 case ValueRole:
231 case StatusRole:
232 case AlarmActionsRole:
233 case AlarmSubActionRole:
234 case EnabledRole:
235 case EnabledTypesRole:
236 case CommandErrorRole:
237 case BaseColourRole:
238 case AlarmTypeRole:
239 case IsStandardRole:
240 break;
241 default:
242 return EntityTreeModel::data(index, role);
245 const Collection collection = index.data(CollectionRole).value<Collection>();
246 if (collection.isValid())
248 // This is a Collection row
249 switch (role)
251 case Qt::DisplayRole:
252 return collection.displayName();
253 case EnabledTypesRole:
254 if (!collection.hasAttribute<CollectionAttribute>())
255 return 0;
256 return static_cast<int>(collection.attribute<CollectionAttribute>()->enabled());
257 case BaseColourRole:
258 role = Qt::BackgroundRole;
259 break;
260 case Qt::BackgroundRole:
262 const QColor colour = backgroundColor_p(collection);
263 if (colour.isValid())
264 return colour;
265 break;
267 case Qt::ForegroundRole:
268 return foregroundColor(collection, collection.contentMimeTypes());
269 case Qt::ToolTipRole:
270 return tooltip(collection, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE);
271 case AlarmTypeRole:
272 return static_cast<int>(types(collection));
273 case IsStandardRole:
274 if (!collection.hasAttribute<CollectionAttribute>()
275 || !isCompatible(collection))
276 return 0;
277 return static_cast<int>(collection.attribute<CollectionAttribute>()->standard());
278 case KeepFormatRole:
279 if (!collection.hasAttribute<CollectionAttribute>())
280 return false;
281 return collection.attribute<CollectionAttribute>()->keepFormat();
282 default:
283 break;
286 else
288 const Item item = index.data(ItemRole).value<Item>();
289 if (item.isValid())
291 // This is an Item row
292 const QString mime = item.mimeType();
293 if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE)
294 || !item.hasPayload<KAEvent>())
295 return QVariant();
296 switch (role)
298 case StatusRole:
299 // Mime type has a one-to-one relationship to event's category()
300 if (mime == KAlarmCal::MIME_ACTIVE)
301 return CalEvent::ACTIVE;
302 if (mime == KAlarmCal::MIME_ARCHIVED)
303 return CalEvent::ARCHIVED;
304 if (mime == KAlarmCal::MIME_TEMPLATE)
305 return CalEvent::TEMPLATE;
306 return QVariant();
307 case CommandErrorRole:
308 if (!item.hasAttribute<EventAttribute>())
309 return KAEvent::CMD_NO_ERROR;
310 return item.attribute<EventAttribute>()->commandError();
311 default:
312 break;
314 const int column = index.column();
315 if (role == Qt::WhatsThisRole)
316 return whatsThisText(column);
317 const KAEvent event(this->event(item));
318 if (!event.isValid())
319 return QVariant();
320 if (role == AlarmActionsRole)
321 return event.actionTypes();
322 if (role == AlarmSubActionRole)
323 return event.actionSubType();
324 bool calendarColour = false;
325 switch (column)
327 case TimeColumn:
328 switch (role)
330 case Qt::BackgroundRole:
331 calendarColour = true;
332 break;
333 case Qt::DisplayRole:
334 if (event.expired())
335 return AlarmTime::alarmTimeText(event.startDateTime());
336 return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER));
337 case SortRole:
339 DateTime due;
340 if (event.expired())
341 due = event.startDateTime();
342 else
343 due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER);
344 return due.isValid() ? due.effectiveKDateTime().toUtc().dateTime()
345 : QDateTime(QDate(9999,12,31), QTime(0,0,0));
347 default:
348 break;
350 break;
351 case TimeToColumn:
352 switch (role)
354 case Qt::BackgroundRole:
355 calendarColour = true;
356 break;
357 case Qt::DisplayRole:
358 if (event.expired())
359 return QString();
360 return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER));
361 case SortRole:
363 if (event.expired())
364 return -1;
365 const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER);
366 const KDateTime now = KDateTime::currentUtcDateTime();
367 if (due.isDateOnly())
368 return now.date().daysTo(due.date()) * 1440;
369 return (now.secsTo(due.effectiveKDateTime()) + 59) / 60;
372 break;
373 case RepeatColumn:
374 switch (role)
376 case Qt::BackgroundRole:
377 calendarColour = true;
378 break;
379 case Qt::DisplayRole:
380 return repeatText(event);
381 case Qt::TextAlignmentRole:
382 return Qt::AlignHCenter;
383 case SortRole:
384 return repeatOrder(event);
386 break;
387 case ColourColumn:
388 switch (role)
390 case Qt::BackgroundRole:
392 const KAEvent::Actions type = event.actionTypes();
393 if (type & KAEvent::ACT_DISPLAY)
394 return event.bgColour();
395 if (type == KAEvent::ACT_COMMAND)
397 if (event.commandError() != KAEvent::CMD_NO_ERROR)
398 return QColor(Qt::red);
400 break;
402 case Qt::ForegroundRole:
403 if (event.commandError() != KAEvent::CMD_NO_ERROR)
405 if (event.actionTypes() == KAEvent::ACT_COMMAND)
406 return QColor(Qt::white);
407 QColor colour = Qt::red;
408 int r, g, b;
409 event.bgColour().getRgb(&r, &g, &b);
410 if (r > 128 && g <= 128 && b <= 128)
411 colour = QColor(Qt::white);
412 return colour;
414 break;
415 case Qt::DisplayRole:
416 if (event.commandError() != KAEvent::CMD_NO_ERROR)
417 return QLatin1String("!");
418 break;
419 case SortRole:
421 const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY)
422 ? event.bgColour().rgb() : 0;
423 return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0'));
425 default:
426 break;
428 break;
429 case TypeColumn:
430 switch (role)
432 case Qt::BackgroundRole:
433 calendarColour = true;
434 break;
435 case Qt::DecorationRole:
437 QVariant v;
438 v.setValue(*eventIcon(event));
439 return v;
441 case Qt::TextAlignmentRole:
442 return Qt::AlignHCenter;
443 case Qt::SizeHintRole:
444 return mIconSize;
445 case Qt::AccessibleTextRole:
446 #ifdef __GNUC__
447 #warning Implement accessibility
448 #endif
449 return QString();
450 case ValueRole:
451 return static_cast<int>(event.actionSubType());
452 case SortRole:
453 return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0'));
455 break;
456 case TextColumn:
457 switch (role)
459 case Qt::BackgroundRole:
460 calendarColour = true;
461 break;
462 case Qt::DisplayRole:
463 case SortRole:
464 return AlarmText::summary(event, 1);
465 case Qt::ToolTipRole:
466 return AlarmText::summary(event, 10);
467 default:
468 break;
470 break;
471 case TemplateNameColumn:
472 switch (role)
474 case Qt::BackgroundRole:
475 calendarColour = true;
476 break;
477 case Qt::DisplayRole:
478 return event.templateName();
479 case SortRole:
480 return event.templateName().toUpper();
482 break;
483 default:
484 break;
487 switch (role)
489 case Qt::ForegroundRole:
490 if (!event.enabled())
491 return Preferences::disabledColour();
492 if (event.expired())
493 return Preferences::archivedColour();
494 break; // use the default for normal active alarms
495 case Qt::ToolTipRole:
496 // Show the last command execution error message
497 switch (event.commandError())
499 case KAEvent::CMD_ERROR:
500 return i18nc("@info:tooltip", "Command execution failed");
501 case KAEvent::CMD_ERROR_PRE:
502 return i18nc("@info:tooltip", "Pre-alarm action execution failed");
503 case KAEvent::CMD_ERROR_POST:
504 return i18nc("@info:tooltip", "Post-alarm action execution failed");
505 case KAEvent::CMD_ERROR_PRE_POST:
506 return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed");
507 default:
508 case KAEvent::CMD_NO_ERROR:
509 break;
511 break;
512 case EnabledRole:
513 return event.enabled();
514 default:
515 break;
518 if (calendarColour)
520 Collection parent = item.parentCollection();
521 const QColor colour = backgroundColor(parent);
522 if (colour.isValid())
523 return colour;
527 return EntityTreeModel::data(index, role);
530 /******************************************************************************
531 * Set the font to use for all items, or the checked state of one item.
532 * The font must always be set at initialisation.
534 bool AkonadiModel::setData(const QModelIndex& index, const QVariant& value, int role)
536 if (!index.isValid())
537 return false;
538 // NOTE: need to Q_EMIT dataChanged() whenever something is updated (except via a job).
539 Collection collection = index.data(CollectionRole).value<Collection>();
540 if (collection.isValid())
542 // This is a Collection row
543 bool updateCollection = false;
544 CollectionAttribute* attr = Q_NULLPTR;
545 switch (role)
547 case Qt::BackgroundRole:
549 const QColor colour = value.value<QColor>();
550 attr = collection.attribute<CollectionAttribute>(Collection::AddIfMissing);
551 if (attr->backgroundColor() == colour)
552 return true; // no change
553 attr->setBackgroundColor(colour);
554 updateCollection = true;
555 break;
557 case EnabledTypesRole:
559 const CalEvent::Types types = static_cast<CalEvent::Types>(value.toInt());
560 attr = collection.attribute<CollectionAttribute>(Collection::AddIfMissing);
561 if (attr->enabled() == types)
562 return true; // no change
563 qCDebug(KALARM_LOG) << "Set enabled:" << types << ", was=" << attr->enabled();
564 attr->setEnabled(types);
565 updateCollection = true;
566 break;
568 case IsStandardRole:
569 if (collection.hasAttribute<CollectionAttribute>()
570 && isCompatible(collection))
572 const CalEvent::Types types = static_cast<CalEvent::Types>(value.toInt());
573 attr = collection.attribute<CollectionAttribute>(Collection::AddIfMissing);
574 qCDebug(KALARM_LOG)<<"Set standard:"<<types<<", was="<<attr->standard();
575 attr->setStandard(types);
576 updateCollection = true;
578 break;
579 case KeepFormatRole:
581 const bool keepFormat = value.toBool();
582 attr = collection.attribute<CollectionAttribute>(Collection::AddIfMissing);
583 if (attr->keepFormat() == keepFormat)
584 return true; // no change
585 attr->setKeepFormat(keepFormat);
586 updateCollection = true;
587 break;
589 default:
590 break;
592 if (updateCollection)
594 // Update the CollectionAttribute value.
595 // Note that we can't supply 'collection' to CollectionModifyJob since
596 // that also contains the CompatibilityAttribute value, which is read-only
597 // for applications. So create a new Collection instance and only set a
598 // value for CollectionAttribute.
599 Collection c(collection.id());
600 CollectionAttribute* att = c.attribute<CollectionAttribute>(Collection::AddIfMissing);
601 *att = *attr;
602 CollectionModifyJob* job = new CollectionModifyJob(c, this);
603 connect(job, &CollectionModifyJob::result, this, &AkonadiModel::modifyCollectionJobDone);
604 return true;
607 else
609 Item item = index.data(ItemRole).value<Item>();
610 if (item.isValid())
612 bool updateItem = false;
613 switch (role)
615 case CommandErrorRole:
617 const KAEvent::CmdErrType err = static_cast<KAEvent::CmdErrType>(value.toInt());
618 switch (err)
620 case KAEvent::CMD_NO_ERROR:
621 case KAEvent::CMD_ERROR:
622 case KAEvent::CMD_ERROR_PRE:
623 case KAEvent::CMD_ERROR_POST:
624 case KAEvent::CMD_ERROR_PRE_POST:
626 if (err == KAEvent::CMD_NO_ERROR && !item.hasAttribute<EventAttribute>())
627 return true; // no change
628 EventAttribute* attr = item.attribute<EventAttribute>(Item::AddIfMissing);
629 if (attr->commandError() == err)
630 return true; // no change
631 attr->setCommandError(err);
632 updateItem = true;
633 qCDebug(KALARM_LOG)<<"Item:"<<item.id()<<" CommandErrorRole ->"<<err;
634 break;
636 default:
637 return false;
639 break;
641 default:
642 qCDebug(KALARM_LOG)<<"Item: passing to EntityTreeModel::setData("<<role<<")";
643 break;
645 if (updateItem)
647 queueItemModifyJob(item);
648 return true;
653 return EntityTreeModel::setData(index, value, role);
656 /******************************************************************************
657 * Return the number of columns for either a collection or an item.
659 int AkonadiModel::entityColumnCount(HeaderGroup group) const
661 switch (group)
663 case CollectionTreeHeaders:
664 return 1;
665 case ItemListHeaders:
666 return ColumnCount;
667 default:
668 return EntityTreeModel::entityColumnCount(group);
672 /******************************************************************************
673 * Return data for a column heading.
675 QVariant AkonadiModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup group) const
677 if (orientation == Qt::Horizontal)
679 switch (group)
681 case CollectionTreeHeaders:
682 if (section != 0)
683 return QVariant();
684 if (role == Qt::DisplayRole)
685 return i18nc("@title:column", "Calendars");
686 break;
688 case ItemListHeaders:
689 if (section < 0 || section >= ColumnCount)
690 return QVariant();
691 if (role == Qt::DisplayRole)
693 switch (section)
695 case TimeColumn:
696 return i18nc("@title:column", "Time");
697 case TimeToColumn:
698 return i18nc("@title:column", "Time To");
699 case RepeatColumn:
700 return i18nc("@title:column", "Repeat");
701 case ColourColumn:
702 return QString();
703 case TypeColumn:
704 return QString();
705 case TextColumn:
706 return i18nc("@title:column", "Message, File or Command");
707 case TemplateNameColumn:
708 return i18nc("@title:column Template name", "Name");
711 else if (role == Qt::WhatsThisRole)
712 return whatsThisText(section);
713 break;
715 default:
716 break;
719 return EntityTreeModel::entityHeaderData(section, orientation, role, group);
722 /******************************************************************************
723 * Recursive function to Q_EMIT the dataChanged() signal for all items in a
724 * specified column range.
726 void AkonadiModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent)
728 int start = -1;
729 int end = -1;
730 for (int row = 0, count = rowCount(parent); row < count; ++row)
732 const QModelIndex ix = index(row, 0, parent);
733 const Item item = data(ix, ItemRole).value<Item>();
734 const bool isItem = item.isValid();
735 if (isItem)
737 if ((*checkFunc)(item))
739 // For efficiency, Q_EMIT a single signal for each group of
740 // consecutive items, rather than a separate signal for each item.
741 if (start < 0)
742 start = row;
743 end = row;
744 continue;
747 if (start >= 0)
748 Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent));
749 start = -1;
750 if (!isItem)
751 signalDataChanged(checkFunc, startColumn, endColumn, ix);
754 if (start >= 0)
755 Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent));
758 /******************************************************************************
759 * Signal every minute that the time-to-alarm values have changed.
761 static bool checkItem_isActive(const Item& item)
762 { return item.mimeType() == KAlarmCal::MIME_ACTIVE; }
764 void AkonadiModel::slotUpdateTimeTo()
766 signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex());
770 /******************************************************************************
771 * Called when the colour used to display archived alarms has changed.
773 static bool checkItem_isArchived(const Item& item)
774 { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; }
776 void AkonadiModel::slotUpdateArchivedColour(const QColor&)
778 qCDebug(KALARM_LOG);
779 signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex());
782 /******************************************************************************
783 * Called when the colour used to display disabled alarms has changed.
785 static bool checkItem_isDisabled(const Item& item)
787 if (item.hasPayload<KAEvent>())
789 const KAEvent event = item.payload<KAEvent>();
790 if (event.isValid())
791 return !event.enabled();
793 return false;
796 void AkonadiModel::slotUpdateDisabledColour(const QColor&)
798 qCDebug(KALARM_LOG);
799 signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex());
802 /******************************************************************************
803 * Called when the definition of holidays has changed.
805 static bool checkItem_excludesHolidays(const Item& item)
807 if (item.hasPayload<KAEvent>())
809 const KAEvent event = item.payload<KAEvent>();
810 if (event.isValid() && event.holidaysExcluded())
811 return true;
813 return false;
816 void AkonadiModel::slotUpdateHolidays()
818 qCDebug(KALARM_LOG);
819 Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns
820 signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex());
823 /******************************************************************************
824 * Called when the definition of working hours has changed.
826 static bool checkItem_workTimeOnly(const Item& item)
828 if (item.hasPayload<KAEvent>())
830 const KAEvent event = item.payload<KAEvent>();
831 if (event.isValid() && event.workTimeOnly())
832 return true;
834 return false;
837 void AkonadiModel::slotUpdateWorkingHours()
839 qCDebug(KALARM_LOG);
840 Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns
841 signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex());
844 /******************************************************************************
845 * Called when the command error status of an alarm has changed, to save the new
846 * status and update the visual command error indication.
848 void AkonadiModel::updateCommandError(const KAEvent& event)
850 const QModelIndex ix = itemIndex(event.itemId());
851 if (ix.isValid())
852 setData(ix, QVariant(static_cast<int>(event.commandError())), CommandErrorRole);
855 /******************************************************************************
856 * Return the foreground color for displaying a collection, based on the
857 * supplied mime types which it contains, and on whether it is fully writable.
859 QColor AkonadiModel::foregroundColor(const Akonadi::Collection& collection, const QStringList& mimeTypes)
861 QColor colour;
862 if (mimeTypes.contains(KAlarmCal::MIME_ACTIVE))
863 colour = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color();
864 else if (mimeTypes.contains(KAlarmCal::MIME_ARCHIVED))
865 colour = Preferences::archivedColour();
866 else if (mimeTypes.contains(KAlarmCal::MIME_TEMPLATE))
867 colour = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color();
868 if (colour.isValid() && isWritable(collection) <= 0)
869 return KColorUtils::lighten(colour, 0.2);
870 return colour;
873 /******************************************************************************
874 * Set the background color for displaying the collection and its alarms.
876 void AkonadiModel::setBackgroundColor(Collection& collection, const QColor& colour)
878 const QModelIndex ix = modelIndexForCollection(this, collection);
879 if (ix.isValid())
880 setData(ix, QVariant(colour), Qt::BackgroundRole);
883 /******************************************************************************
884 * Return the background color for displaying the collection and its alarms,
885 * after updating the collection from the Akonadi database.
887 QColor AkonadiModel::backgroundColor(Akonadi::Collection& collection) const
889 if (!collection.isValid())
890 return QColor();
891 refresh(collection);
892 return backgroundColor_p(collection);
895 /******************************************************************************
896 * Return the background color for displaying the collection and its alarms.
898 QColor AkonadiModel::backgroundColor_p(const Akonadi::Collection& collection) const
900 if (!collection.isValid() || !collection.hasAttribute<CollectionAttribute>())
901 return QColor();
902 return collection.attribute<CollectionAttribute>()->backgroundColor();
905 /******************************************************************************
906 * Return the display name for the collection, after updating the collection
907 * from the Akonadi database.
909 QString AkonadiModel::displayName(Akonadi::Collection& collection) const
911 if (!collection.isValid())
912 return QString();
913 refresh(collection);
914 return collection.displayName();
917 /******************************************************************************
918 * Return the storage type (file, directory, etc.) for the collection.
920 QString AkonadiModel::storageType(const Akonadi::Collection& collection) const
922 const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile);
923 return !url.isLocalFile() ? i18nc("@info", "URL")
924 : QFileInfo(url.toLocalFile()).isDir() ? i18nc("@info Directory in filesystem", "Directory")
925 : i18nc("@info", "File");
928 /******************************************************************************
929 * Return a collection's tooltip text. The collection's enabled status is
930 * evaluated for specified alarm types.
932 QString AkonadiModel::tooltip(const Collection& collection, CalEvent::Types types) const
934 const QString name = QLatin1Char('@') + collection.displayName(); // insert markers for stripping out name
935 const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile);
936 const QString type = QLatin1Char('@') + storageType(collection); // file/directory/URL etc.
937 const QString locn = url.toDisplayString(QUrl::PreferLocalFile);
938 const bool inactive = !collection.hasAttribute<CollectionAttribute>()
939 || !(collection.attribute<CollectionAttribute>()->enabled() & types);
940 const QString disabled = i18nc("@info", "Disabled");
941 const QString readonly = readOnlyTooltip(collection);
942 const bool writable = readonly.isEmpty();
943 if (inactive && !writable)
944 return xi18nc("@info:tooltip",
945 "%1"
946 "<nl/>%2: <filename>%3</filename>"
947 "<nl/>%4, %5",
948 name, type, locn, disabled, readonly);
949 if (inactive || !writable)
950 return xi18nc("@info:tooltip",
951 "%1"
952 "<nl/>%2: <filename>%3</filename>"
953 "<nl/>%4",
954 name, type, locn, (inactive ? disabled : readonly));
955 return xi18nc("@info:tooltip",
956 "%1"
957 "<nl/>%2: <filename>%3</filename>",
958 name, type, locn);
961 /******************************************************************************
962 * Return the read-only status tooltip for a collection.
963 * A null string is returned if the collection is fully writable.
965 QString AkonadiModel::readOnlyTooltip(const Collection& collection)
967 KACalendar::Compat compat;
968 switch (AkonadiModel::isWritable(collection, compat))
970 case 1:
971 return QString();
972 case 0:
973 return i18nc("@info", "Read-only (old format)");
974 default:
975 if (compat == KACalendar::Current)
976 return i18nc("@info", "Read-only");
977 return i18nc("@info", "Read-only (other format)");
981 /******************************************************************************
982 * Return the repetition text.
984 QString AkonadiModel::repeatText(const KAEvent& event) const
986 QString repeatText = event.recurrenceText(true);
987 if (repeatText.isEmpty())
988 repeatText = event.repetitionText(true);
989 return repeatText;
992 /******************************************************************************
993 * Return a string for sorting the repetition column.
995 QString AkonadiModel::repeatOrder(const KAEvent& event) const
997 int repeatOrder = 0;
998 int repeatInterval = 0;
999 if (event.repeatAtLogin())
1000 repeatOrder = 1;
1001 else
1003 repeatInterval = event.recurInterval();
1004 switch (event.recurType())
1006 case KARecurrence::MINUTELY:
1007 repeatOrder = 2;
1008 break;
1009 case KARecurrence::DAILY:
1010 repeatOrder = 3;
1011 break;
1012 case KARecurrence::WEEKLY:
1013 repeatOrder = 4;
1014 break;
1015 case KARecurrence::MONTHLY_DAY:
1016 case KARecurrence::MONTHLY_POS:
1017 repeatOrder = 5;
1018 break;
1019 case KARecurrence::ANNUAL_DATE:
1020 case KARecurrence::ANNUAL_POS:
1021 repeatOrder = 6;
1022 break;
1023 case KARecurrence::NO_RECUR:
1024 default:
1025 break;
1028 return QStringLiteral("%1%2").arg(static_cast<char>('0' + repeatOrder)).arg(repeatInterval, 8, 10, QLatin1Char('0'));
1031 /******************************************************************************
1032 * Return the icon associated with the event's action.
1034 QPixmap* AkonadiModel::eventIcon(const KAEvent& event) const
1036 switch (event.actionTypes())
1038 case KAEvent::ACT_EMAIL:
1039 return mEmailIcon;
1040 case KAEvent::ACT_AUDIO:
1041 return mAudioIcon;
1042 case KAEvent::ACT_COMMAND:
1043 return mCommandIcon;
1044 case KAEvent::ACT_DISPLAY:
1045 if (event.actionSubType() == KAEvent::FILE)
1046 return mFileIcon;
1047 // fall through to ACT_DISPLAY_COMMAND
1048 case KAEvent::ACT_DISPLAY_COMMAND:
1049 default:
1050 return mTextIcon;
1054 /******************************************************************************
1055 * Returns the QWhatsThis text for a specified column.
1057 QString AkonadiModel::whatsThisText(int column) const
1059 switch (column)
1061 case TimeColumn:
1062 return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm");
1063 case TimeToColumn:
1064 return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm");
1065 case RepeatColumn:
1066 return i18nc("@info:whatsthis", "How often the alarm recurs");
1067 case ColourColumn:
1068 return i18nc("@info:whatsthis", "Background color of alarm message");
1069 case TypeColumn:
1070 return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)");
1071 case TextColumn:
1072 return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line");
1073 case TemplateNameColumn:
1074 return i18nc("@info:whatsthis", "Name of the alarm template");
1075 default:
1076 return QString();
1080 /******************************************************************************
1081 * Remove a collection from Akonadi. The calendar file is not removed.
1083 bool AkonadiModel::removeCollection(const Akonadi::Collection& collection)
1085 if (!collection.isValid())
1086 return false;
1087 qCDebug(KALARM_LOG) << collection.id();
1088 Collection col = collection;
1089 mCollectionsDeleting << collection.id();
1090 // Note: CollectionDeleteJob deletes the backend storage also.
1091 AgentManager* agentManager = AgentManager::self();
1092 const AgentInstance instance = agentManager->instance(collection.resource());
1093 if (instance.isValid())
1094 agentManager->removeInstance(instance);
1095 #if 0
1096 CollectionDeleteJob* job = new CollectionDeleteJob(col);
1097 connect(job, &CollectionDeleteJob::result, this, &AkonadiModel::deleteCollectionJobDone);
1098 mPendingCollectionJobs[job] = CollJobData(col.id(), displayName(col));
1099 job->start();
1100 #endif
1101 return true;
1104 /******************************************************************************
1105 * Return whether a collection is currently being deleted.
1107 bool AkonadiModel::isCollectionBeingDeleted(Collection::Id id) const
1109 return mCollectionsDeleting.contains(id);
1112 #if 0
1113 /******************************************************************************
1114 * Called when a collection deletion job has completed.
1115 * Checks for any error.
1117 void AkonadiModel::deleteCollectionJobDone(KJob* j)
1119 QMap<KJob*, CollJobData>::iterator it = mPendingCollectionJobs.find(j);
1120 CollJobData jobData;
1121 if (it != mPendingCollectionJobs.end())
1123 jobData = it.value();
1124 mPendingCollectionJobs.erase(it);
1126 if (j->error())
1128 Q_EMIT collectionDeleted(jobData.id, false);
1129 const QString errMsg = xi18nc("@info", "Failed to remove calendar <resource>%1</resource>.", jobData.displayName);
1130 qCCritical(KALARM_LOG) << errMsg << ":" << j->errorString();
1131 KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1<nl/>(%2)", errMsg, j->errorString()));
1133 else
1134 Q_EMIT collectionDeleted(jobData.id, true);
1136 #endif
1138 /******************************************************************************
1139 * Reload a collection from Akonadi storage. The backend data is not reloaded.
1141 bool AkonadiModel::reloadCollection(const Akonadi::Collection& collection)
1143 if (!collection.isValid())
1144 return false;
1145 qCDebug(KALARM_LOG) << collection.id();
1146 mMonitor->setCollectionMonitored(collection, false);
1147 mMonitor->setCollectionMonitored(collection, true);
1148 return true;
1151 /******************************************************************************
1152 * Reload a collection from Akonadi storage. The backend data is not reloaded.
1154 void AkonadiModel::reload()
1156 qCDebug(KALARM_LOG);
1157 const Collection::List collections = mMonitor->collectionsMonitored();
1158 foreach (const Collection& collection, collections)
1160 mMonitor->setCollectionMonitored(collection, false);
1161 mMonitor->setCollectionMonitored(collection, true);
1165 /******************************************************************************
1166 * Called when a collection modification job has completed.
1167 * Checks for any error.
1169 void AkonadiModel::modifyCollectionJobDone(KJob* j)
1171 Collection collection = static_cast<CollectionModifyJob*>(j)->collection();
1172 const Collection::Id id = collection.id();
1173 if (j->error())
1175 Q_EMIT collectionModified(id, false);
1176 if (mCollectionsDeleted.contains(id))
1177 mCollectionsDeleted.removeAll(id);
1178 else
1180 const QString errMsg = i18nc("@info", "Failed to update calendar \"%1\".", displayName(collection));
1181 qCCritical(KALARM_LOG) << "Id:" << collection.id() << errMsg << ":" << j->errorString();
1182 KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1\n(%2)", errMsg, j->errorString()));
1185 else
1186 Q_EMIT collectionModified(id, true);
1189 /******************************************************************************
1190 * Returns the index to a specified event.
1192 QModelIndex AkonadiModel::eventIndex(const KAEvent& event)
1194 return itemIndex(event.itemId());
1197 /******************************************************************************
1198 * Search for an event's item ID. This method ignores any itemId() value
1199 * contained in the KAEvent. The collectionId() is used if available.
1201 Item::Id AkonadiModel::findItemId(const KAEvent& event)
1203 Collection::Id colId = event.collectionId();
1204 QModelIndex start = (colId < 0) ? index(0, 0) : collectionIndex(Collection(colId));
1205 Qt::MatchFlags flags = (colId < 0) ? Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive | Qt::MatchWrap
1206 : Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive;
1207 const QModelIndexList indexes = match(start, RemoteIdRole, event.id(), -1, flags);
1208 foreach (const QModelIndex& ix, indexes)
1210 if (ix.isValid())
1212 Item::Id id = ix.data(ItemIdRole).toLongLong();
1213 if (id >= 0)
1215 if (colId < 0
1216 || ix.data(ParentCollectionRole).value<Collection>().id() == colId)
1217 return id;
1221 return -1;
1224 #if 0
1225 /******************************************************************************
1226 * Return all events of a given type belonging to a collection.
1228 KAEvent::List AkonadiModel::events(Akonadi::Collection& collection, CalEvent::Type type) const
1230 KAEvent::List list;
1231 const QModelIndex ix = modelIndexForCollection(this, collection);
1232 if (ix.isValid())
1233 getChildEvents(ix, type, list);
1234 return list;
1237 /******************************************************************************
1238 * Recursive function to append all child Events with a given mime type.
1240 void AkonadiModel::getChildEvents(const QModelIndex& parent, CalEvent::Type type, KAEvent::List& events) const
1242 for (int row = 0, count = rowCount(parent); row < count; ++row)
1244 const QModelIndex ix = index(row, 0, parent);
1245 const Item item = data(ix, ItemRole).value<Item>();
1246 if (item.isValid())
1248 if (item.hasPayload<KAEvent>())
1250 KAEvent event = item.payload<KAEvent>();
1251 if (event.isValid() && event.category() == type)
1252 events += event;
1255 else
1257 const Collection c = ix.data(CollectionRole).value<Collection>();
1258 if (c.isValid())
1259 getChildEvents(ix, type, events);
1263 #endif
1265 KAEvent AkonadiModel::event(Item::Id itemId) const
1267 const QModelIndex ix = itemIndex(itemId);
1268 if (!ix.isValid())
1269 return KAEvent();
1270 return event(ix.data(ItemRole).value<Item>(), ix, Q_NULLPTR);
1273 KAEvent AkonadiModel::event(const QModelIndex& index) const
1275 return event(index.data(ItemRole).value<Item>(), index, Q_NULLPTR);
1278 KAEvent AkonadiModel::event(const Item& item, const QModelIndex& index, Collection* collection) const
1280 if (!item.isValid() || !item.hasPayload<KAEvent>())
1281 return KAEvent();
1282 const QModelIndex ix = index.isValid() ? index : itemIndex(item.id());
1283 if (!ix.isValid())
1284 return KAEvent();
1285 KAEvent e = item.payload<KAEvent>();
1286 if (e.isValid())
1289 Collection c = data(ix, ParentCollectionRole).value<Collection>();
1290 // Set collection ID using a const method, to avoid unnecessary copying of KAEvent
1291 e.setCollectionId_const(c.id());
1292 if (collection)
1293 *collection = c;
1295 return e;
1298 #if 0
1299 /******************************************************************************
1300 * Add an event to the default or a user-selected Collection.
1302 AkonadiModel::Result AkonadiModel::addEvent(KAEvent* event, CalEvent::Type type, QWidget* promptParent, bool noPrompt)
1304 qCDebug(KALARM_LOG) << event->id();
1306 // Determine parent collection - prompt or use default
1307 bool cancelled;
1308 const Collection collection = destination(type, Collection::CanCreateItem, promptParent, noPrompt, &cancelled);
1309 if (!collection.isValid())
1311 delete event;
1312 if (cancelled)
1313 return Cancelled;
1314 qCDebug(KALARM_LOG) << "No collection";
1315 return Failed;
1317 if (!addEvent(event, collection))
1319 qCDebug(KALARM_LOG) << "Failed";
1320 return Failed; // event was deleted by addEvent()
1322 return Success;
1324 #endif
1326 /******************************************************************************
1327 * Add events to a specified Collection.
1328 * Events which are scheduled to be added to the collection are updated with
1329 * their Akonadi item ID.
1330 * The caller must connect to the itemDone() signal to check whether events
1331 * have been added successfully. Note that the first signal may be emitted
1332 * before this function returns.
1333 * Reply = true if item creation has been scheduled for all events,
1334 * = false if at least one item creation failed to be scheduled.
1336 bool AkonadiModel::addEvents(const KAEvent::List& events, Collection& collection)
1338 bool ok = true;
1339 for (int i = 0, count = events.count(); i < count; ++i)
1340 ok = ok && addEvent(*events[i], collection);
1341 return ok;
1344 /******************************************************************************
1345 * Add an event to a specified Collection.
1346 * If the event is scheduled to be added to the collection, it is updated with
1347 * its Akonadi item ID.
1348 * The event's 'updated' flag is cleared.
1349 * The caller must connect to the itemDone() signal to check whether events
1350 * have been added successfully.
1351 * Reply = true if item creation has been scheduled.
1353 bool AkonadiModel::addEvent(KAEvent& event, Collection& collection)
1355 qCDebug(KALARM_LOG) << "ID:" << event.id();
1356 Item item;
1357 if (!event.setItemPayload(item, collection.contentMimeTypes()))
1359 qCWarning(KALARM_LOG) << "Invalid mime type for collection";
1360 return false;
1362 event.setItemId(item.id());
1363 qCDebug(KALARM_LOG)<<"-> item id="<<item.id();
1364 ItemCreateJob* job = new ItemCreateJob(item, collection);
1365 connect(job, &ItemCreateJob::result, this, &AkonadiModel::itemJobDone);
1366 mPendingItemJobs[job] = item.id();
1367 job->start();
1368 qCDebug(KALARM_LOG)<<"...exiting";
1369 return true;
1372 /******************************************************************************
1373 * Update an event in its collection.
1374 * The event retains its existing Akonadi item ID.
1375 * The event's 'updated' flag is cleared.
1376 * The caller must connect to the itemDone() signal to check whether the event
1377 * has been updated successfully.
1378 * Reply = true if item update has been scheduled.
1380 bool AkonadiModel::updateEvent(KAEvent& event)
1382 qCDebug(KALARM_LOG) << "ID:" << event.id();
1383 return updateEvent(event.itemId(), event);
1385 bool AkonadiModel::updateEvent(Akonadi::Item::Id itemId, KAEvent& newEvent)
1387 qCDebug(KALARM_LOG)<<"item id="<<itemId;
1388 const QModelIndex ix = itemIndex(itemId);
1389 if (!ix.isValid())
1390 return false;
1391 const Collection collection = ix.data(ParentCollectionRole).value<Collection>();
1392 Item item = ix.data(ItemRole).value<Item>();
1393 qCDebug(KALARM_LOG)<<"item id="<<item.id()<<", revision="<<item.revision();
1394 if (!newEvent.setItemPayload(item, collection.contentMimeTypes()))
1396 qCWarning(KALARM_LOG) << "Invalid mime type for collection";
1397 return false;
1399 // setData(ix, QVariant::fromValue(item), ItemRole);
1400 queueItemModifyJob(item);
1401 return true;
1404 /******************************************************************************
1405 * Delete an event from its collection.
1407 bool AkonadiModel::deleteEvent(const KAEvent& event)
1409 return deleteEvent(event.itemId());
1411 bool AkonadiModel::deleteEvent(Akonadi::Item::Id itemId)
1413 qCDebug(KALARM_LOG) << itemId;
1414 const QModelIndex ix = itemIndex(itemId);
1415 if (!ix.isValid())
1416 return false;
1417 if (mCollectionsDeleting.contains(ix.data(ParentCollectionRole).value<Collection>().id()))
1419 qCDebug(KALARM_LOG) << "Collection being deleted";
1420 return true; // the event's collection is being deleted
1422 const Item item = ix.data(ItemRole).value<Item>();
1423 ItemDeleteJob* job = new ItemDeleteJob(item);
1424 connect(job, &ItemDeleteJob::result, this, &AkonadiModel::itemJobDone);
1425 mPendingItemJobs[job] = itemId;
1426 job->start();
1427 return true;
1430 /******************************************************************************
1431 * Queue an ItemModifyJob for execution. Ensure that only one job is
1432 * simultaneously active for any one Item.
1434 * This is necessary because we can't call two ItemModifyJobs for the same Item
1435 * at the same time; otherwise Akonadi will detect a conflict and require manual
1436 * intervention to resolve it.
1438 void AkonadiModel::queueItemModifyJob(const Item& item)
1440 qCDebug(KALARM_LOG) << item.id();
1441 QMap<Item::Id, Item>::Iterator it = mItemModifyJobQueue.find(item.id());
1442 if (it != mItemModifyJobQueue.end())
1444 // A job is already queued for this item. Replace the queued item value with the new one.
1445 qCDebug(KALARM_LOG) << "Replacing previously queued job";
1446 it.value() = item;
1448 else
1450 // There is no job already queued for this item
1451 if (mItemsBeingCreated.contains(item.id()))
1453 qCDebug(KALARM_LOG) << "Waiting for item initialisation";
1454 mItemModifyJobQueue[item.id()] = item; // wait for item initialisation to complete
1456 else
1458 Item newItem = item;
1459 const Item current = itemById(item.id()); // fetch the up-to-date item
1460 if (current.isValid())
1461 newItem.setRevision(current.revision());
1462 mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing
1463 ItemModifyJob* job = new ItemModifyJob(newItem);
1464 job->disableRevisionCheck();
1465 connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone);
1466 mPendingItemJobs[job] = item.id();
1467 qCDebug(KALARM_LOG) << "Executing Modify job for item" << item.id() << ", revision=" << newItem.revision();
1472 /******************************************************************************
1473 * Called when an item job has completed.
1474 * Checks for any error.
1475 * Note that for an ItemModifyJob, the item revision number may not be updated
1476 * to the post-modification value. The next queued ItemModifyJob is therefore
1477 * not kicked off from here, but instead from the slot attached to the
1478 * itemChanged() signal, which has the revision updated.
1480 void AkonadiModel::itemJobDone(KJob* j)
1482 const QMap<KJob*, Item::Id>::iterator it = mPendingItemJobs.find(j);
1483 Item::Id itemId = -1;
1484 if (it != mPendingItemJobs.end())
1486 itemId = it.value();
1487 mPendingItemJobs.erase(it);
1489 const QByteArray jobClass = j->metaObject()->className();
1490 qCDebug(KALARM_LOG) << jobClass;
1491 if (j->error())
1493 QString errMsg;
1494 if (jobClass == "Akonadi::ItemCreateJob")
1495 errMsg = i18nc("@info", "Failed to create alarm.");
1496 else if (jobClass == "Akonadi::ItemModifyJob")
1497 errMsg = i18nc("@info", "Failed to update alarm.");
1498 else if (jobClass == "Akonadi::ItemDeleteJob")
1499 errMsg = i18nc("@info", "Failed to delete alarm.");
1500 else
1501 Q_ASSERT(0);
1502 qCCritical(KALARM_LOG) << errMsg << itemId << ":" << j->errorString();
1503 Q_EMIT itemDone(itemId, false);
1505 if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob")
1507 // Execute the next queued job for this item
1508 const Item current = itemById(itemId); // fetch the up-to-date item
1509 checkQueuedItemModifyJob(current);
1511 KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1<nl/>(%2)", errMsg, j->errorString()));
1513 else
1515 if (jobClass == "Akonadi::ItemCreateJob")
1517 // Prevent modification of the item until it is fully initialised.
1518 // Either slotMonitoredItemChanged() or slotRowsInserted(), or both,
1519 // will be called when the item is done.
1520 qCDebug(KALARM_LOG) << "item id=" << static_cast<ItemCreateJob*>(j)->item().id();
1521 mItemsBeingCreated << static_cast<ItemCreateJob*>(j)->item().id();
1523 Q_EMIT itemDone(itemId);
1526 /* if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob")
1528 const QMap<Item::Id, Item>::iterator it = mItemModifyJobQueue.find(itemId);
1529 if (it != mItemModifyJobQueue.end())
1531 if (!it.value().isValid())
1532 mItemModifyJobQueue.erase(it); // there are no more jobs queued for the item
1537 /******************************************************************************
1538 * Check whether there are any ItemModifyJobs waiting for a specified item, and
1539 * if so execute the first one provided its creation has completed. This
1540 * prevents clashes in Akonadi conflicts between simultaneous ItemModifyJobs for
1541 * the same item.
1543 * Note that when an item is newly created (e.g. via addEvent()), the KAlarm
1544 * resource itemAdded() function creates an ItemModifyJob to give it a remote
1545 * ID. Until that job is complete, any other ItemModifyJob for the item will
1546 * cause a conflict.
1548 void AkonadiModel::checkQueuedItemModifyJob(const Item& item)
1550 if (mItemsBeingCreated.contains(item.id()))
1551 {qCDebug(KALARM_LOG)<<"Still being created";
1552 return; // the item hasn't been fully initialised yet
1554 const QMap<Item::Id, Item>::iterator it = mItemModifyJobQueue.find(item.id());
1555 if (it == mItemModifyJobQueue.end())
1556 {qCDebug(KALARM_LOG)<<"No jobs queued";
1557 return; // there are no jobs queued for the item
1559 Item qitem = it.value();
1560 if (!qitem.isValid())
1562 // There is no further job queued for the item, so remove the item from the list
1563 qCDebug(KALARM_LOG)<<"No more jobs queued";
1564 mItemModifyJobQueue.erase(it);
1566 else
1568 // Queue the next job for the Item, after updating the Item's
1569 // revision number to match that set by the job just completed.
1570 qitem.setRevision(item.revision());
1571 mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing
1572 ItemModifyJob* job = new ItemModifyJob(qitem);
1573 job->disableRevisionCheck();
1574 connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone);
1575 mPendingItemJobs[job] = qitem.id();
1576 qCDebug(KALARM_LOG) << "Executing queued Modify job for item" << qitem.id() << ", revision=" << qitem.revision();
1580 /******************************************************************************
1581 * Called when rows have been inserted into the model.
1583 void AkonadiModel::slotRowsInserted(const QModelIndex& parent, int start, int end)
1585 qCDebug(KALARM_LOG) << start << "-" << end << "(parent =" << parent << ")";
1586 for (int row = start; row <= end; ++row)
1588 const QModelIndex ix = index(row, 0, parent);
1589 const Collection collection = ix.data(CollectionRole).value<Collection>();
1590 if (collection.isValid())
1592 // A collection has been inserted.
1593 // Ignore it if it isn't owned by a valid resource.
1594 qCDebug(KALARM_LOG) << "Collection" << collection.id() << collection.name();
1595 if (AgentManager::self()->instance(collection.resource()).isValid())
1597 QSet<QByteArray> attrs;
1598 attrs += CollectionAttribute::name();
1599 setCollectionChanged(collection, attrs, true);
1600 Q_EMIT collectionAdded(collection);
1602 if (!mCollectionsBeingCreated.contains(collection.remoteId())
1603 && (collection.rights() & writableRights) == writableRights)
1605 // Update to current KAlarm format if necessary, and if the user agrees
1606 CalendarMigrator::updateToCurrentFormat(collection, false, MainWindow::mainMainWindow());
1610 else
1612 // An item has been inserted
1613 const Item item = ix.data(ItemRole).value<Item>();
1614 if (item.isValid())
1616 qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision();
1617 if (mItemsBeingCreated.removeAll(item.id())) // the new item has now been initialised
1618 checkQueuedItemModifyJob(item); // execute the next job queued for the item
1622 const EventList events = eventList(parent, start, end);
1623 if (!events.isEmpty())
1624 Q_EMIT eventsAdded(events);
1627 /******************************************************************************
1628 * Called when rows are about to be removed from the model.
1630 void AkonadiModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
1632 qCDebug(KALARM_LOG) << start << "-" << end << "(parent =" << parent << ")";
1633 const EventList events = eventList(parent, start, end);
1634 if (!events.isEmpty())
1636 foreach (const Event& event, events)
1637 qCDebug(KALARM_LOG) << "Collection:" << event.collection.id() << ", Event ID:" << event.event.id();
1638 Q_EMIT eventsToBeRemoved(events);
1642 /******************************************************************************
1643 * Return a list of KAEvent/Collection pairs for a given range of rows.
1645 AkonadiModel::EventList AkonadiModel::eventList(const QModelIndex& parent, int start, int end)
1647 EventList events;
1648 for (int row = start; row <= end; ++row)
1650 Collection c;
1651 const QModelIndex ix = index(row, 0, parent);
1652 const KAEvent evnt = event(ix.data(ItemRole).value<Item>(), ix, &c);
1653 if (evnt.isValid())
1654 events += Event(evnt, c);
1656 return events;
1659 /******************************************************************************
1660 * Called when a monitored collection's properties or content have changed.
1661 * Optionally emits a signal if properties of interest have changed.
1663 void AkonadiModel::setCollectionChanged(const Collection& collection, const QSet<QByteArray>& attributeNames, bool rowInserted)
1665 // Check for a read/write permission change
1666 const Collection::Rights oldRights = mCollectionRights.value(collection.id(), Collection::AllRights);
1667 const Collection::Rights newRights = collection.rights() & writableRights;
1668 if (newRights != oldRights)
1670 qCDebug(KALARM_LOG) << "Collection" << collection.id() << ": rights ->" << newRights;
1671 mCollectionRights[collection.id()] = newRights;
1672 Q_EMIT collectionStatusChanged(collection, ReadOnly, (newRights != writableRights), rowInserted);
1675 // Check for a change in content mime types
1676 // (e.g. when a collection is first created at startup).
1677 const CalEvent::Types oldAlarmTypes = mCollectionAlarmTypes.value(collection.id(), CalEvent::EMPTY);
1678 const CalEvent::Types newAlarmTypes = CalEvent::types(collection.contentMimeTypes());
1679 if (newAlarmTypes != oldAlarmTypes)
1681 qCDebug(KALARM_LOG) << "Collection" << collection.id() << ": alarm types ->" << newAlarmTypes;
1682 mCollectionAlarmTypes[collection.id()] = newAlarmTypes;
1683 Q_EMIT collectionStatusChanged(collection, AlarmTypes, static_cast<int>(newAlarmTypes), rowInserted);
1686 // Check for the collection being enabled/disabled
1687 if (attributeNames.contains(CollectionAttribute::name()))
1689 static bool firstEnabled = true;
1690 const CalEvent::Types oldEnabled = mCollectionEnabled.value(collection.id(), CalEvent::EMPTY);
1691 const CalEvent::Types newEnabled = collection.hasAttribute<CollectionAttribute>() ? collection.attribute<CollectionAttribute>()->enabled() : CalEvent::EMPTY;
1692 if (firstEnabled || newEnabled != oldEnabled)
1694 qCDebug(KALARM_LOG) << "Collection" << collection.id() << ": enabled ->" << newEnabled;
1695 firstEnabled = false;
1696 mCollectionEnabled[collection.id()] = newEnabled;
1697 Q_EMIT collectionStatusChanged(collection, Enabled, static_cast<int>(newEnabled), rowInserted);
1701 // Check for the backend calendar format changing
1702 if (attributeNames.contains(CompatibilityAttribute::name()))
1704 // Update to current KAlarm format if necessary, and if the user agrees
1705 qCDebug(KALARM_LOG) << "CompatibilityAttribute";
1706 Collection col(collection);
1707 refresh(col);
1708 CalendarMigrator::updateToCurrentFormat(col, false, MainWindow::mainMainWindow());
1711 if (mMigrating)
1713 mCollectionIdsBeingCreated.removeAll(collection.id());
1714 if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()
1715 && CalendarMigrator::completed())
1717 qCDebug(KALARM_LOG) << "Migration completed";
1718 mMigrating = false;
1719 Q_EMIT migrationCompleted();
1724 /******************************************************************************
1725 * Called when a monitored collection is removed.
1727 void AkonadiModel::slotCollectionRemoved(const Collection& collection)
1729 const Collection::Id id = collection.id();
1730 qCDebug(KALARM_LOG) << id;
1731 mCollectionRights.remove(id);
1732 mCollectionsDeleting.removeAll(id);
1733 while (mCollectionsDeleted.count() > 20) // don't let list grow indefinitely
1734 mCollectionsDeleted.removeFirst();
1735 mCollectionsDeleted << id;
1738 /******************************************************************************
1739 * Called when a collection creation is about to start, or has completed.
1741 void AkonadiModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished)
1743 if (finished)
1745 mCollectionsBeingCreated.removeAll(path);
1746 mCollectionIdsBeingCreated << id;
1748 else
1749 mCollectionsBeingCreated << path;
1752 /******************************************************************************
1753 * Called when calendar migration has completed.
1755 void AkonadiModel::slotMigrationCompleted()
1757 if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty())
1759 qCDebug(KALARM_LOG) << "Migration completed";
1760 mMigrating = false;
1761 Q_EMIT migrationCompleted();
1765 /******************************************************************************
1766 * Called when an item in the monitored collections has changed.
1768 void AkonadiModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet<QByteArray>&)
1770 qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision();
1771 mItemsBeingCreated.removeAll(item.id()); // the new item has now been initialised
1772 checkQueuedItemModifyJob(item); // execute the next job queued for the item
1774 KAEvent evnt = event(item);
1775 if (!evnt.isValid())
1776 return;
1777 const QModelIndexList indexes = modelIndexesForItem(this, item);
1778 foreach (const QModelIndex& index, indexes)
1780 if (index.isValid())
1782 // Wait to ensure that the base EntityTreeModel has processed the
1783 // itemChanged() signal first, before we Q_EMIT eventChanged().
1784 Collection c = data(index, ParentCollectionRole).value<Collection>();
1785 evnt.setCollectionId(c.id());
1786 mPendingEventChanges.enqueue(Event(evnt, c));
1787 QTimer::singleShot(0, this, &AkonadiModel::slotEmitEventChanged);
1788 break;
1793 /******************************************************************************
1794 * Called to Q_EMIT a signal when an event in the monitored collections has
1795 * changed.
1797 void AkonadiModel::slotEmitEventChanged()
1799 while (!mPendingEventChanges.isEmpty())
1801 Q_EMIT eventChanged(mPendingEventChanges.dequeue());
1805 /******************************************************************************
1806 * Refresh the specified Collection with up to date data.
1807 * Return: true if successful, false if collection not found.
1809 bool AkonadiModel::refresh(Akonadi::Collection& collection) const
1811 const QModelIndex ix = modelIndexForCollection(this, collection);
1812 if (!ix.isValid())
1813 return false;
1814 collection = ix.data(CollectionRole).value<Collection>();
1815 return true;
1818 /******************************************************************************
1819 * Refresh the specified Item with up to date data.
1820 * Return: true if successful, false if item not found.
1822 bool AkonadiModel::refresh(Akonadi::Item& item) const
1824 const QModelIndexList ixs = modelIndexesForItem(this, item);
1825 if (ixs.isEmpty() || !ixs[0].isValid())
1826 return false;
1827 item = ixs[0].data(ItemRole).value<Item>();
1828 return true;
1831 /******************************************************************************
1832 * Find the QModelIndex of a collection.
1834 QModelIndex AkonadiModel::collectionIndex(const Collection& collection) const
1836 const QModelIndex ix = modelIndexForCollection(this, collection);
1837 if (!ix.isValid())
1838 return QModelIndex();
1839 return ix;
1842 /******************************************************************************
1843 * Return the up to date collection with the specified Akonadi ID.
1845 Collection AkonadiModel::collectionById(Collection::Id id) const
1847 const QModelIndex ix = modelIndexForCollection(this, Collection(id));
1848 if (!ix.isValid())
1849 return Collection();
1850 return ix.data(CollectionRole).value<Collection>();
1853 /******************************************************************************
1854 * Find the QModelIndex of an item.
1856 QModelIndex AkonadiModel::itemIndex(const Item& item) const
1858 const QModelIndexList ixs = modelIndexesForItem(this, item);
1859 if (ixs.isEmpty() || !ixs[0].isValid())
1860 return QModelIndex();
1861 return ixs[0];
1864 /******************************************************************************
1865 * Return the up to date item with the specified Akonadi ID.
1867 Item AkonadiModel::itemById(Item::Id id) const
1869 const QModelIndexList ixs = modelIndexesForItem(this, Item(id));
1870 if (ixs.isEmpty() || !ixs[0].isValid())
1871 return Item();
1872 return ixs[0].data(ItemRole).value<Item>();
1875 /******************************************************************************
1876 * Find the collection containing the specified Akonadi item ID.
1878 Collection AkonadiModel::collectionForItem(Item::Id id) const
1880 const QModelIndex ix = itemIndex(id);
1881 if (!ix.isValid())
1882 return Collection();
1883 return ix.data(ParentCollectionRole).value<Collection>();
1886 bool AkonadiModel::isCompatible(const Collection& collection)
1888 return collection.hasAttribute<CompatibilityAttribute>()
1889 && collection.attribute<CompatibilityAttribute>()->compatibility() == KACalendar::Current;
1892 /******************************************************************************
1893 * Return whether a collection is fully writable.
1895 int AkonadiModel::isWritable(const Akonadi::Collection& collection)
1897 KACalendar::Compat format;
1898 return isWritable(collection, format);
1901 int AkonadiModel::isWritable(const Akonadi::Collection& collection, KACalendar::Compat& format)
1903 format = KACalendar::Incompatible;
1904 if (!collection.isValid())
1905 return -1;
1906 Collection col = collection;
1907 instance()->refresh(col); // update with latest data
1908 if ((col.rights() & writableRights) != writableRights)
1910 format = KACalendar::Current;
1911 return -1;
1913 if (!col.hasAttribute<CompatibilityAttribute>())
1914 return -1;
1915 format = col.attribute<CompatibilityAttribute>()->compatibility();
1916 switch (format)
1918 case KACalendar::Current:
1919 return 1;
1920 case KACalendar::Converted:
1921 case KACalendar::Convertible:
1922 return 0;
1923 default:
1924 return -1;
1928 CalEvent::Types AkonadiModel::types(const Collection& collection)
1930 return CalEvent::types(collection.contentMimeTypes());
1933 // vim: et sw=4: