commit 124e35885b8cd1b593b7b83a070bd0bdb5758661 Author: Simeon Bird Date: Fri Oct 19 21:16:34 2012 -0400 Fix the plasma spellchecker's 'foreign language' support. Previously this caused segfaults (even if not used) because it called setLanguage(), which is not thread-safe, in match(). Instead, this patch constructs a new speller safely for each new language, without deleting the old one. Old spellers are instead deleted on the teardown() signal. While we're at it, amend the language detection so that the user can type natural language names (eg, 'german') and have the spell-checker find the right language. REVIEW: 106244 BUG: 303831 BUG: 264779 FIXED-IN: 4.9.3 diff --git a/runners/spellchecker/spellcheck.cpp b/runners/spellchecker/spellcheck.cpp index 672732d..cc6aeb2 100644 --- a/runners/spellchecker/spellcheck.cpp +++ b/runners/spellchecker/spellcheck.cpp @@ -24,6 +24,7 @@ // #include #include #include +#include SpellCheckRunner::SpellCheckRunner(QObject* parent, const QVariantList &args) : Plasma::AbstractRunner(parent, args) @@ -43,13 +44,64 @@ void SpellCheckRunner::init() { Plasma::AbstractRunner::init(); - //store all language names, makes it posible to type "spell german TERM" if english locale is set + //Connect prepare and teardown signals + connect(this, SIGNAL(prepare()), this, SLOT(loaddata())); + connect(this, SIGNAL(teardown()), this, SLOT(destroydata())); +} + +//Load a default dictionary and some locale names +void SpellCheckRunner::loaddata() +{ + //Load the default speller, with the default language + if (!m_spellers.contains("")) { + m_spellers[""] = QSharedPointer (new Sonnet::Speller("")); + } + //store all language names, makes it possible to type "spell german TERM" if english locale is set + //Need to construct a map between natual language names and names the spell-check recognises. KLocale *locale = KGlobal::locale(); - QStringList codes = locale->allLanguagesList(); - foreach (const QString &code, codes) { - const QString name = locale->languageCodeToName(code); - m_languages[name.toLower()] = code; + const QStringList avail = m_spellers[""]->availableLanguages(); + //We need to filter the available languages so that we associate the natural language + //name (eg. 'german') with one sub-code. + QSet families; + //First get the families + foreach (const QString &code, avail) { + families +=code.left(2); + } + //Now for each family figure out which is the main code. + foreach (const QString &fcode,families) { + QStringList family = avail.filter(fcode); + QString code; + //If we only have one code, use it. + //If a string is the default language, use it + if (family.contains(m_spellers[""]->language())) { + code = m_spellers[""]->language(); + } else if (fcode == QLatin1String("en")) { + //If the family is english, default to en_US. + if (family.contains("en_US")) { + code = QLatin1String("en_US"); + } + } else if (family.contains(fcode+QLatin1String("_")+fcode.toUpper())) { + //If we have a speller of the form xx_XX, try that. + //This gets us most European languages with more than one spelling. + code = fcode+QLatin1String("_")+fcode.toUpper(); + } else { + //Otherwise, pick the first value as it is highest priority. + code = family.first(); + } + //Finally, add code to the map. + const QString name = locale->languageCodeToName(fcode); + if (!name.isEmpty()) { + m_languages[name.toLower()] = code; + } +// kDebug() << "SPELL lang: " << fcode<< "::"<< name << " : " << code; } + +} + +void SpellCheckRunner::destroydata() +{ + //Clear the data arrays to save memory + m_spellers.clear(); } void SpellCheckRunner::reloadConfiguration() @@ -73,13 +125,52 @@ void SpellCheckRunner::reloadConfiguration() setSyntaxes(syns); } +/* Take the input query, split into a list, and see if it contains a language to spell in. + * Return the empty string if we can't match a language. */ +QString SpellCheckRunner::findlang(const QStringList& terms) +{ + //If first term is a language code (like en_GB), set it as the spell-check language + if (terms.count() >= 1 && m_spellers[""]->availableLanguages().contains(terms[0])) { + return terms[0]; + } + //If we have two terms and the first is a language name (eg 'french'), + //set it as the available language + else if (terms.count() >=2) { + QString code; + { + //Is this a descriptive language name? + QMap::const_iterator it = m_languages.constFind(terms[0].toLower()); + if (it != m_languages.constEnd()) { + code = *it; + } + //Maybe it is a subset of a language code? + else { + QStringList codes = QStringList(m_languages.values()).filter(terms[0]); + if (!codes.isEmpty()) { + code = codes.first(); + } + } + } + + if (!code.isEmpty()) { + //We found a valid language! Check still available + const QStringList avail = m_spellers[""]->availableLanguages(); + //Does the spell-checker like it? + if (avail.contains(code)) { + return code; + } + } + //FIXME: Support things like 'british english' or 'canadian french' + } + return QLatin1String(""); +} + void SpellCheckRunner::match(Plasma::RunnerContext &context) { if (!context.isValid()) { return; } - const QString term = context.query(); QString query = term; @@ -88,53 +179,46 @@ void SpellCheckRunner::match(Plasma::RunnerContext &context) if (query.left(len) != m_triggerWord) { return; } - - QString language = m_speller.defaultLanguage(); query = query.mid(len).trimmed(); - QStringList terms = query.split(' '); - - //two terms specified, check if first is a language - QString customLanguage; - if (terms.count() == 2) { - customLanguage = terms[0]; - query = terms[1]; - } - //three terms specified, check if first two are a language, e.g. "american english" - if (terms.count() == 3) { - customLanguage = terms[0] + ' ' + terms[1]; - query = terms[2]; - } - - if (!customLanguage.isEmpty()) { - language = customLanguage; - m_speller.setLanguage(language); + } - //not valid, maybe it is a language name, not a code - if (!m_speller.isValid()) { - QHash::const_iterator it = m_languages.constFind(language.toLower()); - //is a valid language name - if (it != m_languages.constEnd()) { - language = *it; + //Pointer to speller object with our chosen language + QSharedPointer speller = m_spellers[""]; + + if (speller->isValid()) { + QStringList terms = query.split(' ', QString::SkipEmptyParts); + QString lang = findlang(terms); + //If we found a language, create a new speller object using it. + if (!lang.isEmpty()) { + //First term is the language + terms.removeFirst(); + //New speller object if we don't already have one + if (!m_spellers.contains(lang)) { + QMutexLocker lock (&m_spellLock); + //Check nothing happened while we were acquiring the lock + if (!m_spellers.contains(lang)) { + m_spellers[lang] = QSharedPointer(new Sonnet::Speller(lang)); } } + speller = m_spellers[lang]; + //Rejoin the strings + query = terms.join(QLatin1String(" ")); } - - m_speller.setLanguage(language); } - if (query.size() < 3) { + if (query.size() < 2) { return; } Plasma::QueryMatch match(this); match.setType(Plasma::QueryMatch::InformationalMatch); - if (m_speller.isValid()) { + if (speller->isValid()) { QStringList suggestions; - const bool correct = m_speller.checkAndSuggest(query,suggestions); + const bool correct = speller->checkAndSuggest(query,suggestions); if (correct) { match.setIcon(KIcon(QLatin1String( "checkbox" ))); - match.setText(i18n("Correct")); + match.setText(i18n("Correct")+QLatin1String(": ")+query); } else { match.setIcon(KIcon(QLatin1String( "edit-delete" ))); const QString recommended = i18n("Suggested words: %1", suggestions.join(i18nc("seperator for a list of words", ", "))); diff --git a/runners/spellchecker/spellcheck.h b/runners/spellchecker/spellcheck.h index 492c370..ca65452 100644 --- a/runners/spellchecker/spellcheck.h +++ b/runners/spellchecker/spellcheck.h @@ -22,6 +22,7 @@ #include #include +#include /** * This checks the spelling of query @@ -41,12 +42,17 @@ public: protected slots: void init(); + void loaddata(); + void destroydata(); private: + QString findlang(const QStringList &terms); + QString m_triggerWord; - QHash m_languages;//key=language name, value=language code + QMap m_languages;//key=language name, value=language code bool m_requireTriggerWord; - Sonnet::Speller m_speller; + QMap > m_spellers; //spellers + QMutex m_spellLock; //Lock held when constructing a new speller }; K_EXPORT_PLASMA_RUNNER(spellcheckrunner, SpellCheckRunner)