File indexing completed on 2024-04-28 16:44:43
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 #include <QMutex> 0018 0019 #include <cmath> 0020 0021 static QMutex s_initMutex; 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, const QVariantList &args) 0028 : AbstractRunner(parent, metaData, args) 0029 { 0030 setObjectName(QStringLiteral("Converter")); 0031 0032 const QString description = i18n( 0033 "Converts the value of :q: when :q: is made up of " 0034 "\"value unit [>, to, as, in] unit\". You can use the " 0035 "Unit converter applet to find all available units."); 0036 addSyntax(RunnerSyntax(QStringLiteral(":q:"), description)); 0037 } 0038 0039 void ConverterRunner::init() 0040 { 0041 valueRegex = QRegularExpression(QStringLiteral("^([0-9,./+-]+)")); 0042 const QStringList conversionWords = i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", "in;to;as").split(QLatin1Char(';')); 0043 QString conversionRegex; 0044 for (const auto &word : conversionWords) { 0045 conversionRegex.append(QLatin1Char(' ') + word + QStringLiteral(" |")); 0046 } 0047 conversionRegex.append(QStringLiteral(" ?> ?")); 0048 unitSeperatorRegex = QRegularExpression(conversionRegex); 0049 0050 actionList = {new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy unit and number"), this)}; 0051 setMinLetterCount(2); 0052 setMatchRegex(valueRegex); 0053 } 0054 0055 ConverterRunner::~ConverterRunner() = default; 0056 0057 void ConverterRunner::match(RunnerContext &context) 0058 { 0059 const QRegularExpressionMatch valueRegexMatch = valueRegex.match(context.query()); 0060 if (!valueRegexMatch.hasMatch()) { 0061 return; 0062 } 0063 0064 const QString inputValueString = valueRegexMatch.captured(1); 0065 0066 // Get the different units by splitting up the query with the regex 0067 QStringList unitStrings = context.query().simplified().remove(valueRegex).split(unitSeperatorRegex); 0068 if (unitStrings.isEmpty() || unitStrings.at(0).isEmpty()) { 0069 return; 0070 } 0071 0072 // Initialize if not done already 0073 { 0074 QMutexLocker lock(&s_initMutex); 0075 if (!converter) { 0076 converter = std::make_unique<KUnitConversion::Converter>(); 0077 insertCompatibleUnits(); 0078 } 0079 } 0080 0081 // Check if unit is valid, otherwise check for the value in the compatibleUnits map 0082 QString inputUnitString = unitStrings.first().simplified(); 0083 KUnitConversion::UnitCategory inputCategory = converter->categoryForUnit(inputUnitString); 0084 if (inputCategory.id() == KUnitConversion::InvalidCategory) { 0085 inputUnitString = compatibleUnits.value(inputUnitString.toUpper()); 0086 inputCategory = converter->categoryForUnit(inputUnitString); 0087 if (inputCategory.id() == KUnitConversion::InvalidCategory) { 0088 return; 0089 } 0090 } 0091 0092 QString outputUnitString; 0093 if (unitStrings.size() == 2) { 0094 outputUnitString = unitStrings.at(1).simplified(); 0095 } 0096 0097 const KUnitConversion::Unit inputUnit = inputCategory.unit(inputUnitString); 0098 const QList<KUnitConversion::Unit> outputUnits = createResultUnits(outputUnitString, inputCategory); 0099 const auto numberDataPair = getValidatedNumberValue(inputValueString); 0100 // Return on invalid user input 0101 if (!numberDataPair.first) { 0102 return; 0103 } 0104 0105 const double numberValue = numberDataPair.second; 0106 QList<QueryMatch> matches; 0107 for (const KUnitConversion::Unit &outputUnit : outputUnits) { 0108 KUnitConversion::Value outputValue = inputCategory.convert(KUnitConversion::Value(numberValue, inputUnit), outputUnit); 0109 if (!outputValue.isValid() || inputUnit == outputUnit) { 0110 continue; 0111 } 0112 0113 QueryMatch match(this); 0114 match.setType(QueryMatch::HelperMatch); 0115 match.setIconName(QStringLiteral("accessories-calculator")); 0116 if (outputUnit.categoryId() == KUnitConversion::CurrencyCategory) { 0117 match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(0, 'f', 2), outputUnit.symbol())); 0118 } else { 0119 match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(), outputUnit.symbol())); 0120 } 0121 match.setData(QVariant::fromValue(outputValue)); 0122 match.setRelevance(1.0 - std::abs(std::log10(outputValue.number())) / 50.0); 0123 match.setActions(actionList); 0124 matches.append(match); 0125 } 0126 0127 context.addMatches(matches); 0128 } 0129 0130 void ConverterRunner::run(const RunnerContext &context, const QueryMatch &match) 0131 { 0132 Q_UNUSED(context) 0133 0134 const auto value = match.data().value<KUnitConversion::Value>(); 0135 0136 if (match.selectedAction()) { 0137 QGuiApplication::clipboard()->setText(value.toString()); 0138 } else { 0139 QGuiApplication::clipboard()->setText(QString::number(value.number(), 'f', QLocale::FloatingPointShortest)); 0140 } 0141 } 0142 0143 QMimeData *ConverterRunner::mimeDataForMatch(const QueryMatch &match) 0144 { 0145 const auto value = match.data().value<KUnitConversion::Value>(); 0146 0147 auto *mimeData = new QMimeData(); 0148 mimeData->setText(value.toSymbolString()); 0149 return mimeData; 0150 } 0151 0152 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0153 QPair<bool, double> ConverterRunner::stringToDouble(const QStringRef &value) 0154 #else 0155 QPair<bool, double> ConverterRunner::stringToDouble(const QStringView &value) 0156 #endif 0157 { 0158 bool ok; 0159 double numberValue = locale.toDouble(value, &ok); 0160 if (!ok) { 0161 numberValue = value.toDouble(&ok); 0162 } 0163 return {ok, numberValue}; 0164 } 0165 0166 QPair<bool, double> ConverterRunner::getValidatedNumberValue(const QString &value) 0167 { 0168 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0169 const auto fractionParts = value.splitRef(QLatin1Char('/'), Qt::SkipEmptyParts); 0170 #else 0171 const auto fractionParts = QStringView(value).split(QLatin1Char('/'), Qt::SkipEmptyParts); 0172 #endif 0173 if (fractionParts.isEmpty() || fractionParts.count() > 2) { 0174 return {false, 0}; 0175 } 0176 0177 if (fractionParts.count() == 2) { 0178 const QPair<bool, double> doubleFirstResults = stringToDouble(fractionParts.first()); 0179 if (!doubleFirstResults.first) { 0180 return {false, 0}; 0181 } 0182 const QPair<bool, double> doubleSecondResult = stringToDouble(fractionParts.last()); 0183 if (!doubleSecondResult.first || qFuzzyIsNull(doubleSecondResult.second)) { 0184 return {false, 0}; 0185 } 0186 return {true, doubleFirstResults.second / doubleSecondResult.second}; 0187 } else if (fractionParts.count() == 1) { 0188 const QPair<bool, double> doubleResult = stringToDouble(fractionParts.first()); 0189 if (!doubleResult.first) { 0190 return {false, 0}; 0191 } 0192 return {true, doubleResult.second}; 0193 } else { 0194 return {true, 0}; 0195 } 0196 } 0197 0198 QList<KUnitConversion::Unit> ConverterRunner::createResultUnits(QString &outputUnitString, const KUnitConversion::UnitCategory &category) 0199 { 0200 QList<KUnitConversion::Unit> units; 0201 if (!outputUnitString.isEmpty()) { 0202 KUnitConversion::Unit outputUnit = category.unit(outputUnitString); 0203 if (!outputUnit.isNull() && outputUnit.isValid()) { 0204 units.append(outputUnit); 0205 } else { 0206 // Autocompletion for the target units 0207 outputUnitString = outputUnitString.toUpper(); 0208 for (auto it = compatibleUnits.constBegin(); it != compatibleUnits.constEnd(); it++) { 0209 if (it.key().startsWith(outputUnitString)) { 0210 outputUnit = category.unit(it.value()); 0211 if (!units.contains(outputUnit)) { 0212 units << outputUnit; 0213 } 0214 } 0215 } 0216 } 0217 } else { 0218 units = category.mostCommonUnits(); 0219 // suggest converting to the user's local currency 0220 if (category.id() == KUnitConversion::CurrencyCategory) { 0221 const QString ¤cyIsoCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode); 0222 0223 const KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode); 0224 if (localCurrency.isValid() && !units.contains(localCurrency)) { 0225 units << localCurrency; 0226 } 0227 } 0228 } 0229 0230 return units; 0231 } 0232 0233 void ConverterRunner::insertCompatibleUnits() 0234 { 0235 // Add all currency symbols to the map, if their ISO code is supported by backend 0236 const QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry); 0237 KUnitConversion::UnitCategory currencyCategory = converter->category(QStringLiteral("Currency")); 0238 const QStringList availableISOCodes = currencyCategory.allUnits(); 0239 const QRegularExpression hasCurrencyRegex = QRegularExpression(QStringLiteral("\\p{Sc}")); // clazy:exclude=use-static-qregularexpression 0240 for (const auto ¤cyLocale : allLocales) { 0241 const QString symbol = currencyLocale.currencySymbol(QLocale::CurrencySymbol); 0242 const QString isoCode = currencyLocale.currencySymbol(QLocale::CurrencyIsoCode); 0243 0244 if (isoCode.isEmpty() || !symbol.contains(hasCurrencyRegex)) { 0245 continue; 0246 } 0247 if (availableISOCodes.contains(isoCode)) { 0248 compatibleUnits.insert(symbol.toUpper(), isoCode); 0249 } 0250 } 0251 0252 // Add all units as uppercase in the map 0253 const auto categories = converter->categories(); 0254 for (const auto &category : categories) { 0255 const auto allUnits = category.allUnits(); 0256 for (const auto &unit : allUnits) { 0257 compatibleUnits.insert(unit.toUpper(), unit); 0258 } 0259 } 0260 } 0261 #include "converterrunner.moc"