File indexing completed on 2025-01-05 04:47:55

0001 /*
0002   This file is part of KOrganizer.
0003 
0004   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 
0009 #include "whatsnextview.h"
0010 #include "calendarview_debug.h"
0011 
0012 #include <Akonadi/CalendarUtils>
0013 #include <CalendarSupport/KCalPrefs>
0014 #include <CalendarSupport/Utils>
0015 
0016 #include <KCalUtils/IncidenceFormatter>
0017 
0018 #include <QBoxLayout>
0019 #include <QUrlQuery>
0020 
0021 #include <optional>
0022 
0023 using namespace EventViews;
0024 void WhatsNextTextBrowser::doSetSource(const QUrl &name, QTextDocument::ResourceType type)
0025 {
0026     Q_UNUSED(type);
0027     if (name.scheme() == QLatin1StringView("event")) {
0028         Q_EMIT showIncidence(name);
0029     } else if (name.scheme() == QLatin1StringView("todo")) {
0030         Q_EMIT showIncidence(name);
0031     } else {
0032         QTextBrowser::setSource(name);
0033     }
0034 }
0035 
0036 WhatsNextView::WhatsNextView(QWidget *parent)
0037     : EventView(parent)
0038     , mView(new WhatsNextTextBrowser(this))
0039 {
0040     connect(mView, &WhatsNextTextBrowser::showIncidence, this, &WhatsNextView::showIncidence);
0041 
0042     QBoxLayout *topLayout = new QVBoxLayout(this);
0043     topLayout->setContentsMargins(0, 0, 0, 0);
0044     topLayout->addWidget(mView);
0045 }
0046 
0047 WhatsNextView::~WhatsNextView() = default;
0048 
0049 int WhatsNextView::currentDateCount() const
0050 {
0051     return mStartDate.daysTo(mEndDate);
0052 }
0053 
0054 void WhatsNextView::createTaskRow(KIconLoader *kil)
0055 {
0056     QString ipath;
0057     kil->loadIcon(QStringLiteral("view-calendar-tasks"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath);
0058     mText += QLatin1StringView("<h2><img src=\"");
0059     mText += ipath;
0060     mText += QLatin1StringView(R"(" width="22" height="22">)");
0061     mText += i18n("To-dos:") + QLatin1StringView("</h2>\n");
0062     mText += QLatin1StringView("<ul>\n");
0063 }
0064 
0065 void WhatsNextView::updateView()
0066 {
0067     KIconLoader *kil = KIconLoader::global();
0068     QString ipath;
0069     kil->loadIcon(QStringLiteral("office-calendar"), KIconLoader::NoGroup, 32, KIconLoader::DefaultState, QStringList(), &ipath);
0070 
0071     mText = QStringLiteral("<table width=\"100%\">\n");
0072     mText += QLatin1StringView("<tr bgcolor=\"#3679AD\"><td><h1>");
0073     mText += QLatin1StringView("<img src=\"");
0074     mText += ipath;
0075     mText += QLatin1StringView(R"(" width="32" height="32">)");
0076     mText += QLatin1StringView("<font color=\"white\"> ");
0077     mText += i18n("What's Next?") + QLatin1StringView("</font></h1>");
0078     mText += QLatin1StringView("</td></tr>\n<tr><td>");
0079 
0080     mText += QLatin1StringView("<h2>");
0081     if (mStartDate.daysTo(mEndDate) < 1) {
0082         mText += QLocale::system().toString(mStartDate);
0083     } else {
0084         mText += i18nc("date from - to", "%1 - %2", QLocale::system().toString(mStartDate), QLocale::system().toString(mEndDate));
0085     }
0086     mText += QLatin1StringView("</h2>\n");
0087 
0088     KCalendarCore::Event::List events;
0089     for (const auto &calendar : calendars()) {
0090         events += calendar->events(mStartDate, mEndDate, QTimeZone::systemTimeZone(), false);
0091     }
0092 
0093     events = KCalendarCore::Calendar::sortEvents(std::move(events), KCalendarCore::EventSortStartDate, KCalendarCore::SortDirectionAscending);
0094     if (!events.isEmpty()) {
0095         mText += QLatin1StringView("<p></p>");
0096         kil->loadIcon(QStringLiteral("view-calendar-day"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath);
0097         mText += QLatin1StringView("<h2><img src=\"");
0098         mText += ipath;
0099         mText += QLatin1StringView(R"(" width="22" height="22">)");
0100         mText += i18n("Events:") + QLatin1StringView("</h2>\n");
0101         mText += QLatin1StringView("<table>\n");
0102         for (const KCalendarCore::Event::Ptr &ev : std::as_const(events)) {
0103             const auto calendar = calendar3(ev);
0104             if (!ev->recurs()) {
0105                 appendEvent(calendar, ev);
0106             } else {
0107                 KCalendarCore::Recurrence *recur = ev->recurrence();
0108                 int duration = ev->dtStart().secsTo(ev->dtEnd());
0109                 QDateTime start = recur->getPreviousDateTime(QDateTime(mStartDate, QTime(), Qt::LocalTime));
0110                 QDateTime end = start.addSecs(duration);
0111                 QDateTime endDate(mEndDate, QTime(23, 59, 59), Qt::LocalTime);
0112                 if (end.date() >= mStartDate) {
0113                     appendEvent(calendar, ev, start.toLocalTime(), end.toLocalTime());
0114                 }
0115                 const auto times = recur->timesInInterval(start, endDate);
0116                 int count = times.count();
0117                 if (count > 0) {
0118                     int i = 0;
0119                     if (times[0] == start) {
0120                         ++i; // start has already been appended
0121                     }
0122                     if (!times[count - 1].isValid()) {
0123                         --count; // list overflow
0124                     }
0125                     for (; i < count && times[i].date() <= mEndDate; ++i) {
0126                         appendEvent(calendar, ev, times[i].toLocalTime());
0127                     }
0128                 }
0129             }
0130         }
0131         mText += QLatin1StringView("</table>\n");
0132     }
0133 
0134     mTodos.clear();
0135     KCalendarCore::Todo::List todos;
0136     for (const auto &calendar : calendars()) {
0137         todos += calendar->todos(KCalendarCore::TodoSortDueDate, KCalendarCore::SortDirectionAscending);
0138     }
0139     if (!todos.isEmpty()) {
0140         bool taskHeaderWasCreated = false;
0141         for (const KCalendarCore::Todo::Ptr &todo : std::as_const(todos)) {
0142             const auto calendar = calendar3(todo);
0143             if (!todo->isCompleted() && todo->hasDueDate() && todo->dtDue().date() <= mEndDate) {
0144                 if (!taskHeaderWasCreated) {
0145                     createTaskRow(kil);
0146                     taskHeaderWasCreated = true;
0147                 }
0148                 appendTodo(calendar, todo);
0149             }
0150         }
0151         bool gotone = false;
0152         int priority = 1;
0153         while (!gotone && priority <= 9) {
0154             for (const KCalendarCore::Todo::Ptr &todo : std::as_const(todos)) {
0155                 const auto calendar = calendar3(todo);
0156                 if (!todo->isCompleted() && (todo->priority() == priority)) {
0157                     if (!taskHeaderWasCreated) {
0158                         createTaskRow(kil);
0159                         taskHeaderWasCreated = true;
0160                     }
0161                     appendTodo(calendar, todo);
0162                     gotone = true;
0163                 }
0164             }
0165             priority++;
0166         }
0167         if (taskHeaderWasCreated) {
0168             mText += QLatin1StringView("</ul>\n");
0169         }
0170     }
0171 
0172     QStringList myEmails(CalendarSupport::KCalPrefs::instance()->allEmails());
0173     int replies = 0;
0174     events.clear();
0175     for (const auto &calendar : calendars()) {
0176         events += calendar->events(QDate::currentDate(), QDate(2975, 12, 6), QTimeZone::systemTimeZone());
0177     }
0178     for (const KCalendarCore::Event::Ptr &ev : std::as_const(events)) {
0179         const auto calendar = calendar3(ev);
0180         KCalendarCore::Attendee me = ev->attendeeByMails(myEmails);
0181         if (!me.isNull()) {
0182             if (me.status() == KCalendarCore::Attendee::NeedsAction && me.RSVP()) {
0183                 if (replies == 0) {
0184                     mText += QLatin1StringView("<p></p>");
0185                     kil->loadIcon(QStringLiteral("mail-reply-sender"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath);
0186                     mText += QLatin1StringView("<h2><img src=\"");
0187                     mText += ipath;
0188                     mText += QLatin1StringView(R"(" width="22" height="22">)");
0189                     mText += i18n("Events and to-dos that need a reply:") + QLatin1StringView("</h2>\n");
0190                     mText += QLatin1StringView("<table>\n");
0191                 }
0192                 replies++;
0193                 appendEvent(calendar, ev);
0194             }
0195         }
0196     }
0197 
0198     todos.clear();
0199     for (const auto &calendar : calendars()) {
0200         todos += calendar->todos();
0201     }
0202     for (const KCalendarCore::Todo::Ptr &to : std::as_const(todos)) {
0203         const auto calendar = calendar3(to);
0204         KCalendarCore::Attendee me = to->attendeeByMails(myEmails);
0205         if (!me.isNull()) {
0206             if (me.status() == KCalendarCore::Attendee::NeedsAction && me.RSVP()) {
0207                 if (replies == 0) {
0208                     mText += QLatin1StringView("<p></p>");
0209                     kil->loadIcon(QStringLiteral("mail-reply-sender"), KIconLoader::NoGroup, 22, KIconLoader::DefaultState, QStringList(), &ipath);
0210                     mText += QLatin1StringView("<h2><img src=\"");
0211                     mText += ipath;
0212                     mText += QLatin1StringView(R"(" width="22" height="22">)");
0213                     mText += i18n("Events and to-dos that need a reply:") + QLatin1StringView("</h2>\n");
0214                     mText += QLatin1StringView("<table>\n");
0215                 }
0216                 replies++;
0217                 appendEvent(calendar, to);
0218             }
0219         }
0220     }
0221     if (replies > 0) {
0222         mText += QLatin1StringView("</table>\n");
0223     }
0224 
0225     mText += QLatin1StringView("</td></tr>\n</table>\n");
0226 
0227     mView->setText(mText);
0228 }
0229 
0230 void WhatsNextView::showDates(const QDate &start, const QDate &end, const QDate &)
0231 {
0232     mStartDate = start;
0233     mEndDate = end;
0234     updateView();
0235 }
0236 
0237 void WhatsNextView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
0238 {
0239     Q_UNUSED(incidenceList)
0240     Q_UNUSED(date)
0241 }
0242 
0243 void WhatsNextView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType)
0244 {
0245     updateView();
0246 }
0247 
0248 void WhatsNextView::appendEvent(const Akonadi::CollectionCalendar::Ptr &calendar,
0249                                 const KCalendarCore::Incidence::Ptr &incidence,
0250                                 const QDateTime &start,
0251                                 const QDateTime &end)
0252 {
0253     mText += QLatin1StringView("<tr><td><b>");
0254     if (const KCalendarCore::Event::Ptr event = incidence.dynamicCast<KCalendarCore::Event>()) {
0255         auto starttime = start.toLocalTime();
0256         if (!starttime.isValid()) {
0257             starttime = event->dtStart().toLocalTime();
0258         }
0259         auto endtime = end.toLocalTime();
0260         if (!endtime.isValid()) {
0261             endtime = starttime.addSecs(event->dtStart().secsTo(event->dtEnd()));
0262         }
0263 
0264         if (starttime.date().daysTo(endtime.date()) >= 1) {
0265             if (event->allDay()) {
0266                 mText += i18nc("date from - to",
0267                                "%1 - %2",
0268                                QLocale().toString(starttime.date(), QLocale::ShortFormat),
0269                                QLocale().toString(endtime.date(), QLocale::ShortFormat));
0270             } else {
0271                 mText +=
0272                     i18nc("date from - to", "%1 - %2", QLocale().toString(starttime, QLocale::ShortFormat), QLocale().toString(endtime, QLocale::ShortFormat));
0273             }
0274         } else {
0275             if (event->allDay()) {
0276                 mText += QLocale().toString(starttime.date(), QLocale::ShortFormat);
0277             } else {
0278                 mText += i18nc("date, from - to",
0279                                "%1, %2 - %3",
0280                                QLocale().toString(starttime.date(), QLocale::ShortFormat),
0281                                QLocale().toString(starttime.time(), QLocale::ShortFormat),
0282                                QLocale().toString(endtime.time(), QLocale::ShortFormat));
0283             }
0284         }
0285     }
0286     mText += QLatin1StringView("</b></td>");
0287     const QString proto = incidence->type() == KCalendarCore::Incidence::TypeTodo ? QStringLiteral("todo") : QStringLiteral("event");
0288     mText += QStringLiteral(R"(<td><a href="%1:%2?itemId=%3&calendarId=%5">%4</a></td>)")
0289                  .arg(proto, incidence->uid(), incidence->customProperty("VOLATILE", "AKONADI-ID"), incidence->summary())
0290                  .arg(calendar->collection().id());
0291     mText += QLatin1StringView("</tr>\n");
0292 }
0293 
0294 void WhatsNextView::appendTodo(const Akonadi::CollectionCalendar::Ptr &calendar, const KCalendarCore::Incidence::Ptr &incidence)
0295 {
0296     Akonadi::Item aitem = calendar->item(incidence);
0297     if (mTodos.contains(aitem)) {
0298         return;
0299     }
0300     mTodos.append(aitem);
0301     mText += QLatin1StringView("<li>");
0302     mText += QStringLiteral(R"(<a href="todo:%1?itemId=%2&calendarId=%4">%3</a>)")
0303                  .arg(incidence->uid(), incidence->customProperty("VOLATILE", "AKONADI-ID"), incidence->summary())
0304                  .arg(calendar->collection().id());
0305 
0306     if (const KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(aitem)) {
0307         if (todo->hasDueDate()) {
0308             mText += i18nc("to-do due date", "  (Due: %1)", KCalUtils::IncidenceFormatter::dateTimeToString(todo->dtDue(), todo->allDay()));
0309         }
0310         mText += QLatin1StringView("</li>\n");
0311     }
0312 }
0313 
0314 static std::optional<Akonadi::Item::Id> idFromQuery(const QUrlQuery &query, const QString &queryValue)
0315 {
0316     const auto strVal = query.queryItemValue(queryValue);
0317     if (strVal.isEmpty()) {
0318         return {};
0319     }
0320 
0321     bool ok = false;
0322     const auto intVal = strVal.toLongLong(&ok);
0323     if (!ok) {
0324         return {};
0325     }
0326 
0327     return intVal;
0328 }
0329 
0330 void WhatsNextView::showIncidence(const QUrl &uri)
0331 {
0332     QUrlQuery query(uri);
0333     const auto itemId = idFromQuery(query, QStringLiteral("itemId"));
0334     const auto calendarId = idFromQuery(query, QStringLiteral("calendarId"));
0335 
0336     if (!itemId || !calendarId) {
0337         qCWarning(CALENDARVIEW_LOG) << "Invalid URI:" << uri;
0338         return;
0339     }
0340 
0341     auto calendar = calendarForCollection(*calendarId);
0342     if (!calendar) {
0343         qCWarning(CALENDARVIEW_LOG) << "Calendar for collection " << *calendarId << " not present in current view";
0344         return;
0345     }
0346 
0347     auto item = calendar->item(*itemId);
0348     if (!item.isValid()) {
0349         qCWarning(CALENDARVIEW_LOG) << "Item " << *itemId << " not found in collection " << *calendarId;
0350         return;
0351     }
0352 
0353     Q_EMIT showIncidenceSignal(item);
0354 }
0355 
0356 #include "moc_whatsnextview.cpp"