diff --git a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp index 1f2faf8e..669fcebb 100644 --- a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp +++ b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp @@ -1,306 +1,317 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "semanticurlhandler.h" #include "semanticmemento.h" #include "semantic_debug.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include using namespace KItinerary; bool SemanticUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(viewerInstance); if (path == QLatin1String("semanticAction")) { return true; } if (path.startsWith(QLatin1String("semanticExpand?"))) { auto idx = path.midRef(15).toInt(); auto m = memento(part); m->toggleExpanded(idx); const auto nodeHelper = part->nodeHelper(); emit nodeHelper->update(MimeTreeParser::Delayed); return true; } return false; } -static QString placeName(const QVariant &place) +template +static QString placeName(const T &place) { - const auto name = JsonLdDocument::readProperty(place, "name").toString() - .replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; - if (!name.isEmpty()) { - return name; - } - // airports with no name, as extracted from IATA BCBP messages - return JsonLdDocument::readProperty(place, "iataCode").toString(); + return place.name().replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; } -static void addGoToMapAction(QMenu *menu, const QVariant &place) +template <> +QString placeName(const Airport &place) { - if (place.isNull()) { - return; + if (place.name().isEmpty()) { + return place.iataCode(); } + return place.name().replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; +} - const auto geo = JsonLdDocument::readProperty(place, "geo").value(); +static void addGoToMapAction(QMenu *menu, const GeoCoordinates &geo, const QString &placeName, int zoom = 17) +{ if (geo.isValid()) { - auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", placeName(place))); - // zoom out further from airports, they are larger and you usually want to go further away from them - const auto zoom = place.userType() == qMetaTypeId() ? 12 : 17; + auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", placeName)); QObject::connect(action, &QAction::triggered, menu, [geo, zoom]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/")); const QString fragment = QLatin1String("map=") + QString::number(zoom) + QLatin1Char('/') + QString::number(geo.latitude()) + QLatin1Char('/') + QString::number(geo.longitude()); url.setFragment(fragment); QDesktopServices::openUrl(url); }); - return; } +} - const auto addr = JsonLdDocument::readProperty(place, "address").value(); +static void addGoToMapAction(QMenu *menu, const PostalAddress &addr, const QString &placeName) +{ if (!addr.addressLocality().isEmpty()) { - auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", placeName(place))); + auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Show \'%1\' On Map", placeName)); QObject::connect(action, &QAction::triggered, menu, [addr]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/search")); const QString queryString = addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); QUrlQuery query; query.addQueryItem(QStringLiteral("query"), queryString); url.setQuery(query); QDesktopServices::openUrl(url); }); } } +template +static void addGoToMapAction(QMenu *menu, const T &place) +{ + const auto name = placeName(place); + // zoom out further from airports, they are larger and you usually want to go further away from them + addGoToMapAction(menu, place.geo(), name, std::is_same::value ? 12 : 17); + if (!place.geo().isValid()) { + addGoToMapAction(menu, place.address(), name); + } +} + bool SemanticUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const { Q_UNUSED(part); if (path != QLatin1String("semanticAction")) { return false; } const auto m = memento(part); if (!m || m->extractedData().isEmpty()) { return false; } const auto date = dateForReservation(m); QMenu menu; QAction *action = nullptr; if (date.isValid()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("view-calendar")), i18n("Show Calendar")); QObject::connect(action, &QAction::triggered, this, [this, date](){ showCalendar(date); }); } action = menu.addAction(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("Add To Calendar")); QObject::connect(action, &QAction::triggered, this, [this, m](){ addToCalendar(m); }); QSet places; for (const auto &r : m->extractedData()) { if (JsonLd::isA(r)) { - addGoToMapAction(&menu, r.value().reservationFor()); + addGoToMapAction(&menu, r.value().reservationFor().value()); } else if (JsonLd::isA(r)) { const auto flight = r.value().reservationFor().value(); auto airport = flight.departureAirport(); if (!places.contains(airport.iataCode())) { addGoToMapAction(&menu, airport); places.insert(airport.iataCode()); } airport = flight.arrivalAirport(); if (!places.contains(airport.iataCode())) { addGoToMapAction(&menu, airport); places.insert(airport.iataCode()); } } else if (JsonLd::isA(r)) { const auto trip = r.value().reservationFor().value(); if (!places.contains(trip.departureStation().name())) { addGoToMapAction(&menu, trip.departureStation()); places.insert(trip.departureStation().name()); } if (!places.contains(trip.arrivalStation().name())) { addGoToMapAction(&menu, trip.arrivalStation()); places.insert(trip.arrivalStation().name()); } } else if (JsonLd::isA(r)) { const auto trip = r.value().reservationFor().value(); if (!places.contains(trip.departureStation().name())) { addGoToMapAction(&menu, trip.departureStation()); places.insert(trip.departureStation().name()); } if (!places.contains(trip.arrivalStation().name())) { addGoToMapAction(&menu, trip.arrivalStation()); places.insert(trip.arrivalStation().name()); } } } menu.exec(p); return true; } QString SemanticUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(part); if (path == QLatin1String("semanticAction")) { return i18n("Add reservation to your calendar."); } return {}; } SemanticMemento *SemanticUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const { const auto node = part->content()->topLevel(); const auto nodeHelper = part->nodeHelper(); if (!nodeHelper || !node) { return nullptr; } return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), "org.kde.messageviewer.semanticData")); } QDate SemanticUrlHandler::dateForReservation(SemanticMemento *memento) const { for (const auto &r : memento->extractedData()) { const auto dt = SortUtil::startDateTime(r); if (dt.isValid()) { return dt.date(); } } return {}; } void SemanticUrlHandler::showCalendar(const QDate &date) const { // ensure KOrganizer or Kontact are running QString error, dbusService; const auto result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), {}, &error, &dbusService) == 0; if (!result) { qCWarning(SEMANTIC_LOG) << "Failed to start KOrganizer" << error << dbusService; } // switch to KOrganizer if we are using Kontact std::unique_ptr kontactIface( new QDBusInterface(QStringLiteral("org.kde.kontact"), QStringLiteral("/KontactInterface"), QStringLiteral("org.kde.kontact.KontactInterface"), QDBusConnection::sessionBus())); if (kontactIface->isValid()) { kontactIface->call(QStringLiteral("selectPlugin"), QStringLiteral("kontact_korganizerplugin")); } // select the date of the reservation std::unique_ptr korgIface( new QDBusInterface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QStringLiteral("org.kde.Korganizer.Calendar"), QDBusConnection::sessionBus())); if (!korgIface->isValid()) { qCWarning(SEMANTIC_LOG) << "Calendar interface is not valid! " << korgIface->lastError().message(); return; } korgIface->call(QStringLiteral("showEventView")); korgIface->call(QStringLiteral("showDate"), date); } static void attachPass(const KCalCore::Event::Ptr &event, const QVariant &reservation, SemanticMemento *memento) { if (!JsonLd::canConvert(reservation)) { return; } const auto res = JsonLd::convert(reservation); const auto data = memento->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (data.isEmpty()) { return; } event->deleteAttachments(QStringLiteral("application/vnd.apple.pkpass")); using namespace KCalCore; Attachment::Ptr att(new Attachment(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass"))); att->setLabel(i18n("Boarding Pass")); event->addAttachment(att); } void SemanticUrlHandler::addToCalendar(SemanticMemento *memento) const { using namespace KCalCore; const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &r : memento->extractedData()) { auto event = CalendarHandler::findEvent(calendar, r); if (!event) { event.reset(new Event); CalendarHandler::fillEvent(r, event); if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) { continue; } attachPass(event, r, memento); calendar->addEvent(event); } else { const auto oldRes = CalendarHandler::reservationForEvent(event); const auto mergedRes = JsonLdDocument::apply(oldRes, r); event->startUpdates(); CalendarHandler::fillEvent(mergedRes, event); event->endUpdates(); attachPass(event, r, memento); calendar->modifyIncidence(event); } } }