2 * akonadimodel.cpp - KAlarm calendar file access using Akonadi
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>
58 #include <QApplication>
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 /*=============================================================================
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()
89 mInstance
= new AkonadiModel(new ChangeRecorder(qApp
), qApp
);
93 /******************************************************************************
96 AkonadiModel::AkonadiModel(ChangeRecorder
* monitor
, QObject
* parent
)
97 : EntityTreeModel(monitor
, parent
),
99 mResourcesChecked(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
>();
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());
130 #warning Only want to monitor collection properties, not content, when this becomes possible
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
)
168 case ServerManager::Running
:
169 if (!mResourcesChecked
)
171 qCDebug(KALARM_LOG
) << "Server running";
172 mResourcesChecked
= true;
174 CalendarMigrator::execute();
177 case ServerManager::NotRunning
:
178 qCDebug(KALARM_LOG
) << "Server stopped";
179 mResourcesChecked
= false;
181 mCollectionAlarmTypes
.clear();
182 mCollectionRights
.clear();
183 mCollectionEnabled
.clear();
184 initCalendarMigrator();
185 Q_EMIT
serverStopped();
192 /******************************************************************************
193 * Initialise the calendar migrator so that it can be run (either for the first
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
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
:
232 case AlarmActionsRole
:
233 case AlarmSubActionRole
:
235 case EnabledTypesRole
:
236 case CommandErrorRole
:
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
251 case Qt::DisplayRole
:
252 return collection
.displayName();
253 case EnabledTypesRole
:
254 if (!collection
.hasAttribute
<CollectionAttribute
>())
256 return static_cast<int>(collection
.attribute
<CollectionAttribute
>()->enabled());
258 role
= Qt::BackgroundRole
;
260 case Qt::BackgroundRole
:
262 const QColor colour
= backgroundColor_p(collection
);
263 if (colour
.isValid())
267 case Qt::ForegroundRole
:
268 return foregroundColor(collection
, collection
.contentMimeTypes());
269 case Qt::ToolTipRole
:
270 return tooltip(collection
, CalEvent::ACTIVE
| CalEvent::ARCHIVED
| CalEvent::TEMPLATE
);
272 return static_cast<int>(types(collection
));
274 if (!collection
.hasAttribute
<CollectionAttribute
>()
275 || !isCompatible(collection
))
277 return static_cast<int>(collection
.attribute
<CollectionAttribute
>()->standard());
279 if (!collection
.hasAttribute
<CollectionAttribute
>())
281 return collection
.attribute
<CollectionAttribute
>()->keepFormat();
288 const Item item
= index
.data(ItemRole
).value
<Item
>();
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
>())
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
;
307 case CommandErrorRole
:
308 if (!item
.hasAttribute
<EventAttribute
>())
309 return KAEvent::CMD_NO_ERROR
;
310 return item
.attribute
<EventAttribute
>()->commandError();
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())
320 if (role
== AlarmActionsRole
)
321 return event
.actionTypes();
322 if (role
== AlarmSubActionRole
)
323 return event
.actionSubType();
324 bool calendarColour
= false;
330 case Qt::BackgroundRole
:
331 calendarColour
= true;
333 case Qt::DisplayRole
:
335 return AlarmTime::alarmTimeText(event
.startDateTime());
336 return AlarmTime::alarmTimeText(event
.nextTrigger(KAEvent::DISPLAY_TRIGGER
));
341 due
= event
.startDateTime();
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));
354 case Qt::BackgroundRole
:
355 calendarColour
= true;
357 case Qt::DisplayRole
:
360 return AlarmTime::timeToAlarmText(event
.nextTrigger(KAEvent::DISPLAY_TRIGGER
));
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;
376 case Qt::BackgroundRole
:
377 calendarColour
= true;
379 case Qt::DisplayRole
:
380 return repeatText(event
);
381 case Qt::TextAlignmentRole
:
382 return Qt::AlignHCenter
;
384 return repeatOrder(event
);
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
);
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
;
409 event
.bgColour().getRgb(&r
, &g
, &b
);
410 if (r
> 128 && g
<= 128 && b
<= 128)
411 colour
= QColor(Qt::white
);
415 case Qt::DisplayRole
:
416 if (event
.commandError() != KAEvent::CMD_NO_ERROR
)
417 return QLatin1String("!");
421 const unsigned i
= (event
.actionTypes() == KAEvent::ACT_DISPLAY
)
422 ? event
.bgColour().rgb() : 0;
423 return QStringLiteral("%1").arg(i
, 6, 10, QLatin1Char('0'));
432 case Qt::BackgroundRole
:
433 calendarColour
= true;
435 case Qt::DecorationRole
:
438 v
.setValue(*eventIcon(event
));
441 case Qt::TextAlignmentRole
:
442 return Qt::AlignHCenter
;
443 case Qt::SizeHintRole
:
445 case Qt::AccessibleTextRole
:
447 #warning Implement accessibility
451 return static_cast<int>(event
.actionSubType());
453 return QStringLiteral("%1").arg(event
.actionSubType(), 2, 10, QLatin1Char('0'));
459 case Qt::BackgroundRole
:
460 calendarColour
= true;
462 case Qt::DisplayRole
:
464 return AlarmText::summary(event
, 1);
465 case Qt::ToolTipRole
:
466 return AlarmText::summary(event
, 10);
471 case TemplateNameColumn
:
474 case Qt::BackgroundRole
:
475 calendarColour
= true;
477 case Qt::DisplayRole
:
478 return event
.templateName();
480 return event
.templateName().toUpper();
489 case Qt::ForegroundRole
:
490 if (!event
.enabled())
491 return Preferences::disabledColour();
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");
508 case KAEvent::CMD_NO_ERROR
:
513 return event
.enabled();
520 Collection parent
= item
.parentCollection();
521 const QColor colour
= backgroundColor(parent
);
522 if (colour
.isValid())
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())
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
;
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;
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;
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;
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;
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
);
602 CollectionModifyJob
* job
= new CollectionModifyJob(c
, this);
603 connect(job
, &CollectionModifyJob::result
, this, &AkonadiModel::modifyCollectionJobDone
);
609 Item item
= index
.data(ItemRole
).value
<Item
>();
612 bool updateItem
= false;
615 case CommandErrorRole
:
617 const KAEvent::CmdErrType err
= static_cast<KAEvent::CmdErrType
>(value
.toInt());
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
);
633 qCDebug(KALARM_LOG
)<<"Item:"<<item
.id()<<" CommandErrorRole ->"<<err
;
642 qCDebug(KALARM_LOG
)<<"Item: passing to EntityTreeModel::setData("<<role
<<")";
647 queueItemModifyJob(item
);
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
663 case CollectionTreeHeaders
:
665 case ItemListHeaders
:
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
)
681 case CollectionTreeHeaders
:
684 if (role
== Qt::DisplayRole
)
685 return i18nc("@title:column", "Calendars");
688 case ItemListHeaders
:
689 if (section
< 0 || section
>= ColumnCount
)
691 if (role
== Qt::DisplayRole
)
696 return i18nc("@title:column", "Time");
698 return i18nc("@title:column", "Time To");
700 return i18nc("@title:column", "Repeat");
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
);
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
)
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();
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.
748 Q_EMIT
dataChanged(index(start
, startColumn
, parent
), index(end
, endColumn
, parent
));
751 signalDataChanged(checkFunc
, startColumn
, endColumn
, ix
);
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
&)
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
>();
791 return !event
.enabled();
796 void AkonadiModel::slotUpdateDisabledColour(const QColor
&)
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())
816 void AkonadiModel::slotUpdateHolidays()
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())
837 void AkonadiModel::slotUpdateWorkingHours()
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());
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
)
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);
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
);
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())
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
>())
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())
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",
946 "<nl/>%2: <filename>%3</filename>"
948 name
, type
, locn
, disabled
, readonly
);
949 if (inactive
|| !writable
)
950 return xi18nc("@info:tooltip",
952 "<nl/>%2: <filename>%3</filename>"
954 name
, type
, locn
, (inactive
? disabled
: readonly
));
955 return xi18nc("@info:tooltip",
957 "<nl/>%2: <filename>%3</filename>",
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
))
973 return i18nc("@info", "Read-only (old format)");
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);
992 /******************************************************************************
993 * Return a string for sorting the repetition column.
995 QString
AkonadiModel::repeatOrder(const KAEvent
& event
) const
998 int repeatInterval
= 0;
999 if (event
.repeatAtLogin())
1003 repeatInterval
= event
.recurInterval();
1004 switch (event
.recurType())
1006 case KARecurrence::MINUTELY
:
1009 case KARecurrence::DAILY
:
1012 case KARecurrence::WEEKLY
:
1015 case KARecurrence::MONTHLY_DAY
:
1016 case KARecurrence::MONTHLY_POS
:
1019 case KARecurrence::ANNUAL_DATE
:
1020 case KARecurrence::ANNUAL_POS
:
1023 case KARecurrence::NO_RECUR
:
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
:
1040 case KAEvent::ACT_AUDIO
:
1042 case KAEvent::ACT_COMMAND
:
1043 return mCommandIcon
;
1044 case KAEvent::ACT_DISPLAY
:
1045 if (event
.actionSubType() == KAEvent::FILE)
1047 // fall through to ACT_DISPLAY_COMMAND
1048 case KAEvent::ACT_DISPLAY_COMMAND
:
1054 /******************************************************************************
1055 * Returns the QWhatsThis text for a specified column.
1057 QString
AkonadiModel::whatsThisText(int column
) const
1062 return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm");
1064 return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm");
1066 return i18nc("@info:whatsthis", "How often the alarm recurs");
1068 return i18nc("@info:whatsthis", "Background color of alarm message");
1070 return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)");
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");
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())
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
);
1096 CollectionDeleteJob
* job
= new CollectionDeleteJob(col
);
1097 connect(job
, &CollectionDeleteJob::result
, this, &AkonadiModel::deleteCollectionJobDone
);
1098 mPendingCollectionJobs
[job
] = CollJobData(col
.id(), displayName(col
));
1104 /******************************************************************************
1105 * Return whether a collection is currently being deleted.
1107 bool AkonadiModel::isCollectionBeingDeleted(Collection::Id id
) const
1109 return mCollectionsDeleting
.contains(id
);
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
);
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()));
1134 Q_EMIT
collectionDeleted(jobData
.id
, true);
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())
1145 qCDebug(KALARM_LOG
) << collection
.id();
1146 mMonitor
->setCollectionMonitored(collection
, false);
1147 mMonitor
->setCollectionMonitored(collection
, 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();
1175 Q_EMIT
collectionModified(id
, false);
1176 if (mCollectionsDeleted
.contains(id
))
1177 mCollectionsDeleted
.removeAll(id
);
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()));
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
)
1212 Item::Id id
= ix
.data(ItemIdRole
).toLongLong();
1216 || ix
.data(ParentCollectionRole
).value
<Collection
>().id() == colId
)
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
1231 const QModelIndex ix
= modelIndexForCollection(this, collection
);
1233 getChildEvents(ix
, type
, 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
>();
1248 if (item
.hasPayload
<KAEvent
>())
1250 KAEvent event
= item
.payload
<KAEvent
>();
1251 if (event
.isValid() && event
.category() == type
)
1257 const Collection c
= ix
.data(CollectionRole
).value
<Collection
>();
1259 getChildEvents(ix
, type
, events
);
1265 KAEvent
AkonadiModel::event(Item::Id itemId
) const
1267 const QModelIndex ix
= itemIndex(itemId
);
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
>())
1282 const QModelIndex ix
= index
.isValid() ? index
: itemIndex(item
.id());
1285 KAEvent e
= item
.payload
<KAEvent
>();
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());
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
1308 const Collection collection
= destination(type
, Collection::CanCreateItem
, promptParent
, noPrompt
, &cancelled
);
1309 if (!collection
.isValid())
1314 qCDebug(KALARM_LOG
) << "No collection";
1317 if (!addEvent(event
, collection
))
1319 qCDebug(KALARM_LOG
) << "Failed";
1320 return Failed
; // event was deleted by addEvent()
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
)
1339 for (int i
= 0, count
= events
.count(); i
< count
; ++i
)
1340 ok
= ok
&& addEvent(*events
[i
], collection
);
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();
1357 if (!event
.setItemPayload(item
, collection
.contentMimeTypes()))
1359 qCWarning(KALARM_LOG
) << "Invalid mime type for collection";
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();
1368 qCDebug(KALARM_LOG
)<<"...exiting";
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
);
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";
1399 // setData(ix, QVariant::fromValue(item), ItemRole);
1400 queueItemModifyJob(item
);
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
);
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
;
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";
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
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
;
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.");
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()));
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
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
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
);
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());
1612 // An item has been inserted
1613 const Item item
= ix
.data(ItemRole
).value
<Item
>();
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
)
1648 for (int row
= start
; row
<= end
; ++row
)
1651 const QModelIndex ix
= index(row
, 0, parent
);
1652 const KAEvent evnt
= event(ix
.data(ItemRole
).value
<Item
>(), ix
, &c
);
1654 events
+= Event(evnt
, c
);
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
);
1708 CalendarMigrator::updateToCurrentFormat(col
, false, MainWindow::mainMainWindow());
1713 mCollectionIdsBeingCreated
.removeAll(collection
.id());
1714 if (mCollectionsBeingCreated
.isEmpty() && mCollectionIdsBeingCreated
.isEmpty()
1715 && CalendarMigrator::completed())
1717 qCDebug(KALARM_LOG
) << "Migration completed";
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
)
1745 mCollectionsBeingCreated
.removeAll(path
);
1746 mCollectionIdsBeingCreated
<< id
;
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";
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())
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
);
1793 /******************************************************************************
1794 * Called to Q_EMIT a signal when an event in the monitored collections has
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
);
1814 collection
= ix
.data(CollectionRole
).value
<Collection
>();
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())
1827 item
= ixs
[0].data(ItemRole
).value
<Item
>();
1831 /******************************************************************************
1832 * Find the QModelIndex of a collection.
1834 QModelIndex
AkonadiModel::collectionIndex(const Collection
& collection
) const
1836 const QModelIndex ix
= modelIndexForCollection(this, collection
);
1838 return QModelIndex();
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
));
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();
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())
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
);
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())
1906 Collection col
= collection
;
1907 instance()->refresh(col
); // update with latest data
1908 if ((col
.rights() & writableRights
) != writableRights
)
1910 format
= KACalendar::Current
;
1913 if (!col
.hasAttribute
<CompatibilityAttribute
>())
1915 format
= col
.attribute
<CompatibilityAttribute
>()->compatibility();
1918 case KACalendar::Current
:
1920 case KACalendar::Converted
:
1921 case KACalendar::Convertible
:
1928 CalEvent::Types
AkonadiModel::types(const Collection
& collection
)
1930 return CalEvent::types(collection
.contentMimeTypes());