File indexing completed on 2024-05-12 05:15:03
0001 /* 0002 This file is part of the kcalutils library. 0003 0004 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org> 0005 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 0006 SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net> 0007 SPDX-FileCopyrightText: 2009-2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net> 0008 0009 SPDX-License-Identifier: LGPL-2.0-or-later 0010 */ 0011 /** 0012 @file 0013 This file is part of the API for handling calendar data and provides 0014 static functions for formatting Incidences for various purposes. 0015 0016 @brief 0017 Provides methods to format Incidences in various ways for display purposes. 0018 0019 @author Cornelius Schumacher \<schumacher@kde.org\> 0020 @author Reinhold Kainhofer \<reinhold@kainhofer.com\> 0021 @author Allen Winter \<allen@kdab.com\> 0022 */ 0023 #include "incidenceformatter.h" 0024 #include "grantleetemplatemanager_p.h" 0025 #include "stringify.h" 0026 0027 #include <KCalendarCore/Event> 0028 #include <KCalendarCore/FreeBusy> 0029 #include <KCalendarCore/ICalFormat> 0030 #include <KCalendarCore/Journal> 0031 #include <KCalendarCore/Todo> 0032 #include <KCalendarCore/Visitor> 0033 using namespace KCalendarCore; 0034 0035 #include <KIdentityManagementCore/Utils> 0036 0037 #include <KEmailAddress> 0038 #include <ktexttohtml.h> 0039 0040 #include "kcalutils_debug.h" 0041 #include <KIconLoader> 0042 #include <KLocalizedString> 0043 0044 #include <QApplication> 0045 #include <QBitArray> 0046 #include <QLocale> 0047 #include <QMimeDatabase> 0048 #include <QPalette> 0049 #include <QRegularExpression> 0050 0051 using namespace KCalUtils; 0052 using namespace IncidenceFormatter; 0053 0054 /******************* 0055 * General helpers 0056 *******************/ 0057 0058 static QVariantHash inviteButton(const QString &id, const QString &text, const QString &iconName, InvitationFormatterHelper *helper); 0059 0060 //@cond PRIVATE 0061 static QString string2HTML(const QString &str) 0062 { 0063 // use convertToHtml so we get clickable links and other goodies 0064 return KTextToHTML::convertToHtml(str, KTextToHTML::HighlightText | KTextToHTML::ReplaceSmileys); 0065 } 0066 0067 static bool thatIsMe(const QString &email) 0068 { 0069 return KIdentityManagementCore::thatIsMe(email); 0070 } 0071 0072 static bool iamAttendee(const Attendee &attendee) 0073 { 0074 // Check if this attendee is the user 0075 return thatIsMe(attendee.email()); 0076 } 0077 0078 static QString htmlAddTag(const QString &tag, const QString &text) 0079 { 0080 int numLineBreaks = text.count(QLatin1Char('\n')); 0081 const QString str = QLatin1Char('<') + tag + QLatin1Char('>'); 0082 QString tmpText = text; 0083 QString tmpStr = str; 0084 if (numLineBreaks >= 0) { 0085 if (numLineBreaks > 0) { 0086 QString tmp; 0087 for (int i = 0; i <= numLineBreaks; ++i) { 0088 int pos = tmpText.indexOf(QLatin1Char('\n')); 0089 tmp = tmpText.left(pos); 0090 tmpText = tmpText.right(tmpText.length() - pos - 1); 0091 tmpStr += tmp + QLatin1StringView("<br>"); 0092 } 0093 } else { 0094 tmpStr += tmpText; 0095 } 0096 } 0097 tmpStr += QLatin1StringView("</") + tag + QLatin1Char('>'); 0098 return tmpStr; 0099 } 0100 0101 static QPair<QString, QString> searchNameAndUid(const QString &email, const QString &name, const QString &uid) 0102 { 0103 // Yes, this is a silly method now, but it's predecessor was quite useful in e35. 0104 // For now, please keep this sillyness until e35 is frozen to ease forward porting. 0105 // -Allen 0106 QPair<QString, QString> s; 0107 s.first = name; 0108 s.second = uid; 0109 if (!email.isEmpty() && (name.isEmpty() || uid.isEmpty())) { 0110 s.second.clear(); 0111 } 0112 return s; 0113 } 0114 0115 static QString searchName(const QString &email, const QString &name) 0116 { 0117 const QString printName = name.isEmpty() ? email : name; 0118 return printName; 0119 } 0120 0121 static bool iamOrganizer(const Incidence::Ptr &incidence) 0122 { 0123 // Check if the user is the organizer for this incidence 0124 0125 if (!incidence) { 0126 return false; 0127 } 0128 0129 return thatIsMe(incidence->organizer().email()); 0130 } 0131 0132 static bool senderIsOrganizer(const Incidence::Ptr &incidence, const QString &sender) 0133 { 0134 // Check if the specified sender is the organizer 0135 0136 if (!incidence || sender.isEmpty()) { 0137 return true; 0138 } 0139 0140 bool isorg = true; 0141 QString senderName; 0142 QString senderEmail; 0143 if (KEmailAddress::extractEmailAddressAndName(sender, senderEmail, senderName)) { 0144 // for this heuristic, we say the sender is the organizer if either the name or the email match. 0145 if (incidence->organizer().email() != senderEmail && incidence->organizer().name() != senderName) { 0146 isorg = false; 0147 } 0148 } 0149 return isorg; 0150 } 0151 0152 static bool attendeeIsOrganizer(const Incidence::Ptr &incidence, const Attendee &attendee) 0153 { 0154 if (incidence && !attendee.isNull() && (incidence->organizer().email() == attendee.email())) { 0155 return true; 0156 } else { 0157 return false; 0158 } 0159 } 0160 0161 static QString organizerName(const Incidence::Ptr &incidence, const QString &defName) 0162 { 0163 QString tName; 0164 if (!defName.isEmpty()) { 0165 tName = defName; 0166 } else { 0167 tName = i18n("Organizer Unknown"); 0168 } 0169 0170 QString name; 0171 if (incidence) { 0172 name = incidence->organizer().name(); 0173 if (name.isEmpty()) { 0174 name = incidence->organizer().email(); 0175 } 0176 } 0177 if (name.isEmpty()) { 0178 name = tName; 0179 } 0180 return name; 0181 } 0182 0183 static QString firstAttendeeName(const Incidence::Ptr &incidence, const QString &defName) 0184 { 0185 QString tName; 0186 if (!defName.isEmpty()) { 0187 tName = defName; 0188 } else { 0189 tName = i18n("Sender"); 0190 } 0191 0192 QString name; 0193 if (incidence) { 0194 const Attendee::List attendees = incidence->attendees(); 0195 if (!attendees.isEmpty()) { 0196 const Attendee attendee = attendees.at(0); 0197 name = attendee.name(); 0198 if (name.isEmpty()) { 0199 name = attendee.email(); 0200 } 0201 } 0202 } 0203 if (name.isEmpty()) { 0204 name = tName; 0205 } 0206 return name; 0207 } 0208 0209 static QString rsvpStatusIconName(Attendee::PartStat status) 0210 { 0211 switch (status) { 0212 case Attendee::Accepted: 0213 return QStringLiteral("dialog-ok-apply"); 0214 case Attendee::Declined: 0215 return QStringLiteral("dialog-cancel"); 0216 case Attendee::NeedsAction: 0217 return QStringLiteral("help-about"); 0218 case Attendee::InProcess: 0219 return QStringLiteral("help-about"); 0220 case Attendee::Tentative: 0221 return QStringLiteral("dialog-ok"); 0222 case Attendee::Delegated: 0223 return QStringLiteral("mail-forward"); 0224 case Attendee::Completed: 0225 return QStringLiteral("mail-mark-read"); 0226 default: 0227 return QString(); 0228 } 0229 } 0230 0231 //@endcond 0232 0233 /******************************************************************* 0234 * Helper functions for the extensive display (display viewer) 0235 *******************************************************************/ 0236 0237 //@cond PRIVATE 0238 static QVariantHash displayViewFormatPerson(const QString &email, const QString &name, const QString &uid, const QString &iconName) 0239 { 0240 // Search for new print name or uid, if needed. 0241 QPair<QString, QString> s = searchNameAndUid(email, name, uid); 0242 const QString printName = s.first; 0243 const QString printUid = s.second; 0244 0245 QVariantHash personData; 0246 personData[QStringLiteral("icon")] = iconName; 0247 personData[QStringLiteral("uid")] = printUid; 0248 personData[QStringLiteral("name")] = printName; 0249 personData[QStringLiteral("email")] = email; 0250 0251 // Make the mailto link 0252 if (!email.isEmpty()) { 0253 Person person(name, email); 0254 QString path = person.fullName().simplified(); 0255 if (path.isEmpty() || path.startsWith(QLatin1Char('"'))) { 0256 path = email; 0257 } 0258 QUrl mailto; 0259 mailto.setScheme(QStringLiteral("mailto")); 0260 mailto.setPath(path); 0261 0262 personData[QStringLiteral("mailto")] = mailto.url(); 0263 } 0264 0265 return personData; 0266 } 0267 0268 static QVariantHash displayViewFormatPerson(const QString &email, const QString &name, const QString &uid, Attendee::PartStat status) 0269 { 0270 return displayViewFormatPerson(email, name, uid, rsvpStatusIconName(status)); 0271 } 0272 0273 static bool incOrganizerOwnsCalendar(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence) 0274 { 0275 // PORTME! Look at e35's CalHelper::incOrganizerOwnsCalendar 0276 0277 // For now, use iamOrganizer() which is only part of the check 0278 Q_UNUSED(calendar) 0279 return iamOrganizer(incidence); 0280 } 0281 0282 static QString displayViewFormatDescription(const Incidence::Ptr &incidence) 0283 { 0284 if (!incidence->description().isEmpty()) { 0285 if (!incidence->descriptionIsRich() && !incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) { 0286 return string2HTML(incidence->description()); 0287 } else if (!incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) { 0288 return incidence->richDescription(); 0289 } else { 0290 return incidence->description(); 0291 } 0292 } 0293 0294 return QString(); 0295 } 0296 0297 static QVariantList displayViewFormatAttendeeRoleList(const Incidence::Ptr &incidence, Attendee::Role role, bool showStatus) 0298 { 0299 QVariantList attendeeDataList; 0300 attendeeDataList.reserve(incidence->attendeeCount()); 0301 0302 const Attendee::List attendees = incidence->attendees(); 0303 for (const auto &a : attendees) { 0304 if (a.role() != role) { 0305 // skip this role 0306 continue; 0307 } 0308 if (attendeeIsOrganizer(incidence, a)) { 0309 // skip attendee that is also the organizer 0310 continue; 0311 } 0312 QVariantHash attendeeData = displayViewFormatPerson(a.email(), a.name(), a.uid(), showStatus ? a.status() : Attendee::None); 0313 if (!a.delegator().isEmpty()) { 0314 attendeeData[QStringLiteral("delegator")] = a.delegator(); 0315 } 0316 if (!a.delegate().isEmpty()) { 0317 attendeeData[QStringLiteral("delegate")] = a.delegate(); 0318 } 0319 if (showStatus) { 0320 attendeeData[QStringLiteral("status")] = Stringify::attendeeStatus(a.status()); 0321 } 0322 0323 attendeeDataList << attendeeData; 0324 } 0325 0326 return attendeeDataList; 0327 } 0328 0329 static QVariantHash displayViewFormatOrganizer(const Incidence::Ptr &incidence) 0330 { 0331 // Add organizer link 0332 int attendeeCount = incidence->attendees().count(); 0333 if (attendeeCount > 1 || (attendeeCount == 1 && !attendeeIsOrganizer(incidence, incidence->attendees().at(0)))) { 0334 QPair<QString, QString> s = searchNameAndUid(incidence->organizer().email(), incidence->organizer().name(), QString()); 0335 return displayViewFormatPerson(incidence->organizer().email(), s.first, s.second, QStringLiteral("meeting-organizer")); 0336 } 0337 0338 return QVariantHash(); 0339 } 0340 0341 static QVariantList displayViewFormatAttachments(const Incidence::Ptr &incidence) 0342 { 0343 const Attachment::List as = incidence->attachments(); 0344 0345 QVariantList dataList; 0346 dataList.reserve(as.count()); 0347 0348 for (auto it = as.cbegin(), end = as.cend(); it != end; ++it) { 0349 QVariantHash attData; 0350 if ((*it).isUri()) { 0351 QString name; 0352 if ((*it).uri().startsWith(QLatin1StringView("kmail:"))) { 0353 name = i18n("Show mail"); 0354 } else { 0355 if ((*it).label().isEmpty()) { 0356 name = (*it).uri(); 0357 } else { 0358 name = (*it).label(); 0359 } 0360 } 0361 attData[QStringLiteral("uri")] = (*it).uri(); 0362 attData[QStringLiteral("label")] = name; 0363 } else { 0364 attData[QStringLiteral("uri")] = QStringLiteral("ATTACH:%1").arg(QString::fromUtf8((*it).label().toUtf8().toBase64())); 0365 attData[QStringLiteral("label")] = (*it).label(); 0366 } 0367 dataList << attData; 0368 } 0369 return dataList; 0370 } 0371 0372 static QVariantHash displayViewFormatBirthday(const Event::Ptr &event) 0373 { 0374 if (!event) { 0375 return QVariantHash(); 0376 } 0377 0378 // It's callees duty to ensure this 0379 Q_ASSERT(event->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES") || event->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")); 0380 0381 const QString uid_1 = event->customProperty("KABC", "UID-1"); 0382 const QString name_1 = event->customProperty("KABC", "NAME-1"); 0383 const QString email_1 = event->customProperty("KABC", "EMAIL-1"); 0384 const KCalendarCore::Person p = Person::fromFullName(email_1); 0385 return displayViewFormatPerson(p.email(), name_1, uid_1, QString()); 0386 } 0387 0388 static QVariantHash incidenceTemplateHeader(const Incidence::Ptr &incidence) 0389 { 0390 QVariantHash incidenceData; 0391 if (incidence->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")) { 0392 incidenceData[QStringLiteral("icon")] = QStringLiteral("view-calendar-birthday"); 0393 } else if (incidence->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) { 0394 incidenceData[QStringLiteral("icon")] = QStringLiteral("view-calendar-wedding-anniversary"); 0395 } else { 0396 incidenceData[QStringLiteral("icon")] = incidence->iconName(); 0397 } 0398 0399 switch (incidence->type()) { 0400 case IncidenceBase::IncidenceType::TypeEvent: 0401 incidenceData[QStringLiteral("alarmIcon")] = QStringLiteral("appointment-reminder"); 0402 incidenceData[QStringLiteral("recursIcon")] = QStringLiteral("appointment-recurring"); 0403 break; 0404 case IncidenceBase::IncidenceType::TypeTodo: 0405 incidenceData[QStringLiteral("alarmIcon")] = QStringLiteral("task-reminder"); 0406 incidenceData[QStringLiteral("recursIcon")] = QStringLiteral("task-recurring"); 0407 break; 0408 default: 0409 // Others don't repeat and don't have reminders. 0410 break; 0411 } 0412 0413 incidenceData[QStringLiteral("hasEnabledAlarms")] = incidence->hasEnabledAlarms(); 0414 incidenceData[QStringLiteral("recurs")] = incidence->recurs(); 0415 incidenceData[QStringLiteral("isReadOnly")] = incidence->isReadOnly(); 0416 incidenceData[QStringLiteral("summary")] = incidence->summary(); 0417 incidenceData[QStringLiteral("allDay")] = incidence->allDay(); 0418 0419 return incidenceData; 0420 } 0421 0422 static QString displayViewFormatEvent(const Calendar::Ptr &calendar, const QString &sourceName, const Event::Ptr &event, QDate date) 0423 { 0424 if (!event) { 0425 return QString(); 0426 } 0427 0428 QVariantHash incidence = incidenceTemplateHeader(event); 0429 0430 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, event) : sourceName; 0431 const QString richLocation = event->richLocation(); 0432 if (richLocation.startsWith(QLatin1StringView("http:/")) || richLocation.startsWith(QLatin1StringView("https:/"))) { 0433 incidence[QStringLiteral("location")] = QStringLiteral("<a href=\"%1\">%1</a>").arg(richLocation); 0434 } else { 0435 incidence[QStringLiteral("location")] = richLocation; 0436 } 0437 0438 const auto startDts = event->startDateTimesForDate(date, QTimeZone::systemTimeZone()); 0439 const auto startDt = startDts.empty() ? event->dtStart().toLocalTime() : startDts[0].toLocalTime(); 0440 const auto endDt = event->endDateForStart(startDt).toLocalTime(); 0441 0442 incidence[QStringLiteral("isAllDay")] = event->allDay(); 0443 incidence[QStringLiteral("isMultiDay")] = event->isMultiDay(); 0444 incidence[QStringLiteral("startDate")] = startDt.date(); 0445 incidence[QStringLiteral("endDate")] = endDt.date(); 0446 incidence[QStringLiteral("startTime")] = startDt.time(); 0447 incidence[QStringLiteral("endTime")] = endDt.time(); 0448 incidence[QStringLiteral("duration")] = durationString(event); 0449 incidence[QStringLiteral("isException")] = event->hasRecurrenceId(); 0450 incidence[QStringLiteral("recurrence")] = recurrenceString(event); 0451 0452 if (event->customProperty("KABC", "BIRTHDAY") == QLatin1StringView("YES")) { 0453 incidence[QStringLiteral("birthday")] = displayViewFormatBirthday(event); 0454 } 0455 0456 if (event->customProperty("KABC", "ANNIVERSARY") == QLatin1StringView("YES")) { 0457 incidence[QStringLiteral("anniversary")] = displayViewFormatBirthday(event); 0458 } 0459 0460 incidence[QStringLiteral("description")] = displayViewFormatDescription(event); 0461 // TODO: print comments? 0462 0463 incidence[QStringLiteral("reminders")] = reminderStringList(event); 0464 0465 incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(event); 0466 const bool showStatus = incOrganizerOwnsCalendar(calendar, event); 0467 incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(event, Attendee::Chair, showStatus); 0468 incidence[QStringLiteral("requiredParticipants")] = displayViewFormatAttendeeRoleList(event, Attendee::ReqParticipant, showStatus); 0469 incidence[QStringLiteral("optionalParticipants")] = displayViewFormatAttendeeRoleList(event, Attendee::OptParticipant, showStatus); 0470 incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(event, Attendee::NonParticipant, showStatus); 0471 0472 incidence[QStringLiteral("categories")] = event->categories(); 0473 0474 incidence[QStringLiteral("attachments")] = displayViewFormatAttachments(event); 0475 incidence[QStringLiteral("creationDate")] = event->created().toLocalTime(); 0476 0477 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/event.html"), incidence); 0478 } 0479 0480 static QString displayViewFormatTodo(const Calendar::Ptr &calendar, const QString &sourceName, const Todo::Ptr &todo, QDate ocurrenceDueDate) 0481 { 0482 if (!todo) { 0483 qCDebug(KCALUTILS_LOG) << "IncidenceFormatter::displayViewFormatTodo was called without to-do, quitting"; 0484 return QString(); 0485 } 0486 0487 QVariantHash incidence = incidenceTemplateHeader(todo); 0488 0489 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, todo) : sourceName; 0490 incidence[QStringLiteral("location")] = todo->richLocation(); 0491 0492 const bool hastStartDate = todo->hasStartDate(); 0493 const bool hasDueDate = todo->hasDueDate(); 0494 0495 if (hastStartDate) { 0496 QDateTime startDt = todo->dtStart(true /**first*/).toLocalTime(); 0497 if (todo->recurs() && ocurrenceDueDate.isValid()) { 0498 if (hasDueDate) { 0499 // In kdepim all recurring to-dos have due date. 0500 const qint64 length = startDt.daysTo(todo->dtDue(true /**first*/)); 0501 if (length >= 0) { 0502 startDt.setDate(ocurrenceDueDate.addDays(-length)); 0503 } else { 0504 qCritical() << "DTSTART is bigger than DTDUE, todo->uid() is " << todo->uid(); 0505 startDt.setDate(ocurrenceDueDate); 0506 } 0507 } else { 0508 qCritical() << "To-do is recurring but has no DTDUE set, todo->uid() is " << todo->uid(); 0509 startDt.setDate(ocurrenceDueDate); 0510 } 0511 } 0512 incidence[QStringLiteral("startDate")] = startDt; 0513 } 0514 0515 if (hasDueDate) { 0516 QDateTime dueDt = todo->dtDue().toLocalTime(); 0517 if (todo->recurs()) { 0518 if (ocurrenceDueDate.isValid()) { 0519 QDateTime kdt(ocurrenceDueDate, QTime(0, 0, 0), Qt::LocalTime); 0520 kdt = kdt.addSecs(-1); 0521 dueDt.setDate(todo->recurrence()->getNextDateTime(kdt).date()); 0522 } 0523 } 0524 incidence[QStringLiteral("dueDate")] = dueDt; 0525 } 0526 0527 incidence[QStringLiteral("duration")] = durationString(todo); 0528 incidence[QStringLiteral("isException")] = todo->hasRecurrenceId(); 0529 if (todo->recurs()) { 0530 incidence[QStringLiteral("recurrence")] = recurrenceString(todo); 0531 } 0532 0533 incidence[QStringLiteral("description")] = displayViewFormatDescription(todo); 0534 0535 // TODO: print comments? 0536 0537 incidence[QStringLiteral("reminders")] = reminderStringList(todo); 0538 0539 incidence[QStringLiteral("organizer")] = displayViewFormatOrganizer(todo); 0540 const bool showStatus = incOrganizerOwnsCalendar(calendar, todo); 0541 incidence[QStringLiteral("chair")] = displayViewFormatAttendeeRoleList(todo, Attendee::Chair, showStatus); 0542 incidence[QStringLiteral("requiredParticipants")] = displayViewFormatAttendeeRoleList(todo, Attendee::ReqParticipant, showStatus); 0543 incidence[QStringLiteral("optionalParticipants")] = displayViewFormatAttendeeRoleList(todo, Attendee::OptParticipant, showStatus); 0544 incidence[QStringLiteral("observers")] = displayViewFormatAttendeeRoleList(todo, Attendee::NonParticipant, showStatus); 0545 0546 incidence[QStringLiteral("categories")] = todo->categories(); 0547 incidence[QStringLiteral("priority")] = todo->priority(); 0548 if (todo->isCompleted()) { 0549 incidence[QStringLiteral("completedDate")] = todo->completed(); 0550 } else { 0551 incidence[QStringLiteral("percent")] = todo->percentComplete(); 0552 } 0553 incidence[QStringLiteral("attachments")] = displayViewFormatAttachments(todo); 0554 incidence[QStringLiteral("creationDate")] = todo->created().toLocalTime(); 0555 0556 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/todo.html"), incidence); 0557 } 0558 0559 static QString displayViewFormatJournal(const Calendar::Ptr &calendar, const QString &sourceName, const Journal::Ptr &journal) 0560 { 0561 if (!journal) { 0562 return QString(); 0563 } 0564 0565 QVariantHash incidence = incidenceTemplateHeader(journal); 0566 incidence[QStringLiteral("calendar")] = calendar ? resourceString(calendar, journal) : sourceName; 0567 incidence[QStringLiteral("date")] = journal->dtStart().toLocalTime(); 0568 incidence[QStringLiteral("description")] = displayViewFormatDescription(journal); 0569 incidence[QStringLiteral("categories")] = journal->categories(); 0570 incidence[QStringLiteral("creationDate")] = journal->created().toLocalTime(); 0571 0572 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/journal.html"), incidence); 0573 } 0574 0575 static QString displayViewFormatFreeBusy(const Calendar::Ptr &calendar, const QString &sourceName, const FreeBusy::Ptr &fb) 0576 { 0577 Q_UNUSED(calendar) 0578 Q_UNUSED(sourceName) 0579 if (!fb) { 0580 return QString(); 0581 } 0582 0583 QVariantHash fbData; 0584 fbData[QStringLiteral("organizer")] = fb->organizer().fullName(); 0585 fbData[QStringLiteral("start")] = fb->dtStart().toLocalTime().date(); 0586 fbData[QStringLiteral("end")] = fb->dtEnd().toLocalTime().date(); 0587 0588 Period::List periods = fb->busyPeriods(); 0589 QVariantList periodsData; 0590 periodsData.reserve(periods.size()); 0591 for (auto it = periods.cbegin(), end = periods.cend(); it != end; ++it) { 0592 const Period per = *it; 0593 QVariantHash periodData; 0594 if (per.hasDuration()) { 0595 int dur = per.duration().asSeconds(); 0596 QString cont; 0597 if (dur >= 3600) { 0598 cont += i18ncp("hours part of duration", "1 hour ", "%1 hours ", dur / 3600); 0599 dur %= 3600; 0600 } 0601 if (dur >= 60) { 0602 cont += i18ncp("minutes part duration", "1 minute ", "%1 minutes ", dur / 60); 0603 dur %= 60; 0604 } 0605 if (dur > 0) { 0606 cont += i18ncp("seconds part of duration", "1 second", "%1 seconds", dur); 0607 } 0608 periodData[QStringLiteral("dtStart")] = per.start().toLocalTime(); 0609 periodData[QStringLiteral("duration")] = cont; 0610 } else { 0611 const QDateTime pStart = per.start().toLocalTime(); 0612 const QDateTime pEnd = per.end().toLocalTime(); 0613 if (per.start().date() == per.end().date()) { 0614 periodData[QStringLiteral("date")] = pStart.date(); 0615 periodData[QStringLiteral("start")] = pStart.time(); 0616 periodData[QStringLiteral("end")] = pEnd.time(); 0617 } else { 0618 periodData[QStringLiteral("start")] = pStart; 0619 periodData[QStringLiteral("end")] = pEnd; 0620 } 0621 } 0622 0623 periodsData << periodData; 0624 } 0625 0626 fbData[QStringLiteral("periods")] = periodsData; 0627 0628 return GrantleeTemplateManager::instance()->render(QStringLiteral(":/freebusy.html"), fbData); 0629 } 0630 0631 //@endcond 0632 0633 //@cond PRIVATE 0634 class KCalUtils::IncidenceFormatter::EventViewerVisitor : public Visitor 0635 { 0636 public: 0637 EventViewerVisitor() 0638 : mCalendar(nullptr) 0639 { 0640 } 0641 0642 ~EventViewerVisitor() override; 0643 0644 bool act(const Calendar::Ptr &calendar, const IncidenceBase::Ptr &incidence, QDate date) 0645 { 0646 mCalendar = calendar; 0647 mSourceName.clear(); 0648 mDate = date; 0649 mResult = QLatin1StringView(""); 0650 return incidence->accept(*this, incidence); 0651 } 0652 0653 bool act(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date) 0654 { 0655 mSourceName = sourceName; 0656 mDate = date; 0657 mResult = QLatin1StringView(""); 0658 return incidence->accept(*this, incidence); 0659 } 0660 0661 [[nodiscard]] QString result() const 0662 { 0663 return mResult; 0664 } 0665 0666 protected: 0667 bool visit(const Event::Ptr &event) override 0668 { 0669 mResult = displayViewFormatEvent(mCalendar, mSourceName, event, mDate); 0670 return !mResult.isEmpty(); 0671 } 0672 0673 bool visit(const Todo::Ptr &todo) override 0674 { 0675 mResult = displayViewFormatTodo(mCalendar, mSourceName, todo, mDate); 0676 return !mResult.isEmpty(); 0677 } 0678 0679 bool visit(const Journal::Ptr &journal) override 0680 { 0681 mResult = displayViewFormatJournal(mCalendar, mSourceName, journal); 0682 return !mResult.isEmpty(); 0683 } 0684 0685 bool visit(const FreeBusy::Ptr &fb) override 0686 { 0687 mResult = displayViewFormatFreeBusy(mCalendar, mSourceName, fb); 0688 return !mResult.isEmpty(); 0689 } 0690 0691 protected: 0692 Calendar::Ptr mCalendar; 0693 QString mSourceName; 0694 QDate mDate; 0695 QString mResult; 0696 }; 0697 //@endcond 0698 0699 EventViewerVisitor::~EventViewerVisitor() 0700 { 0701 } 0702 0703 QString IncidenceFormatter::extensiveDisplayStr(const Calendar::Ptr &calendar, const IncidenceBase::Ptr &incidence, QDate date) 0704 { 0705 if (!incidence) { 0706 return QString(); 0707 } 0708 0709 EventViewerVisitor v; 0710 if (v.act(calendar, incidence, date)) { 0711 return v.result(); 0712 } else { 0713 return QString(); 0714 } 0715 } 0716 0717 QString IncidenceFormatter::extensiveDisplayStr(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date) 0718 { 0719 if (!incidence) { 0720 return QString(); 0721 } 0722 0723 EventViewerVisitor v; 0724 if (v.act(sourceName, incidence, date)) { 0725 return v.result(); 0726 } else { 0727 return QString(); 0728 } 0729 } 0730 0731 /*********************************************************************** 0732 * Helper functions for the body part formatter of kmail (Invitations) 0733 ***********************************************************************/ 0734 0735 //@cond PRIVATE 0736 static QString cleanHtml(const QString &html) 0737 { 0738 static QRegularExpression rx = QRegularExpression(QStringLiteral("<body[^>]*>(.*)</body>"), QRegularExpression::CaseInsensitiveOption); 0739 QRegularExpressionMatch match = rx.match(html); 0740 if (match.hasMatch()) { 0741 QString body = match.captured(1); 0742 return body.remove(QRegularExpression(QStringLiteral("<[^>]*>"))).trimmed().toHtmlEscaped(); 0743 } 0744 return html; 0745 } 0746 0747 static QString invitationSummary(const Incidence::Ptr &incidence, bool noHtmlMode) 0748 { 0749 QString summaryStr = i18n("Summary unspecified"); 0750 if (!incidence->summary().isEmpty()) { 0751 if (!incidence->summaryIsRich()) { 0752 summaryStr = incidence->summary().toHtmlEscaped(); 0753 } else { 0754 summaryStr = incidence->richSummary(); 0755 if (noHtmlMode) { 0756 summaryStr = cleanHtml(summaryStr); 0757 } 0758 } 0759 } 0760 return summaryStr; 0761 } 0762 0763 static QString invitationLocation(const Incidence::Ptr &incidence, bool noHtmlMode) 0764 { 0765 QString locationStr = i18n("Location unspecified"); 0766 if (!incidence->location().isEmpty()) { 0767 if (!incidence->locationIsRich()) { 0768 locationStr = incidence->location().toHtmlEscaped(); 0769 } else { 0770 locationStr = incidence->richLocation(); 0771 if (noHtmlMode) { 0772 locationStr = cleanHtml(locationStr); 0773 } 0774 } 0775 } 0776 return locationStr; 0777 } 0778 0779 static QString diffColor() 0780 { 0781 // Color for printing comparison differences inside invitations. 0782 0783 // return "#DE8519"; // hard-coded color from Outlook2007 0784 return QColor(Qt::red).name(); // krazy:exclude=qenums TODO make configurable 0785 } 0786 0787 static QString noteColor() 0788 { 0789 // Color for printing notes inside invitations. 0790 return qApp->palette().color(QPalette::Active, QPalette::Highlight).name(); 0791 } 0792 0793 static QString htmlCompare(const QString &value, const QString &oldvalue) 0794 { 0795 // if 'value' is empty, then print nothing 0796 if (value.isEmpty()) { 0797 return QString(); 0798 } 0799 0800 // if 'value' is new or unchanged, then print normally 0801 if (oldvalue.isEmpty() || value == oldvalue) { 0802 return value; 0803 } 0804 0805 // if 'value' has changed, then make a special print 0806 return QStringLiteral("<font color=\"%1\">%2</font> (<strike>%3</strike>)").arg(diffColor(), value, oldvalue); 0807 } 0808 0809 static Attendee findDelegatedFromMyAttendee(const Incidence::Ptr &incidence) 0810 { 0811 // Return the first attendee that was delegated-from the user 0812 0813 Attendee attendee; 0814 if (!incidence) { 0815 return attendee; 0816 } 0817 0818 QString delegatorName; 0819 QString delegatorEmail; 0820 const Attendee::List attendees = incidence->attendees(); 0821 for (const auto &a : attendees) { 0822 KEmailAddress::extractEmailAddressAndName(a.delegator(), delegatorEmail, delegatorName); 0823 if (thatIsMe(delegatorEmail)) { 0824 attendee = a; 0825 break; 0826 } 0827 } 0828 0829 return attendee; 0830 } 0831 0832 static Attendee findMyAttendee(const Incidence::Ptr &incidence) 0833 { 0834 // Return the attendee for the incidence that is probably the user 0835 0836 Attendee attendee; 0837 if (!incidence) { 0838 return attendee; 0839 } 0840 0841 const Attendee::List attendees = incidence->attendees(); 0842 for (const auto &a : attendees) { 0843 if (iamAttendee(a)) { 0844 attendee = a; 0845 break; 0846 } 0847 } 0848 0849 return attendee; 0850 } 0851 0852 static Attendee findAttendee(const Incidence::Ptr &incidence, const QString &email) 0853 { 0854 // Search for an attendee by email address 0855 0856 Attendee attendee; 0857 if (!incidence) { 0858 return attendee; 0859 } 0860 0861 const Attendee::List attendees = incidence->attendees(); 0862 for (const auto &a : attendees) { 0863 if (email == a.email()) { 0864 attendee = a; 0865 break; 0866 } 0867 } 0868 return attendee; 0869 } 0870 0871 static bool rsvpRequested(const Incidence::Ptr &incidence) 0872 { 0873 if (!incidence) { 0874 return false; 0875 } 0876 0877 // use a heuristic to determine if a response is requested. 0878 0879 bool rsvp = true; // better send superfluously than not at all 0880 Attendee::List attendees = incidence->attendees(); 0881 Attendee::List::ConstIterator it; 0882 const Attendee::List::ConstIterator end(attendees.constEnd()); 0883 for (it = attendees.constBegin(); it != end; ++it) { 0884 if (it == attendees.constBegin()) { 0885 rsvp = (*it).RSVP(); // use what the first one has 0886 } else { 0887 if ((*it).RSVP() != rsvp) { 0888 rsvp = true; // they differ, default 0889 break; 0890 } 0891 } 0892 } 0893 return rsvp; 0894 } 0895 0896 static QString rsvpRequestedStr(bool rsvpRequested, const QString &role) 0897 { 0898 if (rsvpRequested) { 0899 if (role.isEmpty()) { 0900 return i18n("Your response is requested."); 0901 } else { 0902 return i18n("Your response as <b>%1</b> is requested.", role); 0903 } 0904 } else { 0905 if (role.isEmpty()) { 0906 return i18n("No response is necessary."); 0907 } else { 0908 return i18n("No response as <b>%1</b> is necessary.", role); 0909 } 0910 } 0911 } 0912 0913 static QString myStatusStr(const Incidence::Ptr &incidence) 0914 { 0915 QString ret; 0916 const Attendee a = findMyAttendee(incidence); 0917 if (!a.isNull() && a.status() != Attendee::NeedsAction && a.status() != Attendee::Delegated) { 0918 ret = i18n("(<b>Note</b>: the Organizer preset your response to <b>%1</b>)", Stringify::attendeeStatus(a.status())); 0919 } 0920 return ret; 0921 } 0922 0923 static QVariantHash invitationNote(const QString &title, const QString ¬e, const QString &color) 0924 { 0925 QVariantHash noteHash; 0926 if (note.isEmpty()) { 0927 return noteHash; 0928 } 0929 0930 noteHash[QStringLiteral("color")] = color; 0931 noteHash[QStringLiteral("title")] = title; 0932 noteHash[QStringLiteral("note")] = note; 0933 return noteHash; 0934 } 0935 0936 static QString invitationDescriptionIncidence(const Incidence::Ptr &incidence, bool noHtmlMode) 0937 { 0938 if (!incidence->description().isEmpty()) { 0939 // use description too 0940 if (!incidence->descriptionIsRich() && !incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) { 0941 return string2HTML(incidence->description()); 0942 } else { 0943 QString descr; 0944 if (!incidence->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) { 0945 descr = incidence->richDescription(); 0946 } else { 0947 descr = incidence->description(); 0948 } 0949 if (noHtmlMode) { 0950 descr = cleanHtml(descr); 0951 } 0952 return htmlAddTag(QStringLiteral("p"), descr); 0953 } 0954 } 0955 0956 return QString(); 0957 } 0958 0959 static bool slicesInterval(const Event::Ptr &event, const QDateTime &startDt, const QDateTime &endDt) 0960 { 0961 QDateTime closestStart = event->dtStart(); 0962 QDateTime closestEnd = event->dtEnd(); 0963 if (event->recurs()) { 0964 if (!event->recurrence()->timesInInterval(startDt, endDt).isEmpty()) { 0965 // If there is a recurrence in this interval we know already that we slice. 0966 return true; 0967 } 0968 closestStart = event->recurrence()->getPreviousDateTime(startDt); 0969 if (event->hasEndDate()) { 0970 closestEnd = closestStart.addSecs(event->dtStart().secsTo(event->dtEnd())); 0971 } 0972 } else { 0973 if (!event->hasEndDate() && event->hasDuration()) { 0974 closestEnd = closestStart.addSecs(event->duration()); 0975 } 0976 } 0977 0978 if (!closestEnd.isValid()) { 0979 // All events without an ending still happen if they are 0980 // started. 0981 return closestStart <= startDt; 0982 } 0983 0984 if (closestStart <= startDt) { 0985 // It starts before the interval and ends after the start of the interval. 0986 return closestEnd > startDt; 0987 } 0988 0989 // Are start and end both in this interval? 0990 return (closestStart >= startDt && closestStart <= endDt) && (closestEnd >= startDt && closestEnd <= endDt); 0991 } 0992 0993 static QVariantList eventsOnSameDays(InvitationFormatterHelper *helper, const Event::Ptr &event, bool noHtmlMode) 0994 { 0995 if (!event || !helper || !helper->calendar()) { 0996 return QVariantList(); 0997 } 0998 0999 QDateTime startDay = event->dtStart(); 1000 QDateTime endDay = event->hasEndDate() ? event->dtEnd() : event->dtStart(); 1001 startDay.setTime(QTime(0, 0, 0)); 1002 endDay.setTime(QTime(23, 59, 59)); 1003 1004 Event::List matchingEvents = helper->calendar()->events(startDay.date(), endDay.date(), QTimeZone::systemTimeZone()); 1005 if (matchingEvents.isEmpty()) { 1006 return QVariantList(); 1007 } 1008 1009 QVariantList events; 1010 int count = 0; 1011 for (auto it = matchingEvents.cbegin(), end = matchingEvents.cend(); it != end && count < 50; ++it) { 1012 if ((*it)->schedulingID() == event->uid()) { 1013 // Exclude the same event from the list. 1014 continue; 1015 } 1016 if (!slicesInterval(*it, startDay, endDay)) { 1017 /* Calendar::events includes events that have a recurrence that is 1018 * "active" in the specified interval. Whether or not the event is actually 1019 * happening ( has a recurrence that falls into the interval ). 1020 * This appears to be done deliberately and not to be a bug so we additionally 1021 * check if the event is actually happening here. */ 1022 continue; 1023 } 1024 ++count; 1025 QVariantHash ev; 1026 ev[QStringLiteral("summary")] = invitationSummary(*it, noHtmlMode); 1027 ev[QStringLiteral("dateTime")] = IncidenceFormatter::formatStartEnd((*it)->dtStart(), (*it)->dtEnd(), (*it)->allDay()); 1028 events.push_back(ev); 1029 } 1030 if (count == 50) { 1031 /* Abort after 50 entries to limit resource usage */ 1032 events.push_back({}); 1033 } 1034 return events; 1035 } 1036 1037 static QVariantHash invitationDetailsEvent(InvitationFormatterHelper *helper, const Event::Ptr &event, bool noHtmlMode) 1038 { 1039 // Invitation details are formatted into an HTML table 1040 if (!event) { 1041 return QVariantHash(); 1042 } 1043 1044 QVariantHash incidence; 1045 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-calendar"); 1046 incidence[QStringLiteral("summary")] = invitationSummary(event, noHtmlMode); 1047 incidence[QStringLiteral("location")] = invitationLocation(event, noHtmlMode); 1048 incidence[QStringLiteral("recurs")] = event->recurs(); 1049 incidence[QStringLiteral("recurrence")] = recurrenceString(event); 1050 incidence[QStringLiteral("isMultiDay")] = event->isMultiDay(QTimeZone::systemTimeZone()); 1051 incidence[QStringLiteral("isAllDay")] = event->allDay(); 1052 incidence[QStringLiteral("dateTime")] = IncidenceFormatter::formatStartEnd(event->dtStart(), event->dtEnd(), event->allDay()); 1053 incidence[QStringLiteral("duration")] = durationString(event); 1054 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(event, noHtmlMode); 1055 1056 incidence[QStringLiteral("checkCalendarButton")] = 1057 inviteButton(QStringLiteral("check_calendar"), i18n("Check my calendar"), QStringLiteral("go-jump-today"), helper); 1058 incidence[QStringLiteral("eventsOnSameDays")] = eventsOnSameDays(helper, event, noHtmlMode); 1059 1060 return incidence; 1061 } 1062 1063 QString IncidenceFormatter::formatStartEnd(const QDateTime &start, const QDateTime &end, bool isAllDay) 1064 { 1065 QString tmpStr; 1066 // <startDate[time> [- <[endDate][Time]>] 1067 // The startDate is always printed. 1068 // If the event does float the time is omitted. 1069 // 1070 // If it has an end dateTime: 1071 // on the same day -> Only add end time. 1072 // if it floats also omit the time 1073 tmpStr += IncidenceFormatter::dateTimeToString(start, isAllDay, false); 1074 1075 if (end.isValid()) { 1076 if (start.date() == end.date()) { 1077 // same day 1078 if (start.time().isValid()) { 1079 tmpStr += QLatin1StringView(" - ") + IncidenceFormatter::timeToString(end.toLocalTime().time(), true); 1080 } 1081 } else { 1082 tmpStr += QLatin1StringView(" - ") + IncidenceFormatter::dateTimeToString(end, isAllDay, false); 1083 } 1084 } 1085 return tmpStr; 1086 } 1087 1088 static QVariantHash invitationDetailsEvent(InvitationFormatterHelper *helper, 1089 const Event::Ptr &event, 1090 const Event::Ptr &oldevent, 1091 const ScheduleMessage::Ptr &message, 1092 bool noHtmlMode) 1093 { 1094 if (!oldevent) { 1095 return invitationDetailsEvent(helper, event, noHtmlMode); 1096 } 1097 1098 QVariantHash incidence; 1099 1100 // Print extra info typically dependent on the iTIP 1101 if (message->method() == iTIPDeclineCounter) { 1102 incidence[QStringLiteral("note")] = invitationNote(QString(), i18n("Please respond again to the original proposal."), noteColor()); 1103 } 1104 1105 incidence[QStringLiteral("isDiff")] = true; 1106 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-calendar"); 1107 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(event, noHtmlMode), invitationSummary(oldevent, noHtmlMode)); 1108 incidence[QStringLiteral("location")] = htmlCompare(invitationLocation(event, noHtmlMode), invitationLocation(oldevent, noHtmlMode)); 1109 incidence[QStringLiteral("recurs")] = event->recurs() || oldevent->recurs(); 1110 incidence[QStringLiteral("recurrence")] = htmlCompare(recurrenceString(event), recurrenceString(oldevent)); 1111 incidence[QStringLiteral("dateTime")] = htmlCompare(IncidenceFormatter::formatStartEnd(event->dtStart(), event->dtEnd(), event->allDay()), 1112 IncidenceFormatter::formatStartEnd(oldevent->dtStart(), oldevent->dtEnd(), oldevent->allDay())); 1113 incidence[QStringLiteral("duration")] = htmlCompare(durationString(event), durationString(oldevent)); 1114 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(event, noHtmlMode); 1115 1116 incidence[QStringLiteral("checkCalendarButton")] = 1117 inviteButton(QStringLiteral("check_calendar"), i18n("Check my calendar"), QStringLiteral("go-jump-today"), helper); 1118 incidence[QStringLiteral("eventsOnSameDays")] = eventsOnSameDays(helper, event, noHtmlMode); 1119 1120 return incidence; 1121 } 1122 1123 static QVariantHash invitationDetailsTodo(const Todo::Ptr &todo, bool noHtmlMode) 1124 { 1125 // To-do details are formatted into an HTML table 1126 if (!todo) { 1127 return QVariantHash(); 1128 } 1129 1130 QVariantHash incidence; 1131 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-tasks"); 1132 incidence[QStringLiteral("summary")] = invitationSummary(todo, noHtmlMode); 1133 incidence[QStringLiteral("location")] = invitationLocation(todo, noHtmlMode); 1134 incidence[QStringLiteral("isAllDay")] = todo->allDay(); 1135 incidence[QStringLiteral("hasStartDate")] = todo->hasStartDate(); 1136 bool isMultiDay = false; 1137 if (todo->hasStartDate()) { 1138 if (todo->allDay()) { 1139 incidence[QStringLiteral("dtStartStr")] = dateToString(todo->dtStart().toLocalTime().date(), true); 1140 } else { 1141 incidence[QStringLiteral("dtStartStr")] = dateTimeToString(todo->dtStart(), false, true); 1142 } 1143 isMultiDay = todo->dtStart().date() != todo->dtDue().date(); 1144 } 1145 if (todo->allDay()) { 1146 incidence[QStringLiteral("dtDueStr")] = dateToString(todo->dtDue().toLocalTime().date(), true); 1147 } else { 1148 incidence[QStringLiteral("dtDueStr")] = dateTimeToString(todo->dtDue(), false, true); 1149 } 1150 incidence[QStringLiteral("isMultiDay")] = isMultiDay; 1151 incidence[QStringLiteral("duration")] = durationString(todo); 1152 if (todo->percentComplete() > 0) { 1153 incidence[QStringLiteral("percentComplete")] = i18n("%1%", todo->percentComplete()); 1154 } 1155 incidence[QStringLiteral("recurs")] = todo->recurs(); 1156 incidence[QStringLiteral("recurrence")] = recurrenceString(todo); 1157 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(todo, noHtmlMode); 1158 1159 return incidence; 1160 } 1161 1162 static QVariantHash invitationDetailsTodo(const Todo::Ptr &todo, const Todo::Ptr &oldtodo, const ScheduleMessage::Ptr &message, bool noHtmlMode) 1163 { 1164 if (!oldtodo) { 1165 return invitationDetailsTodo(todo, noHtmlMode); 1166 } 1167 1168 QVariantHash incidence; 1169 1170 // Print extra info typically dependent on the iTIP 1171 if (message->method() == iTIPDeclineCounter) { 1172 incidence[QStringLiteral("note")] = invitationNote(QString(), i18n("Please respond again to the original proposal."), noteColor()); 1173 } 1174 1175 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-tasks"); 1176 incidence[QStringLiteral("isDiff")] = true; 1177 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(todo, noHtmlMode), invitationSummary(oldtodo, noHtmlMode)); 1178 incidence[QStringLiteral("location")] = htmlCompare(invitationLocation(todo, noHtmlMode), invitationLocation(oldtodo, noHtmlMode)); 1179 incidence[QStringLiteral("isAllDay")] = todo->allDay(); 1180 incidence[QStringLiteral("hasStartDate")] = todo->hasStartDate(); 1181 incidence[QStringLiteral("dtStartStr")] = htmlCompare(dateTimeToString(todo->dtStart(), false, false), dateTimeToString(oldtodo->dtStart(), false, false)); 1182 incidence[QStringLiteral("dtDueStr")] = htmlCompare(dateTimeToString(todo->dtDue(), false, false), dateTimeToString(oldtodo->dtDue(), false, false)); 1183 incidence[QStringLiteral("duration")] = htmlCompare(durationString(todo), durationString(oldtodo)); 1184 incidence[QStringLiteral("percentComplete")] = htmlCompare(i18n("%1%", todo->percentComplete()), i18n("%1%", oldtodo->percentComplete())); 1185 1186 incidence[QStringLiteral("recurs")] = todo->recurs() || oldtodo->recurs(); 1187 incidence[QStringLiteral("recurrence")] = htmlCompare(recurrenceString(todo), recurrenceString(oldtodo)); 1188 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(todo, noHtmlMode); 1189 1190 return incidence; 1191 } 1192 1193 static QVariantHash invitationDetailsJournal(const Journal::Ptr &journal, bool noHtmlMode) 1194 { 1195 if (!journal) { 1196 return QVariantHash(); 1197 } 1198 1199 QVariantHash incidence; 1200 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-journal"); 1201 incidence[QStringLiteral("summary")] = invitationSummary(journal, noHtmlMode); 1202 incidence[QStringLiteral("date")] = journal->dtStart(); 1203 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(journal, noHtmlMode); 1204 1205 return incidence; 1206 } 1207 1208 static QVariantHash invitationDetailsJournal(const Journal::Ptr &journal, const Journal::Ptr &oldjournal, bool noHtmlMode) 1209 { 1210 if (!oldjournal) { 1211 return invitationDetailsJournal(journal, noHtmlMode); 1212 } 1213 1214 QVariantHash incidence; 1215 incidence[QStringLiteral("iconName")] = QStringLiteral("view-pim-journal"); 1216 incidence[QStringLiteral("summary")] = htmlCompare(invitationSummary(journal, noHtmlMode), invitationSummary(oldjournal, noHtmlMode)); 1217 incidence[QStringLiteral("dateStr")] = 1218 htmlCompare(dateToString(journal->dtStart().toLocalTime().date(), false), dateToString(oldjournal->dtStart().toLocalTime().date(), false)); 1219 incidence[QStringLiteral("description")] = invitationDescriptionIncidence(journal, noHtmlMode); 1220 1221 return incidence; 1222 } 1223 1224 static QVariantHash invitationDetailsFreeBusy(const FreeBusy::Ptr &fb, bool noHtmlMode) 1225 { 1226 Q_UNUSED(noHtmlMode) 1227 1228 if (!fb) { 1229 return QVariantHash(); 1230 } 1231 1232 QVariantHash incidence; 1233 incidence[QStringLiteral("organizer")] = fb->organizer().fullName(); 1234 incidence[QStringLiteral("dtStart")] = fb->dtStart(); 1235 incidence[QStringLiteral("dtEnd")] = fb->dtEnd(); 1236 1237 QVariantList periodsList; 1238 const Period::List periods = fb->busyPeriods(); 1239 for (auto it = periods.cbegin(), end = periods.cend(); it != end; ++it) { 1240 QVariantHash period; 1241 period[QStringLiteral("hasDuration")] = it->hasDuration(); 1242 if (it->hasDuration()) { 1243 int dur = it->duration().asSeconds(); 1244 QString cont; 1245 if (dur >= 3600) { 1246 cont += i18ncp("hours part of duration", "1 hour ", "%1 hours ", dur / 3600); 1247 dur %= 3600; 1248 } 1249 if (dur >= 60) { 1250 cont += i18ncp("minutes part of duration", "1 minute", "%1 minutes ", dur / 60); 1251 dur %= 60; 1252 } 1253 if (dur > 0) { 1254 cont += i18ncp("seconds part of duration", "1 second", "%1 seconds", dur); 1255 } 1256 period[QStringLiteral("duration")] = cont; 1257 } 1258 period[QStringLiteral("start")] = it->start(); 1259 period[QStringLiteral("end")] = it->end(); 1260 1261 periodsList.push_back(period); 1262 } 1263 incidence[QStringLiteral("periods")] = periodsList; 1264 1265 return incidence; 1266 } 1267 1268 static QVariantHash invitationDetailsFreeBusy(const FreeBusy::Ptr &fb, const FreeBusy::Ptr &oldfb, bool noHtmlMode) 1269 { 1270 Q_UNUSED(oldfb) 1271 return invitationDetailsFreeBusy(fb, noHtmlMode); 1272 } 1273 1274 static bool replyMeansCounter(const Incidence::Ptr &incidence) 1275 { 1276 Q_UNUSED(incidence) 1277 return false; 1278 /** 1279 see kolab/issue 3665 for an example of when we might use this for something 1280 1281 bool status = false; 1282 if ( incidence ) { 1283 // put code here that looks at the incidence and determines that 1284 // the reply is meant to be a counter proposal. We think this happens 1285 // with Outlook counter proposals, but we aren't sure how yet. 1286 if ( condition ) { 1287 status = true; 1288 } 1289 } 1290 return status; 1291 */ 1292 } 1293 1294 static QString invitationHeaderEvent(const Event::Ptr &event, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender) 1295 { 1296 if (!msg || !event) { 1297 return QString(); 1298 } 1299 1300 switch (msg->method()) { 1301 case iTIPPublish: 1302 return i18n("This invitation has been published."); 1303 case iTIPRequest: 1304 if (existingIncidence && event->revision() > 0) { 1305 QString orgStr = organizerName(event, sender); 1306 if (senderIsOrganizer(event, sender)) { 1307 return i18n("This invitation has been updated by the organizer %1.", orgStr); 1308 } else { 1309 return i18n("This invitation has been updated by %1 as a representative of %2.", sender, orgStr); 1310 } 1311 } 1312 if (iamOrganizer(event)) { 1313 return i18n("I created this invitation."); 1314 } else { 1315 QString orgStr = organizerName(event, sender); 1316 if (senderIsOrganizer(event, sender)) { 1317 return i18n("You received an invitation from %1.", orgStr); 1318 } else { 1319 return i18n("You received an invitation from %1 as a representative of %2.", sender, orgStr); 1320 } 1321 } 1322 case iTIPRefresh: 1323 return i18n("This invitation was refreshed."); 1324 case iTIPCancel: 1325 if (iamOrganizer(event)) { 1326 return i18n("This invitation has been canceled."); 1327 } else { 1328 return i18n("The organizer has revoked the invitation."); 1329 } 1330 case iTIPAdd: 1331 return i18n("Addition to the invitation."); 1332 case iTIPReply: { 1333 if (replyMeansCounter(event)) { 1334 return i18n("%1 makes this counter proposal.", firstAttendeeName(event, sender)); 1335 } 1336 1337 Attendee::List attendees = event->attendees(); 1338 if (attendees.isEmpty()) { 1339 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!"; 1340 return QString(); 1341 } 1342 if (attendees.count() != 1) { 1343 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1" 1344 << "but is" << attendees.count(); 1345 } 1346 QString attendeeName = firstAttendeeName(event, sender); 1347 1348 QString delegatorName; 1349 QString dummy; 1350 const Attendee attendee = *attendees.begin(); 1351 KEmailAddress::extractEmailAddressAndName(attendee.delegator(), dummy, delegatorName); 1352 if (delegatorName.isEmpty()) { 1353 delegatorName = attendee.delegator(); 1354 } 1355 1356 switch (attendee.status()) { 1357 case Attendee::NeedsAction: 1358 return i18n("%1 indicates this invitation still needs some action.", attendeeName); 1359 case Attendee::Accepted: 1360 if (event->revision() > 0) { 1361 if (!sender.isEmpty()) { 1362 return i18n("This invitation has been updated by attendee %1.", sender); 1363 } else { 1364 return i18n("This invitation has been updated by an attendee."); 1365 } 1366 } else { 1367 if (delegatorName.isEmpty()) { 1368 return i18n("%1 accepts this invitation.", attendeeName); 1369 } else { 1370 return i18n("%1 accepts this invitation on behalf of %2.", attendeeName, delegatorName); 1371 } 1372 } 1373 case Attendee::Tentative: 1374 if (delegatorName.isEmpty()) { 1375 return i18n("%1 tentatively accepts this invitation.", attendeeName); 1376 } else { 1377 return i18n("%1 tentatively accepts this invitation on behalf of %2.", attendeeName, delegatorName); 1378 } 1379 case Attendee::Declined: 1380 if (delegatorName.isEmpty()) { 1381 return i18n("%1 declines this invitation.", attendeeName); 1382 } else { 1383 return i18n("%1 declines this invitation on behalf of %2.", attendeeName, delegatorName); 1384 } 1385 case Attendee::Delegated: { 1386 QString delegate; 1387 QString dummy; 1388 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegate); 1389 if (delegate.isEmpty()) { 1390 delegate = attendee.delegate(); 1391 } 1392 if (!delegate.isEmpty()) { 1393 return i18n("%1 has delegated this invitation to %2.", attendeeName, delegate); 1394 } else { 1395 return i18n("%1 has delegated this invitation.", attendeeName); 1396 } 1397 } 1398 case Attendee::Completed: 1399 return i18n("This invitation is now completed."); 1400 case Attendee::InProcess: 1401 return i18n("%1 is still processing the invitation.", attendeeName); 1402 case Attendee::None: 1403 return i18n("Unknown response to this invitation."); 1404 } 1405 break; 1406 } 1407 case iTIPCounter: 1408 return i18n("%1 makes this counter proposal.", firstAttendeeName(event, i18n("Sender"))); 1409 1410 case iTIPDeclineCounter: { 1411 QString orgStr = organizerName(event, sender); 1412 if (senderIsOrganizer(event, sender)) { 1413 return i18n("%1 declines your counter proposal.", orgStr); 1414 } else { 1415 return i18n("%1 declines your counter proposal on behalf of %2.", sender, orgStr); 1416 } 1417 } 1418 1419 case iTIPNoMethod: 1420 return i18n("Error: Event iTIP message with unknown method."); 1421 } 1422 qCritical() << "encountered an iTIP method that we do not support."; 1423 return QString(); 1424 } 1425 1426 static QString invitationHeaderTodo(const Todo::Ptr &todo, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender) 1427 { 1428 if (!msg || !todo) { 1429 return QString(); 1430 } 1431 1432 switch (msg->method()) { 1433 case iTIPPublish: 1434 return i18n("This to-do has been published."); 1435 case iTIPRequest: 1436 if (existingIncidence && todo->revision() > 0) { 1437 QString orgStr = organizerName(todo, sender); 1438 if (senderIsOrganizer(todo, sender)) { 1439 return i18n("This to-do has been updated by the organizer %1.", orgStr); 1440 } else { 1441 return i18n("This to-do has been updated by %1 as a representative of %2.", sender, orgStr); 1442 } 1443 } else { 1444 if (iamOrganizer(todo)) { 1445 return i18n("I created this to-do."); 1446 } else { 1447 QString orgStr = organizerName(todo, sender); 1448 if (senderIsOrganizer(todo, sender)) { 1449 return i18n("You have been assigned this to-do by %1.", orgStr); 1450 } else { 1451 return i18n("You have been assigned this to-do by %1 as a representative of %2.", sender, orgStr); 1452 } 1453 } 1454 } 1455 case iTIPRefresh: 1456 return i18n("This to-do was refreshed."); 1457 case iTIPCancel: 1458 if (iamOrganizer(todo)) { 1459 return i18n("This to-do was canceled."); 1460 } else { 1461 return i18n("The organizer has revoked this to-do."); 1462 } 1463 case iTIPAdd: 1464 return i18n("Addition to the to-do."); 1465 case iTIPReply: { 1466 if (replyMeansCounter(todo)) { 1467 return i18n("%1 makes this counter proposal.", firstAttendeeName(todo, sender)); 1468 } 1469 1470 Attendee::List attendees = todo->attendees(); 1471 if (attendees.isEmpty()) { 1472 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!"; 1473 return QString(); 1474 } 1475 if (attendees.count() != 1) { 1476 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1." 1477 << "but is" << attendees.count(); 1478 } 1479 QString attendeeName = firstAttendeeName(todo, sender); 1480 1481 QString delegatorName; 1482 QString dummy; 1483 const Attendee attendee = *attendees.begin(); 1484 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegatorName); 1485 if (delegatorName.isEmpty()) { 1486 delegatorName = attendee.delegator(); 1487 } 1488 1489 switch (attendee.status()) { 1490 case Attendee::NeedsAction: 1491 return i18n("%1 indicates this to-do assignment still needs some action.", attendeeName); 1492 case Attendee::Accepted: 1493 if (todo->revision() > 0) { 1494 if (!sender.isEmpty()) { 1495 if (todo->isCompleted()) { 1496 return i18n("This to-do has been completed by assignee %1.", sender); 1497 } else { 1498 return i18n("This to-do has been updated by assignee %1.", sender); 1499 } 1500 } else { 1501 if (todo->isCompleted()) { 1502 return i18n("This to-do has been completed by an assignee."); 1503 } else { 1504 return i18n("This to-do has been updated by an assignee."); 1505 } 1506 } 1507 } else { 1508 if (delegatorName.isEmpty()) { 1509 return i18n("%1 accepts this to-do.", attendeeName); 1510 } else { 1511 return i18n("%1 accepts this to-do on behalf of %2.", attendeeName, delegatorName); 1512 } 1513 } 1514 case Attendee::Tentative: 1515 if (delegatorName.isEmpty()) { 1516 return i18n("%1 tentatively accepts this to-do.", attendeeName); 1517 } else { 1518 return i18n("%1 tentatively accepts this to-do on behalf of %2.", attendeeName, delegatorName); 1519 } 1520 case Attendee::Declined: 1521 if (delegatorName.isEmpty()) { 1522 return i18n("%1 declines this to-do.", attendeeName); 1523 } else { 1524 return i18n("%1 declines this to-do on behalf of %2.", attendeeName, delegatorName); 1525 } 1526 case Attendee::Delegated: { 1527 QString delegate; 1528 QString dummy; 1529 KEmailAddress::extractEmailAddressAndName(attendee.delegate(), dummy, delegate); 1530 if (delegate.isEmpty()) { 1531 delegate = attendee.delegate(); 1532 } 1533 if (!delegate.isEmpty()) { 1534 return i18n("%1 has delegated this to-do to %2.", attendeeName, delegate); 1535 } else { 1536 return i18n("%1 has delegated this to-do.", attendeeName); 1537 } 1538 } 1539 case Attendee::Completed: 1540 return i18n("The request for this to-do is now completed."); 1541 case Attendee::InProcess: 1542 return i18n("%1 is still processing the to-do.", attendeeName); 1543 case Attendee::None: 1544 return i18n("Unknown response to this to-do."); 1545 } 1546 break; 1547 } 1548 case iTIPCounter: 1549 return i18n("%1 makes this counter proposal.", firstAttendeeName(todo, sender)); 1550 1551 case iTIPDeclineCounter: { 1552 const QString orgStr = organizerName(todo, sender); 1553 if (senderIsOrganizer(todo, sender)) { 1554 return i18n("%1 declines the counter proposal.", orgStr); 1555 } else { 1556 return i18n("%1 declines the counter proposal on behalf of %2.", sender, orgStr); 1557 } 1558 } 1559 1560 case iTIPNoMethod: 1561 return i18n("Error: To-do iTIP message with unknown method."); 1562 } 1563 qCritical() << "encountered an iTIP method that we do not support"; 1564 return QString(); 1565 } 1566 1567 static QString invitationHeaderJournal(const Journal::Ptr &journal, const ScheduleMessage::Ptr &msg) 1568 { 1569 if (!msg || !journal) { 1570 return QString(); 1571 } 1572 1573 switch (msg->method()) { 1574 case iTIPPublish: 1575 return i18n("This journal has been published."); 1576 case iTIPRequest: 1577 return i18n("You have been assigned this journal."); 1578 case iTIPRefresh: 1579 return i18n("This journal was refreshed."); 1580 case iTIPCancel: 1581 return i18n("This journal was canceled."); 1582 case iTIPAdd: 1583 return i18n("Addition to the journal."); 1584 case iTIPReply: { 1585 if (replyMeansCounter(journal)) { 1586 return i18n("Sender makes this counter proposal."); 1587 } 1588 1589 Attendee::List attendees = journal->attendees(); 1590 if (attendees.isEmpty()) { 1591 qCDebug(KCALUTILS_LOG) << "No attendees in the iCal reply!"; 1592 return QString(); 1593 } 1594 if (attendees.count() != 1) { 1595 qCDebug(KCALUTILS_LOG) << "Warning: attendeecount in the reply should be 1 " 1596 << "but is " << attendees.count(); 1597 } 1598 const Attendee attendee = *attendees.begin(); 1599 1600 switch (attendee.status()) { 1601 case Attendee::NeedsAction: 1602 return i18n("Sender indicates this journal assignment still needs some action."); 1603 case Attendee::Accepted: 1604 return i18n("Sender accepts this journal."); 1605 case Attendee::Tentative: 1606 return i18n("Sender tentatively accepts this journal."); 1607 case Attendee::Declined: 1608 return i18n("Sender declines this journal."); 1609 case Attendee::Delegated: 1610 return i18n("Sender has delegated this request for the journal."); 1611 case Attendee::Completed: 1612 return i18n("The request for this journal is now completed."); 1613 case Attendee::InProcess: 1614 return i18n("Sender is still processing the invitation."); 1615 case Attendee::None: 1616 return i18n("Unknown response to this journal."); 1617 } 1618 break; 1619 } 1620 case iTIPCounter: 1621 return i18n("Sender makes this counter proposal."); 1622 case iTIPDeclineCounter: 1623 return i18n("Sender declines the counter proposal."); 1624 case iTIPNoMethod: 1625 return i18n("Error: Journal iTIP message with unknown method."); 1626 } 1627 qCritical() << "encountered an iTIP method that we do not support"; 1628 return QString(); 1629 } 1630 1631 static QString invitationHeaderFreeBusy(const FreeBusy::Ptr &fb, const ScheduleMessage::Ptr &msg) 1632 { 1633 if (!msg || !fb) { 1634 return QString(); 1635 } 1636 1637 switch (msg->method()) { 1638 case iTIPPublish: 1639 return i18n("This free/busy list has been published."); 1640 case iTIPRequest: 1641 return i18n("The free/busy list has been requested."); 1642 case iTIPRefresh: 1643 return i18n("This free/busy list was refreshed."); 1644 case iTIPCancel: 1645 return i18n("This free/busy list was canceled."); 1646 case iTIPAdd: 1647 return i18n("Addition to the free/busy list."); 1648 case iTIPReply: 1649 return i18n("Reply to the free/busy list."); 1650 case iTIPCounter: 1651 return i18n("Sender makes this counter proposal."); 1652 case iTIPDeclineCounter: 1653 return i18n("Sender declines the counter proposal."); 1654 case iTIPNoMethod: 1655 return i18n("Error: Free/Busy iTIP message with unknown method."); 1656 } 1657 qCritical() << "encountered an iTIP method that we do not support"; 1658 return QString(); 1659 } 1660 1661 //@endcond 1662 1663 static QVariantList invitationAttendeeList(const Incidence::Ptr &incidence) 1664 { 1665 if (!incidence) { 1666 return QVariantList(); 1667 } 1668 1669 QVariantList attendees; 1670 const Attendee::List lstAttendees = incidence->attendees(); 1671 for (const Attendee &a : lstAttendees) { 1672 if (iamAttendee(a)) { 1673 continue; 1674 } 1675 1676 QVariantHash attendee; 1677 attendee[QStringLiteral("name")] = a.name(); 1678 attendee[QStringLiteral("email")] = a.email(); 1679 attendee[QStringLiteral("delegator")] = a.delegator(); 1680 attendee[QStringLiteral("delegate")] = a.delegate(); 1681 attendee[QStringLiteral("isOrganizer")] = attendeeIsOrganizer(incidence, a); 1682 attendee[QStringLiteral("status")] = Stringify::attendeeStatus(a.status()); 1683 attendee[QStringLiteral("icon")] = rsvpStatusIconName(a.status()); 1684 1685 attendees.push_back(attendee); 1686 } 1687 1688 return attendees; 1689 } 1690 1691 static QVariantList invitationRsvpList(const Incidence::Ptr &incidence, const Attendee &sender) 1692 { 1693 if (!incidence) { 1694 return QVariantList(); 1695 } 1696 1697 QVariantList attendees; 1698 const Attendee::List lstAttendees = incidence->attendees(); 1699 for (const Attendee &a_ : lstAttendees) { 1700 Attendee a = a_; 1701 if (!attendeeIsOrganizer(incidence, a)) { 1702 continue; 1703 } 1704 QVariantHash attendee; 1705 attendee[QStringLiteral("status")] = Stringify::attendeeStatus(a.status()); 1706 if (!sender.isNull() && (a.email() == sender.email())) { 1707 // use the attendee taken from the response incidence, 1708 // rather than the attendee from the calendar incidence. 1709 if (a.status() != sender.status()) { 1710 attendee[QStringLiteral("status")] = i18n("%1 (<i>unrecorded</i>", Stringify::attendeeStatus(sender.status())); 1711 } 1712 a = sender; 1713 } 1714 1715 attendee[QStringLiteral("name")] = a.name(); 1716 attendee[QStringLiteral("email")] = a.email(); 1717 attendee[QStringLiteral("delegator")] = a.delegator(); 1718 attendee[QStringLiteral("delegate")] = a.delegate(); 1719 attendee[QStringLiteral("isOrganizer")] = attendeeIsOrganizer(incidence, a); 1720 attendee[QStringLiteral("isMyself")] = iamAttendee(a); 1721 attendee[QStringLiteral("icon")] = rsvpStatusIconName(a.status()); 1722 1723 attendees.push_back(attendee); 1724 } 1725 1726 return attendees; 1727 } 1728 1729 static QVariantList invitationAttachments(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper) 1730 { 1731 if (!incidence) { 1732 return QVariantList(); 1733 } 1734 1735 if (incidence->type() == Incidence::TypeFreeBusy) { 1736 // A FreeBusy does not have a valid attachment due to the static-cast from IncidenceBase 1737 return QVariantList(); 1738 } 1739 1740 QVariantList attachments; 1741 const Attachment::List lstAttachments = incidence->attachments(); 1742 for (const Attachment &a : lstAttachments) { 1743 QVariantHash attachment; 1744 QMimeDatabase mimeDb; 1745 auto mimeType = mimeDb.mimeTypeForName(a.mimeType()); 1746 attachment[QStringLiteral("icon")] = (mimeType.isValid() ? mimeType.iconName() : QStringLiteral("application-octet-stream")); 1747 attachment[QStringLiteral("name")] = a.label(); 1748 const QString attachementStr = helper->generateLinkURL(QStringLiteral("ATTACH:%1").arg(QString::fromLatin1(a.label().toUtf8().toBase64()))); 1749 attachment[QStringLiteral("uri")] = attachementStr; 1750 attachments.push_back(attachment); 1751 } 1752 1753 return attachments; 1754 } 1755 1756 //@cond PRIVATE 1757 template<typename T> 1758 class KCalUtils::IncidenceFormatter::ScheduleMessageVisitor : public Visitor 1759 { 1760 public: 1761 bool act(const IncidenceBase::Ptr &incidence, const Incidence::Ptr &existingIncidence, const ScheduleMessage::Ptr &msg, const QString &sender) 1762 { 1763 mExistingIncidence = existingIncidence; 1764 mMessage = msg; 1765 mSender = sender; 1766 return incidence->accept(*this, incidence); 1767 } 1768 1769 [[nodiscard]] T result() const 1770 { 1771 return mResult; 1772 } 1773 1774 protected: 1775 T mResult; 1776 Incidence::Ptr mExistingIncidence; 1777 ScheduleMessage::Ptr mMessage; 1778 QString mSender; 1779 }; 1780 1781 class KCalUtils::IncidenceFormatter::InvitationHeaderVisitor : public IncidenceFormatter::ScheduleMessageVisitor<QString> 1782 { 1783 protected: 1784 bool visit(const Event::Ptr &event) override 1785 { 1786 mResult = invitationHeaderEvent(event, mExistingIncidence, mMessage, mSender); 1787 return !mResult.isEmpty(); 1788 } 1789 1790 bool visit(const Todo::Ptr &todo) override 1791 { 1792 mResult = invitationHeaderTodo(todo, mExistingIncidence, mMessage, mSender); 1793 return !mResult.isEmpty(); 1794 } 1795 1796 bool visit(const Journal::Ptr &journal) override 1797 { 1798 mResult = invitationHeaderJournal(journal, mMessage); 1799 return !mResult.isEmpty(); 1800 } 1801 1802 bool visit(const FreeBusy::Ptr &fb) override 1803 { 1804 mResult = invitationHeaderFreeBusy(fb, mMessage); 1805 return !mResult.isEmpty(); 1806 } 1807 }; 1808 1809 class KCalUtils::IncidenceFormatter::InvitationBodyVisitor : public IncidenceFormatter::ScheduleMessageVisitor<QVariantHash> 1810 { 1811 public: 1812 InvitationBodyVisitor(InvitationFormatterHelper *helper, bool noHtmlMode) 1813 : ScheduleMessageVisitor() 1814 , mHelper(helper) 1815 , mNoHtmlMode(noHtmlMode) 1816 { 1817 } 1818 1819 protected: 1820 bool visit(const Event::Ptr &event) override 1821 { 1822 Event::Ptr oldevent = mExistingIncidence.dynamicCast<Event>(); 1823 mResult = invitationDetailsEvent(mHelper, event, oldevent, mMessage, mNoHtmlMode); 1824 return !mResult.isEmpty(); 1825 } 1826 1827 bool visit(const Todo::Ptr &todo) override 1828 { 1829 Todo::Ptr oldtodo = mExistingIncidence.dynamicCast<Todo>(); 1830 mResult = invitationDetailsTodo(todo, oldtodo, mMessage, mNoHtmlMode); 1831 return !mResult.isEmpty(); 1832 } 1833 1834 bool visit(const Journal::Ptr &journal) override 1835 { 1836 Journal::Ptr oldjournal = mExistingIncidence.dynamicCast<Journal>(); 1837 mResult = invitationDetailsJournal(journal, oldjournal, mNoHtmlMode); 1838 return !mResult.isEmpty(); 1839 } 1840 1841 bool visit(const FreeBusy::Ptr &fb) override 1842 { 1843 mResult = invitationDetailsFreeBusy(fb, FreeBusy::Ptr(), mNoHtmlMode); 1844 return !mResult.isEmpty(); 1845 } 1846 1847 private: 1848 InvitationFormatterHelper *mHelper; 1849 bool mNoHtmlMode; 1850 }; 1851 //@endcond 1852 1853 class KCalUtils::InvitationFormatterHelperPrivate 1854 { 1855 }; 1856 1857 InvitationFormatterHelper::InvitationFormatterHelper() 1858 : d(nullptr) 1859 { 1860 } 1861 1862 InvitationFormatterHelper::~InvitationFormatterHelper() 1863 { 1864 } 1865 1866 QString InvitationFormatterHelper::generateLinkURL(const QString &id) 1867 { 1868 return id; 1869 } 1870 1871 QString InvitationFormatterHelper::makeLink(const QString &id, const QString &text) 1872 { 1873 if (!id.startsWith(QLatin1StringView("ATTACH:"))) { 1874 const QString res = QStringLiteral("<a href=\"%1\"><font size=\"-1\"><b>%2</b></font></a>").arg(generateLinkURL(id), text); 1875 return res; 1876 } else { 1877 // draw the attachment links in non-bold face 1878 const QString res = QStringLiteral("<a href=\"%1\">%2</a>").arg(generateLinkURL(id), text); 1879 return res; 1880 } 1881 } 1882 1883 // Check if the given incidence is likely one that we own instead one from 1884 // a shared calendar (Kolab-specific) 1885 static bool incidenceOwnedByMe(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence) 1886 { 1887 Q_UNUSED(calendar) 1888 Q_UNUSED(incidence) 1889 return true; 1890 } 1891 1892 static QVariantHash inviteButton(const QString &id, const QString &text, const QString &iconName, InvitationFormatterHelper *helper) 1893 { 1894 QVariantHash button; 1895 button[QStringLiteral("uri")] = helper->generateLinkURL(id); 1896 button[QStringLiteral("icon")] = iconName; 1897 button[QStringLiteral("label")] = text; 1898 return button; 1899 } 1900 1901 static QVariantList responseButtons(const Incidence::Ptr &incidence, 1902 bool rsvpReq, 1903 bool rsvpRec, 1904 InvitationFormatterHelper *helper, 1905 const Incidence::Ptr &existingInc = Incidence::Ptr()) 1906 { 1907 bool hideAccept = false; 1908 bool hideTentative = false; 1909 bool hideDecline = false; 1910 1911 if (existingInc) { 1912 const Attendee ea = findMyAttendee(existingInc); 1913 if (!ea.isNull()) { 1914 // If this is an update of an already accepted incidence 1915 // to not show the buttons that confirm the status. 1916 hideAccept = ea.status() == Attendee::Accepted; 1917 hideDecline = ea.status() == Attendee::Declined; 1918 hideTentative = ea.status() == Attendee::Tentative; 1919 } 1920 } 1921 1922 QVariantList buttons; 1923 if (!rsvpReq && (incidence && incidence->revision() == 0)) { 1924 // Record only 1925 buttons << inviteButton(QStringLiteral("record"), i18n("Record"), QStringLiteral("dialog-ok"), helper); 1926 1927 // Move to trash 1928 buttons << inviteButton(QStringLiteral("delete"), i18n("Move to Trash"), QStringLiteral("edittrash"), helper); 1929 } else { 1930 // Accept 1931 if (!hideAccept) { 1932 buttons << inviteButton(QStringLiteral("accept"), i18nc("accept invitation", "Accept"), QStringLiteral("dialog-ok-apply"), helper); 1933 } 1934 1935 // Tentative 1936 if (!hideTentative) { 1937 buttons << inviteButton(QStringLiteral("accept_conditionally"), 1938 i18nc("Accept invitation conditionally", "Tentative"), 1939 QStringLiteral("dialog-ok"), 1940 helper); 1941 } 1942 1943 // Decline 1944 if (!hideDecline) { 1945 buttons << inviteButton(QStringLiteral("decline"), i18nc("decline invitation", "Decline"), QStringLiteral("dialog-cancel"), helper); 1946 } 1947 1948 // Counter proposal 1949 buttons << inviteButton(QStringLiteral("counter"), i18nc("invitation counter proposal", "Counter proposal ..."), QStringLiteral("edit-undo"), helper); 1950 } 1951 1952 if (!rsvpRec || (incidence && incidence->revision() > 0)) { 1953 // Delegate 1954 buttons << inviteButton(QStringLiteral("delegate"), i18nc("delegate invitation to another", "Delegate ..."), QStringLiteral("mail-forward"), helper); 1955 } 1956 return buttons; 1957 } 1958 1959 static QVariantList counterButtons(InvitationFormatterHelper *helper) 1960 { 1961 QVariantList buttons; 1962 1963 // Accept proposal 1964 buttons << inviteButton(QStringLiteral("accept_counter"), i18n("Accept"), QStringLiteral("dialog-ok-apply"), helper); 1965 1966 // Decline proposal 1967 buttons << inviteButton(QStringLiteral("decline_counter"), i18n("Decline"), QStringLiteral("dialog-cancel"), helper); 1968 1969 return buttons; 1970 } 1971 1972 static QVariantList recordButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper) 1973 { 1974 QVariantList buttons; 1975 if (incidence) { 1976 buttons << inviteButton(QStringLiteral("reply"), 1977 incidence->type() == Incidence::TypeTodo ? i18n("Record invitation in my to-do list") 1978 : i18n("Record invitation in my calendar"), 1979 QStringLiteral("dialog-ok"), 1980 helper); 1981 } 1982 return buttons; 1983 } 1984 1985 static QVariantList recordResponseButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper) 1986 { 1987 QVariantList buttons; 1988 1989 if (incidence) { 1990 buttons << inviteButton(QStringLiteral("reply"), 1991 incidence->type() == Incidence::TypeTodo ? i18n("Record response in my to-do list") : i18n("Record response in my calendar"), 1992 QStringLiteral("dialog-ok"), 1993 helper); 1994 } 1995 return buttons; 1996 } 1997 1998 static QVariantList cancelButtons(const Incidence::Ptr &incidence, InvitationFormatterHelper *helper) 1999 { 2000 QVariantList buttons; 2001 2002 // Remove invitation 2003 if (incidence) { 2004 buttons << inviteButton(QStringLiteral("cancel"), 2005 incidence->type() == Incidence::TypeTodo ? i18n("Remove invitation from my to-do list") 2006 : i18n("Remove invitation from my calendar"), 2007 QStringLiteral("dialog-cancel"), 2008 helper); 2009 } 2010 2011 return buttons; 2012 } 2013 2014 static QVariantHash invitationStyle() 2015 { 2016 QVariantHash style; 2017 QPalette p; 2018 p.setCurrentColorGroup(QPalette::Normal); 2019 style[QStringLiteral("buttonBg")] = p.color(QPalette::Button).name(); 2020 style[QStringLiteral("buttonBorder")] = p.shadow().color().name(); 2021 style[QStringLiteral("buttonFg")] = p.color(QPalette::ButtonText).name(); 2022 return style; 2023 } 2024 2025 Calendar::Ptr InvitationFormatterHelper::calendar() const 2026 { 2027 return Calendar::Ptr(); 2028 } 2029 2030 static QString 2031 formatICalInvitationHelper(const QString &invitation, const Calendar::Ptr &mCalendar, InvitationFormatterHelper *helper, bool noHtmlMode, const QString &sender) 2032 { 2033 if (invitation.isEmpty()) { 2034 return QString(); 2035 } 2036 2037 ICalFormat format; 2038 // parseScheduleMessage takes the tz from the calendar, 2039 // no need to set it manually here for the format! 2040 ScheduleMessage::Ptr msg = format.parseScheduleMessage(mCalendar, invitation); 2041 2042 if (!msg) { 2043 qCDebug(KCALUTILS_LOG) << "Failed to parse the scheduling message"; 2044 Q_ASSERT(format.exception()); 2045 qCDebug(KCALUTILS_LOG) << Stringify::errorMessage(*format.exception()); 2046 return QString(); 2047 } 2048 2049 IncidenceBase::Ptr incBase = msg->event(); 2050 2051 incBase->shiftTimes(mCalendar->timeZone(), QTimeZone::systemTimeZone()); 2052 2053 // Determine if this incidence is in my calendar (and owned by me) 2054 Incidence::Ptr existingIncidence; 2055 if (incBase && helper->calendar()) { 2056 existingIncidence = helper->calendar()->incidence(incBase->uid(), incBase->recurrenceId()); 2057 2058 if (!incidenceOwnedByMe(helper->calendar(), existingIncidence)) { 2059 existingIncidence.clear(); 2060 } 2061 if (!existingIncidence) { 2062 const Incidence::List list = helper->calendar()->incidences(); 2063 for (Incidence::List::ConstIterator it = list.begin(), end = list.end(); it != end; ++it) { 2064 if ((*it)->schedulingID() == incBase->uid() && incidenceOwnedByMe(helper->calendar(), *it) 2065 && (*it)->recurrenceId() == incBase->recurrenceId()) { 2066 existingIncidence = *it; 2067 break; 2068 } 2069 } 2070 } 2071 } 2072 2073 Incidence::Ptr inc = incBase.staticCast<Incidence>(); // the incidence in the invitation email 2074 2075 // If the IncidenceBase is a FreeBusy, then we cannot access the revision number in 2076 // the static-casted Incidence; so for sake of nothing better use 0 as the revision. 2077 int incRevision = 0; 2078 if (inc && inc->type() != Incidence::TypeFreeBusy) { 2079 incRevision = inc->revision(); 2080 } 2081 2082 IncidenceFormatter::InvitationHeaderVisitor headerVisitor; 2083 // The InvitationHeaderVisitor returns false if the incidence is somehow invalid, or not handled 2084 if (!headerVisitor.act(inc, existingIncidence, msg, sender)) { 2085 return QString(); 2086 } 2087 2088 QVariantHash incidence; 2089 2090 // use the Outlook 2007 Comparison Style 2091 IncidenceFormatter::InvitationBodyVisitor bodyVisitor(helper, noHtmlMode); 2092 bool bodyOk; 2093 if (msg->method() == iTIPRequest || msg->method() == iTIPReply || msg->method() == iTIPDeclineCounter) { 2094 if (inc && existingIncidence && incRevision < existingIncidence->revision()) { 2095 bodyOk = bodyVisitor.act(existingIncidence, inc, msg, sender); 2096 } else { 2097 bodyOk = bodyVisitor.act(inc, existingIncidence, msg, sender); 2098 } 2099 } else { 2100 bodyOk = bodyVisitor.act(inc, Incidence::Ptr(), msg, sender); 2101 } 2102 if (!bodyOk) { 2103 return QString(); 2104 } 2105 2106 incidence = bodyVisitor.result(); 2107 incidence[QStringLiteral("style")] = invitationStyle(); 2108 incidence[QStringLiteral("head")] = headerVisitor.result(); 2109 2110 // determine if I am the organizer for this invitation 2111 bool myInc = iamOrganizer(inc); 2112 2113 // determine if the invitation response has already been recorded 2114 bool rsvpRec = false; 2115 Attendee ea; 2116 if (!myInc) { 2117 Incidence::Ptr rsvpIncidence = existingIncidence; 2118 if (!rsvpIncidence && inc && incRevision > 0) { 2119 rsvpIncidence = inc; 2120 } 2121 if (rsvpIncidence) { 2122 ea = findMyAttendee(rsvpIncidence); 2123 } 2124 if (!ea.isNull() && (ea.status() == Attendee::Accepted || ea.status() == Attendee::Declined || ea.status() == Attendee::Tentative)) { 2125 rsvpRec = true; 2126 } 2127 } 2128 2129 // determine invitation role 2130 QString role; 2131 bool isDelegated = false; 2132 Attendee a = findMyAttendee(inc); 2133 if (a.isNull() && inc) { 2134 if (!inc->attendees().isEmpty()) { 2135 a = inc->attendees().at(0); 2136 } 2137 } 2138 if (!a.isNull()) { 2139 isDelegated = (a.status() == Attendee::Delegated); 2140 role = Stringify::attendeeRole(a.role()); 2141 } 2142 2143 // determine if RSVP needed, not-needed, or response already recorded 2144 bool rsvpReq = rsvpRequested(inc); 2145 if (!rsvpReq && !a.isNull() && a.status() == Attendee::NeedsAction) { 2146 rsvpReq = true; 2147 } 2148 2149 QString eventInfo; 2150 if (!myInc && !a.isNull()) { 2151 if (rsvpRec && inc) { 2152 if (incRevision == 0) { 2153 eventInfo = i18n("Your <b>%1</b> response has been recorded.", Stringify::attendeeStatus(ea.status())); 2154 } else { 2155 eventInfo = i18n("Your status for this invitation is <b>%1</b>.", Stringify::attendeeStatus(ea.status())); 2156 } 2157 rsvpReq = false; 2158 } else if (msg->method() == iTIPCancel) { 2159 eventInfo = i18n("This invitation was canceled."); 2160 } else if (msg->method() == iTIPAdd) { 2161 eventInfo = i18n("This invitation was accepted."); 2162 } else if (msg->method() == iTIPDeclineCounter) { 2163 rsvpReq = true; 2164 eventInfo = rsvpRequestedStr(rsvpReq, role); 2165 } else { 2166 if (!isDelegated) { 2167 eventInfo = rsvpRequestedStr(rsvpReq, role); 2168 } else { 2169 eventInfo = i18n("Awaiting delegation response."); 2170 } 2171 } 2172 } 2173 incidence[QStringLiteral("eventInfo")] = eventInfo; 2174 2175 // Print if the organizer gave you a preset status 2176 QString myStatus; 2177 if (!myInc) { 2178 if (inc && incRevision == 0) { 2179 myStatus = myStatusStr(inc); 2180 } 2181 } 2182 incidence[QStringLiteral("myStatus")] = myStatus; 2183 2184 // Add groupware links 2185 QVariantList buttons; 2186 switch (msg->method()) { 2187 case iTIPPublish: 2188 case iTIPRequest: 2189 case iTIPRefresh: 2190 case iTIPAdd: 2191 if (inc && incRevision > 0 && (existingIncidence || !helper->calendar())) { 2192 buttons += recordButtons(inc, helper); 2193 } 2194 2195 if (!myInc) { 2196 if (!a.isNull()) { 2197 buttons += responseButtons(inc, rsvpReq, rsvpRec, helper); 2198 } else { 2199 buttons += responseButtons(inc, false, false, helper); 2200 } 2201 } 2202 break; 2203 2204 case iTIPCancel: 2205 buttons = cancelButtons(inc, helper); 2206 break; 2207 2208 case iTIPReply: { 2209 // Record invitation response 2210 Attendee a; 2211 Attendee ea; 2212 if (inc) { 2213 // First, determine if this reply is really a counter in disguise. 2214 if (replyMeansCounter(inc)) { 2215 buttons = counterButtons(helper); 2216 break; 2217 } 2218 2219 // Next, maybe this is a declined reply that was delegated from me? 2220 // find first attendee who is delegated-from me 2221 // look a their PARTSTAT response, if the response is declined, 2222 // then we need to start over which means putting all the action 2223 // buttons and NOT putting on the [Record response..] button 2224 a = findDelegatedFromMyAttendee(inc); 2225 if (!a.isNull()) { 2226 if (a.status() != Attendee::Accepted || a.status() != Attendee::Tentative) { 2227 buttons = responseButtons(inc, rsvpReq, rsvpRec, helper); 2228 break; 2229 } 2230 } 2231 2232 // Finally, simply allow a Record of the reply 2233 if (!inc->attendees().isEmpty()) { 2234 a = inc->attendees().at(0); 2235 } 2236 if (!a.isNull() && helper->calendar()) { 2237 ea = findAttendee(existingIncidence, a.email()); 2238 } 2239 } 2240 if (!ea.isNull() && (ea.status() != Attendee::NeedsAction) && (ea.status() == a.status())) { 2241 const QString tStr = i18n("The <b>%1</b> response has been recorded", Stringify::attendeeStatus(ea.status())); 2242 buttons << inviteButton(QString(), tStr, QString(), helper); 2243 } else { 2244 if (inc) { 2245 buttons = recordResponseButtons(inc, helper); 2246 } 2247 } 2248 break; 2249 } 2250 2251 case iTIPCounter: 2252 // Counter proposal 2253 buttons = counterButtons(helper); 2254 break; 2255 2256 case iTIPDeclineCounter: 2257 buttons << responseButtons(inc, rsvpReq, rsvpRec, helper); 2258 break; 2259 2260 case iTIPNoMethod: 2261 break; 2262 } 2263 2264 incidence[QStringLiteral("buttons")] = buttons; 2265 2266 // Add the attendee list 2267 if (inc->type() == Incidence::TypeTodo) { 2268 incidence[QStringLiteral("attendeesTitle")] = i18n("Assignees:"); 2269 } else { 2270 incidence[QStringLiteral("attendeesTitle")] = i18n("Participants:"); 2271 } 2272 if (myInc) { 2273 incidence[QStringLiteral("attendees")] = invitationRsvpList(existingIncidence, a); 2274 } else { 2275 incidence[QStringLiteral("attendees")] = invitationAttendeeList(inc); 2276 } 2277 2278 // Add the attachment list 2279 incidence[QStringLiteral("attachments")] = invitationAttachments(inc, helper); 2280 2281 if (!inc->comments().isEmpty()) { 2282 incidence[QStringLiteral("comments")] = inc->comments(); 2283 } 2284 2285 QString templateName; 2286 switch (inc->type()) { 2287 case KCalendarCore::IncidenceBase::TypeEvent: 2288 templateName = QStringLiteral(":/itip_event.html"); 2289 break; 2290 case KCalendarCore::IncidenceBase::TypeTodo: 2291 templateName = QStringLiteral(":/itip_todo.html"); 2292 break; 2293 case KCalendarCore::IncidenceBase::TypeJournal: 2294 templateName = QStringLiteral(":/itip_journal.html"); 2295 break; 2296 case KCalendarCore::IncidenceBase::TypeFreeBusy: 2297 templateName = QStringLiteral(":/itip_freebusy.html"); 2298 break; 2299 case KCalendarCore::IncidenceBase::TypeUnknown: 2300 return QString(); 2301 } 2302 2303 return GrantleeTemplateManager::instance()->render(templateName, incidence); 2304 } 2305 2306 //@endcond 2307 2308 QString IncidenceFormatter::formatICalInvitation(const QString &invitation, const Calendar::Ptr &calendar, InvitationFormatterHelper *helper) 2309 { 2310 return formatICalInvitationHelper(invitation, calendar, helper, false, QString()); 2311 } 2312 2313 QString IncidenceFormatter::formatICalInvitationNoHtml(const QString &invitation, 2314 const Calendar::Ptr &calendar, 2315 InvitationFormatterHelper *helper, 2316 const QString &sender) 2317 { 2318 return formatICalInvitationHelper(invitation, calendar, helper, true, sender); 2319 } 2320 2321 /******************************************************************* 2322 * Helper functions for the Incidence tooltips 2323 *******************************************************************/ 2324 2325 //@cond PRIVATE 2326 class KCalUtils::IncidenceFormatter::ToolTipVisitor : public Visitor 2327 { 2328 public: 2329 ToolTipVisitor() = default; 2330 2331 bool act(const Calendar::Ptr &calendar, const IncidenceBase::Ptr &incidence, QDate date = QDate(), bool richText = true) 2332 { 2333 mCalendar = calendar; 2334 mLocation.clear(); 2335 mDate = date; 2336 mRichText = richText; 2337 mResult = QLatin1StringView(""); 2338 return incidence ? incidence->accept(*this, incidence) : false; 2339 } 2340 2341 bool act(const QString &location, const IncidenceBase::Ptr &incidence, QDate date = QDate(), bool richText = true) 2342 { 2343 mLocation = location; 2344 mDate = date; 2345 mRichText = richText; 2346 mResult = QLatin1StringView(""); 2347 return incidence ? incidence->accept(*this, incidence) : false; 2348 } 2349 2350 [[nodiscard]] QString result() const 2351 { 2352 return mResult; 2353 } 2354 2355 protected: 2356 bool visit(const Event::Ptr &event) override; 2357 bool visit(const Todo::Ptr &todo) override; 2358 bool visit(const Journal::Ptr &journal) override; 2359 bool visit(const FreeBusy::Ptr &fb) override; 2360 2361 QString dateRangeText(const Event::Ptr &event, QDate date); 2362 QString dateRangeText(const Todo::Ptr &todo, QDate asOfDate); 2363 QString dateRangeText(const Journal::Ptr &journal); 2364 QString dateRangeText(const FreeBusy::Ptr &fb); 2365 2366 QString generateToolTip(const Incidence::Ptr &incidence, const QString &dtRangeText); 2367 2368 protected: 2369 Calendar::Ptr mCalendar; 2370 QString mLocation; 2371 QDate mDate; 2372 bool mRichText = true; 2373 QString mResult; 2374 }; 2375 2376 QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Event::Ptr &event, QDate date) 2377 { 2378 // FIXME: support mRichText==false 2379 QString ret; 2380 QString tmp; 2381 2382 const auto startDts = event->startDateTimesForDate(date, QTimeZone::systemTimeZone()); 2383 const auto startDt = startDts.empty() ? event->dtStart().toLocalTime() : startDts[0].toLocalTime(); 2384 const auto endDt = event->endDateForStart(startDt).toLocalTime(); 2385 2386 if (event->isMultiDay()) { 2387 tmp = dateToString(startDt.date(), true); 2388 ret += QLatin1StringView("<br>") + i18nc("Event start", "<i>From:</i> %1", tmp); 2389 2390 tmp = dateToString(endDt.date(), true); 2391 ret += QLatin1StringView("<br>") + i18nc("Event end", "<i>To:</i> %1", tmp); 2392 } else { 2393 ret += QLatin1StringView("<br>") + i18n("<i>Date:</i> %1", dateToString(startDt.date(), false)); 2394 if (!event->allDay()) { 2395 const QString dtStartTime = timeToString(startDt.time(), true); 2396 const QString dtEndTime = timeToString(endDt.time(), true); 2397 if (dtStartTime == dtEndTime) { 2398 // to prevent 'Time: 17:00 - 17:00' 2399 tmp = QLatin1StringView("<br>") + i18nc("time for event", "<i>Time:</i> %1", dtStartTime); 2400 } else { 2401 tmp = QLatin1StringView("<br>") + i18nc("time range for event", "<i>Time:</i> %1 - %2", dtStartTime, dtEndTime); 2402 } 2403 ret += tmp; 2404 } 2405 } 2406 return ret.replace(QLatin1Char(' '), QLatin1StringView(" ")); 2407 } 2408 2409 QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Todo::Ptr &todo, QDate asOfDate) 2410 { 2411 // FIXME: support mRichText==false 2412 // FIXME: doesn't handle to-dos that occur more than once per day. 2413 2414 QDateTime startDt{todo->dtStart(false)}; 2415 QDateTime dueDt{todo->dtDue(false)}; 2416 2417 if (todo->recurs() && asOfDate.isValid()) { 2418 const QDateTime limit{asOfDate.addDays(1), QTime(0, 0, 0), Qt::LocalTime}; 2419 startDt = todo->recurrence()->getPreviousDateTime(limit); 2420 if (startDt.isValid() && todo->hasDueDate()) { 2421 if (todo->allDay()) { 2422 // Days, not seconds, because not all days are 24 hours long. 2423 const auto duration{todo->dtStart(true).daysTo(todo->dtDue(true))}; 2424 dueDt = startDt.addDays(duration); 2425 } else { 2426 const auto duration{todo->dtStart(true).secsTo(todo->dtDue(true))}; 2427 dueDt = startDt.addSecs(duration); 2428 } 2429 } 2430 } 2431 2432 QString ret; 2433 if (startDt.isValid()) { 2434 ret = QLatin1StringView("<br>") % i18nc("To-do's start date", "<i>Start:</i> %1", dateTimeToString(startDt, todo->allDay(), false)); 2435 } 2436 if (dueDt.isValid()) { 2437 ret += QLatin1StringView("<br>") % i18nc("To-do's due date", "<i>Due:</i> %1", dateTimeToString(dueDt, todo->allDay(), false)); 2438 } 2439 2440 // Print priority and completed info here, for lack of a better place 2441 2442 if (todo->priority() > 0) { 2443 ret += QLatin1StringView("<br>") % i18nc("To-do's priority number", "<i>Priority:</i> %1", QString::number(todo->priority())); 2444 } 2445 2446 ret += QLatin1StringView("<br>"); 2447 if (todo->hasCompletedDate()) { 2448 ret += i18nc("To-do's completed date", "<i>Completed:</i> %1", dateTimeToString(todo->completed(), false, false)); 2449 } else { 2450 int pct = todo->percentComplete(); 2451 if (todo->recurs() && asOfDate.isValid()) { 2452 const QDate recurrenceDate = todo->dtRecurrence().date(); 2453 if (recurrenceDate < startDt.date()) { 2454 pct = 0; 2455 } else if (recurrenceDate > startDt.date()) { 2456 pct = 100; 2457 } 2458 } 2459 ret += i18nc("To-do's percent complete:", "<i>Percent Done:</i> %1%", pct); 2460 } 2461 2462 return ret.replace(QLatin1Char(' '), QLatin1StringView(" ")); 2463 } 2464 2465 QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const Journal::Ptr &journal) 2466 { 2467 // FIXME: support mRichText==false 2468 QString ret; 2469 if (journal->dtStart().isValid()) { 2470 ret += QLatin1StringView("<br>") + i18n("<i>Date:</i> %1", dateToString(journal->dtStart().toLocalTime().date(), false)); 2471 } 2472 return ret.replace(QLatin1Char(' '), QLatin1StringView(" ")); 2473 } 2474 2475 QString IncidenceFormatter::ToolTipVisitor::dateRangeText(const FreeBusy::Ptr &fb) 2476 { 2477 // FIXME: support mRichText==false 2478 QString ret = QLatin1StringView("<br>") + i18n("<i>Period start:</i> %1", QLocale().toString(fb->dtStart(), QLocale::ShortFormat)); 2479 ret += QLatin1StringView("<br>") + i18n("<i>Period start:</i> %1", QLocale().toString(fb->dtEnd(), QLocale::ShortFormat)); 2480 return ret.replace(QLatin1Char(' '), QLatin1StringView(" ")); 2481 } 2482 2483 bool IncidenceFormatter::ToolTipVisitor::visit(const Event::Ptr &event) 2484 { 2485 mResult = generateToolTip(event, dateRangeText(event, mDate)); 2486 return !mResult.isEmpty(); 2487 } 2488 2489 bool IncidenceFormatter::ToolTipVisitor::visit(const Todo::Ptr &todo) 2490 { 2491 mResult = generateToolTip(todo, dateRangeText(todo, mDate)); 2492 return !mResult.isEmpty(); 2493 } 2494 2495 bool IncidenceFormatter::ToolTipVisitor::visit(const Journal::Ptr &journal) 2496 { 2497 mResult = generateToolTip(journal, dateRangeText(journal)); 2498 return !mResult.isEmpty(); 2499 } 2500 2501 bool IncidenceFormatter::ToolTipVisitor::visit(const FreeBusy::Ptr &fb) 2502 { 2503 // FIXME: support mRichText==false 2504 mResult = QLatin1StringView("<qt><b>") + i18n("Free/Busy information for %1", fb->organizer().fullName()) + QLatin1StringView("</b>"); 2505 mResult += dateRangeText(fb); 2506 mResult += QLatin1StringView("</qt>"); 2507 return !mResult.isEmpty(); 2508 } 2509 2510 static QString tooltipPerson(const QString &email, const QString &name, Attendee::PartStat status) 2511 { 2512 // Search for a new print name, if needed. 2513 const QString printName = searchName(email, name); 2514 2515 // Get the icon corresponding to the attendee participation status. 2516 const QString iconPath = KIconLoader::global()->iconPath(rsvpStatusIconName(status), KIconLoader::Small); 2517 2518 // Make the return string. 2519 QString personString; 2520 if (!iconPath.isEmpty()) { 2521 personString += QLatin1StringView(R"(<img valign="top" src=")") + iconPath + QLatin1StringView("\">") + QLatin1StringView(" "); 2522 } 2523 if (status != Attendee::None) { 2524 personString += i18nc("attendee name (attendee status)", "%1 (%2)", printName.isEmpty() ? email : printName, Stringify::attendeeStatus(status)); 2525 } else { 2526 personString += i18n("%1", printName.isEmpty() ? email : printName); 2527 } 2528 return personString; 2529 } 2530 2531 static QString tooltipFormatOrganizer(const QString &email, const QString &name) 2532 { 2533 // Search for a new print name, if needed 2534 const QString printName = searchName(email, name); 2535 2536 // Get the icon for organizer 2537 // TODO fixme laurent: use another icon. It doesn't exist in breeze. 2538 const QString iconPath = KIconLoader::global()->iconPath(QStringLiteral("meeting-organizer"), KIconLoader::Small, true); 2539 2540 // Make the return string. 2541 QString personString; 2542 if (!iconPath.isEmpty()) { 2543 personString += QLatin1StringView(R"(<img valign="top" src=")") + iconPath + QLatin1StringView("\">") + QLatin1StringView(" "); 2544 } 2545 personString += (printName.isEmpty() ? email : printName); 2546 return personString; 2547 } 2548 2549 static QString tooltipFormatAttendeeRoleList(const Incidence::Ptr &incidence, Attendee::Role role, bool showStatus) 2550 { 2551 int maxNumAtts = 8; // maximum number of people to print per attendee role 2552 const QString etc = i18nc("ellipsis", "..."); 2553 2554 int i = 0; 2555 QString tmpStr; 2556 const Attendee::List attendees = incidence->attendees(); 2557 for (const auto &a : attendees) { 2558 if (a.role() != role) { 2559 // skip not this role 2560 continue; 2561 } 2562 if (attendeeIsOrganizer(incidence, a)) { 2563 // skip attendee that is also the organizer 2564 continue; 2565 } 2566 if (i == maxNumAtts) { 2567 tmpStr += QLatin1StringView(" ") + etc; 2568 break; 2569 } 2570 tmpStr += QLatin1StringView(" ") + tooltipPerson(a.email(), a.name(), showStatus ? a.status() : Attendee::None); 2571 if (!a.delegator().isEmpty()) { 2572 tmpStr += i18n(" (delegated by %1)", a.delegator()); 2573 } 2574 if (!a.delegate().isEmpty()) { 2575 tmpStr += i18n(" (delegated to %1)", a.delegate()); 2576 } 2577 tmpStr += QLatin1StringView("<br>"); 2578 i++; 2579 } 2580 if (tmpStr.endsWith(QLatin1StringView("<br>"))) { 2581 tmpStr.chop(4); 2582 } 2583 return tmpStr; 2584 } 2585 2586 static QString tooltipFormatAttendees(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence) 2587 { 2588 QString tmpStr; 2589 QString str; 2590 2591 // Add organizer link 2592 const int attendeeCount = incidence->attendees().count(); 2593 if (attendeeCount > 1 || (attendeeCount == 1 && !attendeeIsOrganizer(incidence, incidence->attendees().at(0)))) { 2594 tmpStr += QLatin1StringView("<i>") + i18n("Organizer:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2595 tmpStr += QLatin1StringView(" ") + tooltipFormatOrganizer(incidence->organizer().email(), incidence->organizer().name()); 2596 } 2597 2598 // Show the attendee status if the incidence's organizer owns the resource calendar, 2599 // which means they are running the show and have all the up-to-date response info. 2600 const bool showStatus = attendeeCount > 0 && incOrganizerOwnsCalendar(calendar, incidence); 2601 2602 // Add "chair" 2603 str = tooltipFormatAttendeeRoleList(incidence, Attendee::Chair, showStatus); 2604 if (!str.isEmpty()) { 2605 tmpStr += QLatin1StringView("<br><i>") + i18n("Chair:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2606 tmpStr += str; 2607 } 2608 2609 // Add required participants 2610 str = tooltipFormatAttendeeRoleList(incidence, Attendee::ReqParticipant, showStatus); 2611 if (!str.isEmpty()) { 2612 tmpStr += QLatin1StringView("<br><i>") + i18n("Required Participants:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2613 tmpStr += str; 2614 } 2615 2616 // Add optional participants 2617 str = tooltipFormatAttendeeRoleList(incidence, Attendee::OptParticipant, showStatus); 2618 if (!str.isEmpty()) { 2619 tmpStr += QLatin1StringView("<br><i>") + i18n("Optional Participants:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2620 tmpStr += str; 2621 } 2622 2623 // Add observers 2624 str = tooltipFormatAttendeeRoleList(incidence, Attendee::NonParticipant, showStatus); 2625 if (!str.isEmpty()) { 2626 tmpStr += QLatin1StringView("<br><i>") + i18n("Observers:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2627 tmpStr += str; 2628 } 2629 2630 return tmpStr; 2631 } 2632 2633 QString IncidenceFormatter::ToolTipVisitor::generateToolTip(const Incidence::Ptr &incidence, const QString &dtRangeText) 2634 { 2635 // FIXME: support mRichText==false 2636 if (!incidence) { 2637 return QString(); 2638 } 2639 2640 QString tmp = QStringLiteral("<qt>"); 2641 2642 // header 2643 tmp += QLatin1StringView("<b>") + incidence->richSummary() + QLatin1StringView("</b>"); 2644 tmp += QLatin1StringView("<hr>"); 2645 2646 QString calStr = mLocation; 2647 if (mCalendar) { 2648 calStr = resourceString(mCalendar, incidence); 2649 } 2650 if (!calStr.isEmpty()) { 2651 tmp += QLatin1StringView("<i>") + i18n("Calendar:") + QLatin1StringView("</i>") + QLatin1StringView(" "); 2652 tmp += calStr; 2653 } 2654 2655 tmp += dtRangeText; 2656 2657 if (!incidence->location().isEmpty()) { 2658 tmp += QLatin1StringView("<br>"); 2659 tmp += QLatin1StringView("<i>") + i18n("Location:") + QLatin1StringView("</i>") + QLatin1StringView(" "); 2660 tmp += incidence->richLocation(); 2661 } 2662 2663 QString durStr = durationString(incidence); 2664 if (!durStr.isEmpty()) { 2665 tmp += QLatin1StringView("<br>"); 2666 tmp += QLatin1StringView("<i>") + i18n("Duration:") + QLatin1StringView("</i>") + QLatin1StringView(" "); 2667 tmp += durStr; 2668 } 2669 2670 if (incidence->recurs()) { 2671 tmp += QLatin1StringView("<br>"); 2672 tmp += QLatin1StringView("<i>") + i18n("Recurrence:") + QLatin1StringView("</i>") + QLatin1StringView(" "); 2673 tmp += recurrenceString(incidence); 2674 } 2675 2676 if (incidence->hasRecurrenceId()) { 2677 tmp += QLatin1StringView("<br>"); 2678 tmp += QLatin1StringView("<i>") + i18n("Recurrence:") + QLatin1StringView("</i>") + QLatin1StringView(" "); 2679 tmp += i18n("Exception"); 2680 } 2681 2682 if (!incidence->description().isEmpty()) { 2683 QString desc(incidence->description()); 2684 if (!incidence->descriptionIsRich()) { 2685 int maxDescLen = 120; // maximum description chars to print (before ellipsis) 2686 if (desc.length() > maxDescLen) { 2687 desc = desc.left(maxDescLen) + i18nc("ellipsis", "..."); 2688 } 2689 desc = desc.toHtmlEscaped().replace(QLatin1Char('\n'), QLatin1StringView("<br>")); 2690 } else { 2691 // TODO: truncate the description when it's rich text 2692 } 2693 tmp += QLatin1StringView("<hr>"); 2694 tmp += QLatin1StringView("<i>") + i18n("Description:") + QLatin1StringView("</i>") + QLatin1StringView("<br>"); 2695 tmp += desc; 2696 } 2697 2698 bool needAnHorizontalLine = true; 2699 const int reminderCount = incidence->alarms().count(); 2700 if (reminderCount > 0 && incidence->hasEnabledAlarms()) { 2701 if (needAnHorizontalLine) { 2702 tmp += QLatin1StringView("<hr>"); 2703 needAnHorizontalLine = false; 2704 } 2705 tmp += QLatin1StringView("<br>"); 2706 tmp += QLatin1StringView("<i>") + i18np("Reminder:", "Reminders:", reminderCount) + QLatin1StringView("</i>") + QLatin1StringView(" "); 2707 tmp += reminderStringList(incidence).join(QLatin1StringView(", ")); 2708 } 2709 2710 const QString attendees = tooltipFormatAttendees(mCalendar, incidence); 2711 if (!attendees.isEmpty()) { 2712 if (needAnHorizontalLine) { 2713 tmp += QLatin1StringView("<hr>"); 2714 needAnHorizontalLine = false; 2715 } 2716 tmp += QLatin1StringView("<br>"); 2717 tmp += attendees; 2718 } 2719 2720 int categoryCount = incidence->categories().count(); 2721 if (categoryCount > 0) { 2722 if (needAnHorizontalLine) { 2723 tmp += QLatin1StringView("<hr>"); 2724 } 2725 tmp += QLatin1StringView("<br>"); 2726 tmp += QLatin1StringView("<i>") + i18np("Tag:", "Tags:", categoryCount) + QLatin1StringView("</i>") + QLatin1StringView(" "); 2727 tmp += incidence->categories().join(QLatin1StringView(", ")); 2728 } 2729 2730 tmp += QLatin1StringView("</qt>"); 2731 return tmp; 2732 } 2733 2734 //@endcond 2735 2736 QString IncidenceFormatter::toolTipStr(const QString &sourceName, const IncidenceBase::Ptr &incidence, QDate date, bool richText) 2737 { 2738 ToolTipVisitor v; 2739 if (incidence && v.act(sourceName, incidence, date, richText)) { 2740 return v.result(); 2741 } else { 2742 return QString(); 2743 } 2744 } 2745 2746 /******************************************************************* 2747 * Helper functions for the Incidence tooltips 2748 *******************************************************************/ 2749 2750 //@cond PRIVATE 2751 static QString mailBodyIncidence(const Incidence::Ptr &incidence) 2752 { 2753 QString body; 2754 if (!incidence->summary().trimmed().isEmpty()) { 2755 body += i18n("Summary: %1\n", incidence->richSummary()); 2756 } 2757 if (!incidence->organizer().isEmpty()) { 2758 body += i18n("Organizer: %1\n", incidence->organizer().fullName()); 2759 } 2760 if (!incidence->location().trimmed().isEmpty()) { 2761 body += i18n("Location: %1\n", incidence->richLocation()); 2762 } 2763 return body; 2764 } 2765 2766 //@endcond 2767 2768 //@cond PRIVATE 2769 class KCalUtils::IncidenceFormatter::MailBodyVisitor : public Visitor 2770 { 2771 public: 2772 bool act(const IncidenceBase::Ptr &incidence) 2773 { 2774 mResult = QLatin1StringView(""); 2775 return incidence ? incidence->accept(*this, incidence) : false; 2776 } 2777 2778 [[nodiscard]] QString result() const 2779 { 2780 return mResult; 2781 } 2782 2783 protected: 2784 bool visit(const Event::Ptr &event) override; 2785 bool visit(const Todo::Ptr &todo) override; 2786 bool visit(const Journal::Ptr &journal) override; 2787 bool visit(const FreeBusy::Ptr &) override 2788 { 2789 mResult = i18n("This is a Free Busy Object"); 2790 return true; 2791 } 2792 2793 protected: 2794 QString mResult; 2795 }; 2796 2797 bool IncidenceFormatter::MailBodyVisitor::visit(const Event::Ptr &event) 2798 { 2799 QString recurrence[] = {i18nc("no recurrence", "None"), 2800 i18nc("event recurs by minutes", "Minutely"), 2801 i18nc("event recurs by hours", "Hourly"), 2802 i18nc("event recurs by days", "Daily"), 2803 i18nc("event recurs by weeks", "Weekly"), 2804 i18nc("event recurs same position (e.g. first monday) each month", "Monthly Same Position"), 2805 i18nc("event recurs same day each month", "Monthly Same Day"), 2806 i18nc("event recurs same month each year", "Yearly Same Month"), 2807 i18nc("event recurs same day each year", "Yearly Same Day"), 2808 i18nc("event recurs same position (e.g. first monday) each year", "Yearly Same Position")}; 2809 2810 mResult = mailBodyIncidence(event); 2811 mResult += i18n("Start Date: %1\n", dateToString(event->dtStart().toLocalTime().date(), true)); 2812 if (!event->allDay()) { 2813 mResult += i18n("Start Time: %1\n", timeToString(event->dtStart().toLocalTime().time(), true)); 2814 } 2815 if (event->dtStart() != event->dtEnd()) { 2816 mResult += i18n("End Date: %1\n", dateToString(event->dtEnd().toLocalTime().date(), true)); 2817 } 2818 if (!event->allDay()) { 2819 mResult += i18n("End Time: %1\n", timeToString(event->dtEnd().toLocalTime().time(), true)); 2820 } 2821 if (event->recurs()) { 2822 Recurrence *recur = event->recurrence(); 2823 // TODO: Merge these two to one of the form "Recurs every 3 days" 2824 mResult += i18n("Recurs: %1\n", recurrence[recur->recurrenceType()]); 2825 mResult += i18n("Frequency: %1\n", event->recurrence()->frequency()); 2826 2827 if (recur->duration() > 0) { 2828 mResult += i18np("Repeats once", "Repeats %1 times", recur->duration()); 2829 mResult += QLatin1Char('\n'); 2830 } else { 2831 if (recur->duration() != -1) { 2832 // TODO_Recurrence: What to do with all-day 2833 QString endstr; 2834 if (event->allDay()) { 2835 endstr = QLocale().toString(recur->endDate()); 2836 } else { 2837 endstr = QLocale().toString(recur->endDateTime(), QLocale::ShortFormat); 2838 } 2839 mResult += i18n("Repeat until: %1\n", endstr); 2840 } else { 2841 mResult += i18n("Repeats forever\n"); 2842 } 2843 } 2844 } 2845 2846 if (!event->description().isEmpty()) { 2847 QString descStr; 2848 if (event->descriptionIsRich() || event->description().startsWith(QLatin1StringView("<!DOCTYPE HTML"))) { 2849 descStr = cleanHtml(event->description()); 2850 } else { 2851 descStr = event->description(); 2852 } 2853 if (!descStr.isEmpty()) { 2854 mResult += i18n("Details:\n%1\n", descStr); 2855 } 2856 } 2857 return !mResult.isEmpty(); 2858 } 2859 2860 bool IncidenceFormatter::MailBodyVisitor::visit(const Todo::Ptr &todo) 2861 { 2862 mResult = mailBodyIncidence(todo); 2863 2864 if (todo->hasStartDate() && todo->dtStart().isValid()) { 2865 mResult += i18n("Start Date: %1\n", dateToString(todo->dtStart(false).toLocalTime().date(), true)); 2866 if (!todo->allDay()) { 2867 mResult += i18n("Start Time: %1\n", timeToString(todo->dtStart(false).toLocalTime().time(), true)); 2868 } 2869 } 2870 if (todo->hasDueDate() && todo->dtDue().isValid()) { 2871 mResult += i18n("Due Date: %1\n", dateToString(todo->dtDue().toLocalTime().date(), true)); 2872 if (!todo->allDay()) { 2873 mResult += i18n("Due Time: %1\n", timeToString(todo->dtDue().toLocalTime().time(), true)); 2874 } 2875 } 2876 QString details = todo->richDescription(); 2877 if (!details.isEmpty()) { 2878 mResult += i18n("Details:\n%1\n", details); 2879 } 2880 return !mResult.isEmpty(); 2881 } 2882 2883 bool IncidenceFormatter::MailBodyVisitor::visit(const Journal::Ptr &journal) 2884 { 2885 mResult = mailBodyIncidence(journal); 2886 mResult += i18n("Date: %1\n", dateToString(journal->dtStart().toLocalTime().date(), true)); 2887 if (!journal->allDay()) { 2888 mResult += i18n("Time: %1\n", timeToString(journal->dtStart().toLocalTime().time(), true)); 2889 } 2890 if (!journal->description().isEmpty()) { 2891 mResult += i18n("Text of the journal:\n%1\n", journal->richDescription()); 2892 } 2893 return true; 2894 } 2895 2896 //@endcond 2897 2898 QString IncidenceFormatter::mailBodyStr(const IncidenceBase::Ptr &incidence) 2899 { 2900 if (!incidence) { 2901 return QString(); 2902 } 2903 2904 MailBodyVisitor v; 2905 if (v.act(incidence)) { 2906 return v.result(); 2907 } 2908 return QString(); 2909 } 2910 2911 //@cond PRIVATE 2912 static QString recurEnd(const Incidence::Ptr &incidence) 2913 { 2914 QString endstr; 2915 if (incidence->allDay()) { 2916 endstr = QLocale().toString(incidence->recurrence()->endDate()); 2917 } else { 2918 endstr = QLocale().toString(incidence->recurrence()->endDateTime().toLocalTime(), QLocale::ShortFormat); 2919 } 2920 return endstr; 2921 } 2922 2923 //@endcond 2924 2925 /************************************ 2926 * More static formatting functions 2927 ************************************/ 2928 2929 QString IncidenceFormatter::recurrenceString(const Incidence::Ptr &incidence) 2930 { 2931 if (incidence->hasRecurrenceId()) { 2932 return QStringLiteral("Recurrence exception"); 2933 } 2934 2935 if (!incidence->recurs()) { 2936 return i18n("No recurrence"); 2937 } 2938 static QStringList dayList; 2939 if (dayList.isEmpty()) { 2940 dayList.append(i18n("31st Last")); 2941 dayList.append(i18n("30th Last")); 2942 dayList.append(i18n("29th Last")); 2943 dayList.append(i18n("28th Last")); 2944 dayList.append(i18n("27th Last")); 2945 dayList.append(i18n("26th Last")); 2946 dayList.append(i18n("25th Last")); 2947 dayList.append(i18n("24th Last")); 2948 dayList.append(i18n("23rd Last")); 2949 dayList.append(i18n("22nd Last")); 2950 dayList.append(i18n("21st Last")); 2951 dayList.append(i18n("20th Last")); 2952 dayList.append(i18n("19th Last")); 2953 dayList.append(i18n("18th Last")); 2954 dayList.append(i18n("17th Last")); 2955 dayList.append(i18n("16th Last")); 2956 dayList.append(i18n("15th Last")); 2957 dayList.append(i18n("14th Last")); 2958 dayList.append(i18n("13th Last")); 2959 dayList.append(i18n("12th Last")); 2960 dayList.append(i18n("11th Last")); 2961 dayList.append(i18n("10th Last")); 2962 dayList.append(i18n("9th Last")); 2963 dayList.append(i18n("8th Last")); 2964 dayList.append(i18n("7th Last")); 2965 dayList.append(i18n("6th Last")); 2966 dayList.append(i18n("5th Last")); 2967 dayList.append(i18n("4th Last")); 2968 dayList.append(i18n("3rd Last")); 2969 dayList.append(i18n("2nd Last")); 2970 dayList.append(i18nc("last day of the month", "Last")); 2971 dayList.append(i18nc("unknown day of the month", "unknown")); //#31 - zero offset from UI 2972 dayList.append(i18n("1st")); 2973 dayList.append(i18n("2nd")); 2974 dayList.append(i18n("3rd")); 2975 dayList.append(i18n("4th")); 2976 dayList.append(i18n("5th")); 2977 dayList.append(i18n("6th")); 2978 dayList.append(i18n("7th")); 2979 dayList.append(i18n("8th")); 2980 dayList.append(i18n("9th")); 2981 dayList.append(i18n("10th")); 2982 dayList.append(i18n("11th")); 2983 dayList.append(i18n("12th")); 2984 dayList.append(i18n("13th")); 2985 dayList.append(i18n("14th")); 2986 dayList.append(i18n("15th")); 2987 dayList.append(i18n("16th")); 2988 dayList.append(i18n("17th")); 2989 dayList.append(i18n("18th")); 2990 dayList.append(i18n("19th")); 2991 dayList.append(i18n("20th")); 2992 dayList.append(i18n("21st")); 2993 dayList.append(i18n("22nd")); 2994 dayList.append(i18n("23rd")); 2995 dayList.append(i18n("24th")); 2996 dayList.append(i18n("25th")); 2997 dayList.append(i18n("26th")); 2998 dayList.append(i18n("27th")); 2999 dayList.append(i18n("28th")); 3000 dayList.append(i18n("29th")); 3001 dayList.append(i18n("30th")); 3002 dayList.append(i18n("31st")); 3003 } 3004 3005 const int weekStart = QLocale().firstDayOfWeek(); 3006 QString dayNames; 3007 3008 Recurrence *recur = incidence->recurrence(); 3009 3010 QString recurStr; 3011 static QString noRecurrence = i18n("No recurrence"); 3012 switch (recur->recurrenceType()) { 3013 case Recurrence::rNone: 3014 return noRecurrence; 3015 3016 case Recurrence::rMinutely: 3017 if (recur->duration() != -1) { 3018 recurStr = i18np("Recurs every minute until %2", "Recurs every %1 minutes until %2", recur->frequency(), recurEnd(incidence)); 3019 if (recur->duration() > 0) { 3020 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration())); 3021 } 3022 } else { 3023 recurStr = i18np("Recurs every minute", "Recurs every %1 minutes", recur->frequency()); 3024 } 3025 break; 3026 3027 case Recurrence::rHourly: 3028 if (recur->duration() != -1) { 3029 recurStr = i18np("Recurs hourly until %2", "Recurs every %1 hours until %2", recur->frequency(), recurEnd(incidence)); 3030 if (recur->duration() > 0) { 3031 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration())); 3032 } 3033 } else { 3034 recurStr = i18np("Recurs hourly", "Recurs every %1 hours", recur->frequency()); 3035 } 3036 break; 3037 3038 case Recurrence::rDaily: 3039 if (recur->duration() != -1) { 3040 recurStr = i18np("Recurs daily until %2", "Recurs every %1 days until %2", recur->frequency(), recurEnd(incidence)); 3041 if (recur->duration() > 0) { 3042 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration())); 3043 } 3044 } else { 3045 recurStr = i18np("Recurs daily", "Recurs every %1 days", recur->frequency()); 3046 } 3047 break; 3048 3049 case Recurrence::rWeekly: { 3050 bool addSpace = false; 3051 for (int i = 0; i < 7; ++i) { 3052 if (recur->days().testBit((i + weekStart + 6) % 7)) { 3053 if (addSpace) { 3054 dayNames.append(i18nc("separator for list of days", ", ")); 3055 } 3056 dayNames.append(QLocale().dayName(((i + weekStart + 6) % 7) + 1, QLocale::ShortFormat)); 3057 addSpace = true; 3058 } 3059 } 3060 if (dayNames.isEmpty()) { 3061 dayNames = i18nc("Recurs weekly on no days", "no days"); 3062 } 3063 if (recur->duration() != -1) { 3064 recurStr = i18ncp("Recurs weekly on [list of days] until end-date", 3065 "Recurs weekly on %2 until %3", 3066 "Recurs every %1 weeks on %2 until %3", 3067 recur->frequency(), 3068 dayNames, 3069 recurEnd(incidence)); 3070 if (recur->duration() > 0) { 3071 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration()); 3072 } 3073 } else { 3074 recurStr = i18ncp("Recurs weekly on [list of days]", "Recurs weekly on %2", "Recurs every %1 weeks on %2", recur->frequency(), dayNames); 3075 } 3076 break; 3077 } 3078 case Recurrence::rMonthlyPos: 3079 if (!recur->monthPositions().isEmpty()) { 3080 RecurrenceRule::WDayPos rule = recur->monthPositions().at(0); 3081 if (recur->duration() != -1) { 3082 recurStr = i18ncp( 3083 "Recurs every N months on the [2nd|3rd|...]" 3084 " weekdayname until end-date", 3085 "Recurs every month on the %2 %3 until %4", 3086 "Recurs every %1 months on the %2 %3 until %4", 3087 recur->frequency(), 3088 dayList[rule.pos() + 31], 3089 QLocale().dayName(rule.day(), QLocale::LongFormat), 3090 recurEnd(incidence)); 3091 if (recur->duration() > 0) { 3092 recurStr += xi18nc("number of occurrences", " (%1 occurrences)", recur->duration()); 3093 } 3094 } else { 3095 recurStr = i18ncp("Recurs every N months on the [2nd|3rd|...] weekdayname", 3096 "Recurs every month on the %2 %3", 3097 "Recurs every %1 months on the %2 %3", 3098 recur->frequency(), 3099 dayList[rule.pos() + 31], 3100 QLocale().dayName(rule.day(), QLocale::LongFormat)); 3101 } 3102 } 3103 break; 3104 case Recurrence::rMonthlyDay: 3105 if (!recur->monthDays().isEmpty()) { 3106 int days = recur->monthDays().at(0); 3107 if (recur->duration() != -1) { 3108 recurStr = i18ncp("Recurs monthly on the [1st|2nd|...] day until end-date", 3109 "Recurs monthly on the %2 day until %3", 3110 "Recurs every %1 months on the %2 day until %3", 3111 recur->frequency(), 3112 dayList[days + 31], 3113 recurEnd(incidence)); 3114 if (recur->duration() > 0) { 3115 recurStr += xi18nc("number of occurrences", " (%1 occurrences)", recur->duration()); 3116 } 3117 } else { 3118 recurStr = i18ncp("Recurs monthly on the [1st|2nd|...] day", 3119 "Recurs monthly on the %2 day", 3120 "Recurs every %1 month on the %2 day", 3121 recur->frequency(), 3122 dayList[days + 31]); 3123 } 3124 } 3125 break; 3126 case Recurrence::rYearlyMonth: 3127 if (recur->duration() != -1) { 3128 if (!recur->yearDates().isEmpty() && !recur->yearMonths().isEmpty()) { 3129 recurStr = i18ncp( 3130 "Recurs Every N years on month-name [1st|2nd|...]" 3131 " until end-date", 3132 "Recurs yearly on %2 %3 until %4", 3133 "Recurs every %1 years on %2 %3 until %4", 3134 recur->frequency(), 3135 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat), 3136 dayList.at(recur->yearDates().at(0) + 31), 3137 recurEnd(incidence)); 3138 if (recur->duration() > 0) { 3139 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration()); 3140 } 3141 } 3142 } else { 3143 if (!recur->yearDates().isEmpty() && !recur->yearMonths().isEmpty()) { 3144 recurStr = i18ncp("Recurs Every N years on month-name [1st|2nd|...]", 3145 "Recurs yearly on %2 %3", 3146 "Recurs every %1 years on %2 %3", 3147 recur->frequency(), 3148 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat), 3149 dayList[recur->yearDates().at(0) + 31]); 3150 } else { 3151 if (!recur->yearMonths().isEmpty()) { 3152 recurStr = i18nc("Recurs Every year on month-name [1st|2nd|...]", 3153 "Recurs yearly on %1 %2", 3154 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat), 3155 dayList[recur->startDate().day() + 31]); 3156 } else { 3157 recurStr = i18nc("Recurs Every year on month-name [1st|2nd|...]", 3158 "Recurs yearly on %1 %2", 3159 QLocale().monthName(recur->startDate().month(), QLocale::LongFormat), 3160 dayList[recur->startDate().day() + 31]); 3161 } 3162 } 3163 } 3164 break; 3165 case Recurrence::rYearlyDay: 3166 if (!recur->yearDays().isEmpty()) { 3167 if (recur->duration() != -1) { 3168 recurStr = i18ncp("Recurs every N years on day N until end-date", 3169 "Recurs every year on day %2 until %3", 3170 "Recurs every %1 years" 3171 " on day %2 until %3", 3172 recur->frequency(), 3173 QString::number(recur->yearDays().at(0)), 3174 recurEnd(incidence)); 3175 if (recur->duration() > 0) { 3176 recurStr += i18nc("number of occurrences", " (%1 occurrences)", QString::number(recur->duration())); 3177 } 3178 } else { 3179 recurStr = i18ncp("Recurs every N YEAR[S] on day N", 3180 "Recurs every year on day %2", 3181 "Recurs every %1 years" 3182 " on day %2", 3183 recur->frequency(), 3184 QString::number(recur->yearDays().at(0))); 3185 } 3186 } 3187 break; 3188 case Recurrence::rYearlyPos: 3189 if (!recur->yearMonths().isEmpty() && !recur->yearPositions().isEmpty()) { 3190 RecurrenceRule::WDayPos rule = recur->yearPositions().at(0); 3191 if (recur->duration() != -1) { 3192 recurStr = i18ncp( 3193 "Every N years on the [2nd|3rd|...] weekdayname " 3194 "of monthname until end-date", 3195 "Every year on the %2 %3 of %4 until %5", 3196 "Every %1 years on the %2 %3 of %4" 3197 " until %5", 3198 recur->frequency(), 3199 dayList[rule.pos() + 31], 3200 QLocale().dayName(rule.day(), QLocale::LongFormat), 3201 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat), 3202 recurEnd(incidence)); 3203 if (recur->duration() > 0) { 3204 recurStr += i18nc("number of occurrences", " (%1 occurrences)", recur->duration()); 3205 } 3206 } else { 3207 recurStr = xi18ncp( 3208 "Every N years on the [2nd|3rd|...] weekdayname " 3209 "of monthname", 3210 "Every year on the %2 %3 of %4", 3211 "Every %1 years on the %2 %3 of %4", 3212 recur->frequency(), 3213 dayList[rule.pos() + 31], 3214 QLocale().dayName(rule.day(), QLocale::LongFormat), 3215 QLocale().monthName(recur->yearMonths().at(0), QLocale::LongFormat)); 3216 } 3217 } 3218 break; 3219 } 3220 3221 if (recurStr.isEmpty()) { 3222 recurStr = i18n("Incidence recurs"); 3223 } 3224 3225 // Now, append the EXDATEs 3226 const auto l = recur->exDateTimes(); 3227 QStringList exStr; 3228 for (auto il = l.cbegin(), end = l.cend(); il != end; ++il) { 3229 switch (recur->recurrenceType()) { 3230 case Recurrence::rMinutely: 3231 exStr << i18n("minute %1", (*il).time().minute()); 3232 break; 3233 case Recurrence::rHourly: 3234 exStr << QLocale().toString((*il).time(), QLocale::ShortFormat); 3235 break; 3236 case Recurrence::rWeekly: 3237 exStr << QLocale().dayName((*il).date().dayOfWeek(), QLocale::ShortFormat); 3238 break; 3239 case Recurrence::rYearlyMonth: 3240 exStr << QString::number((*il).date().year()); 3241 break; 3242 case Recurrence::rDaily: 3243 case Recurrence::rMonthlyPos: 3244 case Recurrence::rMonthlyDay: 3245 case Recurrence::rYearlyDay: 3246 case Recurrence::rYearlyPos: 3247 exStr << QLocale().toString((*il).date(), QLocale::ShortFormat); 3248 break; 3249 } 3250 } 3251 3252 DateList d = recur->exDates(); 3253 DateList::ConstIterator dl; 3254 const DateList::ConstIterator dlEdnd(d.constEnd()); 3255 for (dl = d.constBegin(); dl != dlEdnd; ++dl) { 3256 switch (recur->recurrenceType()) { 3257 case Recurrence::rDaily: 3258 exStr << QLocale().toString((*dl), QLocale::ShortFormat); 3259 break; 3260 case Recurrence::rWeekly: 3261 // exStr << calSys->weekDayName( (*dl), KCalendarSystem::ShortDayName ); 3262 // kolab/issue4735, should be ( excluding 3 days ), instead of excluding( Fr,Fr,Fr ) 3263 if (exStr.isEmpty()) { 3264 exStr << i18np("1 day", "%1 days", recur->exDates().count()); 3265 } 3266 break; 3267 case Recurrence::rMonthlyPos: 3268 exStr << QLocale().toString((*dl), QLocale::ShortFormat); 3269 break; 3270 case Recurrence::rMonthlyDay: 3271 exStr << QLocale().toString((*dl), QLocale::ShortFormat); 3272 break; 3273 case Recurrence::rYearlyMonth: 3274 exStr << QString::number((*dl).year()); 3275 break; 3276 case Recurrence::rYearlyDay: 3277 exStr << QLocale().toString((*dl), QLocale::ShortFormat); 3278 break; 3279 case Recurrence::rYearlyPos: 3280 exStr << QLocale().toString((*dl), QLocale::ShortFormat); 3281 break; 3282 } 3283 } 3284 3285 if (!exStr.isEmpty()) { 3286 recurStr = i18n("%1 (excluding %2)", recurStr, exStr.join(QLatin1Char(','))); 3287 } 3288 3289 return recurStr; 3290 } 3291 3292 QString IncidenceFormatter::timeToString(QTime time, bool shortfmt) 3293 { 3294 return QLocale().toString(time, shortfmt ? QLocale::ShortFormat : QLocale::LongFormat); 3295 } 3296 3297 QString IncidenceFormatter::dateToString(QDate date, bool shortfmt) 3298 { 3299 return QLocale().toString(date, (shortfmt ? QLocale::ShortFormat : QLocale::LongFormat)); 3300 } 3301 3302 QString IncidenceFormatter::dateTimeToString(const QDateTime &date, bool allDay, bool shortfmt) 3303 { 3304 if (allDay) { 3305 return dateToString(date.toLocalTime().date(), shortfmt); 3306 } 3307 3308 return QLocale().toString(date.toLocalTime(), (shortfmt ? QLocale::ShortFormat : QLocale::LongFormat)); 3309 } 3310 3311 QString IncidenceFormatter::resourceString(const Calendar::Ptr &calendar, const Incidence::Ptr &incidence) 3312 { 3313 Q_UNUSED(calendar) 3314 Q_UNUSED(incidence) 3315 return QString(); 3316 } 3317 3318 static QString secs2Duration(qint64 secs) 3319 { 3320 QString tmp; 3321 qint64 days = secs / 86400; 3322 if (days > 0) { 3323 tmp += i18np("1 day", "%1 days", days); 3324 tmp += QLatin1Char(' '); 3325 secs -= (days * 86400); 3326 } 3327 qint64 hours = secs / 3600; 3328 if (hours > 0) { 3329 tmp += i18np("1 hour", "%1 hours", hours); 3330 tmp += QLatin1Char(' '); 3331 secs -= (hours * 3600); 3332 } 3333 qint64 mins = secs / 60; 3334 if (mins > 0) { 3335 tmp += i18np("1 minute", "%1 minutes", mins); 3336 } 3337 return tmp; 3338 } 3339 3340 QString IncidenceFormatter::durationString(const Incidence::Ptr &incidence) 3341 { 3342 QString tmp; 3343 if (incidence->type() == Incidence::TypeEvent) { 3344 Event::Ptr event = incidence.staticCast<Event>(); 3345 if (event->hasEndDate()) { 3346 if (!event->allDay()) { 3347 tmp = secs2Duration(event->dtStart().secsTo(event->dtEnd())); 3348 } else { 3349 tmp = i18np("1 day", "%1 days", event->dtStart().date().daysTo(event->dtEnd().date()) + 1); 3350 } 3351 } else { 3352 tmp = i18n("forever"); 3353 } 3354 } else if (incidence->type() == Incidence::TypeTodo) { 3355 Todo::Ptr todo = incidence.staticCast<Todo>(); 3356 if (todo->hasDueDate()) { 3357 if (todo->hasStartDate()) { 3358 if (!todo->allDay()) { 3359 tmp = secs2Duration(todo->dtStart().secsTo(todo->dtDue())); 3360 } else { 3361 tmp = i18np("1 day", "%1 days", todo->dtStart().date().daysTo(todo->dtDue().date()) + 1); 3362 } 3363 } 3364 } 3365 } 3366 return tmp; 3367 } 3368 3369 QStringList IncidenceFormatter::reminderStringList(const Incidence::Ptr &incidence, bool shortfmt) 3370 { 3371 // TODO: implement shortfmt=false 3372 Q_UNUSED(shortfmt) 3373 3374 QStringList reminderStringList; 3375 3376 if (incidence) { 3377 Alarm::List alarms = incidence->alarms(); 3378 Alarm::List::ConstIterator it; 3379 const Alarm::List::ConstIterator end(alarms.constEnd()); 3380 reminderStringList.reserve(alarms.count()); 3381 for (it = alarms.constBegin(); it != end; ++it) { 3382 Alarm::Ptr alarm = *it; 3383 int offset = 0; 3384 QString remStr; 3385 QString atStr; 3386 QString offsetStr; 3387 if (alarm->hasTime()) { 3388 offset = 0; 3389 if (alarm->time().isValid()) { 3390 atStr = QLocale().toString(alarm->time().toLocalTime(), QLocale::ShortFormat); 3391 } 3392 } else if (alarm->hasStartOffset()) { 3393 offset = alarm->startOffset().asSeconds(); 3394 if (offset < 0) { 3395 offset = -offset; 3396 offsetStr = i18nc("N days/hours/minutes before the start datetime", "%1 before the start", secs2Duration(offset)); 3397 } else if (offset > 0) { 3398 offsetStr = i18nc("N days/hours/minutes after the start datetime", "%1 after the start", secs2Duration(offset)); 3399 } else { // offset is 0 3400 if (incidence->dtStart().isValid()) { 3401 atStr = QLocale().toString(incidence->dtStart().toLocalTime(), QLocale::ShortFormat); 3402 } 3403 } 3404 } else if (alarm->hasEndOffset()) { 3405 offset = alarm->endOffset().asSeconds(); 3406 if (offset < 0) { 3407 offset = -offset; 3408 if (incidence->type() == Incidence::TypeTodo) { 3409 offsetStr = i18nc("N days/hours/minutes before the due datetime", "%1 before the to-do is due", secs2Duration(offset)); 3410 } else { 3411 offsetStr = i18nc("N days/hours/minutes before the end datetime", "%1 before the end", secs2Duration(offset)); 3412 } 3413 } else if (offset > 0) { 3414 if (incidence->type() == Incidence::TypeTodo) { 3415 offsetStr = i18nc("N days/hours/minutes after the due datetime", "%1 after the to-do is due", secs2Duration(offset)); 3416 } else { 3417 offsetStr = i18nc("N days/hours/minutes after the end datetime", "%1 after the end", secs2Duration(offset)); 3418 } 3419 } else { // offset is 0 3420 if (incidence->type() == Incidence::TypeTodo) { 3421 Todo::Ptr t = incidence.staticCast<Todo>(); 3422 if (t->dtDue().isValid()) { 3423 atStr = QLocale().toString(t->dtDue().toLocalTime(), QLocale::ShortFormat); 3424 } 3425 } else { 3426 Event::Ptr e = incidence.staticCast<Event>(); 3427 if (e->dtEnd().isValid()) { 3428 atStr = QLocale().toString(e->dtEnd().toLocalTime(), QLocale::ShortFormat); 3429 } 3430 } 3431 } 3432 } 3433 if (offset == 0) { 3434 if (!atStr.isEmpty()) { 3435 remStr = i18nc("reminder occurs at datetime", "at %1", atStr); 3436 } 3437 } else { 3438 remStr = offsetStr; 3439 } 3440 3441 if (alarm->repeatCount() > 0) { 3442 QString countStr = i18np("repeats once", "repeats %1 times", alarm->repeatCount()); 3443 QString intervalStr = i18nc("interval is N days/hours/minutes", "interval is %1", secs2Duration(alarm->snoozeTime().asSeconds())); 3444 QString repeatStr = i18nc("(repeat string, interval string)", "(%1, %2)", countStr, intervalStr); 3445 remStr = remStr + QLatin1Char(' ') + repeatStr; 3446 } 3447 reminderStringList << remStr; 3448 } 3449 } 3450 3451 return reminderStringList; 3452 }