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

KDE's Doxygen guidelines are available online.