KCalUtils

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

KDE's Doxygen guidelines are available online.