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