File indexing completed on 2024-05-05 17:34:05

0001 /*
0002  *   SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org>
0003  *   SPDX-FileCopyrightText: 2010 Marco Martin <notmart@gmail.com>
0004  *   SPDX-FileCopyrightText: 2015 Vishesh Handa <vhanda@kde.org>
0005  *   SPDX-FileCopyrightText: 2022 Natalie Clarius <natalie_clarius@yahoo.de>
0006  *
0007  *   SPDX-License-Identifier: LGPL-2.0-only
0008  */
0009 
0010 #include "datetimerunner.h"
0011 
0012 #include <QIcon>
0013 #include <QLocale>
0014 #include <QTimeZone>
0015 
0016 #include <KFormat>
0017 #include <KLocalizedString>
0018 
0019 #include <math.h>
0020 
0021 static const QString dateWord = i18nc("Note this is a KRunner keyword", "date");
0022 static const QString timeWord = i18nc("Note this is a KRunner keyword", "time");
0023 
0024 DateTimeRunner::DateTimeRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
0025     : AbstractRunner(parent, metaData, args)
0026 {
0027     setObjectName(QLatin1String("DateTimeRunner"));
0028 
0029     addSyntax(RunnerSyntax(dateWord, i18n("Displays the current date")));
0030     addSyntax(RunnerSyntax(timeWord, i18n("Displays the current time")));
0031     addSyntax(RunnerSyntax(dateWord + i18nc("The <> and space are part of the example query", " <timezone>"), //
0032                            i18n("Displays the current date and difference to system date in a given timezone")));
0033     addSyntax(RunnerSyntax(timeWord + i18nc("The <> and space are part of the example query", " <timezone>"), //
0034                            i18n("Displays the current time and difference to system time in a given timezone")));
0035     setTriggerWords({timeWord, dateWord});
0036 }
0037 
0038 DateTimeRunner::~DateTimeRunner()
0039 {
0040 }
0041 
0042 void DateTimeRunner::match(RunnerContext &context)
0043 {
0044     const QString term = context.query();
0045     if (term.compare(dateWord, Qt::CaseInsensitive) == 0) {
0046         const QDate date = QDate::currentDate();
0047         const QString dateStr = QLocale().toString(date);
0048         addMatch(i18n("Today's date is %1", dateStr), dateStr, 1.0, QStringLiteral("view-calendar-day"), context);
0049     } else if (term.startsWith(dateWord + QLatin1Char(' '), Qt::CaseInsensitive)) {
0050 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0051         const auto zoneTerm = term.rightRef(term.length() - dateWord.length() - 1);
0052 #else
0053         const auto zoneTerm = QStringView(term).right(term.length() - dateWord.length() - 1);
0054 #endif
0055         const auto zones = datetimeAt(zoneTerm);
0056         for (auto it = zones.constBegin(), itEnd = zones.constEnd(); it != itEnd; ++it) {
0057             const QString zoneStr = it.key();
0058             const QDateTime datetime = it.value();
0059             const QString dateStr = QLocale().toString(datetime.date());
0060 
0061             const qint64 dateDiff = QDateTime::currentDateTime().daysTo(QDateTime(datetime.date(), datetime.time())) * (24 * 60 * 60 * 1000); // full days in ms
0062             const QString dateDiffStr = dateDiff > 0 ? i18nc("date difference between time zones, e.g. in Stockholm it's 1 calendar day later than in Brasilia",
0063                                                              "%1 later",
0064                                                              KFormat().formatSpelloutDuration(abs(dateDiff)))
0065                 : dateDiff < 0
0066                 ? i18nc("date difference between time zones, e.g. in Brasilia it's 1 calendar day earlier than in Stockholm",
0067                         "%1 earlier",
0068                         KFormat().formatSpelloutDuration(abs(dateDiff)))
0069                 : i18nc("no date difference between time zones, e.g. in Stockholm it's the same calendar day as in Berlin", "no date difference");
0070 
0071             addMatch(QStringLiteral("%1: %2 (%3)").arg(zoneStr, dateStr, dateDiffStr),
0072                      dateStr,
0073                      ((qreal)(zoneStr.count(zoneTerm, Qt::CaseInsensitive)) * zoneTerm.length() - (qreal)zoneStr.indexOf(zoneTerm, Qt::CaseInsensitive))
0074                          / zoneStr.length(),
0075                      QStringLiteral("view-calendar-day"),
0076                      context);
0077         }
0078     } else if (term.compare(timeWord, Qt::CaseInsensitive) == 0) {
0079         const QTime time = QTime::currentTime();
0080         const QString timeStr = QLocale().toString(time);
0081         addMatch(i18n("Current time is %1", timeStr), timeStr, 1.0, QStringLiteral("clock"), context);
0082     } else if (term.startsWith(timeWord + QLatin1Char(' '), Qt::CaseInsensitive)) {
0083 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0084         const auto zoneTerm = term.rightRef(term.length() - timeWord.length() - 1);
0085 #else
0086         const auto zoneTerm = QStringView(term).right(term.length() - timeWord.length() - 1);
0087 #endif
0088         const auto zones = datetimeAt(zoneTerm);
0089         for (auto it = zones.constBegin(), itEnd = zones.constEnd(); it != itEnd; ++it) {
0090             const QString zoneStr = it.key();
0091             const QDateTime datetime = it.value();
0092             const QString timeStr = QLocale().toString(datetime.time(), QLocale::ShortFormat);
0093 
0094             const qint64 dateDiff = QDateTime::currentDateTime().daysTo(QDateTime(datetime.date(), datetime.time())) * (24 * 60 * 60 * 1000); // full days in ms
0095             const QString dayDiffStr = dateDiff > 0 ? QString(" + %1").arg(KFormat().formatSpelloutDuration(abs(dateDiff)))
0096                 : dateDiff < 0                      ? QString(" - %1").arg(KFormat().formatSpelloutDuration(abs(dateDiff)))
0097                                                     : QString();
0098 
0099             const qint64 timeDiff = round((double)QDateTime::currentDateTime().secsTo(QDateTime(datetime.date(), datetime.time())) / 60)
0100                 * (60 * 1000); // time in ms rounded to the nearest full minutes
0101             const QString timeDiffStr = timeDiff > 0 ? i18nc("time difference between time zones, e.g. in Stockholm it's 4 hours later than in Brasilia",
0102                                                              "%1 later",
0103                                                              KFormat().formatSpelloutDuration(abs(timeDiff)))
0104                 : timeDiff < 0                       ? i18nc("time difference between time zones, e.g. in Brasilia it's 4 hours ealier than in Stockholm",
0105                                        "%1 earlier",
0106                                        KFormat().formatSpelloutDuration(abs(timeDiff)))
0107                                : i18nc("no time difference between time zones, e.g. in Stockholm it's the same time as in Berlin", "no time difference");
0108 
0109             addMatch(QStringLiteral("%1: %2%3 (%4)").arg(zoneStr, timeStr, dayDiffStr, timeDiffStr),
0110                      timeStr,
0111                      ((qreal)(zoneStr.count(zoneTerm, Qt::CaseInsensitive)) * zoneTerm.length() - (qreal)zoneStr.indexOf(zoneTerm, Qt::CaseInsensitive))
0112                          / zoneStr.length(),
0113                      QStringLiteral("clock"),
0114                      context);
0115         }
0116     }
0117 }
0118 
0119 void DateTimeRunner::run(const RunnerContext &context, const QueryMatch &match)
0120 {
0121     const QString clipboardText = match.data().toString();
0122     context.requestQueryStringUpdate(clipboardText, clipboardText.length());
0123 }
0124 
0125 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0126 QHash<QString, QDateTime> DateTimeRunner::datetimeAt(const QStringRef &zoneTerm, const QDateTime referenceTime)
0127 #else
0128 QHash<QString, QDateTime> DateTimeRunner::datetimeAt(const QStringView &zoneTerm, const QDateTime referenceTime)
0129 #endif
0130 {
0131     QHash<QString, QDateTime> ret;
0132     const QList<QByteArray> timeZoneIds = QTimeZone::availableTimeZoneIds();
0133     for (const QByteArray &zoneId : timeZoneIds) {
0134         QTimeZone timeZone(zoneId);
0135         QDateTime datetime = referenceTime.toTimeZone(QTimeZone(zoneId));
0136 
0137         const QString zoneName = QString::fromUtf8(zoneId);
0138         if (zoneName.startsWith(QStringLiteral("UTC+")) || zoneName.startsWith(QStringLiteral("UTC-"))) {
0139             // Qt time zones are either of the form
0140             // (where {zone name} {long name} {abbreviation} {short name} {offset name} {country})
0141             // - "Europe/Stockholm" "Central European Standard Time" "CET" "GMT+1" "UTC+01:00" "Sweden"
0142             // - "UTC+01:00" "UTC+01:00" "UTC+01:00" "UTC+01:00" "UTC+01:00" "Default"
0143             // The latter are already covered by the offset name of the former, which we want to match exactly, so skip these
0144             continue;
0145         }
0146 
0147         // eg "Sweden"
0148         const QString country = QLocale::countryToString(timeZone.country());
0149         const QString comment = timeZone.comment();
0150         if (country.contains(zoneTerm, Qt::CaseInsensitive) || comment.contains(zoneTerm, Qt::CaseInsensitive)) {
0151             const QString regionName = comment.isEmpty() ? country : QLatin1String("%1 - %2").arg(country, comment);
0152             ret[regionName] = datetime;
0153             continue;
0154         }
0155 
0156         // eg "Stockholm"
0157         const QString city = zoneName.mid(zoneName.indexOf(QStringLiteral("/")) + 1).replace("_", " ");
0158         if (city.contains(zoneTerm, Qt::CaseInsensitive)) {
0159             ret[city] = datetime;
0160             continue;
0161         }
0162 
0163         // eg "Central European Standard Time"
0164         const QString longName = timeZone.displayName(datetime, QTimeZone::LongName);
0165         if (longName.contains(zoneTerm, Qt::CaseInsensitive)) {
0166             ret[longName] = datetime;
0167             continue;
0168         }
0169 
0170         // eg "CET"
0171         // FIXME: This only includes the current abbreviation and not old abbreviation or other possible names.
0172         // Eg - depending on the current date, only CET or CEST will work
0173         const QString abbr = timeZone.abbreviation(datetime);
0174         if (abbr.contains(zoneTerm, Qt::CaseInsensitive)) {
0175             // Combine abbreviation with display name to disambiguate
0176             // Eg - Pacific Standard Time (PST) and Philippine Standard Time (PST)
0177             const QString abbrName = QLatin1String("%1 (%2)").arg(longName, abbr);
0178             ret[abbrName] = datetime;
0179             continue;
0180         }
0181 
0182         // eg "GMT+1"
0183         const QString shortName = timeZone.displayName(datetime, QTimeZone::ShortName);
0184         if (shortName.compare(zoneTerm, Qt::CaseInsensitive) == 0) {
0185             ret[shortName] = datetime;
0186             continue;
0187         }
0188 
0189         // eg "UTC+01:00"
0190         const QString offsetName = timeZone.displayName(datetime, QTimeZone::OffsetName);
0191         if (offsetName.compare(zoneTerm, Qt::CaseInsensitive) == 0) {
0192             ret[offsetName] = datetime;
0193             continue;
0194         }
0195     }
0196 
0197     return ret;
0198 }
0199 
0200 void DateTimeRunner::addMatch(const QString &text, const QString &clipboardText, const qreal &relevance, const QString &iconName, RunnerContext &context)
0201 {
0202     QueryMatch match(this);
0203     match.setText(text);
0204     match.setData(clipboardText);
0205     match.setType(QueryMatch::HelperMatch);
0206     match.setRelevance(relevance);
0207     match.setIconName(iconName);
0208     match.setMultiLine(true);
0209 
0210     context.addMatch(match);
0211 }
0212 
0213 K_PLUGIN_CLASS_WITH_JSON(DateTimeRunner, "plasma-runner-datetime.json")
0214 
0215 #include "datetimerunner.moc"