File indexing completed on 2024-05-19 09:22:10
0001 /* 0002 * SPDX-FileCopyrightText: 2007 Ryan P. Bitanga <ephebiphobic@gmail.com> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-only 0005 */ 0006 0007 #include "spellcheck.h" 0008 0009 #include <QClipboard> 0010 #include <QDebug> 0011 #include <QGuiApplication> 0012 #include <QIcon> 0013 #include <QLocale> 0014 #include <QMimeData> 0015 #include <QSet> 0016 0017 #include <KConfigGroup> 0018 #include <KLocalizedString> 0019 0020 SpellCheckRunner::SpellCheckRunner(QObject *parent, const KPluginMetaData &metaData) 0021 : AbstractRunner(parent, metaData) 0022 { 0023 } 0024 0025 SpellCheckRunner::~SpellCheckRunner() = default; 0026 0027 // Load a default dictionary and some locale names 0028 void SpellCheckRunner::loadData() 0029 { 0030 // Load the default speller, with the default language 0031 auto defaultSpellerIt = m_spellers.find(QString()); 0032 if (defaultSpellerIt == m_spellers.end()) { 0033 defaultSpellerIt = m_spellers.insert(QString(), QSharedPointer<Sonnet::Speller>(new Sonnet::Speller(QString()))); 0034 } 0035 auto &defaultSpeller = defaultSpellerIt.value(); 0036 0037 // store all language names, makes it possible to type "spell german TERM" if english locale is set 0038 // Need to construct a map between natual language names and names the spell-check recognises. 0039 m_availableLangCodes = defaultSpeller->availableLanguages(); 0040 // We need to filter the available languages so that we associate the natural language 0041 // name (eg. 'german') with one sub-code. 0042 QSet<QString> families; 0043 // First get the families 0044 for (const QString &code : std::as_const(m_availableLangCodes)) { 0045 families += code.left(2); 0046 } 0047 // Now for each family figure out which is the main code. 0048 for (const QString &fcode : std::as_const(families)) { 0049 const QStringList family = m_availableLangCodes.filter(fcode); 0050 QString code; 0051 // If we only have one code, use it. 0052 // If a string is the default language, use it 0053 if (family.contains(defaultSpeller->language())) { 0054 code = defaultSpeller->language(); 0055 } else if (fcode == QLatin1String("en")) { 0056 // If the family is english, default to en_US. 0057 const auto enUS = QStringLiteral("en_US"); 0058 if (family.contains(enUS)) { 0059 code = enUS; 0060 } 0061 } else if (family.contains(fcode + QLatin1Char('_') + fcode.toUpper())) { 0062 // If we have a speller of the form xx_XX, try that. 0063 // This gets us most European languages with more than one spelling. 0064 code = fcode + QLatin1Char('_') + fcode.toUpper(); 0065 } else { 0066 // Otherwise, pick the first value as it is highest priority. 0067 code = family.first(); 0068 } 0069 // Finally, add code to the map. 0070 // FIXME: We need someway to map languageCodeToName 0071 const QString name; // = locale->languageCodeToName(fcode); 0072 if (!name.isEmpty()) { 0073 m_languages[name.toLower()] = code; 0074 } 0075 } 0076 } 0077 0078 void SpellCheckRunner::reloadConfiguration() 0079 { 0080 const KConfigGroup cfg = config(); 0081 m_triggerWord = cfg.readEntry("trigger", i18n("spell")); 0082 // Processing will be triggered by "keyword " 0083 m_requireTriggerWord = cfg.readEntry("requireTriggerWord", true) && !m_triggerWord.isEmpty(); 0084 m_triggerWord += QLatin1Char(' '); 0085 0086 QStringList exampleQueries{i18nc("Spelling checking runner syntax, first word is trigger word, e.g. \"spell\".", "%1:q:", m_triggerWord)}; 0087 if (!m_requireTriggerWord) { 0088 exampleQueries.append(QStringLiteral(":q:")); 0089 } 0090 RunnerSyntax s(exampleQueries, i18n("Checks the spelling of :q:.")); 0091 0092 if (m_requireTriggerWord) { 0093 setTriggerWords({m_triggerWord}); 0094 setMinLetterCount(minLetterCount() + 2); // We want at least two letters after the trigger word 0095 } else { 0096 setMinLetterCount(2); 0097 setMatchRegex(QRegularExpression()); 0098 } 0099 0100 setSyntaxes({RunnerSyntax(s)}); 0101 // Clear the data arrays to save memory 0102 m_spellers.clear(); 0103 m_availableLangCodes.clear(); 0104 } 0105 0106 /* Take the input query, split into a list, and see if it contains a language to spell in. 0107 * Return the empty string if we can't match a language. */ 0108 QString SpellCheckRunner::findLang(const QStringList &terms) 0109 { 0110 if (terms.isEmpty()) { 0111 return QString(); 0112 } 0113 0114 // If first term is a language code (like en_GB, en_gb or en), set it as the spell-check language 0115 const auto langCodeIt = std::find_if(m_availableLangCodes.cbegin(), m_availableLangCodes.cend(), [&terms](const QString &languageCode) { 0116 return languageCode.startsWith(terms[0], Qt::CaseInsensitive); 0117 }); 0118 if (langCodeIt != m_availableLangCodes.cend()) { 0119 return *langCodeIt; 0120 } 0121 0122 // If we have two terms and the first is a language name (eg 'french'), 0123 // set it as the available language 0124 if (terms.count() >= 2) { 0125 QString code; 0126 { 0127 // Is this a descriptive language name? 0128 QMap<QString, QString>::const_iterator it = m_languages.constFind(terms[0].toLower()); 0129 if (it != m_languages.constEnd()) { 0130 code = *it; 0131 } 0132 // Maybe it is a subset of a language code? 0133 else { 0134 QStringList codes = QStringList(m_languages.values()).filter(terms[0]); 0135 if (!codes.isEmpty()) { 0136 code = codes.first(); 0137 } 0138 } 0139 } 0140 0141 if (!code.isEmpty()) { 0142 // We found a valid language! Check still available 0143 // Does the spell-checker like it? 0144 if (m_availableLangCodes.contains(code)) { 0145 return code; 0146 } 0147 } 0148 // FIXME: Support things like 'british english' or 'canadian french' 0149 } 0150 return QString(); 0151 } 0152 0153 void SpellCheckRunner::match(RunnerContext &context) 0154 { 0155 const QString term = context.query(); 0156 QString query = term; 0157 0158 if (m_requireTriggerWord) { 0159 int len = m_triggerWord.length(); 0160 if (query.left(len) != m_triggerWord) { 0161 return; 0162 } 0163 query = query.mid(len).trimmed(); 0164 } 0165 if (m_spellers.isEmpty()) { 0166 loadData(); 0167 } 0168 0169 // Pointer to speller object with our chosen language 0170 QSharedPointer<Sonnet::Speller> speller = m_spellers[QString()]; 0171 0172 QString lang; 0173 if (speller->isValid()) { 0174 QStringList terms = query.split(QLatin1Char(' '), Qt::SkipEmptyParts); 0175 lang = findLang(terms); 0176 // If we found a language, create a new speller object using it. 0177 if (!lang.isEmpty()) { 0178 // First term is the language 0179 terms.removeFirst(); 0180 // New speller object if we don't already have one 0181 if (!m_spellers.contains(lang)) { 0182 // Check nothing happened while we were acquiring the lock 0183 if (!m_spellers.contains(lang)) { 0184 m_spellers[lang] = QSharedPointer<Sonnet::Speller>(new Sonnet::Speller(lang)); 0185 } 0186 } 0187 speller = m_spellers[lang]; 0188 // Rejoin the strings 0189 query = terms.join(QLatin1Char(' ')); 0190 } 0191 } 0192 0193 if (query.size() < 2) { 0194 return; 0195 } 0196 0197 if (speller->isValid()) { 0198 const auto fillMatch = [this, &context, &query, &speller](const QString &langCode = QString()) { 0199 if (!langCode.isEmpty()) { 0200 speller->setLanguage(langCode); 0201 } 0202 0203 QStringList suggestions; 0204 const bool correct = speller->checkAndSuggest(query, suggestions); 0205 0206 if (correct) { 0207 QueryMatch match(this); 0208 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0209 match.setIconName(QStringLiteral("checkbox")); 0210 match.setText(query); 0211 match.setSubtext(i18nc("Term is spelled correctly", "Correct")); 0212 match.setData(query); 0213 context.addMatch(match); 0214 } else if (!suggestions.isEmpty()) { 0215 for (const auto &suggestion : std::as_const(suggestions)) { 0216 QueryMatch match(this); 0217 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0218 match.setIconName(QStringLiteral("edit-rename")); 0219 match.setText(suggestion); 0220 match.setSubtext(i18n("Suggested term")); 0221 match.setData(suggestion); 0222 context.addMatch(match); 0223 } 0224 } else { 0225 return false; 0226 } 0227 0228 return true; 0229 }; 0230 0231 if (!fillMatch() && lang.isEmpty() && m_availableLangCodes.count() >= 2) { 0232 // Perhaps the term is not in the default dictionary, try other dictionaries. 0233 const QString defaultLangCode = speller->language(); 0234 for (const QString &langCode : std::as_const(m_availableLangCodes)) { 0235 if (langCode == defaultLangCode) { 0236 continue; 0237 } 0238 0239 if (fillMatch(langCode)) { 0240 // The dictionary returns valid results 0241 break; 0242 } 0243 } 0244 // No need to reset the default language as the speller will be reset in destroydata() 0245 } 0246 } else { 0247 QueryMatch match(this); 0248 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest); 0249 match.setIconName(QStringLiteral("data-error")); 0250 match.setText(xi18nc("@info", "No dictionary found. Please install <resource>hunspell</resource> package using your package manager")); 0251 context.addMatch(match); 0252 } 0253 } 0254 0255 void SpellCheckRunner::run(const RunnerContext & /*context*/, const QueryMatch &match) 0256 { 0257 QGuiApplication::clipboard()->setText(match.data().toString()); 0258 } 0259 0260 QMimeData *SpellCheckRunner::mimeDataForMatch(const QueryMatch &match) 0261 { 0262 QMimeData *result = new QMimeData(); 0263 const QString text = match.data().toString(); 0264 result->setText(text); 0265 return result; 0266 } 0267 0268 K_PLUGIN_CLASS_WITH_JSON(SpellCheckRunner, "plasma-runner-spellchecker.json") 0269 0270 #include "spellcheck.moc"