File indexing completed on 2024-11-10 04:55:18
0001 /* 0002 * SPDX-FileCopyrightText: 2007, 2008 Petri Damstén <damu@iki.fi> 0003 * SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "converterrunner.h" 0009 0010 #include <KLocalizedString> 0011 #include <QClipboard> 0012 #include <QDebug> 0013 #include <QDesktopServices> 0014 #include <QGuiApplication> 0015 #include <QLocale> 0016 #include <QMimeData> 0017 0018 #include <chrono> 0019 #include <cmath> 0020 0021 using namespace std::chrono_literals; 0022 0023 K_PLUGIN_CLASS_WITH_JSON(ConverterRunner, "plasma-runner-converter.json") 0024 0025 Q_DECLARE_METATYPE(KUnitConversion::Value) 0026 0027 ConverterRunner::ConverterRunner(QObject *parent, const KPluginMetaData &metaData) 0028 : AbstractRunner(parent, metaData) 0029 , actionList({Action(QStringLiteral("copy"), QStringLiteral("edit-copy"), i18n("Copy unit and number"))}) 0030 { 0031 addSyntax(QStringLiteral(":q:"), 0032 i18n("Converts the value of :q: when :q: is made up of value unit [>, to, as, in] unit." 0033 "You can use the Unit converter applet to find all available units.")); 0034 } 0035 0036 void ConverterRunner::init() 0037 { 0038 valueRegex = QRegularExpression(QStringLiteral("^([0-9,./+-]+)")); 0039 const QStringList conversionWords = i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", "in;to;as").split(QLatin1Char(';')); 0040 QString conversionRegex; 0041 for (const auto &word : conversionWords) { 0042 conversionRegex.append(QLatin1Char(' ') + word + QStringLiteral(" |")); 0043 } 0044 conversionRegex.append(QStringLiteral(" ?> ?")); 0045 unitSeperatorRegex = QRegularExpression(conversionRegex); 0046 0047 setMinLetterCount(2); 0048 setMatchRegex(valueRegex); 0049 0050 converter = std::make_unique<KUnitConversion::Converter>(); 0051 updateCompatibleUnits(); 0052 0053 m_currencyUpdateTimer.setInterval(24h); 0054 connect(&m_currencyUpdateTimer, &QTimer::timeout, this, &ConverterRunner::updateCompatibleUnits); 0055 m_currencyUpdateTimer.start(); 0056 } 0057 0058 ConverterRunner::~ConverterRunner() = default; 0059 0060 void ConverterRunner::match(RunnerContext &context) 0061 { 0062 const QRegularExpressionMatch valueRegexMatch = valueRegex.match(context.query()); 0063 if (!valueRegexMatch.hasMatch()) { 0064 return; 0065 } 0066 0067 const QString inputValueString = valueRegexMatch.captured(1); 0068 0069 // Get the different units by splitting up the query with the regex 0070 QStringList unitStrings = context.query().simplified().remove(valueRegex).split(unitSeperatorRegex); 0071 if (unitStrings.isEmpty() || unitStrings.at(0).isEmpty()) { 0072 return; 0073 } 0074 0075 // Check if unit is valid, otherwise check for the value in the compatibleUnits map 0076 QString inputUnitString = unitStrings.first().simplified(); 0077 KUnitConversion::UnitCategory inputCategory = converter->categoryForUnit(inputUnitString); 0078 if (inputCategory.id() == KUnitConversion::InvalidCategory) { 0079 inputUnitString = compatibleUnits.value(inputUnitString.toUpper()); 0080 inputCategory = converter->categoryForUnit(inputUnitString); 0081 if (inputCategory.id() == KUnitConversion::InvalidCategory) { 0082 return; 0083 } 0084 } 0085 0086 QString outputUnitString; 0087 if (unitStrings.size() == 2) { 0088 outputUnitString = unitStrings.at(1).simplified(); 0089 } 0090 0091 const KUnitConversion::Unit inputUnit = inputCategory.unit(inputUnitString); 0092 const QList<KUnitConversion::Unit> outputUnits = createResultUnits(outputUnitString, inputCategory); 0093 const auto numberDataPair = getValidatedNumberValue(inputValueString); 0094 // Return on invalid user input 0095 if (!numberDataPair.first) { 0096 return; 0097 } 0098 0099 const double numberValue = numberDataPair.second; 0100 QList<QueryMatch> matches; 0101 for (const KUnitConversion::Unit &outputUnit : outputUnits) { 0102 KUnitConversion::Value outputValue = inputCategory.convert(KUnitConversion::Value(numberValue, inputUnit), outputUnit); 0103 if (!outputValue.isValid() || inputUnit == outputUnit) { 0104 continue; 0105 } 0106 0107 QueryMatch match(this); 0108 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Moderate); 0109 match.setIconName(QStringLiteral("accessories-calculator")); 0110 if (outputUnit.categoryId() == KUnitConversion::CurrencyCategory) { 0111 match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(0, 'f', 2), outputUnit.symbol())); 0112 } else { 0113 match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(), outputUnit.symbol())); 0114 } 0115 match.setData(QVariant::fromValue(outputValue)); 0116 match.setRelevance(1.0 - std::abs(std::log10(outputValue.number())) / 50.0); 0117 match.setActions(actionList); 0118 matches.append(match); 0119 } 0120 0121 context.addMatches(matches); 0122 } 0123 0124 void ConverterRunner::run(const RunnerContext & /*context*/, const QueryMatch &match) 0125 { 0126 const auto value = match.data().value<KUnitConversion::Value>(); 0127 0128 if (match.selectedAction()) { 0129 QGuiApplication::clipboard()->setText(value.toString()); 0130 } else { 0131 QGuiApplication::clipboard()->setText(QString::number(value.number(), 'f', QLocale::FloatingPointShortest)); 0132 } 0133 } 0134 0135 QMimeData *ConverterRunner::mimeDataForMatch(const QueryMatch &match) 0136 { 0137 const auto value = match.data().value<KUnitConversion::Value>(); 0138 0139 auto *mimeData = new QMimeData(); 0140 mimeData->setText(value.toSymbolString()); 0141 return mimeData; 0142 } 0143 0144 QPair<bool, double> ConverterRunner::stringToDouble(const QStringView &value) 0145 { 0146 bool ok; 0147 double numberValue = locale.toDouble(value, &ok); 0148 if (!ok) { 0149 numberValue = value.toDouble(&ok); 0150 } 0151 return {ok, numberValue}; 0152 } 0153 0154 QPair<bool, double> ConverterRunner::getValidatedNumberValue(const QString &value) 0155 { 0156 const auto fractionParts = QStringView(value).split(QLatin1Char('/'), Qt::SkipEmptyParts); 0157 if (fractionParts.isEmpty() || fractionParts.count() > 2) { 0158 return {false, 0}; 0159 } 0160 0161 if (fractionParts.count() == 2) { 0162 const QPair<bool, double> doubleFirstResults = stringToDouble(fractionParts.first()); 0163 if (!doubleFirstResults.first) { 0164 return {false, 0}; 0165 } 0166 const QPair<bool, double> doubleSecondResult = stringToDouble(fractionParts.last()); 0167 if (!doubleSecondResult.first || qFuzzyIsNull(doubleSecondResult.second)) { 0168 return {false, 0}; 0169 } 0170 return {true, doubleFirstResults.second / doubleSecondResult.second}; 0171 } else if (fractionParts.count() == 1) { 0172 const QPair<bool, double> doubleResult = stringToDouble(fractionParts.first()); 0173 if (!doubleResult.first) { 0174 return {false, 0}; 0175 } 0176 return {true, doubleResult.second}; 0177 } else { 0178 return {true, 0}; 0179 } 0180 } 0181 0182 QList<KUnitConversion::Unit> ConverterRunner::createResultUnits(QString &outputUnitString, const KUnitConversion::UnitCategory &category) 0183 { 0184 QList<KUnitConversion::Unit> units; 0185 if (!outputUnitString.isEmpty()) { 0186 KUnitConversion::Unit outputUnit = category.unit(outputUnitString); 0187 if (!outputUnit.isNull() && outputUnit.isValid()) { 0188 units.append(outputUnit); 0189 } else { 0190 // Autocompletion for the target units 0191 outputUnitString = outputUnitString.toUpper(); 0192 for (auto it = compatibleUnits.constBegin(); it != compatibleUnits.constEnd(); it++) { 0193 if (it.key().startsWith(outputUnitString)) { 0194 outputUnit = category.unit(it.value()); 0195 if (!units.contains(outputUnit)) { 0196 units << outputUnit; 0197 } 0198 } 0199 } 0200 } 0201 } else { 0202 units = category.mostCommonUnits(); 0203 // suggest converting to the user's local currency 0204 if (category.id() == KUnitConversion::CurrencyCategory) { 0205 const QString ¤cyIsoCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode); 0206 0207 const KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode); 0208 if (localCurrency.isValid() && !units.contains(localCurrency)) { 0209 units << localCurrency; 0210 } 0211 } 0212 } 0213 0214 return units; 0215 } 0216 0217 void ConverterRunner::updateCompatibleUnits() 0218 { 0219 // Add all currency symbols to the map, if their ISO code is supported by backend 0220 bool isLatest = false; 0221 QMetaObject::invokeMethod( 0222 QCoreApplication::instance(), 0223 [this] { 0224 KUnitConversion::UnitCategory currencyCategory = converter->category(KUnitConversion::CurrencyCategory); 0225 auto updateJob = currencyCategory.syncConversionTable(); 0226 if (!updateJob) [[unlikely]] { 0227 return !compatibleUnits.empty(); 0228 } 0229 QEventLoop loop; 0230 loop.connect(updateJob, &KUnitConversion::UpdateJob::finished, &loop, &QEventLoop::quit); 0231 loop.exec(); 0232 return false; 0233 }, 0234 Qt::BlockingQueuedConnection, 0235 &isLatest); 0236 if (isLatest) { 0237 return; 0238 } 0239 0240 KUnitConversion::UnitCategory currencyCategory = converter->category(KUnitConversion::CurrencyCategory); 0241 const QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry); 0242 const QStringList availableISOCodes = currencyCategory.allUnits(); 0243 const QRegularExpression hasCurrencyRegex = QRegularExpression(QStringLiteral("\\p{Sc}")); // clazy:exclude=use-static-qregularexpression 0244 for (const auto ¤cyLocale : allLocales) { 0245 const QString symbol = currencyLocale.currencySymbol(QLocale::CurrencySymbol); 0246 const QString isoCode = currencyLocale.currencySymbol(QLocale::CurrencyIsoCode); 0247 0248 if (isoCode.isEmpty() || !symbol.contains(hasCurrencyRegex)) { 0249 continue; 0250 } 0251 if (availableISOCodes.contains(isoCode)) { 0252 compatibleUnits.insert(symbol.toUpper(), isoCode); 0253 } 0254 } 0255 0256 // Add all units as uppercase in the map 0257 const auto categories = converter->categories(); 0258 for (const auto &category : categories) { 0259 const auto allUnits = category.allUnits(); 0260 for (const auto &unit : allUnits) { 0261 compatibleUnits.insert(unit.toUpper(), unit); 0262 } 0263 } 0264 } 0265 #include "converterrunner.moc"