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"