KCalUtils

incidenceformatter.cpp
Go to the documentation of this file.
1/*
2 This file is part of the kcalutils library.
3
4 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
5 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
6 SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
7 SPDX-FileCopyrightText: 2009-2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
8
9 SPDX-License-Identifier: LGPL-2.0-or-later
10*/
11/**
12 @file
13 This file is part of the API for handling calendar data and provides
14 static functions for formatting Incidences for various purposes.
15
16 @brief
17 Provides methods to format Incidences in various ways for display purposes.
18
19 @author Cornelius Schumacher <schumacher@kde.org>
20 @author Reinhold Kainhofer <reinhold@kainhofer.com>
21 @author Allen Winter <allen@kdab.com>
22*/
23#include "incidenceformatter.h"
24#include "grantleetemplatemanager_p.h"
25#include "stringify.h"
26
27#include <KCalendarCore/Event>
28#include <KCalendarCore/FreeBusy>
29#include <KCalendarCore/ICalFormat>
30#include <KCalendarCore/Journal>
31#include <KCalendarCore/Todo>
32#include <KCalendarCore/Visitor>
33using namespace KCalendarCore;
34
35#include <KIdentityManagementCore/Utils>
36
37#include <KEmailAddress>
38#include <ktexttohtml.h>
39
40#include "kcalutils_debug.h"
41#include <KIconLoader>
42#include <KLocalizedString>
43
44#include <QApplication>
45#include <QBitArray>
46#include <QLocale>
47#include <QMimeDatabase>
48#include <QPalette>
49#include <QRegularExpression>
50
51using namespace KCalUtils;
52using namespace IncidenceFormatter;
53
54/*******************
55 * General helpers
56 *******************/
57
58static QVariantHash inviteButton(const QString &id, const QString &text, const QString &iconName, InvitationFormatterHelper *helper);
59
60//@cond PRIVATE
61[[nodiscard]] static QString string2HTML(const QString &str)
62{
63 // use convertToHtml so we get clickable links and other goodies
65}
66
67[[nodiscard]] static bool thatIsMe(const QString &email)
68{
69 return KIdentityManagementCore::thatIsMe(email);
70}
71
72[[nodiscard]] static bool iamAttendee(const Attendee &attendee)
73{
74 // Check if this attendee is the user
75 return thatIsMe(attendee.email());
76}
77
78static QString htmlAddTag(const QString &tag, const QString &text)
79{
80 int numLineBreaks = text.count(QLatin1Char('\n'));
81 const QString str = QLatin1Char('<') + tag + QLatin1Char('>');
82 QString tmpText = text;
83 QString tmpStr = str;
84 if (numLineBreaks >= 0) {
85 if (numLineBreaks > 0) {
86 QString tmp;
87 for (int i = 0; i <= numLineBreaks; ++i) {
88 int pos = tmpText.indexOf(QLatin1Char('\n'));
89 tmp = tmpText.left(pos);
90 tmpText = tmpText.right(tmpText.length() - pos - 1);
91 tmpStr += tmp + QLatin1StringView("<br>");
92 }
93 } else {
94 tmpStr += tmpText;
95 }
96 }
97 tmpStr += QLatin1StringView("</") + tag + QLatin1Char('>');
98 return tmpStr;
99}
100
101[[nodiscard]] static QPair<QString, QString> searchNameAndUid(const QString &email, const QString &name, const QString &uid)
102{
103 // Yes, this is a silly method now, but it's predecessor was quite useful in e35.
104 // For now, please keep this sillyness until e35 is frozen to ease forward porting.
105 // -Allen
106 QPair<QString, QString> s;
107 s.first = name;
108 s.second = uid;
109 if (!email.isEmpty() && (name.isEmpty() || uid.isEmpty())) {
110 s.second.clear();
111 }
112 return s;
113}
114
115[[nodiscard]] static QString searchName(const QString &email, const QString &name)
116{
117 const QString printName = name.isEmpty() ? email : name;
118 return printName;
119}
120
121[[nodiscard]] static bool iamOrganizer(const Incidence::Ptr &incidence)
122{
123 // Check if the user is the organizer for this incidence
124
125 if (!incidence) {
126 return false;
127 }
128
129 return thatIsMe(incidence->organizer().email());
130}
131
132[[nodiscard]] static bool senderIsOrganizer(const Incidence::Ptr &incidence, const QString &sender)
133{
134 // Check if the specified sender is the organizer
135
136 if (!incidence || sender.isEmpty()) {
137 return true;
138 }
139
140 bool isorg = true;
141 QString senderName;
142 QString senderEmail;
143 if (KEmailAddress::extractEmailAddressAndName(sender, senderEmail, senderName)) {
144 // for this heuristic, we say the sender is the organizer if either the name or the email match.
145 if (incidence->organizer().email() != senderEmail && incidence->organizer().name() != senderName) {
146 isorg = false;
147 }
148 }
149 return isorg;
150}
151
152[[nodiscard]] static bool attendeeIsOrganizer(const Incidence::Ptr &incidence, const Attendee &attendee)
153{
154 if (incidence && !attendee.isNull() && (incidence->organizer().email() == attendee.email())) {
155 return true;
156 } else {
157 return false;
158 }
159}
160
161[[nodiscard]] static QString organizerName(const Incidence::Ptr &incidence, const QString &defName)
162{
163 QString tName;
164 if (!defName.isEmpty()) {
165 tName = defName;
166 } else {
167 tName = i18n("Organizer Unknown");
168 }
169
171 if (incidence) {
172 name = incidence->organizer().name();
173 if (name.isEmpty()) {
174 name = incidence->organizer().email();
175 }
176 }
177 if (name.isEmpty()) {
178 name = tName;
179 }
180 return name;
181}
182
183[[nodiscard]] static QString firstAttendeeName(const Incidence::Ptr &incidence, const QString &defName)
184{
185 QString tName;
186 if (!defName.isEmpty()) {
187 tName = defName;
188 } else {
189 tName = i18n("Sender");
190 }
191
193 if (incidence) {
194 const Attendee::List attendees = incidence->attendees();
195 if (!attendees.isEmpty()) {
196 const Attendee attendee = attendees.at(0);
197 name = attendee.name();
198 if (name.isEmpty()) {
199 name = attendee.email();
200 }
201 }
202 }
203 if (name.isEmpty()) {
204 name = tName;
205 }
206 return name;
207}
208
209[[nodiscard]] static QString rsvpStatusIconName(Attendee::PartStat status)
210{
211 switch (status) {
213 return QStringLiteral("dialog-ok-apply");
215 return QStringLiteral("dialog-cancel");
217 return QStringLiteral("help-about");
219 return QStringLiteral("help-about");
221 return QStringLiteral("dialog-ok");
223 return QStringLiteral("mail-forward");
225 return QStringLiteral("mail-mark-read");
226 default:
227 return QString();
228 }
229}
230
231//@endcond
232
233/*******************************************************************
234 * Helper functions for the extensive display (display viewer)
235 *******************************************************************/
236
237//@cond PRIVATE
238[[nodiscard]] static QVariantHash displayViewFormatPerson(const QString &email, const QString &name, const QString &uid, const QString &iconName)
239{
240 // Search for new print name or uid, if needed.
241 QPair<QString, QString> s = searchNameAndUid(email, name, uid);
242 const QString printName = s.first;
243 const QString printUid = s.second;
244
245 QVariantHash personData;
246 personData[QStringLiteral("icon")] = iconName;
247 personData[QStringLiteral("uid")] = printUid;
248 personData[QStringLiteral("name")] = printName;
249 personData[QStringLiteral("email")] = email;
250
251 // Make the mailto link
252 if (!email.isEmpty()) {
253 Person person(name, email);
254 QString path = person.fullName().simplified();
255 if (path.isEmpty() || path.startsWith(QLatin1Char('"'))) {
256 path = email;
257 }
258 QUrl mailto;
259 mailto.setScheme(QStringLiteral("mailto"));
260 mailto.setPath(path);
261
262 personData[QStringLiteral("mailto")] = mailto.url();
263 }
264
265 return personData;
266}
267
268[[nodiscard]] static QVariantHash displayViewFormatPerson(const QString &email, const QString &name, const QString &uid, Attendee::PartStat status)
269{
270 return displayViewFormatPerson(email, name, uid, rsvpStatusIconName(status));
271}
272
273[[nodiscard]] static bool incOrganizerOwnsCalendar(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence)
274{
275 // PORTME! Look at e35's CalHelper::incOrganizerOwnsCalendar
276
277 // For now, use iamOrganizer() which is only part of the check
278 Q_UNUSED(calendar)
279 return iamOrganizer(incidence);
280}
281
282[[nodiscard]] static QString displayViewFormatDescription(const Incidence::Ptr &incidence)
283{
284 if (!incidence->description().isEmpty()) {
285 if (!incidence->descriptionIsRich() && !incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) {
286 return string2HTML(incidence->description());
287 } else if (!incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) {
288 return incidence->richDescription();
289 } else {
290 return incidence->description();
291 }
292 }
293
294 return QString();
295}
296
297[[nodiscard]] static QVariantList displayViewFormatAttendeeRoleList(const Incidence::Ptr &incidence, Attendee::Role role, bool showStatus)
298{
299 QVariantList attendeeDataList;
300 attendeeDataList.reserve(incidence->attendeeCount());
301
302 const Attendee::List attendees = incidence->attendees();
303 for (const auto &a : attendees) {
304 if (a.role() != role) {
305 // skip this role
306 continue;
307 }
308 if (attendeeIsOrganizer(incidence, a)) {
309 // skip attendee that is also the organizer
310 continue;
311 }
312 QVariantHash attendeeData = displayViewFormatPerson(a.email(), a.name(), a.uid(), showStatus ? a.status() : Attendee::None);
313 if (!a.delegator().isEmpty()) {
314 attendeeData[QStringLiteral("delegator")] = a.delegator();
315 }
316 if (!a.delegate().isEmpty()) {
317 attendeeData[QStringLiteral("delegate")] = a.delegate();
318 }
319 if (showStatus) {
320 attendeeData[QStringLiteral("status")] = Stringify::attendeeStatus(a.status());
321 }
322
323 attendeeDataList << attendeeData;
324 }
325
326 return attendeeDataList;
327}
328
329[[nodiscard]] static QVariantHash displayViewFormatOrganizer(const Incidence::Ptr &incidence)
330{
331 // Add organizer link
332 int attendeeCount = incidence->attendees().count();
333 if (attendeeCount > 1 || (attendeeCount == 1 && !attendeeIsOrganizer(incidence, incidence->attendees().at(0)))) {
334 QPair<QString, QString> s = searchNameAndUid(incidence->organizer().email(), incidence->organizer().name(), QString());
335 return displayViewFormatPerson(incidence->organizer().email(), s.first, s.second, QStringLiteral("meeting-organizer"));
336 }
337
338 return QVariantHash();
339}
340
341[[nodiscard]] static QVariantList displayViewFormatAttachments(const Incidence::Ptr &incidence)
342{
343 const Attachment::List as = incidence->attachments();
344
345 QVariantList dataList;
346 dataList.reserve(as.count());
347
348 for (auto it = as.cbegin(), end = as.cend(); it != end; ++it) {
349 QVariantHash attData;
350 if ((*it).isUri()) {
352 if ((*it).uri().startsWith(QLatin1StringView("kmail:"))) {
353 name = i18n("Show mail");
354 } else {
355 if ((*it).label().isEmpty()) {
356 name = (*it).uri();
357 } else {
358 name = (*it).label();
359 }
360 }
361 attData[QStringLiteral("uri")] = (*it).uri();
362 attData[QStringLiteral("label")] = name;
363 } else {
364 attData[QStringLiteral("uri")] = QStringLiteral("ATTACH:%1").arg(QString::fromUtf8((*it).label().toUtf8().toBase64()));
365 attData[QStringLiteral("label")] = (*it).label();
366 }
367 dataList << attData;
368 }
369 return dataList;
370}
371
372[[nodiscard]] static QVariantHash displayViewFormatBirthday(const Event::Ptr &event)
373{
374 if (!event) {
375 return QVariantHash();
376 }
377
378 // It's callees duty to ensure this
379 Q_ASSERT(event->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES") || event->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES"));
380
381 const QString uid_1 = event->customProperty("KABC", "UID-1");
382 const QString name_1 = event->customProperty("KABC", "NAME-1");
383 const QString email_1 = event->customProperty("KABC", "EMAIL-1");
385 return displayViewFormatPerson(p.email(), name_1, uid_1, QString());
386}
387
388[[nodiscard]] static QVariantHash incidenceTemplateHeader(const Incidence::Ptr &incidence)
389{
390 QVariantHash incidenceData;
391 if (incidence->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")) {
392 incidenceData[QStringLiteral("icon")] = QStringLiteral("view-calendar-birthday");
393 } else if (incidence->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) {
394 incidenceData[QStringLiteral("icon")] = QStringLiteral("view-calendar-wedding-anniversary");
395 } else {
396 incidenceData[QStringLiteral("icon")] = incidence->iconName();
397 }
398
399 switch (incidence->type()) {
400 case IncidenceBase::IncidenceType::TypeEvent:
401 incidenceData[QStringLiteral("alarmIcon")] = QStringLiteral("appointment-reminder");
402 incidenceData[QStringLiteral("recursIcon")] = QStringLiteral("appointment-recurring");
403 break;
404 case IncidenceBase::IncidenceType::TypeTodo:
405 incidenceData[QStringLiteral("alarmIcon")] = QStringLiteral("task-reminder");
406 incidenceData[QStringLiteral("recursIcon")] = QStringLiteral("task-recurring");
407 break;
408 default:
409 // Others don't repeat and don't have reminders.
410 break;
411 }
412
413 incidenceData[QStringLiteral("hasEnabledAlarms")] = incidence->hasEnabledAlarms();
414 incidenceData[QStringLiteral("recurs")] = incidence->recurs();
415 incidenceData[QStringLiteral("isReadOnly")] = incidence->isReadOnly();
416 incidenceData[QStringLiteral("summary")] = incidence->summary();
417 incidenceData[QStringLiteral("allDay")] = incidence->allDay();
418
419 return incidenceData;
420}
421
422[[nodiscard]] static QString displayViewFormatEvent(const Calendar::Ptr &calendar, const QString &sourceName, const Event::Ptr &event, QDate date)
423{
424 if (!event) {
425 return QString();
426 }
427
428 QVariantHash incidence = incidenceTemplateHeader(event);
429
430 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, event) : sourceName;
431 const QString richLocation = event->richLocation();
432 if (richLocation.startsWith(QLatin1StringView("http:/")) || richLocation.startsWith(QLatin1StringView("https:/"))) {
433 incidence[QStringLiteral("location")] = QStringLiteral("<a href=\"%1\">%1</a>").arg(richLocation);
434 } else {
435 incidence[QStringLiteral("location")] = richLocation;
436 }
437
438 const auto startDts = event->startDateTimesForDate(date, QTimeZone::systemTimeZone());
439 const auto startDt = startDts.empty() ? event->dtStart().toLocalTime() : startDts[0].toLocalTime();
440 const auto endDt = event->endDateForStart(startDt).toLocalTime();
441
442 incidence[QStringLiteral("isAllDay")] = event->allDay();
443 incidence[QStringLiteral("isMultiDay")] = event->isMultiDay();
444 incidence[QStringLiteral("startDate")] = startDt.date();
445 incidence[QStringLiteral("endDate")] = endDt.date();
446 incidence[QStringLiteral("startTime")] = startDt.time();
447 incidence[QStringLiteral("endTime")] = endDt.time();
448 incidence[QStringLiteral("duration")] = durationString(event);
449 incidence[QStringLiteral("isException")] = event->hasRecurrenceId();
450 incidence[QStringLiteral("recurrence")] = recurrenceString(event);
451
452 if (event->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")) {
453 incidence[QStringLiteral("birthday")] = displayViewFormatBirthday(event);
454 }
455
456 if (event->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) {
457 incidence[QStringLiteral("anniversary")] = displayViewFormatBirthday(event);
458 }
459
460 incidence[QStringLiteral("description")] = displayViewFormatDescription(event);
461 // TODO: print comments?
462
463 incidence[QStringLiteral("reminders")] = reminderStringList(event);
464
465 incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(event);
466 const bool showStatus = incOrganizerOwnsCalendar(calendar, event);
467 incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(event, Attendee::Chair, showStatus);
468 incidence[QStringLiteral("requiredParticipants")] = displayViewFormatAttendeeRoleList(event, Attendee::ReqParticipant, showStatus);
469 incidence[QStringLiteral("optionalParticipants")] = displayViewFormatAttendeeRoleList(event, Attendee::OptParticipant, showStatus);
470 incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(event, Attendee::NonParticipant, showStatus);
471
472 incidence[QStringLiteral("categories")] = event->categories();
473
474 incidence[QStringLiteral("attachments")] = displayViewFormatAttachments(event);
475 incidence[QStringLiteral("creationDate")] = event->created().toLocalTime();
476
477 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/org.kde.pim/kcalutils/event.html"), incidence);
478}
479
480[[nodiscard]] static QString displayViewFormatTodo(const Calendar::Ptr &calendar, const QString &sourceName, const Todo::Ptr &todo, QDate ocurrenceDueDate)
481{
482 if (!todo) {
483 qCDebug(KCALUTILS_LOG) << "IncidenceFormatter::displayViewFormatTodo was called without to-do, quitting";
484 return QString();
485 }
486
487 QVariantHash incidence = incidenceTemplateHeader(todo);
488
489 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, todo) : sourceName;
490 incidence[QStringLiteral("location")] = todo->richLocation();
491
492 const bool hastStartDate = todo->hasStartDate();
493 const bool hasDueDate = todo->hasDueDate();
494
495 if (hastStartDate) {
496 QDateTime startDt = todo->dtStart(true /**first*/).toLocalTime();
497 if (todo->recurs() && ocurrenceDueDate.isValid()) {
498 if (hasDueDate) {
499 // In kdepim all recurring to-dos have due date.
500 const qint64 length = startDt.daysTo(todo->dtDue(true /**first*/));
501 if (length >= 0) {
502 startDt.setDate(ocurrenceDueDate.addDays(-length));
503 } else {
504 qCritical() << "DTSTART is bigger than DTDUE, todo->uid() is " << todo->uid();
505 startDt.setDate(ocurrenceDueDate);
506 }
507 } else {
508 qCritical() << "To-do is recurring but has no DTDUE set, todo->uid() is " << todo->uid();
509 startDt.setDate(ocurrenceDueDate);
510 }
511 }
512 incidence[QStringLiteral("startDate")] = startDt;
513 }
514
515 if (hasDueDate) {
516 QDateTime dueDt = todo->dtDue().toLocalTime();
517 if (todo->recurs()) {
518 if (ocurrenceDueDate.isValid()) {
519 QDateTime kdt(ocurrenceDueDate, QTime(0, 0, 0), QTimeZone::LocalTime);
520 kdt = kdt.addSecs(-1);
521 dueDt.setDate(todo->recurrence()->getNextDateTime(kdt).date());
522 }
523 }
524 incidence[QStringLiteral("dueDate")] = dueDt;
525 }
526
527 incidence[QStringLiteral("duration")] = durationString(todo);
528 incidence[QStringLiteral("isException")] = todo->hasRecurrenceId();
529 if (todo->recurs()) {
530 incidence[QStringLiteral("recurrence")] = recurrenceString(todo);
531 }
532
533 incidence[QStringLiteral("description")] = displayViewFormatDescription(todo);
534
535 // TODO: print comments?
536
537 incidence[QStringLiteral("reminders")] = reminderStringList(todo);
538
539 incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(todo);
540 const bool showStatus = incOrganizerOwnsCalendar(calendar, todo);
541 incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(todo, Attendee::Chair, showStatus);
542 incidence[QStringLiteral("requiredParticipants")] = displayViewFormatAttendeeRoleList(todo, Attendee::ReqParticipant, showStatus);
543 incidence[QStringLiteral("optionalParticipants")] = displayViewFormatAttendeeRoleList(todo, Attendee::OptParticipant, showStatus);
544 incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(todo, Attendee::NonParticipant, showStatus);
545
546 incidence[QStringLiteral("categories")] = todo->categories();
547 incidence[QStringLiteral("priority")] = todo->priority();
548 if (todo->isCompleted()) {
549 incidence[QStringLiteral("completedDate")] = todo->completed();
550 } else {
551 incidence[QStringLiteral("percent")] = todo->percentComplete();
552 }
553 incidence[QStringLiteral("attachments")] = displayViewFormatAttachments(todo);
554 incidence[QStringLiteral("creationDate")] = todo->created().toLocalTime();
555
556 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/org.kde.pim/kcalutils/todo.html"), incidence);
557}
558
559[[nodiscard]] static QString displayViewFormatJournal(const Calendar::Ptr &calendar, const QString &sourceName, const Journal::Ptr &journal)
560{
561 if (!journal) {
562 return QString();
563 }
564
565 QVariantHash incidence = incidenceTemplateHeader(journal);
566 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, journal) : sourceName;
567 incidence[QStringLiteral("date")] = journal->dtStart().toLocalTime();
568 incidence[QStringLiteral("description")] = displayViewFormatDescription(journal);
569 incidence[QStringLiteral("categories")] = journal->categories();
570 incidence[QStringLiteral("creationDate")] = journal->created().toLocalTime();
571
572 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/org.kde.pim/kcalutils/journal.html"), incidence);
573}
574
575[[nodiscard]] static QString displayViewFormatFreeBusy(const Calendar::Ptr &calendar, const QString &sourceName, const FreeBusy::Ptr &fb)
576{
577 Q_UNUSED(calendar)
578 Q_UNUSED(sourceName)
579 if (!fb) {
580 return QString();
581 }
582
583 QVariantHash fbData;
584 fbData[QStringLiteral("organizer")] = fb->organizer().fullName();
585 fbData[QStringLiteral("start")] = fb->dtStart().toLocalTime().date();
586 fbData[QStringLiteral("end")] = fb->dtEnd().toLocalTime().date();
587
588 Period::List periods = fb->busyPeriods();
589 QVariantList periodsData;
590 periodsData.reserve(periods.size());
591 for (auto it = periods.cbegin(), end = periods.cend(); it != end; ++it) {
592 const Period per = *it;
593 QVariantHash periodData;
594 if (per.hasDuration()) {
595 int dur = per.duration().asSeconds();
597 if (dur >= 3600) {
598 cont += i18ncp("hours part of duration", "1 hour ", "%1 hours ", dur / 3600);
599 dur %= 3600;
600 }
601 if (dur >= 60) {
602 cont += i18ncp("minutes part duration", "1 minute ", "%1 minutes ", dur / 60);
603 dur %= 60;
604 }
605 if (dur > 0) {
606 cont += i18ncp("seconds part of duration", "1 second", "%1 seconds", dur);
607 }
608 periodData[QStringLiteral("dtStart")] = per.start().toLocalTime();
609 periodData[QStringLiteral("duration")] = cont;
610 } else {
611 const QDateTime pStart = per.start().toLocalTime();
612 const QDateTime pEnd = per.end().toLocalTime();
613 if (per.start().date() == per.end().date()) {
614 periodData[QStringLiteral("date")] = pStart.date();
615 periodData[QStringLiteral("start")] = pStart.time();
616 periodData[QStringLiteral("end")] = pEnd.time();
617 } else {
618 periodData[QStringLiteral("start")] = pStart;
619 periodData[QStringLiteral("end")] = pEnd;
620 }
621 }
622
623 periodsData << periodData;
624 }
625
626 fbData[QStringLiteral("periods")] = periodsData;
627
628 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/org.kde.pim/kcalutils/freebusy.html"), fbData);
629}
630
631//@endcond
632
633//@cond PRIVATE
634class KCalUtils::IncidenceFormatter::EventViewerVisitor : public Visitor
635{
636public:
637 EventViewerVisitor()
638 : mCalendar(nullptr)
639 {
640 }
641
642 ~EventViewerVisitor() override;
643
644 bool act(const Calendar::Ptr &calendar, const IncidenceBase::Ptr &incidence, QDate date)
645 {
646 mCalendar = calendar;
647 mSourceName.clear();
648 mDate = date;
649 mResult = QLatin1StringView("");
650 return incidence->accept(*this, incidence);
651 }
652
653 bool act(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date)
654 {
655 mSourceName = sourceName;
656 mDate = date;
657 mResult = QLatin1StringView("");
658 return incidence->accept(*this, incidence);
659 }
660
661 [[nodiscard]] QString result() const
662 {
663 return mResult;
664 }
665
666protected:
667 bool visit(const Event::Ptr &event) override
668 {
669 mResult = displayViewFormatEvent(mCalendar, mSourceName, event, mDate);
670 return !mResult.isEmpty();
671 }
672
673 bool visit(const Todo::Ptr &todo) override
674 {
675 mResult = displayViewFormatTodo(mCalendar, mSourceName, todo, mDate);
676 return !mResult.isEmpty();
677 }
678
679 bool visit(const Journal::Ptr &journal) override
680 {
681 mResult = displayViewFormatJournal(mCalendar, mSourceName, journal);
682 return !mResult.isEmpty();
683 }
684
685 bool visit(const FreeBusy::Ptr &fb) override
686 {
687 mResult = displayViewFormatFreeBusy(mCalendar, mSourceName, fb);
688 return !mResult.isEmpty();
689 }
690
691protected:
692 Calendar::Ptr mCalendar;
693 QString mSourceName;
694 QDate mDate;
695 QString mResult;
696};
697//@endcond
698
699EventViewerVisitor::~EventViewerVisitor()
700{
701}
702
704{
705 if (!incidence) {
706 return QString();
707 }
708
709 EventViewerVisitor v;
710 if (v.act(calendar, incidence, date)) {
711 return v.result();
712 } else {
713 return QString();
714 }
715}
716
717QString IncidenceFormatter::extensiveDisplayStr(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date)
718{
719 if (!incidence) {
720 return QString();
721 }
722
723 EventViewerVisitor v;
724 if (v.act(sourceName, incidence, date)) {
725 return v.result();
726 } else {
727 return QString();
728 }
729}
730
731/***********************************************************************
732 * Helper functions for the body part formatter of kmail (Invitations)
733 ***********************************************************************/
734
735//@cond PRIVATE
736static QString cleanHtml(const QString &html)
737{
738 static QRegularExpression rx = QRegularExpression(QStringLiteral("<body[^>]*>(.*)</body>"), QRegularExpression::CaseInsensitiveOption);
740 if (match.hasMatch()) {
741 QString body = match.captured(1);
742 return body.remove(QRegularExpression(QStringLiteral("<[^>]*>"))).trimmed().toHtmlEscaped();
743 }
744 return html;
745}
746
747static QString invitationSummary(const Incidence::Ptr &incidence, bool noHtmlMode)
748{
749 QString summaryStr = i18n("Summary unspecified");
750 if (!incidence->summary().isEmpty()) {
751 if (!incidence->summaryIsRich()) {
752 summaryStr = incidence->summary().toHtmlEscaped();
753 } else {
754 summaryStr = incidence->richSummary();
755 if (noHtmlMode) {
756 summaryStr = cleanHtml(summaryStr);
757 }
758 }
759 }
760 return summaryStr;
761}
762
763static QString invitationLocation(const Incidence::Ptr &incidence, bool noHtmlMode)
764{
765 QString locationStr = i18n("Location unspecified");
766 if (!incidence->location().isEmpty()) {
767 if (!incidence->locationIsRich()) {
768 locationStr = incidence->location().toHtmlEscaped();
769 } else {
770 locationStr = incidence->richLocation();
771 if (noHtmlMode) {
772 locationStr = cleanHtml(locationStr);
773 }
774 }
775 }
776 return locationStr;
777}
778
779[[nodiscard]] static QString diffColor()
780{
781 // Color for printing comparison differences inside invitations.
782
783 // return "#DE8519"; // hard-coded color from Outlook2007
784 return QColor(Qt::red).name(); // krazy:exclude=qenums TODO make configurable
785}
786
787[[nodiscard]] static QString noteColor()
788{
789 // Color for printing notes inside invitations.
790 return qApp->palette().color(QPalette::Active, QPalette::Highlight).name();
791}
792
793[[nodiscard]] static QString htmlCompare(const QString &value, const QString &oldvalue)
794{
795 // if 'value' is empty, then print nothing
796 if (value.isEmpty()) {
797 return QString();
798 }
799
800 // if 'value' is new or unchanged, then print normally
801 if (oldvalue.isEmpty() || value == oldvalue) {
802 return value;
803 }
804
805 // if 'value' has changed, then make a special print
806 return QStringLiteral("<font color=\"%1\">%2</font> (<strike>%3</strike>)").arg(diffColor(), value, oldvalue);
807}
808
809[[nodiscard]] static Attendee findDelegatedFromMyAttendee(const Incidence::Ptr &incidence)
810{
811 // Return the first attendee that was delegated-from the user
812
813 Attendee attendee;
814 if (!incidence) {
815 return attendee;
816 }
817
818 QString delegatorName;
819 QString delegatorEmail;
820 const Attendee::List attendees = incidence->attendees();
821 for (const auto &a : attendees) {
822 KEmailAddress::extractEmailAddressAndName(a.delegator(), delegatorEmail, delegatorName);
823 if (thatIsMe(delegatorEmail)) {
824 attendee = a;
825 break;
826 }
827 }
828
829 return attendee;
830}
831
832[[nodiscard]] static Attendee findMyAttendee(const Incidence::Ptr &incidence)
833{
834 // Return the attendee for the incidence that is probably the user
835
836 Attendee attendee;
837 if (!incidence) {
838 return attendee;
839 }
840
841 const Attendee::List attendees = incidence->attendees();
842 for (const auto &a : attendees) {
843 if (iamAttendee(a)) {
844 attendee = a;
845 break;
846 }
847 }
848
849 return attendee;
850}
851
852[[nodiscard]] static Attendee findAttendee(const Incidence::Ptr &incidence, const QString &email)
853{
854 // Search for an attendee by email address
855
856 Attendee attendee;
857 if (!incidence) {
858 return attendee;
859 }
860
861 const Attendee::List attendees = incidence->attendees();
862 for (const auto &a : attendees) {
863 if (email == a.email()) {
864 attendee = a;
865 break;
866 }
867 }
868 return attendee;
869}
870
871[[nodiscard]] static bool rsvpRequested(const Incidence::Ptr &incidence)
872{
873 if (!incidence) {
874 return false;
875 }
876
877 // use a heuristic to determine if a response is requested.
878
879 bool rsvp = true; // better send superfluously than not at all
880 Attendee::List attendees = incidence->attendees();
882 const Attendee::List::ConstIterator end(attendees.constEnd());
883 for (it = attendees.constBegin(); it != end; ++it) {
884 if (it == attendees.constBegin()) {
885 rsvp = (*it).RSVP(); // use what the first one has
886 } else {
887 if ((*it).RSVP() != rsvp) {
888 rsvp = true; // they differ, default
889 break;
890 }
891 }
892 }
893 return rsvp;
894}
895
896[[nodiscard]] static QString rsvpRequestedStr(bool rsvpRequested, const QString &role)
897{
898 if (rsvpRequested) {
899 if (role.isEmpty()) {
900 return i18n("Your response is requested.");
901 } else {
902 return i18n("Your response as <b>%1</b> is requested.", role);
903 }
904 } else {
905 if (role.isEmpty()) {
906 return i18n("No response is necessary.");
907 } else {
908 return i18n("No response as <b>%1</b> is necessary.", role);
909 }
910 }
911}
912
913[[nodiscard]] static QString myStatusStr(const Incidence::Ptr &incidence)
914{
915 QString ret;
916 const Attendee a = findMyAttendee(incidence);
917 if (!a.isNull() && a.status() != Attendee::NeedsAction && a.status() != Attendee::Delegated) {
918 ret = i18n("(<b>Note</b>: the Organizer preset your response to <b>%1</b>)", Stringify::attendeeStatus(a.status()));
919 }
920 return ret;
921}
922
923[[nodiscard]] static QVariantHash invitationNote(const QString &title, const QString &note, const QString &color)
924{
925 QVariantHash noteHash;
926 if (note.isEmpty()) {
927 return noteHash;
928 }
929
930 noteHash[QStringLiteral("color")] = color;
931 noteHash[QStringLiteral("title")] = title;
932 noteHash[QStringLiteral("note")] = note;
933 return noteHash;
934}
935
936[[nodiscard]] static QString invitationDescriptionIncidence(const Incidence::Ptr &incidence, bool noHtmlMode)
937{
938 if (!incidence->description().isEmpty()) {
939 // use description too
940 if (!incidence->descriptionIsRich() && !incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) {
941 return string2HTML(incidence->description());
942 } else {
943 QString descr;
944 if (!incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) {
945 descr = incidence->richDescription();
946 } else {
947 descr = incidence->description();
948 }
949 if (noHtmlMode) {
950 descr = cleanHtml(descr);
951 }
952 return htmlAddTag(QStringLiteral("p"), descr);
953 }
954 }
955
956 return QString();
957}
958
959[[nodiscard]] static bool slicesInterval(const Event::Ptr &event, const QDateTime &startDt, const QDateTime &endDt)
960{
961 QDateTime closestStart = event->dtStart();
962 QDateTime closestEnd = event->dtEnd();
963 if (event->recurs()) {
964 if (!event->recurrence()->timesInInterval(startDt, endDt).isEmpty()) {
965 // If there is a recurrence in this interval we know already that we slice.
966 return true;
967 }
968 closestStart = event->recurrence()->getPreviousDateTime(startDt);
969 if (event->hasEndDate()) {
970 closestEnd = closestStart.addSecs(event->dtStart().secsTo(event->dtEnd()));
971 }
972 } else {
973 if (!event->hasEndDate() && event->hasDuration()) {
974 closestEnd = closestStart.addSecs(event->duration());
975 }
976 }
977
978 if (!closestEnd.isValid()) {
979 // All events without an ending still happen if they are
980 // started.
981 return closestStart <= startDt;
982 }
983
984 if (closestStart <= startDt) {
985 // It starts before the interval and ends after the start of the interval.
986 return closestEnd > startDt;
987 }
988
989 // Are start and end both in this interval?
990 return (closestStart >= startDt && closestStart <= endDt) && (closestEnd >= startDt && closestEnd <= endDt);
991}
992
993[[nodiscard]] static QVariantList eventsOnSameDays(InvitationFormatterHelper *helper, const Event::Ptr &event, bool noHtmlMode)
994{
995 if (!event || !helper || !helper->calendar()) {
996 return QVariantList();
997 }
998
999 QDateTime startDay = event->dtStart();
1000 QDateTime endDay = event->hasEndDate() ? event->dtEnd() : event->dtStart();
1001 startDay.setTime(QTime(0, 0, 0));
1002 endDay.setTime(QTime(23, 59, 59));
1003
1004 Event::List matchingEvents = helper->calendar()->events(startDay.date(), endDay.date(), QTimeZone::systemTimeZone());
1005 if (matchingEvents.isEmpty()) {
1006 return QVariantList();
1007 }
1008
1009 QVariantList events;
1010 int count = 0;
1011 for (auto it = matchingEvents.cbegin(), end = matchingEvents.cend(); it != end && count < 50; ++it) {
1012 if ((*it)->schedulingID() == event->uid()) {
1013 // Exclude the same event from the list.
1014 continue;
1015 }
1016 if (!slicesInterval(*it, startDay, endDay)) {
1017 /* Calendar::events includes events that have a recurrence that is
1018 * "active" in the specified interval. Whether or not the event is actually
1019 * happening ( has a recurrence that falls into the interval ).
1020 * This appears to be done deliberately and not to be a bug so we additionally
1021 * check if the event is actually happening here. */
1022 continue;
1023 }
1024 ++count;
1025 QVariantHash ev;
1026 ev[QStringLiteral("summary")] = invitationSummary(*it, noHtmlMode);
1027 ev[QStringLiteral("dateTime")] = IncidenceFormatter::formatStartEnd((*it)->dtStart(), (*it)->dtEnd(), (*it)->allDay());
1028 events.push_back(ev);
1029 }
1030 if (count == 50) {
1031 /* Abort after 50 entries to limit resource usage */
1032 events.push_back({});
1033 }
1034 return events;
1035}
1036
1037[[nodiscard]] static QVariantHash invitationDetailsEvent(InvitationFormatterHelper *helper, const Event::Ptr &event, bool noHtmlMode)
1038{
1039 // Invitation details are formatted into an HTML table
1040 if (!event) {
1041 return QVariantHash();
1042 }
1043
1044 QVariantHash incidence;
1045 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-calendar");
1046 incidence[QStringLiteral("summary")] = invitationSummary(event, noHtmlMode);
1047 incidence[QStringLiteral("location")] = invitationLocation(event, noHtmlMode);
1048 incidence[QStringLiteral("recurs")] = event->recurs();
1049 incidence[QStringLiteral("recurrence")] = recurrenceString(event);
1050 incidence[QStringLiteral("isMultiDay")] = event->isMultiDay(QTimeZone::systemTimeZone());
1051 incidence[QStringLiteral("isAllDay")] = event->allDay();
1052 incidence[QStringLiteral("dateTime")] = IncidenceFormatter::formatStartEnd(event->dtStart(), event->dtEnd(), event->allDay());
1053 incidence[QStringLiteral("duration")] = durationString(event);
1054 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(event, noHtmlMode);
1055
1056 incidence[QStringLiteral("checkCalendarButton")] =
1057 inviteButton(QStringLiteral("check_calendar"), i18n("Check my calendar"), QStringLiteral("go-jump-today"), helper);
1058 incidence[QStringLiteral("eventsOnSameDays")] = eventsOnSameDays(helper, event, noHtmlMode);
1059
1060 return incidence;
1061}
1062
1063QString IncidenceFormatter::formatStartEnd(const QDateTime &start, const QDateTime &end, bool isAllDay)
1064{
1065 QString tmpStr;
1066 // <startDate[time> [- <[endDate][Time]>]
1067 // The startDate is always printed.
1068 // If the event does float the time is omitted.
1069 //
1070 // If it has an end dateTime:
1071 // on the same day -> Only add end time.
1072 // if it floats also omit the time
1073 tmpStr += IncidenceFormatter::dateTimeToString(start, isAllDay, false);
1074
1075 if (end.isValid()) {
1076 if (start.date() == end.date()) {
1077 // same day
1078 if (start.time().isValid()) {
1079 tmpStr += QLatin1StringView(" - ") + IncidenceFormatter::timeToString(end.toLocalTime().time(), true);
1080 }
1081 } else {
1082 tmpStr += QLatin1StringView(" - ") + IncidenceFormatter::dateTimeToString(end, isAllDay, false);
1083 }
1084 }
1085 return tmpStr;
1086}
1087
1088[[nodiscard]] static QVariantHash invitationDetailsEvent(InvitationFormatterHelper *helper,
1089 const Event::Ptr &event,
1090 const Event::Ptr &oldevent,
1091 const ScheduleMessage::Ptr &message,
1092 bool noHtmlMode)
1093{
1094 if (!oldevent) {
1095 return invitationDetailsEvent(helper, event, noHtmlMode);
1096 }
1097
1098 QVariantHash incidence;
1099
1100 // Print extra info typically dependent on the iTIP
1101 if (message->method() == iTIPDeclineCounter) {
1102 incidence[QStringLiteral("note")] = invitationNote(QString(), i18n("Please respond again to the original proposal."), noteColor());
1103 }
1104
1105 incidence[QStringLiteral("isDiff")] = true;
1106 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-calendar");
1107 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(event, noHtmlMode), invitationSummary(oldevent, noHtmlMode));
1108 incidence[QStringLiteral("location")] = htmlCompare(invitationLocation(event, noHtmlMode), invitationLocation(oldevent, noHtmlMode));
1109 incidence[QStringLiteral("recurs")] = event->recurs() || oldevent->recurs();
1110 incidence[QStringLiteral("recurrence")] = htmlCompare(recurrenceString(event), recurrenceString(oldevent));
1111 incidence[QStringLiteral("dateTime")] = htmlCompare(IncidenceFormatter::formatStartEnd(event->dtStart(), event->dtEnd(), event->allDay()),
1112 IncidenceFormatter::formatStartEnd(oldevent->dtStart(), oldevent->dtEnd(), oldevent->allDay()));
1113 incidence[QStringLiteral("duration")] = htmlCompare(durationString(event), durationString(oldevent));
1114 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(event, noHtmlMode);
1115
1116 incidence[QStringLiteral("checkCalendarButton")] =
1117 inviteButton(QStringLiteral("check_calendar"), i18n("Check my calendar"), QStringLiteral("go-jump-today"), helper);
1118 incidence[QStringLiteral("eventsOnSameDays")] = eventsOnSameDays(helper, event, noHtmlMode);
1119
1120 return incidence;
1121}
1122
1123[[nodiscard]] static QVariantHash invitationDetailsTodo(const Todo::Ptr &todo, bool noHtmlMode)
1124{
1125 // To-do details are formatted into an HTML table
1126 if (!todo) {
1127 return QVariantHash();
1128 }
1129
1130 QVariantHash incidence;
1131 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-tasks");
1132 incidence[QStringLiteral("summary")] = invitationSummary(todo, noHtmlMode);
1133 incidence[QStringLiteral("location")] = invitationLocation(todo, noHtmlMode);
1134 incidence[QStringLiteral("isAllDay")] = todo->allDay();
1135 incidence[QStringLiteral("hasStartDate")] = todo->hasStartDate();
1136 bool isMultiDay = false;
1137 if (todo->hasStartDate()) {
1138 if (todo->allDay()) {
1139 incidence[QStringLiteral("dtStartStr")] = dateToString(todo->dtStart().toLocalTime().date(), true);
1140 } else {
1141 incidence[QStringLiteral("dtStartStr")] = dateTimeToString(todo->dtStart(), false, true);
1142 }
1143 isMultiDay = todo->dtStart().date() != todo->dtDue().date();
1144 }
1145 if (todo->allDay()) {
1146 incidence[QStringLiteral("dtDueStr")] = dateToString(todo->dtDue().toLocalTime().date(), true);
1147 } else {
1148 incidence[QStringLiteral("dtDueStr")] = dateTimeToString(todo->dtDue(), false, true);
1149 }
1150 incidence[QStringLiteral("isMultiDay")] = isMultiDay;
1151 incidence[QStringLiteral("duration")] = durationString(todo);
1152 if (todo->percentComplete() > 0) {
1153 incidence[QStringLiteral("percentComplete")] = i18n("%1%", todo->percentComplete());
1154 }
1155 incidence[QStringLiteral("recurs")] = todo->recurs();
1156 incidence[QStringLiteral("recurrence")] = recurrenceString(todo);
1157 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(todo, noHtmlMode);
1158
1159 return incidence;
1160}
1161
1162[[nodiscard]] static QVariantHash invitationDetailsTodo(const Todo::Ptr &todo, const Todo::Ptr &oldtodo, const ScheduleMessage::Ptr &message, bool noHtmlMode)
1163{
1164 if (!oldtodo) {
1165 return invitationDetailsTodo(todo, noHtmlMode);
1166 }
1167
1168 QVariantHash incidence;
1169
1170 // Print extra info typically dependent on the iTIP
1171 if (message->method() == iTIPDeclineCounter) {
1172 incidence[QStringLiteral("note")] = invitationNote(QString(), i18n("Please respond again to the original proposal."), noteColor());
1173 }
1174
1175 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-tasks");
1176 incidence[QStringLiteral("isDiff")] = true;
1177 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(todo, noHtmlMode), invitationSummary(oldtodo, noHtmlMode));
1178 incidence[QStringLiteral("location")] = htmlCompare(invitationLocation(todo, noHtmlMode), invitationLocation(oldtodo, noHtmlMode));
1179 incidence[QStringLiteral("isAllDay")] = todo->allDay();
1180 incidence[QStringLiteral("hasStartDate")] = todo->hasStartDate();
1181 incidence[QStringLiteral("dtStartStr")] = htmlCompare(dateTimeToString(todo->dtStart(), false, false), dateTimeToString(oldtodo->dtStart(), false, false));
1182 incidence[QStringLiteral("dtDueStr")] = htmlCompare(dateTimeToString(todo->dtDue(), false, false), dateTimeToString(oldtodo->dtDue(), false, false));
1183 incidence[QStringLiteral("duration")] = htmlCompare(durationString(todo), durationString(oldtodo));
1184 incidence[QStringLiteral("percentComplete")] = htmlCompare(i18n("%1%", todo->percentComplete()), i18n("%1%", oldtodo->percentComplete()));
1185
1186 incidence[QStringLiteral("recurs")] = todo->recurs() || oldtodo->recurs();
1187 incidence[QStringLiteral("recurrence")] = htmlCompare(recurrenceString(todo), recurrenceString(oldtodo));
1188 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(todo, noHtmlMode);
1189
1190 return incidence;
1191}
1192
1193[[nodiscard]] static QVariantHash invitationDetailsJournal(const Journal::Ptr &journal, bool noHtmlMode)
1194{
1195 if (!journal) {
1196 return QVariantHash();
1197 }
1198
1199 QVariantHash incidence;
1200 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-journal");
1201 incidence[QStringLiteral("summary")] = invitationSummary(journal, noHtmlMode);
1202 incidence[QStringLiteral("date")] = journal->dtStart();
1203 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(journal, noHtmlMode);
1204
1205 return incidence;
1206}
1207
1208[[nodiscard]] static QVariantHash invitationDetailsJournal(const Journal::Ptr &journal, const Journal::Ptr &oldjournal, bool noHtmlMode)
1209{
1210 if (!oldjournal) {
1211 return invitationDetailsJournal(journal, noHtmlMode);
1212 }
1213
1214 QVariantHash incidence;
1215 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-journal");
1216 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(journal, noHtmlMode), invitationSummary(oldjournal, noHtmlMode));
1217 incidence[QStringLiteral("dateStr")] =
1218 htmlCompare(dateToString(journal->dtStart().toLocalTime().date(), false), dateToString(oldjournal->dtStart().toLocalTime().date(), false));
1219 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(journal, noHtmlMode);
1220
1221 return incidence;
1222}
1223
1224[[nodiscard]] static QVariantHash invitationDetailsFreeBusy(const FreeBusy::Ptr &fb, bool noHtmlMode)
1225{
1226 Q_UNUSED(noHtmlMode)
1227
1228 if (!fb) {
1229 return QVariantHash();
1230 }
1231
1232 QVariantHash incidence;
1233 incidence[QStringLiteral("organizer")] = fb->organizer().fullName();
1234 incidence[QStringLiteral("dtStart")] = fb->dtStart();
1235 incidence[QStringLiteral("dtEnd")] = fb->dtEnd();
1236
1237 QVariantList periodsList;
1238 const Period::List periods = fb->busyPeriods();
1239 for (auto it = periods.cbegin(), end = periods.cend(); it != end; ++it) {
1240 QVariantHash period;
1241 period[QStringLiteral("hasDuration")] = it->hasDuration();
1242 if (it->hasDuration()) {
1243 int dur = it->duration().asSeconds();
1244 QString cont;
1245 if (dur >= 3600) {
1246 cont += i18ncp("hours part of duration", "1 hour ", "%1 hours ", dur / 3600);
1247 dur %= 3600;
1248 }
1249 if (dur >= 60) {
1250 cont += i18ncp("minutes part of duration", "1 minute", "%1 minutes ", dur / 60);
1251 dur %= 60;
1252 }
1253 if (dur > 0) {
1254 cont += i18ncp("seconds part of duration", "1 second", "%1 seconds", dur);
1255 }
1256 period[QStringLiteral("duration")] = cont;
1257 }
1258 period[QStringLiteral("start")] = it->start();
1259 period[QStringLiteral("end")] = it->end();
1260
1261 periodsList.push_back(period);
1262 }
1263 incidence[QStringLiteral("periods")] = periodsList;
1264
1265 return incidence;
1266}
1267
1268[[nodiscard]] static QVariantHash invitationDetailsFreeBusy(const FreeBusy::Ptr &fb, const FreeBusy::Ptr &oldfb, bool noHtmlMode)
1269{
1270 Q_UNUSED(oldfb)
1271 return invitationDetailsFreeBusy(fb, noHtmlMode);
1272}
1273
1274[[nodiscard]] static bool replyMeansCounter(const Incidence::Ptr &incidence)
1275{
1276 Q_UNUSED(incidence)
1277 return false;
1278 /**
1279 see kolab/issue 3665 for an example of when we might use this for something
1280
1281 bool status = false;
1282 if ( incidence ) {
1283 // put code here that looks at the incidence and determines that
1284 // the reply is meant to be a counter proposal. We think this happens
1285 // with Outlook counter proposals, but we aren't sure how yet.
1286 if ( condition ) {
1287 status = true;
1288 }
1289 }
1290 return status;
1291 */
1292}
1293
1294[[nodiscard]] static QString
1295invitationHeaderEvent(const Event::Ptr &event, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender)
1296{
1297 if (!msg || !event) {
1298 return QString();
1299 }
1300
1301 switch (msg->method()) {
1302 case iTIPPublish:
1303 return i18n("This invitation has been published.");
1304 case iTIPRequest:
1305 if (existingIncidence && event->revision() > 0) {
1306 QString orgStr = organizerName(event, sender);
1307 if (senderIsOrganizer(event, sender)) {
1308 return i18n("This invitation has been updated by the organizer %1.", orgStr);
1309 } else {
1310 return i18n("This invitation has been updated by %1 as a representative of %2.", sender, orgStr);
1311 }
1312 }
1313 if (iamOrganizer(event)) {
1314 return i18n("I created this invitation.");
1315 } else {
1316 QString orgStr = organizerName(event, sender);
1317 if (senderIsOrganizer(event, sender)) {
1318 return i18n("You received an invitation from %1.", orgStr);
1319 } else {
1320 return i18n("You received an invitation from %1 as a representative of %2.", sender, orgStr);
1321 }
1322 }
1323 case iTIPRefresh:
1324 return i18n("This invitation was refreshed.");
1325 case iTIPCancel:
1326 if (iamOrganizer(event)) {
1327 return i18n("This invitation has been canceled.");
1328 } else {
1329 return i18n("The organizer has revoked the invitation.");
1330 }
1331 case iTIPAdd:
1332 return i18n("Addition to the invitation.");
1333 case iTIPReply: {
1334 if (replyMeansCounter(event)) {
1335 return i18n("%1 makes this counter proposal.", firstAttendeeName(event, sender));
1336 }
1337
1338 Attendee::List attendees = event->attendees();
1339 if (attendees.isEmpty()) {
1340 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!";
1341 return QString();
1342 }
1343 if (attendees.count() != 1) {
1344 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1"
1345 << "but is" << attendees.count();
1346 }
1347 QString attendeeName = firstAttendeeName(event, sender);
1348
1349 QString delegatorName;
1350 QString dummy;
1351 const Attendee attendee = *attendees.begin();
1352 KEmailAddress::extractEmailAddressAndName(attendee.delegator(), dummy, delegatorName);
1353 if (delegatorName.isEmpty()) {
1354 delegatorName = attendee.delegator();
1355 }
1356
1357 switch (attendee.status()) {
1359 return i18n("%1 indicates this invitation still needs some action.", attendeeName);
1360 case Attendee::Accepted:
1361 if (event->revision() > 0) {
1362 if (!sender.isEmpty()) {
1363 return i18n("This invitation has been updated by attendee %1.", sender);
1364 } else {
1365 return i18n("This invitation has been updated by an attendee.");
1366 }
1367 } else {
1368 if (delegatorName.isEmpty()) {
1369 return i18n("%1 accepts this invitation.", attendeeName);
1370 } else {
1371 return i18n("%1 accepts this invitation on behalf of %2.", attendeeName, delegatorName);
1372 }
1373 }
1375 if (delegatorName.isEmpty()) {
1376 return i18n("%1 tentatively accepts this invitation.", attendeeName);
1377 } else {
1378 return i18n("%1 tentatively accepts this invitation on behalf of %2.", attendeeName, delegatorName);
1379 }
1380 case Attendee::Declined:
1381 if (delegatorName.isEmpty()) {
1382 return i18n("%1 declines this invitation.", attendeeName);
1383 } else {
1384 return i18n("%1 declines this invitation on behalf of %2.", attendeeName, delegatorName);
1385 }
1386 case Attendee::Delegated: {
1387 QString delegate;
1388 QString dummy;
1389 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegate);
1390 if (delegate.isEmpty()) {
1391 delegate = attendee.delegate();
1392 }
1393 if (!delegate.isEmpty()) {
1394 return i18n("%1 has delegated this invitation to %2.", attendeeName, delegate);
1395 } else {
1396 return i18n("%1 has delegated this invitation.", attendeeName);
1397 }
1398 }
1400 return i18n("This invitation is now completed.");
1402 return i18n("%1 is still processing the invitation.", attendeeName);
1403 case Attendee::None:
1404 return i18n("Unknown response to this invitation.");
1405 }
1406 break;
1407 }
1408 case iTIPCounter:
1409 return i18n("%1 makes this counter proposal.", firstAttendeeName(event, i18n("Sender")));
1410
1411 case iTIPDeclineCounter: {
1412 QString orgStr = organizerName(event, sender);
1413 if (senderIsOrganizer(event, sender)) {
1414 return i18n("%1 declines your counter proposal.", orgStr);
1415 } else {
1416 return i18n("%1 declines your counter proposal on behalf of %2.", sender, orgStr);
1417 }
1418 }
1419
1420 case iTIPNoMethod:
1421 return i18n("Error: Event iTIP message with unknown method.");
1422 }
1423 qCritical() << "encountered an iTIP method that we do not support.";
1424 return QString();
1425}
1426
1427[[nodiscard]] static QString
1428invitationHeaderTodo(const Todo::Ptr &todo, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender)
1429{
1430 if (!msg || !todo) {
1431 return QString();
1432 }
1433
1434 switch (msg->method()) {
1435 case iTIPPublish:
1436 return i18n("This to-do has been published.");
1437 case iTIPRequest:
1438 if (existingIncidence && todo->revision() > 0) {
1439 QString orgStr = organizerName(todo, sender);
1440 if (senderIsOrganizer(todo, sender)) {
1441 return i18n("This to-do has been updated by the organizer %1.", orgStr);
1442 } else {
1443 return i18n("This to-do has been updated by %1 as a representative of %2.", sender, orgStr);
1444 }
1445 } else {
1446 if (iamOrganizer(todo)) {
1447 return i18n("I created this to-do.");
1448 } else {
1449 QString orgStr = organizerName(todo, sender);
1450 if (senderIsOrganizer(todo, sender)) {
1451 return i18n("You have been assigned this to-do by %1.", orgStr);
1452 } else {
1453 return i18n("You have been assigned this to-do by %1 as a representative of %2.", sender, orgStr);
1454 }
1455 }
1456 }
1457 case iTIPRefresh:
1458 return i18n("This to-do was refreshed.");
1459 case iTIPCancel:
1460 if (iamOrganizer(todo)) {
1461 return i18n("This to-do was canceled.");
1462 } else {
1463 return i18n("The organizer has revoked this to-do.");
1464 }
1465 case iTIPAdd:
1466 return i18n("Addition to the to-do.");
1467 case iTIPReply: {
1468 if (replyMeansCounter(todo)) {
1469 return i18n("%1 makes this counter proposal.", firstAttendeeName(todo, sender));
1470 }
1471
1472 Attendee::List attendees = todo->attendees();
1473 if (attendees.isEmpty()) {
1474 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!";
1475 return QString();
1476 }
1477 if (attendees.count() != 1) {
1478 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1."
1479 << "but is" << attendees.count();
1480 }
1481 QString attendeeName = firstAttendeeName(todo, sender);
1482
1483 QString delegatorName;
1484 QString dummy;
1485 const Attendee attendee = *attendees.begin();
1486 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegatorName);
1487 if (delegatorName.isEmpty()) {
1488 delegatorName = attendee.delegator();
1489 }
1490
1491 switch (attendee.status()) {
1493 return i18n("%1 indicates this to-do assignment still needs some action.", attendeeName);
1494 case Attendee::Accepted:
1495 if (todo->revision() > 0) {
1496 if (!sender.isEmpty()) {
1497 if (todo->isCompleted()) {
1498 return i18n("This to-do has been completed by assignee %1.", sender);
1499 } else {
1500 return i18n("This to-do has been updated by assignee %1.", sender);
1501 }
1502 } else {
1503 if (todo->isCompleted()) {
1504 return i18n("This to-do has been completed by an assignee.");
1505 } else {
1506 return i18n("This to-do has been updated by an assignee.");
1507 }
1508 }
1509 } else {
1510 if (delegatorName.isEmpty()) {
1511 return i18n("%1 accepts this to-do.", attendeeName);
1512 } else {
1513 return i18n("%1 accepts this to-do on behalf of %2.", attendeeName, delegatorName);
1514 }
1515 }
1517 if (delegatorName.isEmpty()) {
1518 return i18n("%1 tentatively accepts this to-do.", attendeeName);
1519 } else {
1520 return i18n("%1 tentatively accepts this to-do on behalf of %2.", attendeeName, delegatorName);
1521 }
1522 case Attendee::Declined:
1523 if (delegatorName.isEmpty()) {
1524 return i18n("%1 declines this to-do.", attendeeName);
1525 } else {
1526 return i18n("%1 declines this to-do on behalf of %2.", attendeeName, delegatorName);
1527 }
1528 case Attendee::Delegated: {
1529 QString delegate;
1530 QString dummy;
1531 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegate);
1532 if (delegate.isEmpty()) {
1533 delegate = attendee.delegate();
1534 }
1535 if (!delegate.isEmpty()) {
1536 return i18n("%1 has delegated this to-do to %2.", attendeeName, delegate);
1537 } else {
1538 return i18n("%1 has delegated this to-do.", attendeeName);
1539 }
1540 }
1542 return i18n("The request for this to-do is now completed.");
1544 return i18n("%1 is still processing the to-do.", attendeeName);
1545 case Attendee::None:
1546 return i18n("Unknown response to this to-do.");
1547 }
1548 break;
1549 }
1550 case iTIPCounter:
1551 return i18n("%1 makes this counter proposal.", firstAttendeeName(todo, sender));
1552
1553 case iTIPDeclineCounter: {
1554 const QString orgStr = organizerName(todo, sender);
1555 if (senderIsOrganizer(todo, sender)) {
1556 return i18n("%1 declines the counter proposal.", orgStr);
1557 } else {
1558 return i18n("%1 declines the counter proposal on behalf of %2.", sender, orgStr);
1559 }
1560 }
1561
1562 case iTIPNoMethod:
1563 return i18n("Error: To-do iTIP message with unknown method.");
1564 }
1565 qCritical() << "encountered an iTIP method that we do not support";
1566 return QString();
1567}
1568
1569[[nodiscard]] static QString invitationHeaderJournal(const Journal::Ptr &journal, const ScheduleMessage::Ptr &msg)
1570{
1571 if (!msg || !journal) {
1572 return QString();
1573 }
1574
1575 switch (msg->method()) {
1576 case iTIPPublish:
1577 return i18n("This journal has been published.");
1578 case iTIPRequest:
1579 return i18n("You have been assigned this journal.");
1580 case iTIPRefresh:
1581 return i18n("This journal was refreshed.");
1582 case iTIPCancel:
1583 return i18n("This journal was canceled.");
1584 case iTIPAdd:
1585 return i18n("Addition to the journal.");
1586 case iTIPReply: {
1587 if (replyMeansCounter(journal)) {
1588 return i18n("Sender makes this counter proposal.");
1589 }
1590
1591 Attendee::List attendees = journal->attendees();
1592 if (attendees.isEmpty()) {
1593 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!";
1594 return QString();
1595 }
1596 if (attendees.count() != 1) {
1597 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1 "
1598 << "but is " << attendees.count();
1599 }
1600 const Attendee attendee = *attendees.begin();
1601
1602 switch (attendee.status()) {
1604 return i18n("Sender indicates this journal assignment still needs some action.");
1605 case Attendee::Accepted:
1606 return i18n("Sender accepts this journal.");
1608 return i18n("Sender tentatively accepts this journal.");
1609 case Attendee::Declined:
1610 return i18n("Sender declines this journal.");
1612 return i18n("Sender has delegated this request for the journal.");
1614 return i18n("The request for this journal is now completed.");
1616 return i18n("Sender is still processing the invitation.");
1617 case Attendee::None:
1618 return i18n("Unknown response to this journal.");
1619 }
1620 break;
1621 }
1622 case iTIPCounter:
1623 return i18n("Sender makes this counter proposal.");
1624 case iTIPDeclineCounter:
1625 return i18n("Sender declines the counter proposal.");
1626 case iTIPNoMethod:
1627 return i18n("Error: Journal iTIP message with unknown method.");
1628 }
1629 qCritical() << "encountered an iTIP method that we do not support";
1630 return QString();
1631}
1632
1633[[nodiscard]] static QString invitationHeaderFreeBusy(const FreeBusy::Ptr &fb, const ScheduleMessage::Ptr &msg)
1634{
1635 if (!msg || !fb) {
1636 return QString();
1637 }
1638
1639 switch (msg->method()) {
1640 case iTIPPublish:
1641 return i18n("This free/busy list has been published.");
1642 case iTIPRequest:
1643 return i18n("The free/busy list has been requested.");
1644 case iTIPRefresh:
1645 return i18n("This free/busy list was refreshed.");
1646 case iTIPCancel:
1647 return i18n("This free/busy list was canceled.");
1648 case iTIPAdd:
1649 return i18n("Addition to the free/busy list.");
1650 case iTIPReply:
1651 return i18n("Reply to the free/busy list.");
1652 case iTIPCounter:
1653 return i18n("Sender makes this counter proposal.");
1654 case iTIPDeclineCounter:
1655 return i18n("Sender declines the counter proposal.");
1656 case iTIPNoMethod:
1657 return i18n("Error: Free/Busy iTIP message with unknown method.");
1658 }
1659 qCritical() << "encountered an iTIP method that we do not support";
1660 return QString();
1661}
1662
1663//@endcond
1664
1665[[nodiscard]] static QVariantList invitationAttendeeList(const Incidence::Ptr &incidence)
1666{
1667 if (!incidence) {
1668 return QVariantList();
1669 }
1670
1671 QVariantList attendees;
1672 const Attendee::List lstAttendees = incidence->attendees();
1673 for (const Attendee &a : lstAttendees) {
1674 if (iamAttendee(a)) {
1675 continue;
1676 }
1677
1678 QVariantHash attendee;
1679 attendee[QStringLiteral("name")] = a.name();
1680 attendee[QStringLiteral("email")] = a.email();
1681 attendee[QStringLiteral("delegator")] = a.delegator();
1682 attendee[QStringLiteral("delegate")] = a.delegate();
1683 attendee[QStringLiteral("isOrganizer")] = attendeeIsOrganizer(incidence, a);
1684 attendee[QStringLiteral("status")] = Stringify::attendeeStatus(a.status());
1685 attendee[QStringLiteral("icon")] = rsvpStatusIconName(a.status());
1686
1687 attendees.push_back(attendee);
1688 }
1689
1690 return attendees;
1691}
1692
1693[[nodiscard]] static QVariantList invitationRsvpList(const Incidence::Ptr &incidence, const Attendee &sender)
1694{
1695 if (!incidence) {
1696 return QVariantList();
1697 }
1698
1699 QVariantList attendees;
1700 const Attendee::List lstAttendees = incidence->attendees();
1701 for (const Attendee &a_ : lstAttendees) {
1702 Attendee a = a_;
1703 if (!attendeeIsOrganizer(incidence, a)) {
1704 continue;
1705 }
1706 QVariantHash attendee;
1707 attendee[QStringLiteral("status")] = Stringify::attendeeStatus(a.status());
1708 if (!sender.isNull() && (a.email() == sender.email())) {
1709 // use the attendee taken from the response incidence,
1710 // rather than the attendee from the calendar incidence.
1711 if (a.status() != sender.status()) {
1712 attendee[QStringLiteral("status")] = i18n("%1 (<i>unrecorded</i>", Stringify::attendeeStatus(sender.status()));
1713 }
1714 a = sender;
1715 }
1716
1717 attendee[QStringLiteral("name")] = a.name();
1718 attendee[QStringLiteral("email")] = a.email();
1719 attendee[QStringLiteral("delegator")] = a.delegator();
1720 attendee[QStringLiteral("delegate")] = a.delegate();
1721 attendee[QStringLiteral("isOrganizer")] = attendeeIsOrganizer(incidence, a);
1722 attendee[QStringLiteral("isMyself")] = iamAttendee(a);
1723 attendee[QStringLiteral("icon")] = rsvpStatusIconName(a.status());
1724
1725 attendees.push_back(attendee);
1726 }
1727
1728 return attendees;
1729}
1730
1731[[nodiscard]] static QVariantList invitationAttachments(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper)
1732{
1733 if (!incidence) {
1734 return QVariantList();
1735 }
1736
1737 if (incidence->type() == Incidence::TypeFreeBusy) {
1738 // A FreeBusy does not have a valid attachment due to the static-cast from IncidenceBase
1739 return QVariantList();
1740 }
1741
1742 QVariantList attachments;
1743 const Attachment::List lstAttachments = incidence->attachments();
1744 for (const Attachment &a : lstAttachments) {
1745 QVariantHash attachment;
1746 QMimeDatabase mimeDb;
1747 auto mimeType = mimeDb.mimeTypeForName(a.mimeType());
1748 attachment[QStringLiteral("icon")] = (mimeType.isValid() ? mimeType.iconName() : QStringLiteral("application-octet-stream"));
1749 attachment[QStringLiteral("name")] = a.label();
1750 const QString attachementStr = helper->generateLinkURL(QStringLiteral("ATTACH:%1").arg(QString::fromLatin1(a.label().toUtf8().toBase64())));
1751 attachment[QStringLiteral("uri")] = attachementStr;
1752 attachments.push_back(attachment);
1753 }
1754
1755 return attachments;
1756}
1757
1758//@cond PRIVATE
1759template<typename T>
1760class KCalUtils::IncidenceFormatter::ScheduleMessageVisitor : public Visitor
1761{
1762public:
1763 bool act(const IncidenceBase::Ptr &incidence, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender)
1764 {
1765 mExistingIncidence = existingIncidence;
1766 mMessage = msg;
1767 mSender = sender;
1768 return incidence->accept(*this, incidence);
1769 }
1770
1771 [[nodiscard]] T result() const
1772 {
1773 return mResult;
1774 }
1775
1776protected:
1777 T mResult;
1778 Incidence::Ptr mExistingIncidence;
1779 ScheduleMessage::Ptr mMessage;
1780 QString mSender;
1781};
1782
1783class KCalUtils::IncidenceFormatter::InvitationHeaderVisitor : public IncidenceFormatter::ScheduleMessageVisitor<QString>
1784{
1785protected:
1786 bool visit(const Event::Ptr &event) override
1787 {
1788 mResult = invitationHeaderEvent(event, mExistingIncidence, mMessage, mSender);
1789 return !mResult.isEmpty();
1790 }
1791
1792 bool visit(const Todo::Ptr &todo) override
1793 {
1794 mResult = invitationHeaderTodo(todo, mExistingIncidence, mMessage, mSender);
1795 return !mResult.isEmpty();
1796 }
1797
1798 bool visit(const Journal::Ptr &journal) override
1799 {
1800 mResult = invitationHeaderJournal(journal, mMessage);
1801 return !mResult.isEmpty();
1802 }
1803
1804 bool visit(const FreeBusy::Ptr &fb) override
1805 {
1806 mResult = invitationHeaderFreeBusy(fb, mMessage);
1807 return !mResult.isEmpty();
1808 }
1809};
1810
1811class KCalUtils::IncidenceFormatter::InvitationBodyVisitor : public IncidenceFormatter::ScheduleMessageVisitor<QVariantHash>
1812{
1813public:
1814 InvitationBodyVisitor(InvitationFormatterHelper *helper, bool noHtmlMode)
1815 : ScheduleMessageVisitor()
1816 , mHelper(helper)
1817 , mNoHtmlMode(noHtmlMode)
1818 {
1819 }
1820
1821protected:
1822 bool visit(const Event::Ptr &event) override
1823 {
1824 Event::Ptr oldevent = mExistingIncidence.dynamicCast<Event>();
1825 mResult = invitationDetailsEvent(mHelper, event, oldevent, mMessage, mNoHtmlMode);
1826 return !mResult.isEmpty();
1827 }
1828
1829 bool visit(const Todo::Ptr &todo) override
1830 {
1831 Todo::Ptr oldtodo = mExistingIncidence.dynamicCast<Todo>();
1832 mResult = invitationDetailsTodo(todo, oldtodo, mMessage, mNoHtmlMode);
1833 return !mResult.isEmpty();
1834 }
1835
1836 bool visit(const Journal::Ptr &journal) override
1837 {
1838 Journal::Ptr oldjournal = mExistingIncidence.dynamicCast<Journal>();
1839 mResult = invitationDetailsJournal(journal, oldjournal, mNoHtmlMode);
1840 return !mResult.isEmpty();
1841 }
1842
1843 bool visit(const FreeBusy::Ptr &fb) override
1844 {
1845 mResult = invitationDetailsFreeBusy(fb, FreeBusy::Ptr(), mNoHtmlMode);
1846 return !mResult.isEmpty();
1847 }
1848
1849private:
1851 bool mNoHtmlMode;
1852};
1853//@endcond
1854
1855class KCalUtils::InvitationFormatterHelperPrivate
1856{
1857};
1858
1859InvitationFormatterHelper::InvitationFormatterHelper()
1860 : d(nullptr)
1861{
1862}
1863
1864InvitationFormatterHelper::~InvitationFormatterHelper()
1865{
1866}
1867
1868QString InvitationFormatterHelper::generateLinkURL(const QString &id)
1869{
1870 return id;
1871}
1872
1873QString InvitationFormatterHelper::makeLink(const QString &id, const QString &text)
1874{
1875 if (!id.startsWith(QLatin1StringView("ATTACH:"))) {
1876 const QString res = QStringLiteral("<a href=\"%1\"><font size=\"-1\"><b>%2</b></font></a>").arg(generateLinkURL(id), text);
1877 return res;
1878 } else {
1879 // draw the attachment links in non-bold face
1880 const QString res = QStringLiteral("<a href=\"%1\">%2</a>").arg(generateLinkURL(id), text);
1881 return res;
1882 }
1883}
1884
1885// Check if the given incidence is likely one that we own instead one from
1886// a shared calendar (Kolab-specific)
1887static bool incidenceOwnedByMe(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence)
1888{
1889 Q_UNUSED(calendar)
1890 Q_UNUSED(incidence)
1891 return true;
1892}
1893
1894static QVariantHash inviteButton(const QString &id, const QString &text, const QString &iconName, InvitationFormatterHelper *helper)
1895{
1896 QVariantHash button;
1897 button[QStringLiteral("uri")] = helper->generateLinkURL(id);
1898 button[QStringLiteral("icon")] = iconName;
1899 button[QStringLiteral("label")] = text;
1900 return button;
1901}
1902
1903static QVariantList responseButtons(const Incidence::Ptr &incidence,
1904 bool rsvpReq,
1905 bool rsvpRec,
1907 const Incidence::Ptr &existingInc = Incidence::Ptr())
1908{
1909 bool hideAccept = false;
1910 bool hideTentative = false;
1911 bool hideDecline = false;
1912
1913 if (existingInc) {
1914 const Attendee ea = findMyAttendee(existingInc);
1915 if (!ea.isNull()) {
1916 // If this is an update of an already accepted incidence
1917 // to not show the buttons that confirm the status.
1918 hideAccept = ea.status() == Attendee::Accepted;
1919 hideDecline = ea.status() == Attendee::Declined;
1920 hideTentative = ea.status() == Attendee::Tentative;
1921 }
1922 }
1923
1924 QVariantList buttons;
1925 if (!rsvpReq && (incidence && incidence->revision() == 0)) {
1926 // Record only
1927 buttons << inviteButton(QStringLiteral("record"), i18n("Record"), QStringLiteral("dialog-ok"), helper);
1928
1929 // Move to trash
1930 buttons << inviteButton(QStringLiteral("delete"), i18n("Move to Trash"), QStringLiteral("edittrash"), helper);
1931 } else {
1932 // Accept
1933 if (!hideAccept) {
1934 buttons << inviteButton(QStringLiteral("accept"), i18nc("accept invitation", "Accept"), QStringLiteral("dialog-ok-apply"), helper);
1935 }
1936
1937 // Tentative
1938 if (!hideTentative) {
1939 buttons << inviteButton(QStringLiteral("accept_conditionally"),
1940 i18nc("Accept invitation conditionally", "Tentative"),
1941 QStringLiteral("dialog-ok"),
1942 helper);
1943 }
1944
1945 // Decline
1946 if (!hideDecline) {
1947 buttons << inviteButton(QStringLiteral("decline"), i18nc("decline invitation", "Decline"), QStringLiteral("dialog-cancel"), helper);
1948 }
1949
1950 // Counter proposal
1951 buttons << inviteButton(QStringLiteral("counter"), i18nc("invitation counter proposal", "Counter proposal ..."), QStringLiteral("edit-undo"), helper);
1952 }
1953
1954 if (!rsvpRec || (incidence && incidence->revision() > 0)) {
1955 // Delegate
1956 buttons << inviteButton(QStringLiteral("delegate"), i18nc("delegate invitation to another", "Delegate ..."), QStringLiteral("mail-forward"), helper);
1957 }
1958 return buttons;
1959}
1960
1961[[nodiscard]] static QVariantList counterButtons(InvitationFormatterHelper *helper)
1962{
1963 QVariantList buttons;
1964
1965 // Accept proposal
1966 buttons << inviteButton(QStringLiteral("accept_counter"), i18n("Accept"), QStringLiteral("dialog-ok-apply"), helper);
1967
1968 // Decline proposal
1969 buttons << inviteButton(QStringLiteral("decline_counter"), i18n("Decline"), QStringLiteral("dialog-cancel"), helper);
1970
1971 return buttons;
1972}
1973
1974[[nodiscard]] static QVariantList recordButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper)
1975{
1976 QVariantList buttons;
1977 if (incidence) {
1978 buttons << inviteButton(QStringLiteral("reply"),
1979 incidence->type() == Incidence::TypeTodo ? i18n("Record invitation in my to-do list")
1980 : i18n("Record invitation in my calendar"),
1981 QStringLiteral("dialog-ok"),
1982 helper);
1983 }
1984 return buttons;
1985}
1986
1987[[nodiscard]] static QVariantList recordResponseButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper)
1988{
1989 QVariantList buttons;
1990
1991 if (incidence) {
1992 buttons << inviteButton(QStringLiteral("reply"),
1993 incidence->type() == Incidence::TypeTodo ? i18n("Record response in my to-do list") : i18n("Record response in my calendar"),
1994 QStringLiteral("dialog-ok"),
1995 helper);
1996 }
1997 return buttons;
1998}
1999
2000[[nodiscard]] static QVariantList cancelButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper)
2001{
2002 QVariantList buttons;
2003
2004 // Remove invitation
2005 if (incidence) {
2006 buttons << inviteButton(QStringLiteral("cancel"),
2007 incidence->type() == Incidence::TypeTodo ? i18n("Remove invitation from my to-do list")
2008 : i18n("Remove invitation from my calendar"),
2009 QStringLiteral("dialog-cancel"),
2010 helper);
2011 }
2012
2013 return buttons;
2014}
2015
2016[[nodiscard]] static QVariantHash invitationStyle()
2017{
2018 QVariantHash style;
2019 QPalette p;
2021 style[QStringLiteral("buttonBg")] = p.color(QPalette::Button).name();
2022 style[QStringLiteral("buttonBorder")] = p.shadow().color().name();
2023 style[QStringLiteral("buttonFg")] = p.color(QPalette::ButtonText).name();
2024 return style;
2025}
2026
2027Calendar::Ptr InvitationFormatterHelper::calendar() const
2028{
2029 return Calendar::Ptr();
2030}
2031
2032static QString
2033formatICalInvitationHelper(const QString &invitation, const Calendar::Ptr &mCalendar, InvitationFormatterHelper *helper, bool noHtmlMode, const QString &sender)
2034{
2035 if (invitation.isEmpty()) {
2036 return QString();
2037 }
2038
2039 ICalFormat format;
2040 // parseScheduleMessage takes the tz from the calendar,
2041 // no need to set it manually here for the format!
2042 ScheduleMessage::Ptr msg = format.parseScheduleMessage(mCalendar, invitation);
2043
2044 if (!msg) {
2045 qCDebug(KCALUTILS_LOG) << "Failed to parse the scheduling message";
2046 Q_ASSERT(format.exception());
2047 qCDebug(KCALUTILS_LOG) << Stringify::errorMessage(*format.exception());
2048 return QString();
2049 }
2050
2051 IncidenceBase::Ptr incBase = msg->event();
2052
2053 incBase->shiftTimes(mCalendar->timeZone(), QTimeZone::systemTimeZone());
2054
2055 // Determine if this incidence is in my calendar (and owned by me)
2056 Incidence::Ptr existingIncidence;
2057 if (incBase && helper->calendar()) {
2058 existingIncidence = helper->calendar()->incidence(incBase->uid(), incBase->recurrenceId());
2059
2060 if (!incidenceOwnedByMe(helper->calendar(), existingIncidence)) {
2061 existingIncidence.clear();
2062 }
2063 if (!existingIncidence) {
2064 const Incidence::List list = helper->calendar()->incidences();
2065 for (Incidence::List::ConstIterator it = list.begin(), end = list.end(); it != end; ++it) {
2066 if ((*it)->schedulingID() == incBase->uid() && incidenceOwnedByMe(helper->calendar(), *it)
2067 && (*it)->recurrenceId() == incBase->recurrenceId()) {
2068 existingIncidence = *it;
2069 break;
2070 }
2071 }
2072 }
2073 }
2074
2075 Incidence::Ptr inc = incBase.staticCast<Incidence>(); // the incidence in the invitation email
2076
2077 // If the IncidenceBase is a FreeBusy, then we cannot access the revision number in
2078 // the static-casted Incidence; so for sake of nothing better use 0 as the revision.
2079 int incRevision = 0;
2080 if (inc && inc->type() != Incidence::TypeFreeBusy) {
2081 incRevision = inc->revision();
2082 }
2083
2084 IncidenceFormatter::InvitationHeaderVisitor headerVisitor;
2085 // The InvitationHeaderVisitor returns false if the incidence is somehow invalid, or not handled
2086 if (!headerVisitor.act(inc, existingIncidence, msg, sender)) {
2087 return QString();
2088 }
2089
2090 QVariantHash incidence;
2091
2092 // use the Outlook 2007 Comparison Style
2093 IncidenceFormatter::InvitationBodyVisitor bodyVisitor(helper, noHtmlMode);
2094 bool bodyOk;
2095 if (msg->method() == iTIPRequest || msg->method() == iTIPReply || msg->method() == iTIPDeclineCounter) {
2096 if (inc && existingIncidence && incRevision < existingIncidence->revision()) {
2097 bodyOk = bodyVisitor.act(existingIncidence, inc, msg, sender);
2098 } else {
2099 bodyOk = bodyVisitor.act(inc, existingIncidence, msg, sender);
2100 }
2101 } else {
2102 bodyOk = bodyVisitor.act(inc, Incidence::Ptr(), msg, sender);
2103 }
2104 if (!bodyOk) {
2105 return QString();
2106 }
2107
2108 incidence = bodyVisitor.result();
2109 incidence[QStringLiteral("style")] = invitationStyle();
2110 incidence[QStringLiteral("head")] = headerVisitor.result();
2111
2112 // determine if I am the organizer for this invitation
2113 bool myInc = iamOrganizer(inc);
2114
2115 // determine if the invitation response has already been recorded
2116 bool rsvpRec = false;
2117 Attendee ea;
2118 if (!myInc) {
2119 Incidence::Ptr rsvpIncidence = existingIncidence;
2120 if (!rsvpIncidence && inc && incRevision > 0) {
2121 rsvpIncidence = inc;
2122 }
2123 if (rsvpIncidence) {
2124 ea = findMyAttendee(rsvpIncidence);
2125 }
2126 if (!ea.isNull() && (ea.status() == Attendee::Accepted || ea.status() == Attendee::Declined || ea.status() == Attendee::Tentative)) {
2127 rsvpRec = true;
2128 }
2129 }
2130
2131 // determine invitation role
2132 QString role;
2133 bool isDelegated = false;
2134 Attendee a = findMyAttendee(inc);
2135 if (a.isNull() && inc) {
2136 if (!inc->attendees().isEmpty()) {
2137 a = inc->attendees().at(0);
2138 }
2139 }
2140 if (!a.isNull()) {
2141 isDelegated = (a.status() == Attendee::Delegated);
2142 role = Stringify::attendeeRole(a.role());
2143 }
2144
2145 // determine if RSVP needed, not-needed, or response already recorded
2146 bool rsvpReq = rsvpRequested(inc);
2147 if (!rsvpReq && !a.isNull() && a.status() == Attendee::NeedsAction) {
2148 rsvpReq = true;
2149 }
2150
2151 QString eventInfo;
2152 if (!myInc && !a.isNull()) {
2153 if (rsvpRec && inc) {
2154 if (incRevision == 0) {
2155 eventInfo = i18n("Your <b>%1</b> response has been recorded.", Stringify::attendeeStatus(ea.status()));
2156 } else {
2157 eventInfo = i18n("Your status for this invitation is <b>%1</b>.", Stringify::attendeeStatus(ea.status()));
2158 }
2159 rsvpReq = false;
2160 } else if (msg->method() == iTIPCancel) {
2161 eventInfo = i18n("This invitation was canceled.");
2162 } else if (msg->method() == iTIPAdd) {
2163 eventInfo = i18n("This invitation was accepted.");
2164 } else if (msg->method() == iTIPDeclineCounter) {
2165 rsvpReq = true;
2166 eventInfo = rsvpRequestedStr(rsvpReq, role);
2167 } else {
2168 if (!isDelegated) {
2169 eventInfo = rsvpRequestedStr(rsvpReq, role);
2170 } else {
2171 eventInfo = i18n("Awaiting delegation response.");
2172 }
2173 }
2174 }
2175 incidence[QStringLiteral("eventInfo")] = eventInfo;
2176
2177 // Print if the organizer gave you a preset status
2178 QString myStatus;
2179 if (!myInc) {
2180 if (inc && incRevision == 0) {
2181 myStatus = myStatusStr(inc);
2182 }
2183 }
2184 incidence[QStringLiteral("myStatus")] = myStatus;
2185
2186 // Add groupware links
2187 QVariantList buttons;
2188 switch (msg->method()) {
2189 case iTIPPublish:
2190 case iTIPRequest:
2191 case iTIPRefresh:
2192 case iTIPAdd:
2193 if (inc && incRevision > 0 && (existingIncidence || !helper->calendar())) {
2194 buttons += recordButtons(inc, helper);
2195 }
2196
2197 if (!myInc) {
2198 if (!a.isNull()) {
2199 buttons += responseButtons(inc, rsvpReq, rsvpRec, helper);
2200 } else {
2201 buttons += responseButtons(inc, false, false, helper);
2202 }
2203 }
2204 break;
2205
2206 case iTIPCancel:
2207 buttons = cancelButtons(inc, helper);
2208 break;
2209
2210 case iTIPReply: {
2211 // Record invitation response
2212 Attendee a;
2213 Attendee ea;
2214 if (inc) {
2215 // First, determine if this reply is really a counter in disguise.
2216 if (replyMeansCounter(inc)) {
2217 buttons = counterButtons(helper);
2218 break;
2219 }
2220
2221 // Next, maybe this is a declined reply that was delegated from me?
2222 // find first attendee who is delegated-from me
2223 // look a their PARTSTAT response, if the response is declined,
2224 // then we need to start over which means putting all the action
2225 // buttons and NOT putting on the [Record response..] button
2226 a = findDelegatedFromMyAttendee(inc);
2227 if (!a.isNull()) {
2228 if (a.status() != Attendee::Accepted || a.status() != Attendee::Tentative) {
2229 buttons = responseButtons(inc, rsvpReq, rsvpRec, helper);
2230 break;
2231 }
2232 }
2233
2234 // Finally, simply allow a Record of the reply
2235 if (!inc->attendees().isEmpty()) {
2236 a = inc->attendees().at(0);
2237 }
2238 if (!a.isNull() && helper->calendar()) {
2239 ea = findAttendee(existingIncidence, a.email());
2240 }
2241 }
2242 if (!ea.isNull() && (ea.status() != Attendee::NeedsAction) && (ea.status() == a.status())) {
2243 const QString tStr = i18n("The <b>%1</b> response has been recorded", Stringify::attendeeStatus(ea.status()));
2244 buttons << inviteButton(QString(), tStr, QString(), helper);
2245 } else {
2246 if (inc) {
2247 buttons = recordResponseButtons(inc, helper);
2248 }
2249 }
2250 break;
2251 }
2252
2253 case iTIPCounter:
2254 // Counter proposal
2255 buttons = counterButtons(helper);
2256 break;
2257
2258 case iTIPDeclineCounter:
2259 buttons << responseButtons(inc, rsvpReq, rsvpRec, helper);
2260 break;
2261
2262 case iTIPNoMethod:
2263 break;
2264 }
2265
2266 incidence[QStringLiteral("buttons")] = buttons;
2267
2268 // Add the attendee list
2269 if (inc->type() == Incidence::TypeTodo) {
2270 incidence[QStringLiteral("attendeesTitle")] = i18n("Assignees:");
2271 } else {
2272 incidence[QStringLiteral("attendeesTitle")] = i18n("Participants:");
2273 }
2274 if (myInc) {
2275 incidence[QStringLiteral("attendees")] = invitationRsvpList(existingIncidence, a);
2276 } else {
2277 incidence[QStringLiteral("attendees")] = invitationAttendeeList(inc);
2278 }
2279
2280 // Add the attachment list
2281 incidence[QStringLiteral("attachments")] = invitationAttachments(inc, helper);
2282
2283 if (!inc->comments().isEmpty()) {
2284 incidence[QStringLiteral("comments")] = inc->comments();
2285 }
2286
2287 QString templateName;
2288 switch (inc->type()) {
2290 templateName = QStringLiteral(":/org.kde.pim/kcalutils/itip_event.html");
2291 break;
2293 templateName = QStringLiteral(":/org.kde.pim/kcalutils/itip_todo.html");
2294 break;
2296 templateName = QStringLiteral(":/org.kde.pim/kcalutils/itip_journal.html");
2297 break;
2299 templateName = QStringLiteral(":/org.kde.pim/kcalutils/itip_freebusy.html");
2300 break;
2302 return QString();
2303 }
2304
2305 return GrantleeTemplateManager::instance()->render(templateName, incidence);
2306}
2307
2308//@endcond
2309
2311{
2312 return formatICalInvitationHelper(invitation, calendar, helper, false, QString());
2313}
2314
2316 const Calendar::Ptr &calendar,
2318 const QString &sender)
2319{
2320 return formatICalInvitationHelper(invitation, calendar, helper, true, sender);
2321}
2322
2323/*******************************************************************
2324 * Helper functions for the Incidence tooltips
2325 *******************************************************************/
2326
2327//@cond PRIVATE
2328class KCalUtils::IncidenceFormatter::ToolTipVisitor : public Visitor
2329{
2330public:
2331 ToolTipVisitor() = default;
2332
2333 bool act(const Calendar::Ptr &calendar, const IncidenceBase::Ptr &incidence, QDate date = QDate(), bool richText = true)
2334 {
2335 mCalendar = calendar;
2336 mLocation.clear();
2337 mDate = date;
2338 mRichText = richText;
2339 mResult = QLatin1StringView("");
2340 return incidence ? incidence->accept(*this, incidence) : false;
2341 }
2342
2343 bool act(const QString &location, const IncidenceBase::Ptr &incidence, QDate date = QDate(), bool richText = true)
2344 {
2345 mLocation = location;
2346 mDate = date;
2347 mRichText = richText;
2348 mResult = QLatin1StringView("");
2349 return incidence ? incidence->accept(*this, incidence) : false;
2350 }
2351
2352 [[nodiscard]] QString result() const
2353 {
2354 return mResult;
2355 }
2356
2357protected:
2358 bool visit(const Event::Ptr &event) override;
2359 bool visit(const Todo::Ptr &todo) override;
2360 bool visit(const Journal::Ptr &journal) override;
2361 bool visit(const FreeBusy::Ptr &fb) override;
2362
2363 [[nodiscard]] QString dateRangeText(const Event::Ptr &event, QDate date);
2364 [[nodiscard]] QString dateRangeText(const Todo::Ptr &todo, QDate asOfDate);
2365 [[nodiscard]] QString dateRangeText(const Journal::Ptr &journal);
2366 [[nodiscard]] QString dateRangeText(const FreeBusy::Ptr &fb);
2367
2368 [[nodiscard]] QString generateToolTip(const Incidence::Ptr &incidence, const QString &dtRangeText);
2369
2370protected:
2371 Calendar::Ptr mCalendar;
2372 QString mLocation;
2373 QDate mDate;
2374 bool mRichText = true;
2375 QString mResult;
2376};
2377
2378QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Event::Ptr &event, QDate date)
2379{
2380 // FIXME: support mRichText==false
2381 QString ret;
2382 QString tmp;
2383
2384 const auto startDts = event->startDateTimesForDate(date, QTimeZone::systemTimeZone());
2385 const auto startDt = startDts.empty() ? event->dtStart().toLocalTime() : startDts[0].toLocalTime();
2386 const auto endDt = event->endDateForStart(startDt).toLocalTime();
2387
2388 if (event->isMultiDay()) {
2389 tmp = dateToString(startDt.date(), true);
2390 ret += QLatin1StringView("<br>") + i18nc("Event start", "<i>From:</i> %1", tmp);
2391
2392 tmp = dateToString(endDt.date(), true);
2393 ret += QLatin1StringView("<br>") + i18nc("Event end", "<i>To:</i> %1", tmp);
2394 } else {
2395 ret += QLatin1StringView("<br>") + i18n("<i>Date:</i> %1", dateToString(startDt.date(), false));
2396 if (!event->allDay()) {
2397 const QString dtStartTime = timeToString(startDt.time(), true);
2398 const QString dtEndTime = timeToString(endDt.time(), true);
2399 if (dtStartTime == dtEndTime) {
2400 // to prevent 'Time: 17:00 - 17:00'
2401 tmp = QLatin1StringView("<br>") + i18nc("time for event", "<i>Time:</i> %1", dtStartTime);
2402 } else {
2403 tmp = QLatin1StringView("<br>") + i18nc("time range for event", "<i>Time:</i> %1 - %2", dtStartTime, dtEndTime);
2404 }
2405 ret += tmp;
2406 }
2407 }
2408 return ret.replace(QLatin1Char(' '), QLatin1StringView("&nbsp;"));
2409}
2410
2411QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Todo::Ptr &todo, QDate asOfDate)
2412{
2413 // FIXME: support mRichText==false
2414 // FIXME: doesn't handle to-dos that occur more than once per day.
2415
2416 QDateTime startDt{todo->dtStart(false)};
2417 QDateTime dueDt{todo->dtDue(false)};
2418
2419 if (todo->recurs() && asOfDate.isValid()) {
2420 const QDateTime limit{asOfDate.addDays(1), QTime(0, 0, 0), QTimeZone::LocalTime};
2421 startDt = todo->recurrence()->getPreviousDateTime(limit);
2422 if (startDt.isValid() && todo->hasDueDate()) {
2423 if (todo->allDay()) {
2424 // Days, not seconds, because not all days are 24 hours long.
2425 const auto duration{todo->dtStart(true).daysTo(todo->dtDue(true))};
2426 dueDt = startDt.addDays(duration);
2427 } else {
2428 const auto duration{todo->dtStart(true).secsTo(todo->dtDue(true))};
2429 dueDt = startDt.addSecs(duration);
2430 }
2431 }
2432 }
2433
2434 QString ret;
2435 if (startDt.isValid()) {
2436 ret = QLatin1StringView("<br>") % i18nc("To-do's start date", "<i>Start:</i> %1", dateTimeToString(startDt, todo->allDay(), false));
2437 }
2438 if (dueDt.isValid()) {
2439 ret += QLatin1StringView("<br>") % i18nc("To-do's due date", "<i>Due:</i> %1", dateTimeToString(dueDt, todo->allDay(), false));
2440 }
2441
2442 // Print priority and completed info here, for lack of a better place
2443
2444 if (todo->priority() > 0) {
2445 ret += QLatin1StringView("<br>") % i18nc("To-do's priority number", "<i>Priority:</i> %1", QString::number(todo->priority()));
2446 }
2447
2448 ret += QLatin1StringView("<br>");
2449 if (todo->hasCompletedDate()) {
2450 ret += i18nc("To-do's completed date", "<i>Completed:</i> %1", dateTimeToString(todo->completed(), false, false));
2451 } else {
2452 int pct = todo->percentComplete();
2453 if (todo->recurs() && asOfDate.isValid()) {
2454 const QDate recurrenceDate = todo->dtRecurrence().date();
2455 if (recurrenceDate < startDt.date()) {
2456 pct = 0;
2457 } else if (recurrenceDate > startDt.date()) {
2458 pct = 100;
2459 }
2460 }
2461 ret += i18nc("To-do's percent complete:", "<i>Percent Done:</i> %1%", pct);
2462 }
2463
2464 return ret.replace(QLatin1Char(' '), QLatin1StringView("&nbsp;"));
2465}
2466
2467QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Journal::Ptr &journal)
2468{
2469 // FIXME: support mRichText==false
2470 QString ret;
2471 if (journal->dtStart().isValid()) {
2472 ret += QLatin1StringView("<br>") + i18n("<i>Date:</i> %1", dateToString(journal->dtStart().toLocalTime().date(), false));
2473 }
2474 return ret.replace(QLatin1Char(' '), QLatin1StringView("&nbsp;"));
2475}
2476
2477QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const FreeBusy::Ptr &fb)
2478{
2479 // FIXME: support mRichText==false
2480 QString ret = QLatin1StringView("<br>") + i18n("<i>Period start:</i> %1", QLocale().toString(fb->dtStart(), QLocale::ShortFormat));
2481 ret += QLatin1StringView("<br>") + i18n("<i>Period start:</i> %1", QLocale().toString(fb->dtEnd(), QLocale::ShortFormat));
2482 return ret.replace(QLatin1Char(' '), QLatin1StringView("&nbsp;"));
2483}
2484
2486{
2487 mResult = generateToolTip(event, dateRangeText(event, mDate));
2488 return !mResult.isEmpty();
2489}
2490
2492{
2493 mResult = generateToolTip(todo, dateRangeText(todo, mDate));
2494 return !mResult.isEmpty();
2495}
2496
2498{
2499 mResult = generateToolTip(journal, dateRangeText(journal));
2500 return !mResult.isEmpty();
2501}
2502
2504{
2505 // FIXME: support mRichText==false
2506 mResult = QLatin1StringView("<qt><b>") + i18n("Free/Busy information for %1", fb->organizer().fullName()) + QLatin1StringView("</b>");
2507 mResult += dateRangeText(fb);
2508 mResult += QLatin1StringView("</qt>");
2509 return !mResult.isEmpty();
2510}
2511
2512[[nodiscard]] static QString tooltipPerson(const QString &email, const QString &name, Attendee::PartStat status)
2513{
2514 // Search for a new print name, if needed.
2515 const QString printName = searchName(email, name);
2516
2517 // Get the icon corresponding to the attendee participation status.
2518 const QString iconPath = KIconLoader::global()->iconPath(rsvpStatusIconName(status), KIconLoader::Small);
2519
2520 // Make the return string.
2521 QString personString;
2522 if (!iconPath.isEmpty()) {
2523 personString += QLatin1StringView(R"(<img valign="top" src=")") + iconPath + QLatin1StringView("\">") + QLatin1StringView("&nbsp;");
2524 }
2525 if (status != Attendee::None) {
2526 personString += i18nc("attendee name (attendee status)", "%1 (%2)", printName.isEmpty() ? email : printName, Stringify::attendeeStatus(status));
2527 } else {
2528 personString += i18n("%1", printName.isEmpty() ? email : printName);
2529 }
2530 return personString;
2531}
2532
2533[[nodiscard]] static QString tooltipFormatOrganizer(const QString &email, const QString &name)
2534{
2535 // Search for a new print name, if needed
2536 const QString printName = searchName(email, name);
2537
2538 // Get the icon for organizer
2539 // TODO fixme laurent: use another icon. It doesn't exist in breeze.
2540 const QString iconPath = KIconLoader::global()->iconPath(QStringLiteral("meeting-organizer"), KIconLoader::Small, true);
2541
2542 // Make the return string.
2543 QString personString;
2544 if (!iconPath.isEmpty()) {
2545 personString += QLatin1StringView(R"(<img valign="top" src=")") + iconPath + QLatin1StringView("\">") + QLatin1StringView("&nbsp;");
2546 }
2547 personString += (printName.isEmpty() ? email : printName);
2548 return personString;
2549}
2550
2551[[nodiscard]] static QString tooltipFormatAttendeeRoleList(const Incidence::Ptr &incidence, Attendee::Role role, bool showStatus)
2552{
2553 int maxNumAtts = 8; // maximum number of people to print per attendee role
2554 const QString etc = i18nc("ellipsis", "...");
2555
2556 int i = 0;
2557 QString tmpStr;
2558 const Attendee::List attendees = incidence->attendees();
2559 for (const auto &a : attendees) {
2560 if (a.role() != role) {
2561 // skip not this role
2562 continue;
2563 }
2564 if (attendeeIsOrganizer(incidence, a)) {
2565 // skip attendee that is also the organizer
2566 continue;
2567 }
2568 if (i == maxNumAtts) {
2569 tmpStr += QLatin1StringView("&nbsp;&nbsp;") + etc;
2570 break;
2571 }
2572 tmpStr += QLatin1StringView("&nbsp;&nbsp;") + tooltipPerson(a.email(), a.name(), showStatus ? a.status() : Attendee::None);
2573 if (!a.delegator().isEmpty()) {
2574 tmpStr += i18n(" (delegated by %1)", a.delegator());
2575 }
2576 if (!a.delegate().isEmpty()) {
2577 tmpStr += i18n(" (delegated to %1)", a.delegate());
2578 }
2579 tmpStr += QLatin1StringView("<br>");
2580 i++;
2581 }
2582 if (tmpStr.endsWith(QLatin1StringView("<br>"))) {
2583 tmpStr.chop(4);
2584 }
2585 return tmpStr;
2586}
2587
2588[[nodiscard]] static QString tooltipFormatAttendees(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence)
2589{
2590 QString tmpStr;
2591 QString str;
2592
2593 // Add organizer link
2594 const int attendeeCount = incidence->attendees().count();
2595 if (attendeeCount > 1 || (attendeeCount == 1 && !attendeeIsOrganizer(incidence, incidence->attendees().at(0)))) {
2596 tmpStr += QLatin1StringView("<i>") + i18n("Organizer:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2597 tmpStr += QLatin1StringView("&nbsp;&nbsp;") + tooltipFormatOrganizer(incidence->organizer().email(), incidence->organizer().name());
2598 }
2599
2600 // Show the attendee status if the incidence's organizer owns the resource calendar,
2601 // which means they are running the show and have all the up-to-date response info.
2602 const bool showStatus = attendeeCount > 0 && incOrganizerOwnsCalendar(calendar, incidence);
2603
2604 // Add "chair"
2605 str = tooltipFormatAttendeeRoleList(incidence, Attendee::Chair, showStatus);
2606 if (!str.isEmpty()) {
2607 tmpStr += QLatin1StringView("<br><i>") + i18n("Chair:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2608 tmpStr += str;
2609 }
2610
2611 // Add required participants
2612 str = tooltipFormatAttendeeRoleList(incidence, Attendee::ReqParticipant, showStatus);
2613 if (!str.isEmpty()) {
2614 tmpStr += QLatin1StringView("<br><i>") + i18n("Required Participants:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2615 tmpStr += str;
2616 }
2617
2618 // Add optional participants
2619 str = tooltipFormatAttendeeRoleList(incidence, Attendee::OptParticipant, showStatus);
2620 if (!str.isEmpty()) {
2621 tmpStr += QLatin1StringView("<br><i>") + i18n("Optional Participants:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2622 tmpStr += str;
2623 }
2624
2625 // Add observers
2626 str = tooltipFormatAttendeeRoleList(incidence, Attendee::NonParticipant, showStatus);
2627 if (!str.isEmpty()) {
2628 tmpStr += QLatin1StringView("<br><i>") + i18n("Observers:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2629 tmpStr += str;
2630 }
2631
2632 return tmpStr;
2633}
2634
2635QString IncidenceFormatter::ToolTipVisitor::generateToolTip(const Incidence::Ptr &incidence, const QString &dtRangeText)
2636{
2637 // FIXME: support mRichText==false
2638 if (!incidence) {
2639 return QString();
2640 }
2641
2642 QString tmp = QStringLiteral("<qt>");
2643
2644 // header
2645 tmp += QLatin1StringView("<b>") + incidence->richSummary() + QLatin1StringView("</b>");
2646 tmp += QLatin1StringView("<hr>");
2647
2648 QString calStr = mLocation;
2649 if (mCalendar) {
2650 calStr = resourceString(mCalendar, incidence);
2651 }
2652 if (!calStr.isEmpty()) {
2653 tmp += QLatin1StringView("<i>") + i18n("Calendar:") + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2654 tmp += calStr;
2655 }
2656
2657 tmp += dtRangeText;
2658
2659 if (!incidence->location().isEmpty()) {
2660 tmp += QLatin1StringView("<br>");
2661 tmp += QLatin1StringView("<i>") + i18n("Location:") + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2662 tmp += incidence->richLocation();
2663 }
2664
2665 QString durStr = durationString(incidence);
2666 if (!durStr.isEmpty()) {
2667 tmp += QLatin1StringView("<br>");
2668 tmp += QLatin1StringView("<i>") + i18n("Duration:") + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2669 tmp += durStr;
2670 }
2671
2672 if (incidence->recurs()) {
2673 tmp += QLatin1StringView("<br>");
2674 tmp += QLatin1StringView("<i>") + i18n("Recurrence:") + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2675 tmp += recurrenceString(incidence);
2676 }
2677
2678 if (incidence->hasRecurrenceId()) {
2679 tmp += QLatin1StringView("<br>");
2680 tmp += QLatin1StringView("<i>") + i18n("Recurrence:") + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2681 tmp += i18n("Exception");
2682 }
2683
2684 if (!incidence->description().isEmpty()) {
2685 QString desc(incidence->description());
2686 if (!incidence->descriptionIsRich()) {
2687 int maxDescLen = 120; // maximum description chars to print (before ellipsis)
2688 if (desc.length() > maxDescLen) {
2689 desc = desc.left(maxDescLen) + i18nc("ellipsis", "...");
2690 }
2691 desc = desc.toHtmlEscaped().replace(QLatin1Char('\n'), QLatin1StringView("<br>"));
2692 } else {
2693 // TODO: truncate the description when it's rich text
2694 }
2695 tmp += QLatin1StringView("<hr>");
2696 tmp += QLatin1StringView("<i>") + i18n("Description:") + QLatin1StringView("</i>") + QLatin1StringView("<br>");
2697 tmp += desc;
2698 }
2699
2700 bool needAnHorizontalLine = true;
2701 const int reminderCount = incidence->alarms().count();
2702 if (reminderCount > 0 && incidence->hasEnabledAlarms()) {
2703 if (needAnHorizontalLine) {
2704 tmp += QLatin1StringView("<hr>");
2705 needAnHorizontalLine = false;
2706 }
2707 tmp += QLatin1StringView("<br>");
2708 tmp += QLatin1StringView("<i>") + i18np("Reminder:", "Reminders:", reminderCount) + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2709 tmp += reminderStringList(incidence).join(QLatin1StringView(", "));
2710 }
2711
2712 const QString attendees = tooltipFormatAttendees(mCalendar, incidence);
2713 if (!attendees.isEmpty()) {
2714 if (needAnHorizontalLine) {
2715 tmp += QLatin1StringView("<hr>");
2716 needAnHorizontalLine = false;
2717 }
2718 tmp += QLatin1StringView("<br>");
2719 tmp += attendees;
2720 }
2721
2722 int categoryCount = incidence->categories().count();
2723 if (categoryCount > 0) {
2724 if (needAnHorizontalLine) {
2725 tmp += QLatin1StringView("<hr>");
2726 }
2727 tmp += QLatin1StringView("<br>");
2728 tmp += QLatin1StringView("<i>") + i18np("Tag:", "Tags:", categoryCount) + QLatin1StringView("</i>") + QLatin1StringView("&nbsp;");
2729 tmp += incidence->categories().join(QLatin1StringView(", "));
2730 }
2731
2732 tmp += QLatin1StringView("</qt>");
2733 return tmp;
2734}
2735
2736//@endcond
2737
2738QString IncidenceFormatter::toolTipStr(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date, bool richText)
2739{
2740 ToolTipVisitor v;
2741 if (incidence && v.act(sourceName, incidence, date, richText)) {
2742 return v.result();
2743 } else {
2744 return QString();
2745 }
2746}
2747
2748/*******************************************************************
2749 * Helper functions for the Incidence tooltips
2750 *******************************************************************/
2751
2752//@cond PRIVATE
2753static QString mailBodyIncidence(const Incidence::Ptr &incidence)
2754{
2755 QString body;
2756 if (!incidence->summary().trimmed().isEmpty()) {
2757 body += i18n("Summary: %1\n", incidence->richSummary());
2758 }
2759 if (!incidence->organizer().isEmpty()) {
2760 body += i18n("Organizer: %1\n", incidence->organizer().fullName());
2761 }
2762 if (!incidence->location().trimmed().isEmpty()) {
2763 body += i18n("Location: %1\n", incidence->richLocation());
2764 }
2765 return body;
2766}
2767
2768//@endcond
2769
2770//@cond PRIVATE
2771class KCalUtils::IncidenceFormatter::MailBodyVisitor : public Visitor
2772{
2773public:
2774 bool act(const IncidenceBase::Ptr &incidence)
2775 {
2776 mResult = QLatin1StringView("");
2777 return incidence ? incidence->accept(*this, incidence) : false;
2778 }
2779
2780 [[nodiscard]] QString result() const
2781 {
2782 return mResult;
2783 }
2784
2785protected:
2786 bool visit(const Event::Ptr &event) override;
2787 bool visit(const Todo::Ptr &todo) override;
2788 bool visit(const Journal::Ptr &journal) override;
2789 bool visit(const FreeBusy::Ptr &) override
2790 {
2791 mResult = i18n("This is a Free Busy Object");
2792 return true;
2793 }
2794
2795protected:
2796 QString mResult;
2797};
2798
2800{
2801 QString recurrence[] = {i18nc("no recurrence", "None"),
2802 i18nc("event recurs by minutes", "Minutely"),
2803 i18nc("event recurs by hours", "Hourly"),
2804 i18nc("event recurs by days", "Daily"),
2805 i18nc("event recurs by weeks", "Weekly"),
2806 i18nc("event recurs same position (e.g. first monday) each month", "Monthly Same Position"),
2807 i18nc("event recurs same day each month", "Monthly Same Day"),
2808 i18nc("event recurs same month each year", "Yearly Same Month"),
2809 i18nc("event recurs same day each year", "Yearly Same Day"),
2810 i18nc("event recurs same position (e.g. first monday) each year", "Yearly Same Position")};
2811
2812 mResult = mailBodyIncidence(event);
2813 mResult += i18n("Start Date: %1\n", dateToString(event->dtStart().toLocalTime().date(), true));
2814 if (!event->allDay()) {
2815 mResult += i18n("Start Time: %1\n", timeToString(event->dtStart().toLocalTime().time(), true));
2816 }
2817 if (event->dtStart() != event->dtEnd()) {
2818 mResult += i18n("End Date: %1\n", dateToString(event->dtEnd().toLocalTime().date(), true));
2819 }
2820 if (!event->allDay()) {
2821 mResult += i18n("End Time: %1\n", timeToString(event->dtEnd().toLocalTime().time(), true));
2822 }
2823 if (event->recurs()) {
2824 Recurrence *recur = event->recurrence();
2825 // TODO: Merge these two to one of the form "Recurs every 3 days"
2826 mResult += i18n("Recurs: %1\n", recurrence[recur->recurrenceType()]);
2827 mResult += i18n("Frequency: %1\n", event->recurrence()->frequency());
2828
2829 if (recur->duration() > 0) {
2830 mResult += i18np("Repeats once", "Repeats %1 times", recur->duration());
2831 mResult += QLatin1Char('\n');
2832 } else {
2833 if (recur->duration() != -1) {
2834 // TODO_Recurrence: What to do with all-day
2835 QString endstr;
2836 if (event->allDay()) {
2837 endstr = QLocale().toString(recur->endDate());
2838 } else {
2839 endstr = QLocale().toString(recur->endDateTime(), QLocale::ShortFormat);
2840 }
2841 mResult += i18n("Repeat until: %1\n", endstr);
2842 } else {
2843 mResult += i18n("Repeats forever\n");
2844 }
2845 }
2846 }
2847
2848 if (!event->description().isEmpty()) {
2849 QString descStr;
2850 if (event->descriptionIsRich() || event->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) {
2851 descStr = cleanHtml(event->description());
2852 } else {
2853 descStr = event->description();
2854 }
2855 if (!descStr.isEmpty()) {
2856 mResult += i18n("Details:\n%1\n", descStr);
2857 }
2858 }
2859 return !mResult.isEmpty();
2860}
2861
2863{
2864 mResult = mailBodyIncidence(todo);
2865
2866 if (todo->hasStartDate() && todo->dtStart().isValid()) {
2867 mResult += i18n("Start Date: %1\n", dateToString(todo->dtStart(false).toLocalTime().date(), true));
2868 if (!todo->allDay()) {
2869 mResult += i18n("Start Time: %1\n", timeToString(todo->dtStart(false).toLocalTime().time(), true));
2870 }
2871 }
2872 if (todo->hasDueDate() && todo->dtDue().isValid()) {
2873 mResult += i18n("Due Date: %1\n", dateToString(todo->dtDue().toLocalTime().date(), true));
2874 if (!todo->allDay()) {
2875 mResult += i18n("Due Time: %1\n", timeToString(todo->dtDue().toLocalTime().time(), true));
2876 }
2877 }
2878 QString details = todo->richDescription();
2879 if (!details.isEmpty()) {
2880 mResult += i18n("Details:\n%1\n", details);
2881 }
2882 return !mResult.isEmpty();
2883}
2884
2886{
2887 mResult = mailBodyIncidence(journal);
2888 mResult += i18n("Date: %1\n", dateToString(journal->dtStart().toLocalTime().date(), true));
2889 if (!journal->allDay()) {
2890 mResult += i18n("Time: %1\n", timeToString(journal->dtStart().toLocalTime().time(), true));
2891 }
2892 if (!journal->description().isEmpty()) {
2893 mResult += i18n("Text of the journal:\n%1\n", journal->richDescription());
2894 }
2895 return true;
2896}
2897
2898//@endcond
2899
2901{
2902 if (!incidence) {
2903 return QString();
2904 }
2905
2906 MailBodyVisitor v;
2907 if (v.act(incidence)) {
2908 return v.result();
2909 }
2910 return QString();
2911}
2912
2913//@cond PRIVATE
2914[[nodiscard]] static QString recurEnd(const Incidence::Ptr &incidence)
2915{
2916 QString endstr;
2917 if (incidence->allDay()) {
2918 endstr = QLocale().toString(incidence->recurrence()->endDate());
2919 } else {
2920 endstr = QLocale().toString(incidence->recurrence()->endDateTime().toLocalTime(), QLocale::ShortFormat);
2921 }
2922 return endstr;
2923}
2924
2925//@endcond
2926
2927/************************************
2928 * More static formatting functions
2929 ************************************/
2930
2932{
2933 if (incidence->hasRecurrenceId()) {
2934 return QStringLiteral("Recurrence exception");
2935 }
2936
2937 if (!incidence->recurs()) {
2938 return i18n("No recurrence");
2939 }
2940 static QStringList dayList;
2941 if (dayList.isEmpty()) {
2942 dayList.append(i18n("31st Last"));
2943 dayList.append(i18n("30th Last"));
2944 dayList.append(i18n("29th Last"));
2945 dayList.append(i18n("28th Last"));
2946 dayList.append(i18n("27th Last"));
2947 dayList.append(i18n("26th Last"));
2948 dayList.append(i18n("25th Last"));
2949 dayList.append(i18n("24th Last"));
2950 dayList.append(i18n("23rd Last"));
2951 dayList.append(i18n("22nd Last"));
2952 dayList.append(i18n("21st Last"));
2953 dayList.append(i18n("20th Last"));
2954 dayList.append(i18n("19th Last"));
2955 dayList.append(i18n("18th Last"));
2956 dayList.append(i18n("17th Last"));
2957 dayList.append(i18n("16th Last"));
2958 dayList.append(i18n("15th Last"));
2959 dayList.append(i18n("14th Last"));
2960 dayList.append(i18n("13th Last"));
2961 dayList.append(i18n("12th Last"));
2962 dayList.append(i18n("11th Last"));
2963 dayList.append(i18n("10th Last"));
2964 dayList.append(i18n("9th Last"));
2965 dayList.append(i18n("8th Last"));
2966 dayList.append(i18n("7th Last"));
2967 dayList.append(i18n("6th Last"));
2968 dayList.append(i18n("5th Last"));
2969 dayList.append(i18n("4th Last"));
2970 dayList.append(i18n("3rd Last"));
2971 dayList.append(i18n("2nd Last"));
2972 dayList.append(i18nc("last day of the month", "Last"));
2973 dayList.append(i18nc("unknown day of the month", "unknown")); // #31 - zero offset from UI
2974 dayList.append(i18n("1st"));
2975 dayList.append(i18n("2nd"));
2976 dayList.append(i18n("3rd"));
2977 dayList.append(i18n("4th"));
2978 dayList.append(i18n("5th"));
2979 dayList.append(i18n("6th"));
2980 dayList.append(i18n("7th"));
2981 dayList.append(i18n("8th"));
2982 dayList.append(i18n("9th"));
2983 dayList.append(i18n("10th"));
2984 dayList.append(i18n("11th"));
2985 dayList.append(i18n("12th"));
2986 dayList.append(i18n("13th"));
2987 dayList.append(i18n("14th"));
2988 dayList.append(i18n("15th"));
2989 dayList.append(i18n("16th"));
2990 dayList.append(i18n("17th"));
2991 dayList.append(i18n("18th"));
2992 dayList.append(i18n("19th"));
2993 dayList.append(i18n("20th"));
2994 dayList.append(i18n("21st"));
2995 dayList.append(i18n("22nd"));
2996 dayList.append(i18n("23rd"));
2997 dayList.append(i18n("24th"));
2998 dayList.append(i18n("25th"));
2999 dayList.append(i18n("26th"));
3000 dayList.append(i18n("27th"));
3001 dayList.append(i18n("28th"));
3002 dayList.append(i18n("29th"));
3003 dayList.append(i18n("30th"));
3004 dayList.append(i18n("31st"));
3005 }
3006
3007 const int weekStart = QLocale().firstDayOfWeek();
3008 QString dayNames;
3009
3010 Recurrence *recur = incidence->recurrence();
3011
3012 QString recurStr;
3013 static QString noRecurrence = i18n("No recurrence");
3014 switch (recur->recurrenceType()) {
3015 case Recurrence::rNone:
3016 return noRecurrence;
3017
3018 case Recurrence::rMinutely:
3019 if (recur->duration() != -1) {
3020 recurStr = i18np("Recurs every minute until %2", "Recurs every %1 minutes until %2", recur->frequency(), recurEnd(incidence));
3021 if (recur->duration() > 0) {
3022 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration()));
3023 }
3024 } else {
3025 recurStr = i18np("Recurs every minute", "Recurs every %1 minutes", recur->frequency());
3026 }
3027 break;
3028
3029 case Recurrence::rHourly:
3030 if (recur->duration() != -1) {
3031 recurStr = i18np("Recurs hourly until %2", "Recurs every %1 hours until %2", recur->frequency(), recurEnd(incidence));
3032 if (recur->duration() > 0) {
3033 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration()));
3034 }
3035 } else {
3036 recurStr = i18np("Recurs hourly", "Recurs every %1 hours", recur->frequency());
3037 }
3038 break;
3039
3040 case Recurrence::rDaily:
3041 if (recur->duration() != -1) {
3042 recurStr = i18np("Recurs daily until %2", "Recurs every %1 days until %2", recur->frequency(), recurEnd(incidence));
3043 if (recur->duration() > 0) {
3044 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration()));
3045 }
3046 } else {
3047 recurStr = i18np("Recurs daily", "Recurs every %1 days", recur->frequency());
3048 }
3049 break;
3050
3051 case Recurrence::rWeekly: {
3052 bool addSpace = false;
3053 for (int i = 0; i < 7; ++i) {
3054 if (recur->days().testBit((i + weekStart + 6) % 7)) {
3055 if (addSpace) {
3056 dayNames.append(i18nc("separator for list of days", ", "));
3057 }
3058 dayNames.append(QLocale().dayName(((i + weekStart + 6) % 7) + 1, QLocale::ShortFormat));
3059 addSpace = true;
3060 }
3061 }
3062 if (dayNames.isEmpty()) {
3063 dayNames = i18nc("Recurs weekly on no days", "no days");
3064 }
3065 if (recur->duration() != -1) {
3066 recurStr = i18ncp("Recurs weekly on [list of days] until end-date",
3067 "Recurs weekly on %2 until %3",
3068 "Recurs every %1 weeks on %2 until %3",
3069 recur->frequency(),
3070 dayNames,
3071 recurEnd(incidence));
3072 if (recur->duration() > 0) {
3073 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration());
3074 }
3075 } else {
3076 recurStr = i18ncp("Recurs weekly on [list of days]", "Recurs weekly on %2", "Recurs every %1 weeks on %2", recur->frequency(), dayNames);
3077 }
3078 break;
3079 }
3080 case Recurrence::rMonthlyPos:
3081 if (!recur->monthPositions().isEmpty()) {
3082 RecurrenceRule::WDayPos rule = recur->monthPositions().at(0);
3083 if (recur->duration() != -1) {
3084 recurStr = i18ncp(
3085 "Recurs every N months on the [2nd|3rd|...]"
3086 " weekdayname until end-date",
3087 "Recurs every month on the %2 %3 until %4",
3088 "Recurs every %1 months on the %2 %3 until %4",
3089 recur->frequency(),
3090 dayList[rule.pos() + 31],
3091 QLocale().dayName(rule.day(), QLocale::LongFormat),
3092 recurEnd(incidence));
3093 if (recur->duration() > 0) {
3094 recurStr += xi18nc("number of occurrences", " (%1 occurrences)", recur->duration());
3095 }
3096 } else {
3097 recurStr = i18ncp("Recurs every N months on the [2nd|3rd|...] weekdayname",
3098 "Recurs every month on the %2 %3",
3099 "Recurs every %1 months on the %2 %3",
3100 recur->frequency(),
3101 dayList[rule.pos() + 31],
3102 QLocale().dayName(rule.day(), QLocale::LongFormat));
3103 }
3104 }
3105 break;
3106 case Recurrence::rMonthlyDay:
3107 if (!recur->monthDays().isEmpty()) {
3108 int days = recur->monthDays().at(0);
3109 if (recur->duration() != -1) {
3110 recurStr = i18ncp("Recurs monthly on the [1st|2nd|...] day until end-date",
3111 "Recurs monthly on the %2 day until %3",
3112 "Recurs every %1 months on the %2 day until %3",
3113 recur->frequency(),
3114 dayList[days + 31],
3115 recurEnd(incidence));
3116 if (recur->duration() > 0) {
3117 recurStr += xi18nc("number of occurrences", " (%1 occurrences)", recur->duration());
3118 }
3119 } else {
3120 recurStr = i18ncp("Recurs monthly on the [1st|2nd|...] day",
3121 "Recurs monthly on the %2 day",
3122 "Recurs every %1 month on the %2 day",
3123 recur->frequency(),
3124 dayList[days + 31]);
3125 }
3126 }
3127 break;
3128 case Recurrence::rYearlyMonth:
3129 if (recur->duration() != -1) {
3130 if (!recur->yearDates().isEmpty() && !recur->yearMonths().isEmpty()) {
3131 recurStr = i18ncp(
3132 "Recurs Every N years on month-name [1st|2nd|...]"
3133 " until end-date",
3134 "Recurs yearly on %2 %3 until %4",
3135 "Recurs every %1 years on %2 %3 until %4",
3136 recur->frequency(),
3137 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat),
3138 dayList.at(recur->yearDates().at(0) + 31),
3139 recurEnd(incidence));
3140 if (recur->duration() > 0) {
3141 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration());
3142 }
3143 }
3144 } else {
3145 if (!recur->yearDates().isEmpty() && !recur->yearMonths().isEmpty()) {
3146 recurStr = i18ncp("Recurs Every N years on month-name [1st|2nd|...]",
3147 "Recurs yearly on %2 %3",
3148 "Recurs every %1 years on %2 %3",
3149 recur->frequency(),
3150 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat),
3151 dayList[recur->yearDates().at(0) + 31]);
3152 } else {
3153 if (!recur->yearMonths().isEmpty()) {
3154 recurStr = i18nc("Recurs Every year on month-name [1st|2nd|...]",
3155 "Recurs yearly on %1 %2",
3156 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat),
3157 dayList[recur->startDate().day() + 31]);
3158 } else {
3159 recurStr = i18nc("Recurs Every year on month-name [1st|2nd|...]",
3160 "Recurs yearly on %1 %2",
3161 QLocale().monthName(recur->startDate().month(), QLocale::LongFormat),
3162 dayList[recur->startDate().day() + 31]);
3163 }
3164 }
3165 }
3166 break;
3167 case Recurrence::rYearlyDay:
3168 if (!recur->yearDays().isEmpty()) {
3169 if (recur->duration() != -1) {
3170 recurStr = i18ncp("Recurs every N years on day N until end-date",
3171 "Recurs every year on day %2 until %3",
3172 "Recurs every %1 years"
3173 " on day %2 until %3",
3174 recur->frequency(),
3175 QString::number(recur->yearDays().at(0)),
3176 recurEnd(incidence));
3177 if (recur->duration() > 0) {
3178 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration()));
3179 }
3180 } else {
3181 recurStr = i18ncp("Recurs every N YEAR[S] on day N",
3182 "Recurs every year on day %2",
3183 "Recurs every %1 years"
3184 " on day %2",
3185 recur->frequency(),
3186 QString::number(recur->yearDays().at(0)));
3187 }
3188 }
3189 break;
3190 case Recurrence::rYearlyPos:
3191 if (!recur->yearMonths().isEmpty() && !recur->yearPositions().isEmpty()) {
3192 RecurrenceRule::WDayPos rule = recur->yearPositions().at(0);
3193 if (recur->duration() != -1) {
3194 recurStr = i18ncp(
3195 "Every N years on the [2nd|3rd|...] weekdayname "
3196 "of monthname until end-date",
3197 "Every year on the %2 %3 of %4 until %5",
3198 "Every %1 years on the %2 %3 of %4"
3199 " until %5",
3200 recur->frequency(),
3201 dayList[rule.pos() + 31],
3202 QLocale().dayName(rule.day(), QLocale::LongFormat),
3203 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat),
3204 recurEnd(incidence));
3205 if (recur->duration() > 0) {
3206 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration());
3207 }
3208 } else {
3209 recurStr = xi18ncp(
3210 "Every N years on the [2nd|3rd|...] weekdayname "
3211 "of monthname",
3212 "Every year on the %2 %3 of %4",
3213 "Every %1 years on the %2 %3 of %4",
3214 recur->frequency(),
3215 dayList[rule.pos() + 31],
3216 QLocale().dayName(rule.day(), QLocale::LongFormat),
3217 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat));
3218 }
3219 }
3220 break;
3221 }
3222
3223 if (recurStr.isEmpty()) {
3224 recurStr = i18n("Incidence recurs");
3225 }
3226
3227 // Now, append the EXDATEs
3228 const auto l = recur->exDateTimes();
3229 QStringList exStr;
3230 for (auto il = l.cbegin(), end = l.cend(); il != end; ++il) {
3231 switch (recur->recurrenceType()) {
3232 case Recurrence::rMinutely:
3233 exStr << i18n("minute %1", (*il).time().minute());
3234 break;
3235 case Recurrence::rHourly:
3236 exStr << QLocale().toString((*il).time(), QLocale::ShortFormat);
3237 break;
3238 case Recurrence::rWeekly:
3239 exStr << QLocale().dayName((*il).date().dayOfWeek(), QLocale::ShortFormat);
3240 break;
3241 case Recurrence::rYearlyMonth:
3242 exStr << QString::number((*il).date().year());
3243 break;
3244 case Recurrence::rDaily:
3245 case Recurrence::rMonthlyPos:
3246 case Recurrence::rMonthlyDay:
3247 case Recurrence::rYearlyDay:
3248 case Recurrence::rYearlyPos:
3249 exStr << QLocale().toString((*il).date(), QLocale::ShortFormat);
3250 break;
3251 }
3252 }
3253
3254 DateList d = recur->exDates();
3256 const DateList::ConstIterator dlEdnd(d.constEnd());
3257 for (dl = d.constBegin(); dl != dlEdnd; ++dl) {
3258 switch (recur->recurrenceType()) {
3259 case Recurrence::rDaily:
3260 exStr << QLocale().toString((*dl), QLocale::ShortFormat);
3261 break;
3262 case Recurrence::rWeekly:
3263 // exStr << calSys->weekDayName( (*dl), KCalendarSystem::ShortDayName );
3264 // kolab/issue4735, should be ( excluding 3 days ), instead of excluding( Fr,Fr,Fr )
3265 if (exStr.isEmpty()) {
3266 exStr << i18np("1 day", "%1 days", recur->exDates().count());
3267 }
3268 break;
3269 case Recurrence::rMonthlyPos:
3270 exStr << QLocale().toString((*dl), QLocale::ShortFormat);
3271 break;
3272 case Recurrence::rMonthlyDay:
3273 exStr << QLocale().toString((*dl), QLocale::ShortFormat);
3274 break;
3275 case Recurrence::rYearlyMonth:
3276 exStr << QString::number((*dl).year());
3277 break;
3278 case Recurrence::rYearlyDay:
3279 exStr << QLocale().toString((*dl), QLocale::ShortFormat);
3280 break;
3281 case Recurrence::rYearlyPos:
3282 exStr << QLocale().toString((*dl), QLocale::ShortFormat);
3283 break;
3284 }
3285 }
3286
3287 if (!exStr.isEmpty()) {
3288 recurStr = i18n("%1 (excluding %2)", recurStr, exStr.join(QLatin1Char(',')));
3289 }
3290
3291 return recurStr;
3292}
3293
3295{
3296 return QLocale().toString(time, shortfmt ? QLocale::ShortFormat : QLocale::LongFormat);
3297}
3298
3300{
3301 return QLocale().toString(date, (shortfmt ? QLocale::ShortFormat : QLocale::LongFormat));
3302}
3303
3304QString IncidenceFormatter::dateTimeToString(const QDateTime &date, bool allDay, bool shortfmt)
3305{
3306 if (allDay) {
3307 return dateToString(date.toLocalTime().date(), shortfmt);
3308 }
3309
3310 return QLocale().toString(date.toLocalTime(), (shortfmt ? QLocale::ShortFormat : QLocale::LongFormat));
3311}
3312
3314{
3315 Q_UNUSED(calendar)
3316 Q_UNUSED(incidence)
3317 return QString();
3318}
3319
3320static QString secs2Duration(qint64 secs)
3321{
3322 QString tmp;
3323 qint64 days = secs / 86400;
3324 if (days > 0) {
3325 tmp += i18np("1 day", "%1 days", days);
3326 tmp += QLatin1Char(' ');
3327 secs -= (days * 86400);
3328 }
3329 qint64 hours = secs / 3600;
3330 if (hours > 0) {
3331 tmp += i18np("1 hour", "%1 hours", hours);
3332 tmp += QLatin1Char(' ');
3333 secs -= (hours * 3600);
3334 }
3335 qint64 mins = secs / 60;
3336 if (mins > 0) {
3337 tmp += i18np("1 minute", "%1 minutes", mins);
3338 }
3339 return tmp;
3340}
3341
3343{
3344 QString tmp;
3345 if (incidence->type() == Incidence::TypeEvent) {
3346 Event::Ptr event = incidence.staticCast<Event>();
3347 if (event->hasEndDate()) {
3348 if (!event->allDay()) {
3349 tmp = secs2Duration(event->dtStart().secsTo(event->dtEnd()));
3350 } else {
3351 tmp = i18np("1 day", "%1 days", event->dtStart().date().daysTo(event->dtEnd().date()) + 1);
3352 }
3353 } else {
3354 tmp = i18n("forever");
3355 }
3356 } else if (incidence->type() == Incidence::TypeTodo) {
3357 Todo::Ptr todo = incidence.staticCast<Todo>();
3358 if (todo->hasDueDate()) {
3359 if (todo->hasStartDate()) {
3360 if (!todo->allDay()) {
3361 tmp = secs2Duration(todo->dtStart().secsTo(todo->dtDue()));
3362 } else {
3363 tmp = i18np("1 day", "%1 days", todo->dtStart().date().daysTo(todo->dtDue().date()) + 1);
3364 }
3365 }
3366 }
3367 }
3368 return tmp;
3369}
3370
3372{
3373 // TODO: implement shortfmt=false
3374 Q_UNUSED(shortfmt)
3375
3377
3378 if (incidence) {
3379 Alarm::List alarms = incidence->alarms();
3381 const Alarm::List::ConstIterator end(alarms.constEnd());
3383 for (it = alarms.constBegin(); it != end; ++it) {
3384 Alarm::Ptr alarm = *it;
3385 int offset = 0;
3386 QString remStr;
3387 QString atStr;
3388 QString offsetStr;
3389 if (alarm->hasTime()) {
3390 offset = 0;
3391 if (alarm->time().isValid()) {
3392 atStr = QLocale().toString(alarm->time().toLocalTime(), QLocale::ShortFormat);
3393 }
3394 } else if (alarm->hasStartOffset()) {
3395 offset = alarm->startOffset().asSeconds();
3396 if (offset < 0) {
3397 offset = -offset;
3398 offsetStr = i18nc("N days/hours/minutes before the start datetime", "%1 before the start", secs2Duration(offset));
3399 } else if (offset > 0) {
3400 offsetStr = i18nc("N days/hours/minutes after the start datetime", "%1 after the start", secs2Duration(offset));
3401 } else { // offset is 0
3402 if (incidence->dtStart().isValid()) {
3403 atStr = QLocale().toString(incidence->dtStart().toLocalTime(), QLocale::ShortFormat);
3404 }
3405 }
3406 } else if (alarm->hasEndOffset()) {
3407 offset = alarm->endOffset().asSeconds();
3408 if (offset < 0) {
3409 offset = -offset;
3410 if (incidence->type() == Incidence::TypeTodo) {
3411 offsetStr = i18nc("N days/hours/minutes before the due datetime", "%1 before the to-do is due", secs2Duration(offset));
3412 } else {
3413 offsetStr = i18nc("N days/hours/minutes before the end datetime", "%1 before the end", secs2Duration(offset));
3414 }
3415 } else if (offset > 0) {
3416 if (incidence->type() == Incidence::TypeTodo) {
3417 offsetStr = i18nc("N days/hours/minutes after the due datetime", "%1 after the to-do is due", secs2Duration(offset));
3418 } else {
3419 offsetStr = i18nc("N days/hours/minutes after the end datetime", "%1 after the end", secs2Duration(offset));
3420 }
3421 } else { // offset is 0
3422 if (incidence->type() == Incidence::TypeTodo) {
3423 Todo::Ptr t = incidence.staticCast<Todo>();
3424 if (t->dtDue().isValid()) {
3425 atStr = QLocale().toString(t->dtDue().toLocalTime(), QLocale::ShortFormat);
3426 }
3427 } else {
3428 Event::Ptr e = incidence.staticCast<Event>();
3429 if (e->dtEnd().isValid()) {
3430 atStr = QLocale().toString(e->dtEnd().toLocalTime(), QLocale::ShortFormat);
3431 }
3432 }
3433 }
3434 }
3435 if (offset == 0) {
3436 if (!atStr.isEmpty()) {
3437 remStr = i18nc("reminder occurs at datetime", "at %1", atStr);
3438 }
3439 } else {
3440 remStr = offsetStr;
3441 }
3442
3443 if (alarm->repeatCount() > 0) {
3444 QString countStr = i18np("repeats once", "repeats %1 times", alarm->repeatCount());
3445 QString intervalStr = i18nc("interval is N days/hours/minutes", "interval is %1", secs2Duration(alarm->snoozeTime().asSeconds()));
3446 QString repeatStr = i18nc("(repeat string, interval string)", "(%1, %2)", countStr, intervalStr);
3447 remStr = remStr + QLatin1Char(' ') + repeatStr;
3448 }
3449 reminderStringList << remStr;
3450 }
3451 }
3452
3453 return reminderStringList;
3454}
The InvitationFormatterHelper class.
QString name() const
QString delegate() const
QString delegator() const
QString email() const
PartStat status() const
Exception * exception() const
QSharedPointer< Calendar > Ptr
ScheduleMessage::Ptr parseScheduleMessage(const Calendar::Ptr &calendar, const QString &string)
bool hasDuration() const
QDateTime end() const
Duration duration() const
QDateTime start() const
QString email() const
static Person fromFullName(const QString &fullName)
ushort recurrenceType() const
QList< RecurrenceRule::WDayPos > yearPositions() const
QList< int > yearDates() const
QList< int > monthDays() const
QDateTime endDateTime() const
QBitArray days() const
QList< int > yearMonths() const
QList< int > yearDays() const
QList< RecurrenceRule::WDayPos > monthPositions() const
virtual bool visit(const Event::Ptr &event)
static KIconLoader * global()
QString iconPath(const QString &name, int group_or_size, bool canReturnNull, qreal scale) const
Q_SCRIPTABLE CaptureState status()
Q_SCRIPTABLE Q_NOREPLY void start()
KCODECS_EXPORT bool extractEmailAddressAndName(const QString &aStr, QString &mail, QString &name)
This file is part of the API for handling calendar data and provides static functions for formatting ...
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString xi18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Journal::Ptr journal(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Todo::Ptr todo(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
char * toString(const EngineQuery &query)
KCALUTILS_EXPORT QString mimeType()
Mime-type of iCalendar.
Definition icaldrag.cpp:20
KCALUTILS_EXPORT QString timeToString(QTime time, bool shortfmt=true)
Build a QString time representation of a QTime object.
KCALUTILS_EXPORT QString formatICalInvitationNoHtml(const QString &invitation, const KCalendarCore::Calendar::Ptr &calendar, InvitationFormatterHelper *helper, const QString &sender)
Deliver an HTML formatted string displaying an invitation.
KCALUTILS_EXPORT QStringList reminderStringList(const KCalendarCore::Incidence::Ptr &incidence, bool shortfmt=true)
Returns a reminder string computed for the specified Incidence.
KCALUTILS_EXPORT QString resourceString(const KCalendarCore::Calendar::Ptr &calendar, const KCalendarCore::Incidence::Ptr &incidence)
Returns a Calendar Resource label name for the specified Incidence.
KCALUTILS_EXPORT QString dateToString(QDate date, bool shortfmt=true)
Build a QString date representation of a QDate object.
KCALUTILS_EXPORT QString formatICalInvitation(const QString &invitation, const KCalendarCore::Calendar::Ptr &calendar, InvitationFormatterHelper *helper)
Deliver an HTML formatted string displaying an invitation.
KCALUTILS_EXPORT QString mailBodyStr(const KCalendarCore::IncidenceBase::Ptr &incidence)
Create a QString representation of an Incidence in format suitable for including inside a mail messag...
KCALUTILS_EXPORT QString durationString(const KCalendarCore::Incidence::Ptr &incidence)
Returns a duration string computed for the specified Incidence.
KCALUTILS_EXPORT QString dateTimeToString(const QDateTime &date, bool dateOnly=false, bool shortfmt=true)
Build a QString date/time representation of a QDateTime object.
KCALUTILS_EXPORT QString recurrenceString(const KCalendarCore::Incidence::Ptr &incidence)
Build a pretty QString representation of an Incidence's recurrence info.
KCALUTILS_EXPORT QString extensiveDisplayStr(const KCalendarCore::Calendar::Ptr &calendar, const KCalendarCore::IncidenceBase::Ptr &incidence, QDate date=QDate())
Create a RichText QString representation of an Incidence in a nice format suitable for using in a vie...
KCALUTILS_EXPORT QString toolTipStr(const QString &sourceName, const KCalendarCore::IncidenceBase::Ptr &incidence, QDate date=QDate(), bool richText=true)
Create a QString representation of an Incidence in a nice format suitable for using in a tooltip.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
Build a translated message representing an exception.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString name(GameStandardAction id)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QVariant location(const QVariant &res)
QString path(const QString &relativePath)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem cont()
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
bool testBit(qsizetype i) const const
const QColor & color() const const
QString name(NameFormat format) const const
QDate addDays(qint64 ndays) const const
int day() const const
bool isValid(int year, int month, int day)
int month() const const
QDateTime addDays(qint64 ndays) const const
QDateTime addSecs(qint64 s) const const
QDate date() const const
qint64 daysTo(const QDateTime &other) const const
bool isValid() const const
void setDate(QDate date)
void setTime(QTime time)
QTime time() const const
QDateTime toLocalTime() const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
const_iterator constBegin() const const
const_iterator constEnd() const const
qsizetype count() const const
iterator end()
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
QString dayName(int day, FormatType type) const const
Qt::DayOfWeek firstDayOfWeek() const const
QString toString(QDate date, FormatType format) const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
const QColor & color(ColorGroup group, ColorRole role) const const
void setCurrentColorGroup(ColorGroup cg)
const QBrush & shadow() const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
QSharedPointer< X > dynamicCast() const const
QSharedPointer< X > staticCast() const const
qsizetype count() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString first(qsizetype n) const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString number(double n, char format, int precision)
void push_back(QChar ch)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString right(qsizetype n) const const
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toHtmlEscaped() const const
QString trimmed() const const
QString join(QChar separator) const const
QTimeZone systemTimeZone()
void setPath(const QString &path, ParsingMode mode)
void setScheme(const QString &scheme)
QString url(FormattingOptions options) const const
This file is part of the API for handling calendar data and provides static functions for formatting ...
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:09:16 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.