1 /*
2   SPDX-FileCopyrightText: 2009, 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3   SPDX-FileContributor: Frank Osterfeld <osterfeld@kde.org>
4   SPDX-FileContributor: Andras Mantia <andras@kdab.com>
5 
6   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
7 */
8 
9 #include "utils.h"
10 #include "calendarsupport_debug.h"
11 #include "kcalprefs.h"
12 
13 #include <Akonadi/AgentInstance>
14 #include <Akonadi/AgentManager>
15 #include <Akonadi/EntityDisplayAttribute>
16 #include <Akonadi/EntityTreeModel>
17 
18 #include <Akonadi/CollectionDialog>
19 
20 #include <Akonadi/Calendar/BlockAlarmsAttribute>
21 #include <Akonadi/Calendar/ETMCalendar>
22 
23 #include <KHolidays/HolidayRegion>
24 
25 #include <KCalendarCore/CalFilter>
26 #include <KCalendarCore/FileStorage>
27 #include <KCalendarCore/FreeBusy>
28 #include <KCalendarCore/MemoryCalendar>
29 
30 #include <KCalUtils/DndFactory>
31 #include <KCalUtils/ICalDrag>
32 #include <KCalUtils/VCalDrag>
33 
34 #include <KLocalizedString>
35 
36 #include <QApplication>
37 #include <QDrag>
38 #include <QFile>
39 #include <QMimeData>
40 #include <QPointer>
41 #include <QStyle>
42 #include <QUrlQuery>
43 
incidence(const Akonadi::Item & item)44 KCalendarCore::Incidence::Ptr CalendarSupport::incidence(const Akonadi::Item &item)
45 {
46     // relying on exception for performance reasons
47     try {
48         return item.payload<KCalendarCore::Incidence::Ptr>();
49     } catch (const Akonadi::PayloadException &) {
50         return KCalendarCore::Incidence::Ptr();
51     }
52 }
53 
event(const Akonadi::Item & item)54 KCalendarCore::Event::Ptr CalendarSupport::event(const Akonadi::Item &item)
55 {
56     // relying on exception for performance reasons
57     try {
58         auto incidence = item.payload<KCalendarCore::Incidence::Ptr>();
59         if (hasEvent(incidence)) {
60             return item.payload<KCalendarCore::Event::Ptr>();
61         }
62     } catch (const Akonadi::PayloadException &) {
63         return KCalendarCore::Event::Ptr();
64     }
65     return KCalendarCore::Event::Ptr();
66 }
67 
event(const KCalendarCore::Incidence::Ptr & incidence)68 KCalendarCore::Event::Ptr CalendarSupport::event(const KCalendarCore::Incidence::Ptr &incidence)
69 {
70     if (hasEvent(incidence)) {
71         return incidence.staticCast<KCalendarCore::Event>();
72     }
73     return KCalendarCore::Event::Ptr();
74 }
75 
incidencesFromItems(const Akonadi::Item::List & items)76 KCalendarCore::Incidence::List CalendarSupport::incidencesFromItems(const Akonadi::Item::List &items)
77 {
78     KCalendarCore::Incidence::List incidences;
79     for (const Akonadi::Item &item : items) {
80         if (const KCalendarCore::Incidence::Ptr e = CalendarSupport::incidence(item)) {
81             incidences.push_back(e);
82         }
83     }
84     return incidences;
85 }
86 
todo(const Akonadi::Item & item)87 KCalendarCore::Todo::Ptr CalendarSupport::todo(const Akonadi::Item &item)
88 {
89     try {
90         auto incidence = item.payload<KCalendarCore::Incidence::Ptr>();
91         if (hasTodo(incidence)) {
92             return item.payload<KCalendarCore::Todo::Ptr>();
93         }
94     } catch (const Akonadi::PayloadException &) {
95         return KCalendarCore::Todo::Ptr();
96     }
97     return KCalendarCore::Todo::Ptr();
98 }
99 
todo(const KCalendarCore::Incidence::Ptr & incidence)100 KCalendarCore::Todo::Ptr CalendarSupport::todo(const KCalendarCore::Incidence::Ptr &incidence)
101 {
102     if (hasTodo(incidence)) {
103         return incidence.staticCast<KCalendarCore::Todo>();
104     }
105     return KCalendarCore::Todo::Ptr();
106 }
107 
journal(const Akonadi::Item & item)108 KCalendarCore::Journal::Ptr CalendarSupport::journal(const Akonadi::Item &item)
109 {
110     try {
111         auto incidence = item.payload<KCalendarCore::Incidence::Ptr>();
112         if (hasJournal(incidence)) {
113             return item.payload<KCalendarCore::Journal::Ptr>();
114         }
115     } catch (const Akonadi::PayloadException &) {
116         return KCalendarCore::Journal::Ptr();
117     }
118     return KCalendarCore::Journal::Ptr();
119 }
120 
journal(const KCalendarCore::Incidence::Ptr & incidence)121 KCalendarCore::Journal::Ptr CalendarSupport::journal(const KCalendarCore::Incidence::Ptr &incidence)
122 {
123     if (hasJournal(incidence)) {
124         return incidence.staticCast<KCalendarCore::Journal>();
125     }
126     return KCalendarCore::Journal::Ptr();
127 }
128 
hasIncidence(const Akonadi::Item & item)129 bool CalendarSupport::hasIncidence(const Akonadi::Item &item)
130 {
131     return item.hasPayload<KCalendarCore::Incidence::Ptr>();
132 }
133 
hasEvent(const Akonadi::Item & item)134 bool CalendarSupport::hasEvent(const Akonadi::Item &item)
135 {
136     return item.hasPayload<KCalendarCore::Event::Ptr>();
137 }
138 
hasEvent(const KCalendarCore::Incidence::Ptr & incidence)139 bool CalendarSupport::hasEvent(const KCalendarCore::Incidence::Ptr &incidence)
140 {
141     return incidence && incidence->type() == KCalendarCore::Incidence::TypeEvent;
142 }
143 
hasTodo(const Akonadi::Item & item)144 bool CalendarSupport::hasTodo(const Akonadi::Item &item)
145 {
146     return item.hasPayload<KCalendarCore::Todo::Ptr>();
147 }
148 
hasTodo(const KCalendarCore::Incidence::Ptr & incidence)149 bool CalendarSupport::hasTodo(const KCalendarCore::Incidence::Ptr &incidence)
150 {
151     return incidence && incidence->type() == KCalendarCore::Incidence::TypeTodo;
152 }
153 
hasJournal(const Akonadi::Item & item)154 bool CalendarSupport::hasJournal(const Akonadi::Item &item)
155 {
156     return item.hasPayload<KCalendarCore::Journal::Ptr>();
157 }
158 
hasJournal(const KCalendarCore::Incidence::Ptr & incidence)159 bool CalendarSupport::hasJournal(const KCalendarCore::Incidence::Ptr &incidence)
160 {
161     return incidence && incidence->type() == KCalendarCore::Incidence::TypeJournal;
162 }
163 
createMimeData(const Akonadi::Item::List & items)164 QMimeData *CalendarSupport::createMimeData(const Akonadi::Item::List &items)
165 {
166     if (items.isEmpty()) {
167         return nullptr;
168     }
169 
170     KCalendarCore::MemoryCalendar::Ptr cal(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone()));
171 
172     QList<QUrl> urls;
173     int incidencesFound = 0;
174     for (const Akonadi::Item &item : items) {
175         const KCalendarCore::Incidence::Ptr incidence(CalendarSupport::incidence(item));
176         if (!incidence) {
177             continue;
178         }
179         ++incidencesFound;
180         urls.push_back(item.url());
181         KCalendarCore::Incidence::Ptr i(incidence->clone());
182         cal->addIncidence(i);
183     }
184 
185     if (incidencesFound == 0) {
186         return nullptr;
187     }
188 
189     std::unique_ptr<QMimeData> mimeData(new QMimeData);
190 
191     mimeData->setUrls(urls);
192 
193     if (KCalUtils::ICalDrag::populateMimeData(mimeData.get(), cal)) {
194         return mimeData.release();
195     } else {
196         return nullptr;
197     }
198 }
199 
createMimeData(const Akonadi::Item & item)200 QMimeData *CalendarSupport::createMimeData(const Akonadi::Item &item)
201 {
202     return createMimeData(Akonadi::Item::List() << item);
203 }
204 
205 #ifndef QT_NO_DRAGANDDROP
createDrag(const Akonadi::Item & item,QWidget * parent)206 QDrag *CalendarSupport::createDrag(const Akonadi::Item &item, QWidget *parent)
207 {
208     return createDrag(Akonadi::Item::List() << item, parent);
209 }
210 
211 #endif
212 
findMostCommonType(const Akonadi::Item::List & items)213 static QByteArray findMostCommonType(const Akonadi::Item::List &items)
214 {
215     QByteArray prev;
216     if (items.isEmpty()) {
217         return "Incidence";
218     }
219 
220     for (const Akonadi::Item &item : items) {
221         if (!CalendarSupport::hasIncidence(item)) {
222             continue;
223         }
224         const QByteArray type = CalendarSupport::incidence(item)->typeStr();
225         if (!prev.isEmpty() && type != prev) {
226             return "Incidence";
227         }
228         prev = type;
229     }
230     return prev;
231 }
232 
233 #ifndef QT_NO_DRAGANDDROP
createDrag(const Akonadi::Item::List & items,QWidget * parent)234 QDrag *CalendarSupport::createDrag(const Akonadi::Item::List &items, QWidget *parent)
235 {
236     std::unique_ptr<QDrag> drag(new QDrag(parent));
237     drag->setMimeData(CalendarSupport::createMimeData(items));
238 
239     const QByteArray common = findMostCommonType(items);
240     if (common == "Event") {
241         drag->setPixmap(QIcon::fromTheme(QStringLiteral("view-calendar-day")).pixmap(qApp->style()->pixelMetric(QStyle::PM_ToolBarIconSize)));
242     } else if (common == "Todo") {
243         drag->setPixmap(QIcon::fromTheme(QStringLiteral("view-calendar-tasks")).pixmap(qApp->style()->pixelMetric(QStyle::PM_ToolBarIconSize)));
244     }
245 
246     return drag.release();
247 }
248 
249 #endif
250 
itemMatches(const Akonadi::Item & item,const KCalendarCore::CalFilter * filter)251 static bool itemMatches(const Akonadi::Item &item, const KCalendarCore::CalFilter *filter)
252 {
253     assert(filter);
254     KCalendarCore::Incidence::Ptr inc = CalendarSupport::incidence(item);
255     if (!inc) {
256         return false;
257     }
258     return filter->filterIncidence(inc);
259 }
260 
applyCalFilter(const Akonadi::Item::List & items_,const KCalendarCore::CalFilter * filter)261 Akonadi::Item::List CalendarSupport::applyCalFilter(const Akonadi::Item::List &items_, const KCalendarCore::CalFilter *filter)
262 {
263     Q_ASSERT(filter);
264     Akonadi::Item::List items(items_);
265     items.erase(std::remove_if(items.begin(),
266                                items.end(),
267                                [filter](const Akonadi::Item &item) {
268                                    return !itemMatches(item, filter);
269                                }),
270                 items.end());
271     return items;
272 }
273 
isValidIncidenceItemUrl(const QUrl & url,const QStringList & supportedMimeTypes)274 bool CalendarSupport::isValidIncidenceItemUrl(const QUrl &url, const QStringList &supportedMimeTypes)
275 {
276     if (!url.isValid()) {
277         return false;
278     }
279 
280     if (url.scheme() != QLatin1String("akonadi")) {
281         return false;
282     }
283 
284     return supportedMimeTypes.contains(QUrlQuery(url).queryItemValue(QStringLiteral("type")));
285 }
286 
isValidIncidenceItemUrl(const QUrl & url)287 bool CalendarSupport::isValidIncidenceItemUrl(const QUrl &url)
288 {
289     return isValidIncidenceItemUrl(url,
290                                    QStringList() << KCalendarCore::Event::eventMimeType() << KCalendarCore::Todo::todoMimeType()
291                                                  << KCalendarCore::Journal::journalMimeType() << KCalendarCore::FreeBusy::freeBusyMimeType());
292 }
293 
containsValidIncidenceItemUrl(const QList<QUrl> & urls)294 static bool containsValidIncidenceItemUrl(const QList<QUrl> &urls)
295 {
296     return std::find_if(urls.begin(),
297                         urls.end(),
298                         [](const QUrl &url) {
299                             return CalendarSupport::isValidIncidenceItemUrl(url);
300                         })
301         != urls.constEnd();
302 }
303 
canDecode(const QMimeData * md)304 bool CalendarSupport::canDecode(const QMimeData *md)
305 {
306     if (md) {
307         return containsValidIncidenceItemUrl(md->urls()) || KCalUtils::ICalDrag::canDecode(md) || KCalUtils::VCalDrag::canDecode(md);
308     } else {
309         return false;
310     }
311 }
312 
incidenceItemUrls(const QMimeData * mimeData)313 QList<QUrl> CalendarSupport::incidenceItemUrls(const QMimeData *mimeData)
314 {
315     QList<QUrl> urls;
316     const QList<QUrl> urlsList = mimeData->urls();
317     for (const QUrl &i : urlsList) {
318         if (isValidIncidenceItemUrl(i)) {
319             urls.push_back(i);
320         }
321     }
322     return urls;
323 }
324 
todoItemUrls(const QMimeData * mimeData)325 QList<QUrl> CalendarSupport::todoItemUrls(const QMimeData *mimeData)
326 {
327     QList<QUrl> urls;
328 
329     const QList<QUrl> urlList = mimeData->urls();
330     for (const QUrl &i : urlList) {
331         if (isValidIncidenceItemUrl(i, QStringList() << KCalendarCore::Todo::todoMimeType())) {
332             urls.push_back(i);
333         }
334     }
335     return urls;
336 }
337 
mimeDataHasIncidence(const QMimeData * mimeData)338 bool CalendarSupport::mimeDataHasIncidence(const QMimeData *mimeData)
339 {
340     return !incidenceItemUrls(mimeData).isEmpty() || !incidences(mimeData).isEmpty();
341 }
342 
todos(const QMimeData * mimeData)343 KCalendarCore::Todo::List CalendarSupport::todos(const QMimeData *mimeData)
344 {
345     KCalendarCore::Todo::List todos;
346 
347 #ifndef QT_NO_DRAGANDDROP
348     KCalendarCore::Calendar::Ptr cal(KCalUtils::DndFactory::createDropCalendar(mimeData));
349     if (cal) {
350         const KCalendarCore::Todo::List calTodos = cal->todos();
351         todos.reserve(calTodos.count());
352         for (const KCalendarCore::Todo::Ptr &i : calTodos) {
353             todos.push_back(KCalendarCore::Todo::Ptr(i->clone()));
354         }
355     }
356 #endif
357 
358     return todos;
359 }
360 
incidences(const QMimeData * mimeData)361 KCalendarCore::Incidence::List CalendarSupport::incidences(const QMimeData *mimeData)
362 {
363     KCalendarCore::Incidence::List incidences;
364 
365 #ifndef QT_NO_DRAGANDDROP
366     KCalendarCore::Calendar::Ptr cal(KCalUtils::DndFactory::createDropCalendar(mimeData));
367     if (cal) {
368         const KCalendarCore::Incidence::List calIncidences = cal->incidences();
369         incidences.reserve(calIncidences.count());
370         for (const KCalendarCore::Incidence::Ptr &i : calIncidences) {
371             incidences.push_back(KCalendarCore::Incidence::Ptr(i->clone()));
372         }
373     }
374 #endif
375 
376     return incidences;
377 }
378 
selectCollection(QWidget * parent,int & dialogCode,const QStringList & mimeTypes,const Akonadi::Collection & defCollection)379 Akonadi::Collection CalendarSupport::selectCollection(QWidget *parent, int &dialogCode, const QStringList &mimeTypes, const Akonadi::Collection &defCollection)
380 {
381     QPointer<Akonadi::CollectionDialog> dlg(new Akonadi::CollectionDialog(parent));
382     dlg->setWindowTitle(i18nc("@title:window", "Select Calendar"));
383     dlg->setDescription(i18n("Select the calendar where this item will be stored."));
384     dlg->changeCollectionDialogOptions(Akonadi::CollectionDialog::KeepTreeExpanded);
385     qCDebug(CALENDARSUPPORT_LOG) << "selecting collections with mimeType in " << mimeTypes;
386 
387     dlg->setMimeTypeFilter(mimeTypes);
388     dlg->setAccessRightsFilter(Akonadi::Collection::CanCreateItem);
389     if (defCollection.isValid()) {
390         dlg->setDefaultCollection(defCollection);
391     }
392     Akonadi::Collection collection;
393 
394     // FIXME: don't use exec.
395     dialogCode = dlg->exec();
396     if (dlg && dialogCode == QDialog::Accepted) {
397         collection = dlg->selectedCollection();
398 
399         if (!collection.isValid()) {
400             qCWarning(CALENDARSUPPORT_LOG) << "An invalid collection was selected!";
401         }
402     }
403     delete dlg;
404     return collection;
405 }
406 
itemFromIndex(const QModelIndex & idx)407 Akonadi::Item CalendarSupport::itemFromIndex(const QModelIndex &idx)
408 {
409     auto item = idx.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
410     item.setParentCollection(idx.data(Akonadi::EntityTreeModel::ParentCollectionRole).value<Akonadi::Collection>());
411     return item;
412 }
413 
collectionsFromModel(const QAbstractItemModel * model,const QModelIndex & parentIndex,int start,int end)414 Akonadi::Collection::List CalendarSupport::collectionsFromModel(const QAbstractItemModel *model, const QModelIndex &parentIndex, int start, int end)
415 {
416     const int endRow = end >= 0 ? end : model->rowCount(parentIndex) - 1;
417     Akonadi::Collection::List collections;
418     int row = start;
419     QModelIndex i = model->index(row, 0, parentIndex);
420     while (row <= endRow) {
421         const Akonadi::Collection collection = collectionFromIndex(i);
422         if (collection.isValid()) {
423             collections << collection;
424             QModelIndex childIndex = model->index(0, 0, i);
425             if (childIndex.isValid()) {
426                 collections << collectionsFromModel(model, i);
427             }
428         }
429         ++row;
430         i = i.sibling(row, 0);
431     }
432     return collections;
433 }
434 
itemsFromModel(const QAbstractItemModel * model,const QModelIndex & parentIndex,int start,int end)435 Akonadi::Item::List CalendarSupport::itemsFromModel(const QAbstractItemModel *model, const QModelIndex &parentIndex, int start, int end)
436 {
437     const int endRow = end >= 0 ? end : model->rowCount(parentIndex) - 1;
438     Akonadi::Item::List items;
439     int row = start;
440     QModelIndex i = model->index(row, 0, parentIndex);
441     while (row <= endRow) {
442         const Akonadi::Item item = itemFromIndex(i);
443         if (CalendarSupport::hasIncidence(item)) {
444             items << item;
445         } else {
446             QModelIndex childIndex = model->index(0, 0, i);
447             if (childIndex.isValid()) {
448                 items << itemsFromModel(model, i);
449             }
450         }
451         ++row;
452         i = i.sibling(row, 0);
453     }
454     return items;
455 }
456 
collectionFromIndex(const QModelIndex & index)457 Akonadi::Collection CalendarSupport::collectionFromIndex(const QModelIndex &index)
458 {
459     return index.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
460 }
461 
collectionIdFromIndex(const QModelIndex & index)462 Akonadi::Collection::Id CalendarSupport::collectionIdFromIndex(const QModelIndex &index)
463 {
464     return index.data(Akonadi::EntityTreeModel::CollectionIdRole).value<Akonadi::Collection::Id>();
465 }
466 
collectionsFromIndexes(const QModelIndexList & indexes)467 Akonadi::Collection::List CalendarSupport::collectionsFromIndexes(const QModelIndexList &indexes)
468 {
469     Akonadi::Collection::List l;
470     l.reserve(indexes.count());
471     for (const QModelIndex &idx : indexes) {
472         l.push_back(collectionFromIndex(idx));
473     }
474     return l;
475 }
476 
displayName(Akonadi::ETMCalendar * calendar,const Akonadi::Collection & c)477 QString CalendarSupport::displayName(Akonadi::ETMCalendar *calendar, const Akonadi::Collection &c)
478 {
479     Akonadi::Collection fullCollection;
480     if (calendar && calendar->collection(c.id()).isValid()) {
481         fullCollection = calendar->collection(c.id());
482     } else {
483         fullCollection = c;
484     }
485 
486     QString cName = fullCollection.name();
487     const QString resourceName = fullCollection.resource();
488 
489     // Kolab Groupware
490     if (resourceName.contains(QLatin1String("kolab"))) {
491         QString typeStr = cName; // contents type: "Calendar", "Tasks", etc
492         QString ownerStr; // folder owner: "fred", "ethel", etc
493         QString nameStr; // folder name: "Public", "Test", etc
494         if (calendar) {
495             Akonadi::Collection p = c.parentCollection();
496             while (p != Akonadi::Collection::root()) {
497                 Akonadi::Collection tCol = calendar->collection(p.id());
498                 const QString tName = tCol.name();
499                 if (tName.startsWith(QLatin1String("shared.cal"), Qt::CaseInsensitive)) {
500                     ownerStr = QStringLiteral("Shared");
501                     nameStr = cName;
502                     typeStr = i18n("Calendar");
503                     break;
504                 } else if (tName.startsWith(QLatin1String("shared.tasks"), Qt::CaseInsensitive)
505                            || tName.startsWith(QLatin1String("shared.todo"), Qt::CaseInsensitive)) {
506                     ownerStr = QStringLiteral("Shared");
507                     nameStr = cName;
508                     typeStr = i18n("Tasks");
509                     break;
510                 } else if (tName.startsWith(QLatin1String("shared.journal"), Qt::CaseInsensitive)) {
511                     ownerStr = QStringLiteral("Shared");
512                     nameStr = cName;
513                     typeStr = i18n("Journal");
514                     break;
515                 } else if (tName.startsWith(QLatin1String("shared.notes"), Qt::CaseInsensitive)) {
516                     ownerStr = QStringLiteral("Shared");
517                     nameStr = cName;
518                     typeStr = i18n("Notes");
519                     break;
520                 } else if (tName != i18n("Calendar") && tName != i18n("Tasks") && tName != i18n("Journal") && tName != i18n("Notes")) {
521                     ownerStr = tName;
522                     break;
523                 } else {
524                     nameStr = typeStr;
525                     typeStr = tName;
526                 }
527                 p = p.parentCollection();
528             }
529         }
530 
531         if (!ownerStr.isEmpty()) {
532             if (!ownerStr.compare(QLatin1String("INBOX"), Qt::CaseInsensitive)) {
533                 return i18nc("%1 is folder contents", "My Kolab %1", typeStr);
534             } else if (!ownerStr.compare(QLatin1String("SHARED"), Qt::CaseInsensitive) || !ownerStr.compare(QLatin1String("CALENDAR"), Qt::CaseInsensitive)
535                        || !ownerStr.compare(QLatin1String("RESOURCES"), Qt::CaseInsensitive)) {
536                 return i18nc("%1 is folder name, %2 is folder contents", "Shared Kolab %1 %2", nameStr, typeStr);
537             } else {
538                 if (nameStr.isEmpty()) {
539                     return i18nc("%1 is folder owner name, %2 is folder contents", "%1's Kolab %2", ownerStr, typeStr);
540                 } else {
541                     return i18nc("%1 is folder owner name, %2 is folder name, %3 is folder contents", "%1's %2 Kolab %3", ownerStr, nameStr, typeStr);
542                 }
543             }
544         } else {
545             return i18nc("%1 is folder contents", "Kolab %1", typeStr);
546         }
547     } // end kolab section
548 
549     // Dav Groupware
550     if (resourceName.contains(QLatin1String("davgroupware"))) {
551         const QString resourceDisplayName = Akonadi::AgentManager::self()->instance(resourceName).name();
552         return i18nc("%1 is the folder name", "%1 in %2", fullCollection.displayName(), resourceDisplayName);
553     } // end caldav section
554 
555     // Google
556     if (resourceName.contains(QLatin1String("google"))) {
557         QString ownerStr; // folder owner: "user@gmail.com"
558         if (calendar) {
559             Akonadi::Collection p = c.parentCollection();
560             ownerStr = calendar->collection(p.id()).displayName();
561         }
562 
563         const QString nameStr = c.displayName(); // folder name: can be anything
564 
565         QString typeStr;
566         const QString mimeStr = c.contentMimeTypes().join(QLatin1Char(','));
567         if (mimeStr.contains(QLatin1String(".event"))) {
568             typeStr = i18n("Calendar");
569         } else if (mimeStr.contains(QLatin1String(".todo"))) {
570             typeStr = i18n("Tasks");
571         } else if (mimeStr.contains(QLatin1String(".journal"))) {
572             typeStr = i18n("Journal");
573         } else if (mimeStr.contains(QLatin1String(".note"))) {
574             typeStr = i18n("Notes");
575         } else {
576             typeStr = mimeStr;
577         }
578 
579         if (!ownerStr.isEmpty()) {
580             const int atChar = ownerStr.lastIndexOf(QLatin1Char('@'));
581             if (atChar >= 0) {
582                 ownerStr.truncate(atChar);
583             }
584             if (nameStr.isEmpty()) {
585                 return i18nc("%1 is folder owner name, %2 is folder contents", "%1's Google %2", ownerStr, typeStr);
586             } else {
587                 return i18nc("%1 is folder owner name, %2 is folder name", "%1's %2", ownerStr, nameStr);
588             }
589         } else {
590             return i18nc("%1 is folder contents", "Google %1", typeStr);
591         }
592     } // end google section
593 
594     // Not groupware so the collection is "mine"
595     const QString dName = fullCollection.displayName();
596 
597     if (!dName.isEmpty()) {
598         return fullCollection.name().startsWith(QLatin1String("akonadi_")) ? i18n("My %1", dName) : dName;
599     } else if (!fullCollection.name().isEmpty()) {
600         return fullCollection.name();
601     } else {
602         return i18nc("unknown resource", "Unknown");
603     }
604 }
605 
toolTipString(const Akonadi::Collection & coll,bool richText)606 QString CalendarSupport::toolTipString(const Akonadi::Collection &coll, bool richText)
607 {
608     Q_UNUSED(richText)
609 
610     QString str = QStringLiteral("<qt>");
611 
612     // Display Name
613     QString displayName;
614     if (coll.hasAttribute<Akonadi::EntityDisplayAttribute>()) {
615         displayName = coll.attribute<Akonadi::EntityDisplayAttribute>()->displayName();
616     }
617 
618     if (displayName.isEmpty()) {
619         displayName = coll.name();
620     }
621     if (coll.id() == CalendarSupport::KCalPrefs::instance()->defaultCalendarId()) {
622         displayName = i18nc("this is the default calendar", "%1 (Default Calendar)", displayName);
623     }
624     str += QLatin1String("<b>") + displayName + QLatin1String("</b>");
625     str += QLatin1String("<hr>");
626 
627     // Calendar Type
628     QString calendarType;
629     if (!coll.isVirtual()) {
630         const Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(coll.resource());
631         calendarType = instance.type().name();
632     } else {
633         calendarType = i18nc("a virtual folder type", "Virtual");
634     }
635     str += QLatin1String("<i>") + i18n("Folder type:") + QLatin1String("</i>");
636     str += QLatin1String("&nbsp;") + calendarType;
637 
638     // Content Type
639     QStringList mimeTypes = coll.contentMimeTypes();
640     mimeTypes.removeAll(QStringLiteral("inode/directory"));
641     QString mimeTypeStr;
642     if (!mimeTypes.isEmpty()) {
643         mimeTypeStr = QLocale().createSeparatedList(mimeTypes.replaceInStrings(QStringLiteral("application/x-vnd.akonadi.calendar."), QString()));
644     } else {
645         mimeTypeStr = i18nc("collection has no mimetypes to show the user", "none");
646     }
647     str += QLatin1String("<br>");
648     str += QLatin1String("<i>") + i18n("Content type:") + QLatin1String("</i>");
649     str += QLatin1String("&nbsp;") + mimeTypeStr;
650     str += QLatin1String("</br>");
651 
652     // Read only?
653     bool isReadOnly = !(coll.rights() & Akonadi::Collection::CanChangeItem);
654     str += QLatin1String("<br>");
655     str += QLatin1String("<i>") + i18n("Rights:") + QLatin1String("</i>");
656     str += QLatin1String("&nbsp;");
657     if (isReadOnly) {
658         str += i18nc("the calendar is read-only", "read-only");
659     } else {
660         str += i18nc("the calendar is read and write", "read+write");
661     }
662     str += QLatin1String("</br>");
663 
664     // Blocking reminders?
665     QStringList blockList;
666     if (coll.hasAttribute<Akonadi::BlockAlarmsAttribute>()) {
667         if (coll.attribute<Akonadi::BlockAlarmsAttribute>()->isEverythingBlocked()) {
668             blockList << i18nc("blocking all reminders for this calendar", "all");
669         } else {
670             if (coll.attribute<Akonadi::BlockAlarmsAttribute>()->isAlarmTypeBlocked(KCalendarCore::Alarm::Audio)) {
671                 blockList << i18nc("blocking audio reminders for this calendar", "audio");
672             } else if (coll.attribute<Akonadi::BlockAlarmsAttribute>()->isAlarmTypeBlocked(KCalendarCore::Alarm::Display)) {
673                 blockList << i18nc("blocking display pop-up dialog reminders for this calendar", "display");
674             } else if (coll.attribute<Akonadi::BlockAlarmsAttribute>()->isAlarmTypeBlocked(KCalendarCore::Alarm::Email)) {
675                 blockList << i18nc("blocking email reminders for this calendar", "email");
676             } else if (coll.attribute<Akonadi::BlockAlarmsAttribute>()->isAlarmTypeBlocked(KCalendarCore::Alarm::Procedure)) {
677                 blockList << i18nc("blocking run a command reminders for this calendar", "procedure");
678             } else {
679                 blockList << i18nc("blocking unknown type reminders for this calendar", "other");
680             }
681         }
682     } else {
683         blockList << i18nc("not blocking any reminder types for this calendar", "none");
684     }
685     str += QLatin1String("<br>");
686     str += QLatin1String("<i>") + i18n("Blocked Reminders:") + QLatin1String("</i>");
687     str += QLatin1String("&nbsp;");
688     str += QLocale().createSeparatedList(blockList);
689     str += QLatin1String("</br>");
690 
691     str += QLatin1String("</qt>");
692     return str;
693 }
694 
subMimeTypeForIncidence(const KCalendarCore::Incidence::Ptr & incidence)695 QString CalendarSupport::subMimeTypeForIncidence(const KCalendarCore::Incidence::Ptr &incidence)
696 {
697     return incidence->mimeType();
698 }
699 
workDays(QDate startDate,QDate endDate)700 QList<QDate> CalendarSupport::workDays(QDate startDate, QDate endDate)
701 {
702     QList<QDate> result;
703 
704     const int mask(~(KCalPrefs::instance()->mWorkWeekMask));
705     const int numDays = startDate.daysTo(endDate) + 1;
706 
707     for (int i = 0; i < numDays; ++i) {
708         const QDate date = startDate.addDays(i);
709         if (!(mask & (1 << (date.dayOfWeek() - 1)))) {
710             result.append(date);
711         }
712     }
713 
714     if (KCalPrefs::instance()->mExcludeHolidays) {
715         const QStringList holidays = KCalPrefs::instance()->mHolidays;
716         for (const QString &regionStr : holidays) {
717             KHolidays::HolidayRegion region(regionStr);
718             if (region.isValid()) {
719                 for (auto const &h : region.holidays(startDate, endDate)) {
720                     if (h.dayType() == KHolidays::Holiday::NonWorkday) {
721                         for (int i = 0; i < h.duration(); i++) {
722                             result.removeOne(h.observedStartDate().addDays(i));
723                         }
724                     }
725                 }
726             }
727         }
728     }
729 
730     return result;
731 }
732 
holiday(QDate date)733 QStringList CalendarSupport::holiday(QDate date)
734 {
735     QStringList hdays;
736 
737     bool showCountryCode = (KCalPrefs::instance()->mHolidays.count() > 1);
738     const QStringList holidays = KCalPrefs::instance()->mHolidays;
739     for (const QString &regionStr : holidays) {
740         KHolidays::HolidayRegion region(regionStr);
741         if (region.isValid()) {
742             const KHolidays::Holiday::List list = region.holidays(date);
743             const int listCount = list.count();
744             for (int i = 0; i < listCount; ++i) {
745                 // don't add duplicates.
746                 // TODO: won't find duplicates in different languages however.
747                 const QString name = list.at(i).name();
748                 if (showCountryCode) {
749                     // If more than one holiday region, append the country code to the holiday
750                     // display name to help the user identify which region it belongs to.
751                     const QRegularExpression holidaySE(i18nc("search pattern for holidayname", "^%1", name));
752                     if (hdays.filter(holidaySE).isEmpty()) {
753                         const QString pholiday = i18n("%1 (%2)", name, region.countryCode());
754                         hdays.append(pholiday);
755                     } else {
756                         // More than 1 region has the same holiday => remove the country code
757                         // i.e don't show "Holiday (US)" and "Holiday(FR)"; just show "Holiday".
758                         const QRegularExpression holidayRE(i18nc("replace pattern for holidayname (countrycode)", "^%1 \\(.*\\)", name));
759                         hdays.replaceInStrings(holidayRE, name);
760                         hdays.removeDuplicates();
761                     }
762                 } else {
763                     if (!hdays.contains(name)) {
764                         hdays.append(name);
765                     }
766                 }
767             }
768         }
769     }
770 
771     return hdays;
772 }
773 
categories(const KCalendarCore::Incidence::List & incidences)774 QStringList CalendarSupport::categories(const KCalendarCore::Incidence::List &incidences)
775 {
776     QStringList cats;
777     QStringList thisCats;
778     // @TODO: For now just iterate over all incidences. In the future,
779     // the list of categories should be built when reading the file.
780     for (const KCalendarCore::Incidence::Ptr &incidence : incidences) {
781         thisCats = incidence->categories();
782         const QStringList::ConstIterator send(thisCats.constEnd());
783         for (QStringList::ConstIterator si = thisCats.constBegin(); si != send; ++si) {
784             if (!cats.contains(*si)) {
785                 cats.append(*si);
786             }
787         }
788     }
789     return cats;
790 }
791 
mergeCalendar(const QString & srcFilename,const KCalendarCore::Calendar::Ptr & destCalendar)792 bool CalendarSupport::mergeCalendar(const QString &srcFilename, const KCalendarCore::Calendar::Ptr &destCalendar)
793 {
794     if (srcFilename.isEmpty()) {
795         qCCritical(CALENDARSUPPORT_LOG) << "Empty filename.";
796         return false;
797     }
798 
799     if (!QFile::exists(srcFilename)) {
800         qCCritical(CALENDARSUPPORT_LOG) << "File'" << srcFilename << "' doesn't exist.";
801     }
802 
803     // merge in a file
804     destCalendar->startBatchAdding();
805     KCalendarCore::FileStorage storage(destCalendar);
806     storage.setFileName(srcFilename);
807     bool loadedSuccesfully = storage.load();
808     destCalendar->endBatchAdding();
809 
810     return loadedSuccesfully;
811 }
812 
createAlarmReminder(const KCalendarCore::Alarm::Ptr & alarm,KCalendarCore::IncidenceBase::IncidenceType type)813 void CalendarSupport::createAlarmReminder(const KCalendarCore::Alarm::Ptr &alarm, KCalendarCore::IncidenceBase::IncidenceType type)
814 {
815     int duration; // in secs
816     switch (CalendarSupport::KCalPrefs::instance()->mReminderTimeUnits) {
817     default:
818     case 0: // mins
819         duration = CalendarSupport::KCalPrefs::instance()->mReminderTime * 60;
820         break;
821     case 1: // hours
822         duration = CalendarSupport::KCalPrefs::instance()->mReminderTime * 60 * 60;
823         break;
824     case 2: // days
825         duration = CalendarSupport::KCalPrefs::instance()->mReminderTime * 60 * 60 * 24;
826         break;
827     }
828     alarm->setType(KCalendarCore::Alarm::Display);
829     alarm->setEnabled(true);
830     if (type == KCalendarCore::Incidence::TypeEvent) {
831         alarm->setStartOffset(KCalendarCore::Duration(-duration));
832     } else {
833         alarm->setEndOffset(KCalendarCore::Duration(-duration));
834     }
835 }
836