diff --git a/src/plugins/voikko/voikkoclient.cpp b/src/plugins/voikko/voikkoclient.cpp index 423afcd..7847cc0 100644 --- a/src/plugins/voikko/voikkoclient.cpp +++ b/src/plugins/voikko/voikkoclient.cpp @@ -1,72 +1,72 @@ /** * voikkoclient.cpp * * Copyright (C) 2015 Jesse Jaara * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "voikkoclient.h" #include "voikkodict.h" #include "voikkodebug.h" VoikkoClient::VoikkoClient(QObject* parent) : Sonnet::Client(parent) { qCDebug(SONNET_VOIKKO) << "Initializing Voikko spell checker plugin."; - char **dictionaries = voikkoListSupportedSpellingLanguages(Q_NULLPTR); + char **dictionaries = voikkoListSupportedSpellingLanguages(nullptr); if (!dictionaries) { return; } - for (int i = 0; dictionaries[i] != Q_NULLPTR; ++i) { + for (int i = 0; dictionaries[i] != nullptr; ++i) { QString language = QString::fromUtf8(dictionaries[i]); m_supportedLanguages.append(language); qCDebug(SONNET_VOIKKO) << "Found dictionary for langauge:" << language; } voikkoFreeCstrArray(dictionaries); } VoikkoClient::~VoikkoClient() {} int VoikkoClient::reliability() const { return 50; } Sonnet::SpellerPlugin* VoikkoClient::createSpeller(const QString &language) { VoikkoDict *speller = new VoikkoDict(language); if (speller->initFailed()) { delete speller; - return Q_NULLPTR; + return nullptr; } return speller; } QStringList VoikkoClient::languages() const { return m_supportedLanguages; } QString VoikkoClient::name() const { return QStringLiteral("Voikko"); } \ No newline at end of file diff --git a/src/plugins/voikko/voikkoclient.h b/src/plugins/voikko/voikkoclient.h index e3fc2d0..22e9872 100644 --- a/src/plugins/voikko/voikkoclient.h +++ b/src/plugins/voikko/voikkoclient.h @@ -1,49 +1,49 @@ /** * voikkoclient.h * * Copyright (C) 2015 Jesse Jaara * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef SONNET_VOIKKOCLIENT_H #define SONNET_VOIKKOCLIENT_H #include "client_p.h" class VoikkoClient : public Sonnet::Client { Q_OBJECT Q_INTERFACES(Sonnet::Client) Q_PLUGIN_METADATA(IID "org.kde.Sonnet.VoikkoClient") public: - explicit VoikkoClient(QObject *parent = Q_NULLPTR); + explicit VoikkoClient(QObject *parent = nullptr); ~VoikkoClient(); int reliability() const Q_DECL_OVERRIDE; Sonnet::SpellerPlugin *createSpeller(const QString &language) Q_DECL_OVERRIDE; QStringList languages() const Q_DECL_OVERRIDE; QString name() const Q_DECL_OVERRIDE; private: QStringList m_supportedLanguages; }; #endif //SONNET_VOIKKOCLIENT_H diff --git a/src/plugins/voikko/voikkodict.cpp b/src/plugins/voikko/voikkodict.cpp index 747081e..6e11636 100644 --- a/src/plugins/voikko/voikkodict.cpp +++ b/src/plugins/voikko/voikkodict.cpp @@ -1,344 +1,344 @@ /** * voikkodict.cpp * * Copyright (C) 2015 Jesse Jaara * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "voikkodict.h" #include "voikkodebug.h" #include #include #include #ifdef Q_IS_WIN #include #endif #include #include #include namespace { // QString literals used in loading and storing user dictionary inline const QString replacement_bad_str() Q_DECL_NOEXCEPT { return QStringLiteral("bad"); } inline const QString replacement_good_str() Q_DECL_NOEXCEPT { return QStringLiteral("good"); } inline const QString personal_words_str() Q_DECL_NOEXCEPT { return QStringLiteral("PersonalWords"); } inline const QString replacements_str() Q_DECL_NOEXCEPT { return QStringLiteral("Replacements"); } // Set path to: QStandardPaths::GenericDataLocation/Sonnet/Voikko-user-dictionary.json QString getUserDictionaryPath() Q_DECL_NOEXCEPT { QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); #ifdef Q_OS_WIN // Resolve the windows' Roaming directory manually directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); if (QSysInfo::windowsVersion() == WV_XP || QSysInfo::windowsVersion() == WV_2003) { // In Xp Roaming is "/Application Data" // DataLocation: "/Local Settings/Application Data" directory += QStringLiteral("/../../Application Data"); } else { directory += QStringLiteral("/../Roaming"); } #endif directory += QStringLiteral("/Sonnet"); QDir path(directory); path.mkpath(path.absolutePath()); return path.absoluteFilePath(QStringLiteral("Voikko-user-dictionary.json")); } void addReplacementToNode(QJsonObject &languageNode, const QString &bad, const QString &good) Q_DECL_NOEXCEPT { QJsonObject pair; pair[replacement_bad_str()] = good; pair[replacement_good_str()] = bad; auto replaceList = languageNode[replacements_str()].toArray(); replaceList.append(pair); languageNode[replacements_str()] = replaceList; } void addPersonalWordToNode(QJsonObject &languageNode, const QString &word) Q_DECL_NOEXCEPT { auto arr = languageNode[personal_words_str()].toArray(); arr.append(word); languageNode[personal_words_str()] = arr; } /** * Read and return the root json object from fileName. * * Returns an empty node in case of an IO error or the file is empty. */ QJsonObject readJsonRootObject(const QString &fileName) Q_DECL_NOEXCEPT { QFile userDictFile(fileName); if (!userDictFile.exists()) { return QJsonObject(); // Nothing has been saved so far. } if (!userDictFile.open(QIODevice::ReadOnly)) { qCWarning(SONNET_VOIKKO) << "Could not open personal dictionary. Failed to open file" << fileName; qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); return QJsonObject(); } QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); userDictFile.close(); return dictDoc.object(); } } class VoikkoDictPrivate { public: VoikkoHandle *m_handle; const VoikkoDict *q; QSet m_sessionWords; QSet m_personalWords; QHash m_replacements; QString m_userDictionaryFilepath; // Used when converting Qstring to wchar_t strings QVector m_conversionBuffer; VoikkoDictPrivate(const QString &language, const VoikkoDict *publicPart) Q_DECL_NOEXCEPT : q(publicPart), m_userDictionaryFilepath(getUserDictionaryPath()), m_conversionBuffer(256) { const char *error; m_handle = voikkoInit(&error, language.toUtf8().data(), 0); - if (error != Q_NULLPTR) { + if (error != nullptr) { qCWarning(SONNET_VOIKKO) << "Failed to initialize Voikko spelling backend. Reason:" << error; } else { // Continue to load user's own words loadUserDictionary(); } }; /** * Store a new ignored/personal word or replacement pair in the user's * dictionary m_userDictionaryFilepath. * * returns true on success else false */ bool storePersonal(const QString &personalWord, const QString &bad = QString(), const QString &good = QString()) const Q_DECL_NOEXCEPT { QFile userDictFile(m_userDictionaryFilepath); if (!userDictFile.open(QIODevice::ReadWrite)) { qCWarning(SONNET_VOIKKO) << "Could not save personal dictionary. Failed to open file:" << m_userDictionaryFilepath; qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); return false; } QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); auto root = readJsonRootObject(m_userDictionaryFilepath); auto languageNode = root[q->language()].toObject(); // Empty value means we are storing a bad:good pair if (personalWord.isEmpty()) { addReplacementToNode(languageNode, bad, good); } else { addPersonalWordToNode(languageNode, personalWord); } root[q->language()] = languageNode; dictDoc.setObject(root); userDictFile.reset(); userDictFile.write(dictDoc.toJson()); userDictFile.close(); qCDebug(SONNET_VOIKKO) << "Changes to user dictionary saved to file: " << m_userDictionaryFilepath; return true; }; /** * Load user's own personal words and replacement pairs from * m_userDictionaryFilepath. */ void loadUserDictionary() Q_DECL_NOEXCEPT { // If root is empty we will fail later on when checking if // languageNode is empty. auto root = readJsonRootObject(m_userDictionaryFilepath); auto languageNode = root[q->language()].toObject(); if (languageNode.isEmpty()) { return; // Nothing to load } loadUserWords(languageNode); loadUserReplacements(languageNode); }; /** * Convert the given QString to a \0 terminated wchar_t string. * Uses QVector as a buffer and return it's internal data pointer. */ inline const wchar_t *QStringToWchar(const QString &str) Q_DECL_NOEXCEPT { m_conversionBuffer.resize(str.length() + 1); int size = str.toWCharArray(m_conversionBuffer.data()); m_conversionBuffer[size] = '\0'; return m_conversionBuffer.constData(); } private: /** * Extract and append user defined words from the languageNode. */ inline void loadUserWords(const QJsonObject &languageNode) Q_DECL_NOEXCEPT { auto words = languageNode[personal_words_str()].toArray(); Q_FOREACH(auto word, words) { m_personalWords.insert(word.toString()); } qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 words from the user dictionary.").arg(words.size()); } /** * Extract and append user defined replacement pairs from the languageNode. */ inline void loadUserReplacements(const QJsonObject &languageNode) Q_DECL_NOEXCEPT { auto words = languageNode[replacements_str()].toArray(); Q_FOREACH(auto pair, words) { m_replacements[pair.toObject()[replacement_bad_str()].toString()] = pair.toObject()[replacement_good_str()].toString(); } qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 replacements from the user dictionary.").arg(words.size()); } }; VoikkoDict::VoikkoDict(const QString &language) Q_DECL_NOEXCEPT : SpellerPlugin(language), d(new VoikkoDictPrivate(language, this)) { qCDebug(SONNET_VOIKKO) << "Loading dictionary for langauge:" << language; } VoikkoDict::~VoikkoDict() {} bool VoikkoDict::isCorrect(const QString &word) const { // Check the session word list and personal word list first if (d->m_sessionWords.contains(word) || d->m_personalWords.contains(word)) { return true; } return (voikkoSpellUcs4(d->m_handle, d->QStringToWchar(word)) == VOIKKO_SPELL_OK); } QStringList VoikkoDict::suggest(const QString &word) const { QStringList suggestions; auto userDictPos = d->m_replacements.constFind(word); if (userDictPos != d->m_replacements.constEnd()) { suggestions.append(*userDictPos); } auto voikkoSuggestions = voikkoSuggestUcs4(d->m_handle, d->QStringToWchar(word)); if (!voikkoSuggestions) { return suggestions; } - for (int i = 0; voikkoSuggestions[i] != Q_NULLPTR; ++i) { + for (int i = 0; voikkoSuggestions[i] != nullptr; ++i) { QString suggestion = QString::fromWCharArray(voikkoSuggestions[i]); suggestions.append(suggestion); } qCDebug(SONNET_VOIKKO) << "Misspelled:" << word << "|Suggestons:" << suggestions.join(QStringLiteral(", ")); voikko_free_suggest_ucs4(voikkoSuggestions); return suggestions; } bool VoikkoDict::storeReplacement(const QString &bad, const QString &good) { qCDebug(SONNET_VOIKKO) << "Adding new replacement pair to user dictionary:" << bad << "->" << good; d->m_replacements[bad] = good; return d->storePersonal(QString(), bad, good); } bool VoikkoDict::addToPersonal(const QString &word) { qCDebug(SONNET_VOIKKO()) << "Adding new word to user dictionary" << word; d->m_personalWords.insert(word); return d->storePersonal(word); } bool VoikkoDict::addToSession(const QString &word) { qCDebug(SONNET_VOIKKO()) << "Adding new word to session dictionary" << word; d->m_sessionWords.insert(word); return true; } bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT { return (!d->m_handle); } diff --git a/src/ui/highlighter.cpp b/src/ui/highlighter.cpp index 7dda438..00c3892 100644 --- a/src/ui/highlighter.cpp +++ b/src/ui/highlighter.cpp @@ -1,481 +1,481 @@ /** * highlighter.cpp * * Copyright (C) 2004 Zack Rusin * Copyright (C) 2006 Laurent Montel * Copyright (C) 2013 Martin Sandsmark * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "highlighter.h" #include "speller.h" #include "loader_p.h" #include "tokenizer_p.h" #include "settings_p.h" #include "languagefilter_p.h" #include "ui_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace Sonnet { class LanguageCache : public QTextBlockUserData { public: QMap, QString> languages; void invalidate(int pos) { QMutableMapIterator, QString> it(languages); it.toBack(); while (it.hasPrevious()) { it.previous(); if (it.key().first+it.key().second >=pos) it.remove(); else break; } } }; class HighlighterPrivate { public: HighlighterPrivate(Highlighter *qq, const QColor &col) - : textEdit(Q_NULLPTR), - plainTextEdit(Q_NULLPTR), + : textEdit(nullptr), + plainTextEdit(nullptr), spellColor(col), q(qq) { tokenizer = new WordTokenizer(); active = true; automatic = false; connected = false; wordCount = 0; errorCount = 0; intraWordEditing = false; completeRehighlightRequired = false; spellColor = spellColor.isValid() ? spellColor : Qt::red; languageFilter = new LanguageFilter(new SentenceTokenizer()); loader = Loader::openLoader(); loader->settings()->restore(); spellchecker = new Sonnet::Speller(); spellCheckerFound = spellchecker->isValid(); rehighlightRequest = new QTimer(q); q->connect(rehighlightRequest, SIGNAL(timeout()), q, SLOT(slotRehighlight())); if (!spellCheckerFound) { return; } disablePercentage = loader->settings()->disablePercentageWordError(); disableWordCount = loader->settings()->disableWordErrorCount(); completeRehighlightRequired = true; rehighlightRequest->setInterval(0); rehighlightRequest->setSingleShot(true); rehighlightRequest->start(); } ~HighlighterPrivate(); WordTokenizer *tokenizer; LanguageFilter *languageFilter; Loader *loader; Speller *spellchecker; QTextEdit *textEdit; QPlainTextEdit *plainTextEdit; bool active; bool automatic; bool completeRehighlightRequired; bool intraWordEditing; bool spellCheckerFound; //cached d->dict->isValid() value bool connected; int disablePercentage; int disableWordCount; int wordCount, errorCount; QTimer *rehighlightRequest; QColor spellColor; Highlighter *q; }; HighlighterPrivate::~HighlighterPrivate() { delete spellchecker; delete languageFilter; delete tokenizer; } Highlighter::Highlighter(QTextEdit *edit, const QColor &_col) : QSyntaxHighlighter(edit), d(new HighlighterPrivate(this, _col)) { d->textEdit = edit; d->textEdit->installEventFilter(this); d->textEdit->viewport()->installEventFilter(this); } Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col) : QSyntaxHighlighter(edit), d(new HighlighterPrivate(this, col)) { d->plainTextEdit = edit; setDocument(d->plainTextEdit->document()); d->plainTextEdit->installEventFilter(this); d->plainTextEdit->viewport()->installEventFilter(this); } Highlighter::~Highlighter() { delete d; } bool Highlighter::spellCheckerFound() const { return d->spellCheckerFound; } void Highlighter::slotRehighlight() { if (d->completeRehighlightRequired) { d->wordCount = 0; d->errorCount = 0; rehighlight(); } else { //rehighlight the current para only (undo/redo safe) QTextCursor cursor; if (d->textEdit) cursor = d->textEdit->textCursor(); else cursor = d->plainTextEdit->textCursor(); cursor.insertText(QString()); } //if (d->checksDone == d->checksRequested) //d->completeRehighlightRequired = false; QTimer::singleShot(0, this, SLOT(slotAutoDetection())); } bool Highlighter::automatic() const { return d->automatic; } bool Highlighter::intraWordEditing() const { return d->intraWordEditing; } void Highlighter::setIntraWordEditing(bool editing) { d->intraWordEditing = editing; } void Highlighter::setAutomatic(bool automatic) { if (automatic == d->automatic) { return; } d->automatic = automatic; if (d->automatic) { slotAutoDetection(); } } void Highlighter::slotAutoDetection() { bool savedActive = d->active; //don't disable just because 1 of 4 is misspelled. if (d->automatic && d->wordCount >= 10) { // tme = Too many errors bool tme = (d->errorCount >= d->disableWordCount) && ( d->errorCount * 100 >= d->disablePercentage * d->wordCount); if (d->active && tme) { d->active = false; } else if (!d->active && !tme) { d->active = true; } } if (d->active != savedActive) { if (d->active) { emit activeChanged(tr("As-you-type spell checking enabled.")); } else { qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors"; emit activeChanged(tr("Too many misspelled words. " "As-you-type spell checking disabled.")); } d->completeRehighlightRequired = true; d->rehighlightRequest->setInterval(100); d->rehighlightRequest->setSingleShot(true); } } void Highlighter::setActive(bool active) { if (active == d->active) { return; } d->active = active; rehighlight(); if (d->active) { emit activeChanged(tr("As-you-type spell checking enabled.")); } else { emit activeChanged(tr("As-you-type spell checking disabled.")); } } bool Highlighter::isActive() const { return d->active; } void Highlighter::contentsChange(int pos, int add, int rem) { // Invalidate the cache where the text has changed const QTextBlock &lastBlock = document()->findBlock(pos + add - rem); QTextBlock block = document()->findBlock(pos); do { LanguageCache* cache=dynamic_cast(block.userData()); if (cache) cache->invalidate(pos-block.position()); block = block.next(); } while (block.isValid() && block < lastBlock); } void Highlighter::highlightBlock(const QString &text) { if (text.isEmpty() || !d->active || !d->spellCheckerFound) { return; } if (!d->connected) { connect(document(), SIGNAL(contentsChange(int,int,int)), SLOT(contentsChange(int,int,int))); d->connected = true; } QTextCursor cursor; if (d->textEdit) { cursor = d->textEdit->textCursor(); } else { cursor = d->plainTextEdit->textCursor(); } int index = cursor.position(); const int lengthPosition = text.length() - 1; if ( index != lengthPosition || ( lengthPosition > 0 && !text[lengthPosition-1].isLetter() ) ) { d->languageFilter->setBuffer(text); LanguageCache* cache=dynamic_cast(currentBlockUserData()); if (!cache) { cache = new LanguageCache; setCurrentBlockUserData(cache); } while (d->languageFilter->hasNext()) { QStringRef sentence=d->languageFilter->next(); if (d->spellchecker->testAttribute(Speller::AutoDetectLanguage)) { QString lang; QPair spos=QPair(sentence.position(),sentence.length()); // try cache first if (cache->languages.contains(spos)) { lang=cache->languages.value(spos); } else { lang=d->languageFilter->language(); if (!d->languageFilter->isSpellcheckable()) lang.clear(); cache->languages[spos]=lang; } if (lang.isEmpty()) continue; d->spellchecker->setLanguage(lang); } d->tokenizer->setBuffer(sentence.toString()); int offset=sentence.position(); while (d->tokenizer->hasNext()) { QStringRef word=d->tokenizer->next(); if (!d->tokenizer->isSpellcheckable()) continue; ++d->wordCount; if (d->spellchecker->isMisspelled(word.toString())) { ++d->errorCount; setMisspelled(word.position()+offset, word.length()); } else { unsetMisspelled(word.position()+offset, word.length()); } } } } //QTimer::singleShot( 0, this, SLOT(checkWords()) ); setCurrentBlockState(0); } QString Highlighter::currentLanguage() const { return d->spellchecker->language(); } void Highlighter::setCurrentLanguage(const QString &lang) { QString prevLang=d->spellchecker->language(); d->spellchecker->setLanguage(lang); d->spellCheckerFound = d->spellchecker->isValid(); if (!d->spellCheckerFound) { qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language."; d->spellchecker->setLanguage(prevLang); return; } d->wordCount = 0; d->errorCount = 0; if (d->automatic) { d->rehighlightRequest->start(0); } } void Highlighter::setMisspelled(int start, int count) { QTextCharFormat format; format.setFontUnderline(true); format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); format.setUnderlineColor(d->spellColor); setFormat(start, count, format); } void Highlighter::unsetMisspelled(int start, int count) { setFormat(start, count, QTextCharFormat()); } bool Highlighter::eventFilter(QObject *o, QEvent *e) { if (!d->spellCheckerFound) { return false; } if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) { QKeyEvent *k = static_cast(e); //d->autoReady = true; if (d->rehighlightRequest->isActive()) { // try to stay out of the users way d->rehighlightRequest->start(500); } if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return || k->key() == Qt::Key_Up || k->key() == Qt::Key_Down || k->key() == Qt::Key_Left || k->key() == Qt::Key_Right || k->key() == Qt::Key_PageUp || k->key() == Qt::Key_PageDown || k->key() == Qt::Key_Home || k->key() == Qt::Key_End || ((k->modifiers() == Qt::ControlModifier) && ((k->key() == Qt::Key_A) || (k->key() == Qt::Key_B) || (k->key() == Qt::Key_E) || (k->key() == Qt::Key_N) || (k->key() == Qt::Key_P)))) { if (intraWordEditing()) { setIntraWordEditing(false); d->completeRehighlightRequired = true; d->rehighlightRequest->setInterval(500); d->rehighlightRequest->setSingleShot(true); d->rehighlightRequest->start(); } } else { setIntraWordEditing(true); } if (k->key() == Qt::Key_Space || k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) { QTimer::singleShot(0, this, SLOT(slotAutoDetection())); } } else if ((( d->textEdit && ( o == d->textEdit->viewport())) || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) && (e->type() == QEvent::MouseButtonPress)) { //d->autoReady = true; if (intraWordEditing()) { setIntraWordEditing(false); d->completeRehighlightRequired = true; d->rehighlightRequest->setInterval(0); d->rehighlightRequest->setSingleShot(true); d->rehighlightRequest->start(); } } return false; } void Highlighter::addWordToDictionary(const QString &word) { d->spellchecker->addToPersonal(word); } void Highlighter::ignoreWord(const QString &word) { d->spellchecker->addToSession(word); } QStringList Highlighter::suggestionsForWord(const QString &word, int max) { QStringList suggestions = d->spellchecker->suggest(word); if (max != -1) { while (suggestions.count() > max) { suggestions.removeLast(); } } return suggestions; } bool Highlighter::isWordMisspelled(const QString &word) { return d->spellchecker->isMisspelled(word); } void Highlighter::setMisspelledColor(const QColor &color) { d->spellColor = color; } bool Highlighter::checkerEnabledByDefault() const { return d->loader->settings()->checkerEnabledByDefault(); } void Highlighter::setDocument(QTextDocument* document) { d->connected = false; QSyntaxHighlighter::setDocument(document); } } diff --git a/src/ui/spellcheckdecorator.cpp b/src/ui/spellcheckdecorator.cpp index 2e79ae3..6d8668b 100644 --- a/src/ui/spellcheckdecorator.cpp +++ b/src/ui/spellcheckdecorator.cpp @@ -1,275 +1,275 @@ /* * spellcheckdecorator.h * * Copyright (C) 2013 Aurélien Gâteau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "spellcheckdecorator.h" // Local #include // Qt #include #include #include #include namespace Sonnet { class SpellCheckDecorator::Private { public: Private(SpellCheckDecorator *installer, QPlainTextEdit *textEdit) : q(installer) - , m_textEdit(Q_NULLPTR) + , m_textEdit(nullptr) , m_plainTextEdit(textEdit) - , m_highlighter(Q_NULLPTR) + , m_highlighter(nullptr) { createDefaultHighlighter(); // Catch pressing the "menu" key m_plainTextEdit->installEventFilter(q); // Catch right-click m_plainTextEdit->viewport()->installEventFilter(q); } Private(SpellCheckDecorator *installer, QTextEdit *textEdit) : q(installer) , m_textEdit(textEdit) - , m_plainTextEdit(Q_NULLPTR) + , m_plainTextEdit(nullptr) , m_highlighter(nullptr) { createDefaultHighlighter(); // Catch pressing the "menu" key m_textEdit->installEventFilter(q); // Catch right-click m_textEdit->viewport()->installEventFilter(q); } bool onContextMenuEvent(QContextMenuEvent *event); void execSuggestionMenu(const QPoint &pos, const QString &word, const QTextCursor &cursor); void createDefaultHighlighter(); SpellCheckDecorator *q; QTextEdit *m_textEdit; QPlainTextEdit *m_plainTextEdit; Highlighter *m_highlighter; }; bool SpellCheckDecorator::Private::onContextMenuEvent(QContextMenuEvent *event) { if (!m_highlighter) { createDefaultHighlighter(); } // Obtain the cursor at the mouse position and the current cursor QTextCursor cursorAtMouse; if (m_textEdit) { cursorAtMouse = m_textEdit->cursorForPosition(event->pos()); } else { cursorAtMouse = m_plainTextEdit->cursorForPosition(event->pos()); } const int mousePos = cursorAtMouse.position(); QTextCursor cursor; if (m_textEdit) { cursor = m_textEdit->textCursor(); } else { cursor = m_plainTextEdit->textCursor(); } // Check if the user clicked a selected word const bool selectedWordClicked = cursor.hasSelection() && mousePos >= cursor.selectionStart() && mousePos <= cursor.selectionEnd(); // Get the word under the (mouse-)cursor and see if it is misspelled. // Don't include apostrophes at the start/end of the word in the selection. QTextCursor wordSelectCursor(cursorAtMouse); wordSelectCursor.clearSelection(); wordSelectCursor.select(QTextCursor::WordUnderCursor); QString selectedWord = wordSelectCursor.selectedText(); bool isMouseCursorInsideWord = true; if ((mousePos < wordSelectCursor.selectionStart() || mousePos >= wordSelectCursor.selectionEnd()) && (selectedWord.length() > 1)) { isMouseCursorInsideWord = false; } // Clear the selection again, we re-select it below (without the apostrophes). wordSelectCursor.setPosition(wordSelectCursor.position() - selectedWord.size()); if (selectedWord.startsWith(QLatin1Char('\'')) || selectedWord.startsWith(QLatin1Char('\"'))) { selectedWord = selectedWord.right(selectedWord.size() - 1); wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor); } if (selectedWord.endsWith(QLatin1Char('\'')) || selectedWord.endsWith(QLatin1Char('\"'))) { selectedWord.chop(1); } wordSelectCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selectedWord.size()); const bool wordIsMisspelled = isMouseCursorInsideWord && m_highlighter && m_highlighter->isActive() && !selectedWord.isEmpty() && m_highlighter->isWordMisspelled(selectedWord); // If the user clicked a selected word, do nothing. // If the user clicked somewhere else, move the cursor there. // If the user clicked on a misspelled word, select that word. // Same behavior as in OpenOffice Writer. bool checkBlock = q->isSpellCheckingEnabledForBlock(cursorAtMouse.block().text()); if (!selectedWordClicked) { if (wordIsMisspelled && checkBlock) { if (m_textEdit) { m_textEdit->setTextCursor(wordSelectCursor); } else { m_plainTextEdit->setTextCursor(wordSelectCursor); } } else { if (m_textEdit) { m_textEdit->setTextCursor(cursorAtMouse); } else { m_plainTextEdit->setTextCursor(cursorAtMouse); } } if (m_textEdit) { cursor = m_textEdit->textCursor(); } else { cursor = m_plainTextEdit->textCursor(); } } // Use standard context menu for already selected words, correctly spelled // words and words inside quotes. if (!wordIsMisspelled || selectedWordClicked || !checkBlock) { return false; } execSuggestionMenu(event->globalPos(), selectedWord, cursor); return true; } void SpellCheckDecorator::Private::execSuggestionMenu(const QPoint &pos, const QString &selectedWord, const QTextCursor &_cursor) { QTextCursor cursor = _cursor; QMenu menu; //don't use KMenu here we don't want auto management accelerator //Add the suggestions to the menu const QStringList reps = m_highlighter->suggestionsForWord(selectedWord); if (reps.isEmpty()) { QAction *suggestionsAction = menu.addAction(tr("No suggestions for %1").arg(selectedWord)); suggestionsAction->setEnabled(false); } else { QStringList::const_iterator end(reps.constEnd()); for (QStringList::const_iterator it = reps.constBegin(); it != end; ++it) { menu.addAction(*it); } } menu.addSeparator(); QAction *ignoreAction = menu.addAction(tr("Ignore")); QAction *addToDictAction = menu.addAction(tr("Add to Dictionary")); //Execute the popup inline const QAction *selectedAction = menu.exec(pos); if (selectedAction) { Q_ASSERT(cursor.selectedText() == selectedWord); if (selectedAction == ignoreAction) { m_highlighter->ignoreWord(selectedWord); m_highlighter->rehighlight(); } else if (selectedAction == addToDictAction) { m_highlighter->addWordToDictionary(selectedWord); m_highlighter->rehighlight(); } // Other actions can only be one of the suggested words else { const QString replacement = selectedAction->text(); Q_ASSERT(reps.contains(replacement)); cursor.insertText(replacement); if (m_textEdit) { m_textEdit->setTextCursor(cursor); } else { m_plainTextEdit->setTextCursor(cursor); } } } } void SpellCheckDecorator::Private::createDefaultHighlighter() { if (m_textEdit) { m_highlighter = new Highlighter(m_textEdit); } else { m_highlighter = new Highlighter(m_plainTextEdit); } } SpellCheckDecorator::SpellCheckDecorator(QTextEdit *textEdit) : QObject(textEdit) , d(new Private(this, textEdit)) { } SpellCheckDecorator::SpellCheckDecorator(QPlainTextEdit *textEdit) : QObject(textEdit) , d(new Private(this, textEdit)) { } SpellCheckDecorator::~SpellCheckDecorator() { delete d; } void SpellCheckDecorator::setHighlighter(Highlighter *highlighter) { d->m_highlighter = highlighter; } Highlighter *SpellCheckDecorator::highlighter() const { if (!d->m_highlighter) { d->createDefaultHighlighter(); } return d->m_highlighter; } bool SpellCheckDecorator::eventFilter(QObject * /*obj*/, QEvent *event) { if (event->type() == QEvent::ContextMenu) { return d->onContextMenuEvent(static_cast(event)); } return false; } bool SpellCheckDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const { Q_UNUSED(textBlock); if (d->m_textEdit) { return d->m_textEdit->isEnabled(); } else { return d->m_plainTextEdit->isEnabled(); } } } // namespace