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

KDE's Doxygen guidelines are available online.