diff options
Diffstat (limited to 'src/plugins/hunspell/module')
-rw-r--r-- | src/plugins/hunspell/module/CMakeLists.txt | 53 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellinputmethod.cpp | 350 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellinputmethod_p.cpp | 347 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellinputmethod_p.h | 62 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellinputmethod_p_p.h | 77 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellwordlist.cpp | 308 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellwordlist_p.h | 86 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellworker.cpp | 450 | ||||
-rw-r--r-- | src/plugins/hunspell/module/hunspellworker_p.h | 213 | ||||
-rw-r--r-- | src/plugins/hunspell/module/qhunspellinputmethod_global.h | 10 |
10 files changed, 1956 insertions, 0 deletions
diff --git a/src/plugins/hunspell/module/CMakeLists.txt b/src/plugins/hunspell/module/CMakeLists.txt new file mode 100644 index 00000000..b17ab545 --- /dev/null +++ b/src/plugins/hunspell/module/CMakeLists.txt @@ -0,0 +1,53 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## QtQuickVirtualKeyboardHunspellPlugin Plugin: +##################################################################### + +qt_internal_add_qml_module(HunspellInputMethod + URI "QtQuick.VirtualKeyboard.Plugins.Hunspell" + VERSION "${PROJECT_VERSION}" + PAST_MAJOR_VERSIONS 2 + PLUGIN_TARGET qtvkbhunspellplugin + NO_PLUGIN_OPTIONAL + DEPENDENCIES + QtQuick.VirtualKeyboard/auto + SOURCES + hunspellinputmethod.cpp hunspellinputmethod_p.cpp hunspellinputmethod_p.h + hunspellinputmethod_p_p.h + hunspellworker.cpp hunspellworker_p.h + hunspellwordlist.cpp hunspellwordlist_p.h + qhunspellinputmethod_global.h + DEFINES + QHUNSPELLINPUTMETHOD_LIBRARY + QT_ASCII_CAST_WARNINGS + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_FROM_BYTEARRAY + QT_NO_CAST_TO_ASCII + LIBRARIES + Qt::Core + Qt::Gui + PUBLIC_LIBRARIES + Qt::VirtualKeyboardPrivate +) + +qt_internal_extend_target(HunspellInputMethod CONDITION QT_FEATURE_system_hunspell + LIBRARIES + Hunspell::Hunspell +) + +qt_internal_extend_target(HunspellInputMethod CONDITION NOT QT_FEATURE_system_hunspell AND QT_FEATURE_3rdparty_hunspell + LIBRARIES + Qt::BundledHunspell +) + +if(QT_FEATURE_3rdparty_hunspell) + qt_copy_or_install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/hunspell/data/" + DESTINATION "${VKB_INSTALL_DATA}/hunspell" + FILES_MATCHING + PATTERN "*.dic" + PATTERN "*.aff" + ) +endif() diff --git a/src/plugins/hunspell/module/hunspellinputmethod.cpp b/src/plugins/hunspell/module/hunspellinputmethod.cpp new file mode 100644 index 00000000..a066c927 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellinputmethod.cpp @@ -0,0 +1,350 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "hunspellinputmethod_p.h" +#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +Q_LOGGING_CATEGORY(lcHunspell, "qt.virtualkeyboard.hunspell") + +/*! + \class QtVirtualKeyboard::HunspellInputMethod + \internal +*/ + +HunspellInputMethod::HunspellInputMethod(HunspellInputMethodPrivate &dd, QObject *parent) : + QVirtualKeyboardAbstractInputMethod(dd, parent) +{ +} + +HunspellInputMethod::HunspellInputMethod(QObject *parent) : + QVirtualKeyboardAbstractInputMethod(*new HunspellInputMethodPrivate(this), parent) +{ +} + +HunspellInputMethod::~HunspellInputMethod() +{ +} + +QList<QVirtualKeyboardInputEngine::InputMode> HunspellInputMethod::inputModes(const QString &locale) +{ + QList<QVirtualKeyboardInputEngine::InputMode> result; + switch (QLocale(locale).script()) { + case QLocale::GreekScript: + result.append(QVirtualKeyboardInputEngine::InputMode::Greek); + break; + case QLocale::CyrillicScript: + result.append(QVirtualKeyboardInputEngine::InputMode::Cyrillic); + break; + case QLocale::ArabicScript: + result.append(QVirtualKeyboardInputEngine::InputMode::Arabic); + break; + case QLocale::HebrewScript: + result.append(QVirtualKeyboardInputEngine::InputMode::Hebrew); + break; + default: + break; + } + result.append(QVirtualKeyboardInputEngine::InputMode::Latin); + result.append(QVirtualKeyboardInputEngine::InputMode::Numeric); + return result; +} + +bool HunspellInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode) +{ + Q_UNUSED(inputMode); + Q_D(HunspellInputMethod); + return d->createHunspell(locale); +} + +bool HunspellInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase) +{ + Q_UNUSED(textCase); + return true; +} + +bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers) +{ + Q_D(HunspellInputMethod); + QVirtualKeyboardInputContext *ic = inputContext(); + Qt::InputMethodHints inputMethodHints = ic->inputMethodHints(); + bool accept = false; + switch (key) { + case Qt::Key_Enter: + case Qt::Key_Return: + case Qt::Key_Tab: + case Qt::Key_Space: + update(); + break; + case Qt::Key_Backspace: + { + QString word = d->wordCandidates.wordAt(0); + if (!word.isEmpty()) { + word.remove(word.size() - 1, 1); + ic->setPreeditText(word); + if (!word.isEmpty()) { + d->wordCandidates.updateWord(0, word); + if (d->updateSuggestions()) { + emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); + emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->wordCandidates.index()); + } + } else { + d->reset(); + } + accept = true; + } + break; + } + default: + if (inputMethodHints.testFlag(Qt::ImhNoPredictiveText)) + break; + if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded) { + update(); + break; + } + if (text.size() > 0) { + QChar c = text.at(0); + QString word = d->wordCandidates.wordAt(0); + bool addToWord = d->isValidInputChar(c) && (!word.isEmpty() || !d->isJoiner(c)); + if (addToWord) { + QString newText = text; + /* Automatic space insertion. */ + if (word.isEmpty()) { + QString surroundingText = ic->surroundingText(); + int cursorPosition = ic->cursorPosition(); + /* Rules for automatic space insertion: + - Surrounding text is not empty + - Cursor is at the end of the line + - No space before the cursor + - No spefic characters before the cursor; minus and apostrophe + */ + if (!surroundingText.isEmpty() && cursorPosition == surroundingText.size()) { + QChar lastChar = surroundingText.at(cursorPosition - 1); + if (!lastChar.isSpace() && + lastChar != QLatin1Char(Qt::Key_Minus) && + d->isAutoSpaceAllowed()) { + // auto-insertion of space might trigger auto-capitalization + bool wasShiftActive = ic->isShiftActive(); + ic->commit(QLatin1String(" ")); + if (ic->isShiftActive() && !wasShiftActive) + newText = newText.toUpper(); + } + } + } + /* Ignore possible call to update() function when sending initial + pre-edit text. The update is triggered if the text editor has + a selection which the pre-edit text will replace. + */ + d->ignoreUpdate = word.isEmpty(); + word.append(newText); + d->wordCandidates.updateWord(0, word); + ic->setPreeditText(word); + d->ignoreUpdate = false; + if (d->updateSuggestions()) { + emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); + emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->wordCandidates.index()); + } + accept = true; + } else if (text.size() > 1) { + bool addSpace = !word.isEmpty() || d->autoSpaceAllowed; + update(); + d->autoSpaceAllowed = true; + if (addSpace && d->isAutoSpaceAllowed()) + ic->commit(QLatin1String(" ")); + ic->commit(text); + d->autoSpaceAllowed = addSpace; + accept = true; + } else { + update(); + inputContext()->sendKeyClick(key, text, modifiers); + d->autoSpaceAllowed = true; + accept = true; + } + } + break; + } + return accept; +} + +QList<QVirtualKeyboardSelectionListModel::Type> HunspellInputMethod::selectionLists() +{ + Q_D(const HunspellInputMethod); + QVirtualKeyboardInputContext *ic = inputContext(); + if (!ic) + return QList<QVirtualKeyboardSelectionListModel::Type>(); + Qt::InputMethodHints inputMethodHints = ic->inputMethodHints(); + if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded || inputMethodHints.testFlag(Qt::ImhNoPredictiveText) || inputMethodHints.testFlag(Qt::ImhHiddenText)) + return QList<QVirtualKeyboardSelectionListModel::Type>(); + return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList; +} + +int HunspellInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type) +{ + Q_UNUSED(type); + Q_D(HunspellInputMethod); + return d->wordCandidates.size(); +} + +QVariant HunspellInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role) +{ + QVariant result; + Q_D(HunspellInputMethod); + switch (role) { + case QVirtualKeyboardSelectionListModel::Role::Display: + result = QVariant(d->wordCandidates.wordAt(index)); + break; + case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength: + { + const QString wordCandidate(d->wordCandidates.wordAt(index)); + const QString word(d->wordCandidates.wordAt(0)); + int wordCompletionLength = wordCandidate.size() - word.size(); + result.setValue((wordCompletionLength > 0 && wordCandidate.startsWith(word)) ? wordCompletionLength : 0); + break; + } + case QVirtualKeyboardSelectionListModel::Role::Dictionary: + { + const QString wordCandidate(d->wordCandidates.wordAt(index)); + QVirtualKeyboardSelectionListModel::DictionaryType dictionaryType = + d->userDictionaryWords && d->userDictionaryWords->contains(wordCandidate) ? + QVirtualKeyboardSelectionListModel::DictionaryType::User : QVirtualKeyboardSelectionListModel::DictionaryType::Default; + result = QVariant(static_cast<int>(dictionaryType)); + break; + } + case QVirtualKeyboardSelectionListModel::Role::CanRemoveSuggestion: + result.setValue(index > 0 && d->wordCandidates.wordFlagsAt(index).testFlag(HunspellWordList::SpellCheckOk)); + break; + default: + result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role); + break; + } + return result; +} + +void HunspellInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index) +{ + Q_UNUSED(type); + Q_D(HunspellInputMethod); + d->wordCandidates.setIndex(index); + d->addToDictionary(); + QString finalWord = d->wordCandidates.wordAt(index); + reset(); + inputContext()->commit(finalWord); + d->autoSpaceAllowed = true; +} + +bool HunspellInputMethod::selectionListRemoveItem(QVirtualKeyboardSelectionListModel::Type type, int index) +{ + Q_D(HunspellInputMethod); + Q_UNUSED(type); + + if (index <= 0 || index >= d->wordCandidates.size()) + return false; + + QString word = d->wordCandidates.wordAt(index); + d->removeFromDictionary(word); + + return true; +} + +bool HunspellInputMethod::reselect(int cursorPosition, const QVirtualKeyboardInputEngine::ReselectFlags &reselectFlags) +{ + Q_D(HunspellInputMethod); + QString word(d->wordCandidates.wordAt(0)); + Q_ASSERT(word.isEmpty()); + + if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded) + return false; + + QVirtualKeyboardInputContext *ic = inputContext(); + if (!ic) + return false; + + const QString surroundingText = ic->surroundingText(); + int replaceFrom = 0; + + if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordBeforeCursor)) { + for (int i = cursorPosition - 1; i >= 0; --i) { + QChar c = surroundingText.at(i); + if (!d->isValidInputChar(c)) + break; + word.insert(0, c); + --replaceFrom; + } + + while (replaceFrom < 0 && d->isJoiner(word.at(0))) { + word.remove(0, 1); + ++replaceFrom; + } + } + + if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == 0) + return false; + + if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAfterCursor)) { + for (int i = cursorPosition; i < surroundingText.size(); ++i) { + QChar c = surroundingText.at(i); + if (!d->isValidInputChar(c)) + break; + word.append(c); + } + + while (replaceFrom > -word.size()) { + int lastPos = word.size() - 1; + if (!d->isJoiner(word.at(lastPos))) + break; + word.remove(lastPos, 1); + } + } + + if (word.isEmpty()) + return false; + + if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == -word.size()) + return false; + + if (d->isJoiner(word.at(0))) + return false; + + if (d->isJoiner(word.at(word.size() - 1))) + return false; + + d->wordCandidates.updateWord(0, word); + ic->setPreeditText(word, QList<QInputMethodEvent::Attribute>(), replaceFrom, word.size()); + + d->autoSpaceAllowed = false; + if (d->updateSuggestions()) { + emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); + emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->wordCandidates.index()); + } + + return true; +} + +void HunspellInputMethod::reset() +{ + Q_D(HunspellInputMethod); + d->reset(); +} + +void HunspellInputMethod::update() +{ + Q_D(HunspellInputMethod); + if (d->ignoreUpdate) + return; + + QString finalWord; + if (!d->wordCandidates.isEmpty()) { + d->addToDictionary(); + finalWord = d->wordCandidates.wordAt(d->wordCandidates.index()); + } + d->reset(); + if (!finalWord.isEmpty()) + inputContext()->commit(finalWord); + d->autoSpaceAllowed = false; +} + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE diff --git a/src/plugins/hunspell/module/hunspellinputmethod_p.cpp b/src/plugins/hunspell/module/hunspellinputmethod_p.cpp new file mode 100644 index 00000000..393bbf88 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellinputmethod_p.cpp @@ -0,0 +1,347 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "hunspellinputmethod_p_p.h" +#include "hunspellinputmethod_p.h" +#include "hunspellworker_p.h" +#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> +#include <QStringList> +#include <QDir> +#include <QtCore/QLibraryInfo> +#include <QStandardPaths> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +const int HunspellInputMethodPrivate::userDictionaryMaxSize = 100; + +/*! + \class QtVirtualKeyboard::HunspellInputMethodPrivate + \internal +*/ + +HunspellInputMethodPrivate::HunspellInputMethodPrivate(HunspellInputMethod *q_ptr) : + q_ptr(q_ptr), + hunspellWorker(new HunspellWorker()), + locale(), + wordCompletionPoint(2), + ignoreUpdate(false), + autoSpaceAllowed(false), + dictionaryState(DictionaryNotLoaded), + userDictionaryWords(new HunspellWordList(userDictionaryMaxSize)), + blacklistedWords(new HunspellWordList(userDictionaryMaxSize)), + wordCandidatesUpdateTag(0) +{ + if (hunspellWorker) + hunspellWorker->start(); +} + +HunspellInputMethodPrivate::~HunspellInputMethodPrivate() +{ +} + +bool HunspellInputMethodPrivate::createHunspell(const QString &locale) +{ + Q_Q(HunspellInputMethod); + if (!hunspellWorker) + return false; + if (this->locale != locale) { + clearSuggestionsRelatedTasks(); + hunspellWorker->waitForAllTasks(); + QString hunspellDataPath(qEnvironmentVariable("QT_VIRTUALKEYBOARD_HUNSPELL_DATA_PATH")); + const QString pathListSep( +#if defined(Q_OS_WIN32) + QStringLiteral(";") +#else + QStringLiteral(":") +#endif + ); + QStringList searchPaths(hunspellDataPath.split(pathListSep, Qt::SkipEmptyParts)); + const QStringList defaultPaths = QStringList() + << QDir(QLibraryInfo::path(QLibraryInfo::DataPath) + QStringLiteral("/qtvirtualkeyboard/hunspell")).absolutePath() +#if !defined(Q_OS_WIN32) + << QStringLiteral("/usr/share/hunspell") + << QStringLiteral("/usr/share/myspell/dicts") +#endif + ; + for (const QString &defaultPath : defaultPaths) { + if (!searchPaths.contains(defaultPath)) + searchPaths.append(defaultPath); + } + QSharedPointer<HunspellLoadDictionaryTask> loadDictionaryTask(new HunspellLoadDictionaryTask(locale, searchPaths)); + QObjectPrivate::connect(loadDictionaryTask.data(), &HunspellLoadDictionaryTask::completed, this, &HunspellInputMethodPrivate::dictionaryLoadCompleted); + dictionaryState = HunspellInputMethodPrivate::DictionaryLoading; + emit q->selectionListsChanged(); + hunspellWorker->addTask(loadDictionaryTask); + this->locale = locale; + + loadCustomDictionary(userDictionaryWords, QLatin1String("userdictionary")); + addToHunspell(userDictionaryWords); + loadCustomDictionary(blacklistedWords, QLatin1String("blacklist")); + removeFromHunspell(blacklistedWords); + } + return true; +} + +void HunspellInputMethodPrivate::reset() +{ + if (clearSuggestions(true)) { + Q_Q(HunspellInputMethod); + emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); + emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, wordCandidates.index()); + } + autoSpaceAllowed = false; +} + +bool HunspellInputMethodPrivate::updateSuggestions() +{ + bool wordCandidateListChanged = false; + QString word = wordCandidates.wordAt(0); + if (!word.isEmpty() && dictionaryState != HunspellInputMethodPrivate::DictionaryNotLoaded) { + wordCandidateListChanged = true; + if (word.size() >= wordCompletionPoint) { + if (hunspellWorker) { + QSharedPointer<HunspellWordList> wordList(new HunspellWordList(wordCandidates)); + + // Clear obsolete tasks from the worker queue + clearSuggestionsRelatedTasks(); + + // Build suggestions + QSharedPointer<HunspellBuildSuggestionsTask> buildSuggestionsTask(new HunspellBuildSuggestionsTask()); + buildSuggestionsTask->wordList = wordList; + buildSuggestionsTask->autoCorrect = false; + hunspellWorker->addTask(buildSuggestionsTask); + + // Filter out blacklisted word (sometimes Hunspell suggests, + // e.g. with different text case) + QSharedPointer<HunspellFilterWordTask> filterWordTask(new HunspellFilterWordTask()); + filterWordTask->wordList = wordList; + filterWordTask->filterList = blacklistedWords; + hunspellWorker->addTask(filterWordTask); + + // Boost words from user dictionary + QSharedPointer<HunspellBoostWordTask> boostWordTask(new HunspellBoostWordTask()); + boostWordTask->wordList = wordList; + boostWordTask->boostList = userDictionaryWords; + hunspellWorker->addTask(boostWordTask); + + // Update word candidate list + QSharedPointer<HunspellUpdateSuggestionsTask> updateSuggestionsTask(new HunspellUpdateSuggestionsTask()); + updateSuggestionsTask->wordList = wordList; + updateSuggestionsTask->tag = ++wordCandidatesUpdateTag; + QObjectPrivate::connect(updateSuggestionsTask.data(), &HunspellUpdateSuggestionsTask::updateSuggestions, this, &HunspellInputMethodPrivate::updateSuggestionsCompleted); + hunspellWorker->addTask(updateSuggestionsTask); + } + } + } else { + wordCandidateListChanged = clearSuggestions(); + } + return wordCandidateListChanged; +} + +bool HunspellInputMethodPrivate::clearSuggestions(bool clearInputWord) +{ + clearSuggestionsRelatedTasks(); + return clearInputWord ? wordCandidates.clear() : wordCandidates.clearSuggestions(); +} + +void HunspellInputMethodPrivate::clearSuggestionsRelatedTasks() +{ + if (hunspellWorker) { + hunspellWorker->removeAllTasksOfType<HunspellBuildSuggestionsTask>(); + hunspellWorker->removeAllTasksOfType<HunspellFilterWordTask>(); + hunspellWorker->removeAllTasksOfType<HunspellBoostWordTask>(); + hunspellWorker->removeAllTasksOfType<HunspellUpdateSuggestionsTask>(); + } +} + +bool HunspellInputMethodPrivate::isAutoSpaceAllowed() const +{ + Q_Q(const HunspellInputMethod); + if (!autoSpaceAllowed) + return false; + if (q->inputEngine()->inputMode() == QVirtualKeyboardInputEngine::InputMode::Numeric) + return false; + QVirtualKeyboardInputContext *ic = q->inputContext(); + if (!ic) + return false; + Qt::InputMethodHints inputMethodHints = ic->inputMethodHints(); + return !inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) && + !inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly); +} + +bool HunspellInputMethodPrivate::isValidInputChar(const QChar &c) const +{ + if (c.isLetterOrNumber()) + return true; + if (isJoiner(c)) + return true; + if (c.isMark()) + return true; + return false; +} + +bool HunspellInputMethodPrivate::isJoiner(const QChar &c) const +{ + if (c.isPunct() || c.isSymbol()) { + Q_Q(const HunspellInputMethod); + QVirtualKeyboardInputContext *ic = q->inputContext(); + if (ic) { + Qt::InputMethodHints inputMethodHints = ic->inputMethodHints(); + if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) || inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly)) + return QString(QStringLiteral(":/?#[]@!$&'()*+,;=-_.%")).contains(c); + } + ushort unicode = c.unicode(); + if (unicode == Qt::Key_Apostrophe || unicode == Qt::Key_Minus) + return true; + } + return false; +} + +QString HunspellInputMethodPrivate::customDictionaryLocation(const QString &dictionaryType) const +{ + if (dictionaryType.isEmpty() || locale.isEmpty()) + return QString(); + + QString location = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + if (location.isEmpty()) + return QString(); + + return QStringLiteral("%1/qtvirtualkeyboard/hunspell/%2-%3.txt") + .arg(location) + .arg(dictionaryType) + .arg(locale); +} + +void HunspellInputMethodPrivate::loadCustomDictionary(const QSharedPointer<HunspellWordList> &wordList, + const QString &dictionaryType) const +{ + QSharedPointer<HunspellLoadWordListTask> loadWordsTask(new HunspellLoadWordListTask()); + loadWordsTask->filePath = customDictionaryLocation(dictionaryType); + loadWordsTask->wordList = wordList; + hunspellWorker->addTask(loadWordsTask); +} + +void HunspellInputMethodPrivate::saveCustomDictionary(const QSharedPointer<HunspellWordList> &wordList, + const QString &dictionaryType) const +{ + QSharedPointer<HunspellSaveWordListTask> saveWordsTask(new HunspellSaveWordListTask()); + saveWordsTask->filePath = customDictionaryLocation(dictionaryType); + saveWordsTask->wordList = wordList; + hunspellWorker->addTask(saveWordsTask); +} + +void HunspellInputMethodPrivate::addToHunspell(const QSharedPointer<HunspellWordList> &wordList) const +{ + QSharedPointer<HunspellAddWordTask> addWordTask(new HunspellAddWordTask()); + addWordTask->wordList = wordList; + hunspellWorker->addTask(addWordTask); +} + +void HunspellInputMethodPrivate::removeFromHunspell(const QSharedPointer<HunspellWordList> &wordList) const +{ + QSharedPointer<HunspellRemoveWordTask> removeWordTask(new HunspellRemoveWordTask()); + removeWordTask->wordList = wordList; + hunspellWorker->addTask(removeWordTask); +} + +void HunspellInputMethodPrivate::removeFromDictionary(const QString &word) +{ + if (userDictionaryWords->removeWord(word) > 0) { + saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary")); + } else if (!blacklistedWords->contains(word)) { + blacklistedWords->appendWord(word); + saveCustomDictionary(blacklistedWords, QLatin1String("blacklist")); + } + + QSharedPointer<HunspellWordList> wordList(new HunspellWordList()); + wordList->appendWord(word); + removeFromHunspell(wordList); + + updateSuggestions(); +} + +void HunspellInputMethodPrivate::addToDictionary() +{ + Q_Q(HunspellInputMethod); + // This feature is not allowed when dealing with sensitive information + const Qt::InputMethodHints inputMethodHints(q->inputContext()->inputMethodHints()); + const bool userDictionaryEnabled = + !inputMethodHints.testFlag(Qt::ImhHiddenText) && + !inputMethodHints.testFlag(Qt::ImhSensitiveData); + if (!userDictionaryEnabled) + return; + + if (wordCandidates.isEmpty()) + return; + + QString word; + HunspellWordList::Flags wordFlags; + const int activeWordIndex = wordCandidates.index(); + wordCandidates.wordAt(activeWordIndex, word, wordFlags); + if (activeWordIndex == 0) { + if (blacklistedWords->removeWord(word) > 0) { + saveCustomDictionary(blacklistedWords, QLatin1String("blacklist")); + } else if (word.size() > 1 && !wordFlags.testFlag(HunspellWordList::SpellCheckOk) && !userDictionaryWords->contains(word)) { + userDictionaryWords->appendWord(word); + saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary")); + } else { + // Avoid adding words to Hunspell which are too short or passed spell check + return; + } + + QSharedPointer<HunspellWordList> wordList(new HunspellWordList()); + wordList->appendWord(word); + addToHunspell(wordList); + } else { + // Check if found in the user dictionary and move as last in the list. + // This way the list is always ordered by use. + // If userDictionaryMaxSize is greater than zero the number of words in the + // list will be limited to that amount. By pushing last used items to end of + // list we can avoid (to certain extent) removing frequently used words. + int userDictionaryIndex = userDictionaryWords->indexOfWord(word); + if (userDictionaryIndex != -1) { + userDictionaryWords->moveWord(userDictionaryIndex, userDictionaryWords->size() - 1); + saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary")); + } + } +} + +void HunspellInputMethodPrivate::updateSuggestionsCompleted(const QSharedPointer<HunspellWordList> &wordList, int tag) +{ + if (dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded) { + qCDebug(lcHunspell) << "updateSuggestions: skip (dictionary not loaded)"; + Q_Q(HunspellInputMethod); + q->update(); + return; + } + if (wordCandidatesUpdateTag != tag) { + qCDebug(lcHunspell) << "updateSuggestions: skip tag" << tag << "current" << wordCandidatesUpdateTag; + return; + } + QString word(wordCandidates.wordAt(0)); + wordCandidates = *wordList; + if (wordCandidates.wordAt(0).compare(word) != 0) + wordCandidates.updateWord(0, word); + Q_Q(HunspellInputMethod); + emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); + emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, wordCandidates.index()); +} + +void HunspellInputMethodPrivate::dictionaryLoadCompleted(bool success) +{ + Q_Q(HunspellInputMethod); + QVirtualKeyboardInputContext *ic = q->inputContext(); + if (!ic) + return; + + QList<QVirtualKeyboardSelectionListModel::Type> oldSelectionLists = q->selectionLists(); + dictionaryState = success ? HunspellInputMethodPrivate::DictionaryReady : + HunspellInputMethodPrivate::DictionaryNotLoaded; + QList<QVirtualKeyboardSelectionListModel::Type> newSelectionLists = q->selectionLists(); + if (oldSelectionLists != newSelectionLists) + emit q->selectionListsChanged(); +} + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE diff --git a/src/plugins/hunspell/module/hunspellinputmethod_p.h b/src/plugins/hunspell/module/hunspellinputmethod_p.h new file mode 100644 index 00000000..fb90812a --- /dev/null +++ b/src/plugins/hunspell/module/hunspellinputmethod_p.h @@ -0,0 +1,62 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef HUNSPELLINPUTMETHOD_P_H +#define HUNSPELLINPUTMETHOD_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtVirtualKeyboard/qvirtualkeyboardabstractinputmethod.h> +#include <QtHunspellInputMethod/qhunspellinputmethod_global.h> +#include <QtHunspellInputMethod/private/hunspellinputmethod_p_p.h> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +class HunspellWordList; + +class Q_HUNSPELLINPUTMETHOD_EXPORT HunspellInputMethod : public QVirtualKeyboardAbstractInputMethod +{ + Q_OBJECT + Q_DECLARE_PRIVATE(HunspellInputMethod) + QML_NAMED_ELEMENT(DefaultInputMethod) + QML_ADDED_IN_VERSION(2, 0) + +protected: + HunspellInputMethod(HunspellInputMethodPrivate &dd, QObject *parent); +public: + explicit HunspellInputMethod(QObject *parent = nullptr); + ~HunspellInputMethod(); + + QList<QVirtualKeyboardInputEngine::InputMode> inputModes(const QString &locale) override; + bool setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode) override; + bool setTextCase(QVirtualKeyboardInputEngine::TextCase textCase) override; + + bool keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers) override; + + QList<QVirtualKeyboardSelectionListModel::Type> selectionLists() override; + int selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type) override; + QVariant selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, + QVirtualKeyboardSelectionListModel::Role role) override; + void selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index) override; + bool selectionListRemoveItem(QVirtualKeyboardSelectionListModel::Type type, int index) override; + + bool reselect(int cursorPosition, const QVirtualKeyboardInputEngine::ReselectFlags &reselectFlags) override; + + void reset() override; + void update() override; +}; + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE + +#endif // HUNSPELLINPUTMETHOD_P_H diff --git a/src/plugins/hunspell/module/hunspellinputmethod_p_p.h b/src/plugins/hunspell/module/hunspellinputmethod_p_p.h new file mode 100644 index 00000000..bac0a8ed --- /dev/null +++ b/src/plugins/hunspell/module/hunspellinputmethod_p_p.h @@ -0,0 +1,77 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef HUNSPELLINPUTMETHOD_P_P_H +#define HUNSPELLINPUTMETHOD_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtHunspellInputMethod/qhunspellinputmethod_global.h> +#include <QtHunspellInputMethod/private/hunspellwordlist_p.h> +#include <QtVirtualKeyboard/private/qvirtualkeyboardabstractinputmethod_p.h> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +class HunspellInputMethod; +class HunspellWorker; + +class Q_HUNSPELLINPUTMETHOD_EXPORT HunspellInputMethodPrivate : public QVirtualKeyboardAbstractInputMethodPrivate +{ +public: + Q_DECLARE_PUBLIC(HunspellInputMethod) + + HunspellInputMethodPrivate(HunspellInputMethod *q_ptr); + ~HunspellInputMethodPrivate(); + + enum DictionaryState { + DictionaryNotLoaded, + DictionaryLoading, + DictionaryReady + }; + + bool createHunspell(const QString &locale); + void reset(); + bool updateSuggestions(); + bool clearSuggestions(bool clearInputWord = false); + void clearSuggestionsRelatedTasks(); + bool isAutoSpaceAllowed() const; + bool isValidInputChar(const QChar &c) const; + bool isJoiner(const QChar &c) const; + QString customDictionaryLocation(const QString &dictionaryType) const; + void loadCustomDictionary(const QSharedPointer<HunspellWordList> &wordList, const QString &dictionaryType) const; + void saveCustomDictionary(const QSharedPointer<HunspellWordList> &wordList, const QString &dictionaryType) const; + void addToHunspell(const QSharedPointer<HunspellWordList> &wordList) const; + void removeFromHunspell(const QSharedPointer<HunspellWordList> &wordList) const; + void removeFromDictionary(const QString &word); + void addToDictionary(); + void updateSuggestionsCompleted(const QSharedPointer<HunspellWordList> &wordList, int tag); + void dictionaryLoadCompleted(bool success); + + HunspellInputMethod *q_ptr; + QScopedPointer<HunspellWorker> hunspellWorker; + QString locale; + HunspellWordList wordCandidates; + int wordCompletionPoint; + bool ignoreUpdate; + bool autoSpaceAllowed; + DictionaryState dictionaryState; + QSharedPointer<HunspellWordList> userDictionaryWords; + QSharedPointer<HunspellWordList> blacklistedWords; + int wordCandidatesUpdateTag; + static const int userDictionaryMaxSize; +}; + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE + +#endif // HUNSPELLINPUTMETHOD_P_P_H diff --git a/src/plugins/hunspell/module/hunspellwordlist.cpp b/src/plugins/hunspell/module/hunspellwordlist.cpp new file mode 100644 index 00000000..6c4a8df0 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellwordlist.cpp @@ -0,0 +1,308 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "hunspellwordlist_p.h" +#include <QtAlgorithms> +#include <hunspell/hunspell.h> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +/*! + \class QtVirtualKeyboard::HunspellWordList + \internal +*/ + +HunspellWordList::HunspellWordList(int limit) : + _index(0), + _limit(limit) +{ +} + +HunspellWordList::HunspellWordList(HunspellWordList &other) +{ + *this = other; +} + +HunspellWordList &HunspellWordList::operator=(HunspellWordList &other) +{ + if (this != &other) { + QMutexLocker guard(&_lock); + QMutexLocker otherGuard(&other._lock); + _list = other._list; + _flags = other._flags; + _index = other._index; + _limit = other._limit; + _searchIndex = other._searchIndex; + } + return *this; +} + +int HunspellWordList::index() const +{ + return _index < _list.size() ? _index : -1; +} + +void HunspellWordList::setIndex(int index) +{ + QMutexLocker guard(&_lock); + _index = index; +} + +bool HunspellWordList::clear() +{ + QMutexLocker guard(&_lock); + bool result = !_list.isEmpty(); + _list.clear(); + _flags.clear(); + _index = 0; + _searchIndex.clear(); + return result; +} + +bool HunspellWordList::clearSuggestions() +{ + QMutexLocker guard(&_lock); + if (_list.isEmpty()) + return false; + + _searchIndex.clear(); + if (_list.size() > 1) { + QString word = _list.at(0); + Flags flags = _flags.at(0); + _list.clear(); + _flags.clear(); + if (!word.isEmpty()) { + _index = 0; + _list.append(word); + _flags.append(flags); + } + return true; + } else if (_list.at(0).isEmpty()) { + _list.clear(); + _flags.clear(); + _index = 0; + return true; + } + return false; +} + +bool HunspellWordList::hasSuggestions() const +{ + return _list.size() > 1; +} + +int HunspellWordList::size() const +{ + return _list.size(); +} + +int HunspellWordList::isEmpty() const +{ + return _list.isEmpty() || _list.at(0).isEmpty(); +} + +bool HunspellWordList::contains(const QString &word) +{ + QMutexLocker guard(&_lock); + + // Use index search when the search index is available. + // This provides a lot faster search than QList::contains(). + // Search index is available when it has been rebuilt using + // rebuildSearchIndex() method. Search index is automatically + // cleared when the word list is modified. + if (!_searchIndex.isEmpty()) { + Q_ASSERT(_searchIndex.size() == _list.size()); + + SearchContext searchContext(word, _list); + return std::binary_search(_searchIndex.begin(), _searchIndex.end(), -1, [searchContext](const int &a, const int &b) { + const QString &wordA = (a == -1) ? searchContext.word : searchContext.list[a]; + const QString &wordB = (b == -1) ? searchContext.word : searchContext.list[b]; + return wordA.compare(wordB, Qt::CaseInsensitive) < 0; + }); + } + + return _list.contains(word, Qt::CaseInsensitive); +} + +QString HunspellWordList::findWordCompletion(const QString &word) +{ + QMutexLocker guard(&_lock); + + if (!_searchIndex.isEmpty()) { + Q_ASSERT(_searchIndex.size() == _list.size()); + + SearchContext searchContext(word, _list); + auto match = std::lower_bound(_searchIndex.begin(), _searchIndex.end(), -1, [searchContext](const int &a, const int &b) { + const QString &wordA = (a == -1) ? searchContext.word : searchContext.list[a]; + const QString &wordB = (b == -1) ? searchContext.word : searchContext.list[b]; + return wordA.compare(wordB, Qt::CaseInsensitive) < 0; + }); + + if (match == _searchIndex.end()) + return QString(); + + if (!word.compare(_list[*match], Qt::CaseInsensitive)) { + match++; + if (match == _searchIndex.end()) + return QString(); + } + + return _list[*match].startsWith(word, Qt::CaseInsensitive) ? _list[*match] : QString(); + } + + QString bestMatch; + for (int i = 0, count = _list.size(); i < count; ++i) { + const QString &wordB(_list[i]); + if (wordB.size() > bestMatch.size() && + word.size() < wordB.size() && + wordB.startsWith(word, Qt::CaseInsensitive)) + bestMatch = wordB; + } + + return bestMatch; +} + +int HunspellWordList::indexOfWord(const QString &word) +{ + QMutexLocker guard(&_lock); + + if (!_searchIndex.isEmpty()) { + Q_ASSERT(_searchIndex.size() == _list.size()); + + SearchContext searchContext(word, _list); + auto match = std::lower_bound(_searchIndex.begin(), _searchIndex.end(), -1, [searchContext](int a, int b) { + const QString &wordA = (a == -1) ? searchContext.word : searchContext.list[a]; + const QString &wordB = (b == -1) ? searchContext.word : searchContext.list[b]; + return wordA.compare(wordB, Qt::CaseInsensitive) < 0; + }); + return (match != _searchIndex.end()) ? *match : -1; + } + + return _list.indexOf(word); +} + +QString HunspellWordList::wordAt(int index) +{ + QMutexLocker guard(&_lock); + + return index >= 0 && index < _list.size() ? _list.at(index) : QString(); +} + +void HunspellWordList::wordAt(int index, QString &word, Flags &flags) +{ + QMutexLocker guard(&_lock); + Q_ASSERT(index >= 0 && index < _list.size()); + + word = _list.at(index); + flags = _flags.at(index); +} + +const HunspellWordList::Flags &HunspellWordList::wordFlagsAt(int index) +{ + QMutexLocker guard(&_lock); + + return _flags[index]; +} + +void HunspellWordList::appendWord(const QString &word, const Flags &flags) +{ + QMutexLocker guard(&_lock); + + _searchIndex.clear(); + if (_limit > 0) { + while (_list.size() >= _limit) { + _list.removeAt(0); + _flags.removeAt(0); + } + } + _list.append(word); + _flags.append(flags); +} + +void HunspellWordList::insertWord(int index, const QString &word, const Flags &flags) +{ + QMutexLocker guard(&_lock); + Q_ASSERT(_limit == 0); + + _searchIndex.clear(); + _list.insert(index, word); + _flags.insert(index, flags); +} + +void HunspellWordList::updateWord(int index, const QString &word, const Flags &flags) +{ + Q_ASSERT(index >= 0); + QMutexLocker guard(&_lock); + + if (index < _list.size()) { + if (word != _list[index]) + _searchIndex.clear(); + _list[index] = word; + _flags[index] = flags; + } else { + _searchIndex.clear(); + _list.append(word); + _flags.append(flags); + } +} + +void HunspellWordList::moveWord(int from, int to) +{ + QMutexLocker guard(&_lock); + + if (from < 0 || from >= _list.size()) + return; + if (to < 0 || to >= _list.size()) + return; + if (from == to) + return; + + _searchIndex.clear(); + _list.move(from, to); + _flags.move(from, to); +} + +int HunspellWordList::removeWord(const QString &word) +{ + QMutexLocker guard(&_lock); + int removeCount = 0; + for (int i = 0, count = _list.size(); i < count;) { + if (!_list[i].compare(word, Qt::CaseInsensitive)) { + _list.removeAt(i); + _flags.removeAt(i); + --count; + ++removeCount; + } else { + ++i; + } + } + if (removeCount > 0) + _searchIndex.clear(); + return removeCount; +} + +void HunspellWordList::removeWordAt(int index) +{ + QMutexLocker guard(&_lock); + + _list.removeAt(index); +} + +void HunspellWordList::rebuildSearchIndex() +{ + QMutexLocker guard(&_lock); + _searchIndex.clear(); + + if (_list.isEmpty()) + return; + + _searchIndex.resize(_list.size()); + std::iota(_searchIndex.begin(), _searchIndex.end(), 0); + + const QStringList list(_list); + std::sort(_searchIndex.begin(), _searchIndex.end(), [list](int a, int b) { return list[a].compare(list[b], Qt::CaseInsensitive) < 0; }); +} + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE diff --git a/src/plugins/hunspell/module/hunspellwordlist_p.h b/src/plugins/hunspell/module/hunspellwordlist_p.h new file mode 100644 index 00000000..c795dd27 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellwordlist_p.h @@ -0,0 +1,86 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef HUNSPELLWORDLIST_P_H +#define HUNSPELLWORDLIST_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QMutex> +#include <QStringList> +#include <QtHunspellInputMethod/qhunspellinputmethod_global.h> + +QT_BEGIN_NAMESPACE + +namespace QtVirtualKeyboard { + +class Q_HUNSPELLINPUTMETHOD_EXPORT HunspellWordList +{ +public: + enum Flag + { + SpellCheckOk = 0x1, + CompoundWord = 0x2 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + HunspellWordList(int limit = 0); + HunspellWordList(HunspellWordList &other); + + HunspellWordList &operator=(HunspellWordList &other); + + int index() const; + void setIndex(int index); + bool clear(); + bool clearSuggestions(); + bool hasSuggestions() const; + int size() const; + int isEmpty() const; + bool contains(const QString &word); + QString findWordCompletion(const QString &word); + int indexOfWord(const QString &word); + QString wordAt(int index); + void wordAt(int index, QString &word, Flags &flags); + const Flags &wordFlagsAt(int index); + void appendWord(const QString &word, const Flags &flags = Flags()); + void insertWord(int index, const QString &word, const Flags &flags = Flags()); + void updateWord(int index, const QString &word, const Flags &flags = Flags()); + void moveWord(int from, int to); + int removeWord(const QString &word); + void removeWordAt(int index); + void rebuildSearchIndex(); + +private: + class SearchContext { + public: + SearchContext(const QString &word, + const QStringList &list) : + word(word), + list(list) + {} + const QString &word; + const QStringList &list; + }; + +private: + QMutex _lock; + QStringList _list; + QList<Flags> _flags; + QList<int> _searchIndex; + int _index; + int _limit; +}; + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE + +#endif // HUNSPELLWORDLIST_P_H diff --git a/src/plugins/hunspell/module/hunspellworker.cpp b/src/plugins/hunspell/module/hunspellworker.cpp new file mode 100644 index 00000000..85a94888 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellworker.cpp @@ -0,0 +1,450 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtHunspellInputMethod/private/hunspellworker_p.h> +#include <QList> +#include <QFileInfo> +#include <QRegularExpression> +#include <QElapsedTimer> +#include <QFile> +#include <QDir> +#include <QtAlgorithms> + +QT_BEGIN_NAMESPACE +namespace QtVirtualKeyboard { + +/*! + \class QtVirtualKeyboard::HunspellTask + \internal +*/ + +/*! + \class QtVirtualKeyboard::HunspellLoadDictionaryTask + \internal +*/ + +HunspellLoadDictionaryTask::HunspellLoadDictionaryTask(const QString &locale, const QStringList &searchPaths) : + HunspellTask(), + hunspellPtr(nullptr), + locale(locale), + searchPaths(searchPaths) +{ +} + +void HunspellLoadDictionaryTask::run() +{ + Q_ASSERT(hunspellPtr != nullptr); + + qCDebug(lcHunspell) << "HunspellLoadDictionaryTask::run(): locale:" << locale; + + if (*hunspellPtr) { + Hunspell_destroy(*hunspellPtr); + *hunspellPtr = nullptr; + } + + QString affPath; + QString dicPath; + for (const QString &searchPath : searchPaths) { + affPath = QStringLiteral("%1/%2.aff").arg(searchPath, locale); + if (QFileInfo::exists(affPath)) { + dicPath = QStringLiteral("%1/%2.dic").arg(searchPath, locale); + if (QFileInfo::exists(dicPath)) + break; + dicPath.clear(); + } + affPath.clear(); + } + + if (!affPath.isEmpty() && !dicPath.isEmpty()) { + *hunspellPtr = Hunspell_create(affPath.toUtf8().constData(), dicPath.toUtf8().constData()); + if (*hunspellPtr) { + /* Make sure the encoding used by the dictionary is supported + by the QStringConverter. + */ + if (!QStringConverter::encodingForName(Hunspell_get_dic_encoding(*hunspellPtr))) { + qCWarning(lcHunspell) << "The Hunspell dictionary" << dicPath << "cannot be used because it uses an unknown text codec" << QLatin1String(Hunspell_get_dic_encoding(*hunspellPtr)); + Hunspell_destroy(*hunspellPtr); + *hunspellPtr = nullptr; + } + } + } else { + qCWarning(lcHunspell).nospace() << "Hunspell dictionary is missing for " << locale << ". Search paths " << searchPaths; + } + + emit completed(*hunspellPtr != nullptr); +} + +/*! + \class QtVirtualKeyboard::HunspellBuildSuggestionsTask + \internal +*/ + +void HunspellBuildSuggestionsTask::run() +{ + if (wordList->isEmpty()) + return; + + wordList->clearSuggestions(); + QString word = wordList->wordAt(0); + + /* Select text codec based on the dictionary encoding. + Hunspell_get_dic_encoding() should always return at least + "ISO8859-1", but you can never be too sure. + */ + textDecoder = QStringDecoder(Hunspell_get_dic_encoding(hunspell)); + textEncoder = QStringEncoder(Hunspell_get_dic_encoding(hunspell)); + if (!textDecoder.isValid() || !textEncoder.isValid()) + return; + + char **slst = nullptr; + int n = Hunspell_suggest(hunspell, &slst, QByteArray { textEncoder(word) }.constData()); + if (n > 0) { + /* Collect word candidates from the Hunspell suggestions. + Insert word completions in the beginning of the list. + */ + const int firstWordCompletionIndex = wordList->size(); + int lastWordCompletionIndex = firstWordCompletionIndex; + bool suggestCapitalization = false; + for (int i = 0; i < n; i++) { + QString wordCandidate(textDecoder(slst[i])); + wordCandidate.replace(QChar(0x2019), QLatin1Char('\'')); + QString normalizedWordCandidate = removeAccentsAndDiacritics(wordCandidate); + /* Prioritize word Capitalization */ + if (!wordCandidate.compare(word, Qt::CaseInsensitive)) { + if (suggestCapitalization) { + bool wordCandidateIsCapital = wordCandidate.at(0).isUpper(); + bool wordIsCapital = word.at(0).isUpper(); + if (wordCandidateIsCapital == wordIsCapital) { + if (wordCandidateIsCapital) + wordCandidate = wordCandidate.toLower(); + else + wordCandidate[0] = wordCandidate.at(0).toUpper(); + } + wordList->insertWord(1, wordCandidate); + lastWordCompletionIndex++; + suggestCapitalization = true; + } + /* Prioritize word completions, missing punctuation or missing accents */ + } else if ((normalizedWordCandidate.size() > word.size() && + normalizedWordCandidate.startsWith(word)) || + wordCandidate.contains(QLatin1Char('\''))) { + wordList->insertWord(lastWordCompletionIndex++, wordCandidate); + } else { + wordList->appendWord(wordCandidate); + } + } + /* Prioritize words with missing spaces next to word completions. + */ + for (int i = lastWordCompletionIndex; i < wordList->size(); i++) { + QString wordCandidate(wordList->wordAt(i)); + if (wordCandidate.contains(QLatin1String(" "))) { + wordList->updateWord(i, wordCandidate, wordList->wordFlagsAt(i) | HunspellWordList::CompoundWord); + if (i != lastWordCompletionIndex) { + wordList->moveWord(i, lastWordCompletionIndex); + } + lastWordCompletionIndex++; + } + } + /* Do spell checking and suggest the first candidate, if: + - the word matches partly the suggested word; or + - the quality of the suggested word is good enough. + + The quality is measured here using the Levenshtein Distance, + which may be suboptimal for the purpose, but gives some clue + how much the suggested word differs from the given word. + */ + if (autoCorrect && wordList->size() > 1 && (!spellCheck(word) || suggestCapitalization)) { + if (lastWordCompletionIndex > firstWordCompletionIndex || levenshteinDistance(word, wordList->wordAt(firstWordCompletionIndex)) < 3) + wordList->setIndex(firstWordCompletionIndex); + } + } + Hunspell_free_list(hunspell, &slst, n); + + for (int i = 0, count = wordList->size(); i < count; ++i) { + HunspellWordList::Flags flags; + wordList->wordAt(i, word, flags); + if (flags.testFlag(HunspellWordList::CompoundWord)) + continue; + if (Hunspell_spell(hunspell, QByteArray { textEncoder(word) }.constData()) != 0) + wordList->updateWord(i, word, wordList->wordFlagsAt(i) | HunspellWordList::SpellCheckOk); + } +} + +bool HunspellBuildSuggestionsTask::spellCheck(const QString &word) +{ + if (!hunspell) + return false; + if (word.contains(QRegularExpression(QLatin1String("[0-9]")))) + return true; + return Hunspell_spell(hunspell, QByteArray { textEncoder(word) }.constData()) != 0; +} + +// source: http://en.wikipedia.org/wiki/Levenshtein_distance +int HunspellBuildSuggestionsTask::levenshteinDistance(const QString &s, const QString &t) +{ + if (s == t) + return 0; + if (s.size() == 0) + return t.size(); + if (t.size() == 0) + return s.size(); + QList<int> v0(t.size() + 1); + QList<int> v1(t.size() + 1); + for (int i = 0; i < v0.size(); i++) + v0[i] = i; + for (int i = 0; i < s.size(); i++) { + v1[0] = i + 1; + for (int j = 0; j < t.size(); j++) { + int cost = (s[i].toLower() == t[j].toLower()) ? 0 : 1; + v1[j + 1] = qMin(qMin(v1[j] + 1, v0[j + 1] + 1), v0[j] + cost); + } + for (int j = 0; j < v0.size(); j++) + v0[j] = v1[j]; + } + return v1[t.size()]; +} + +QString HunspellBuildSuggestionsTask::removeAccentsAndDiacritics(const QString& s) +{ + QString normalized = s.normalized(QString::NormalizationForm_D); + for (int i = 0; i < normalized.size();) { + QChar::Category category = normalized[i].category(); + if (category <= QChar::Mark_Enclosing) { + normalized.remove(i, 1); + } else { + i++; + } + } + return normalized; +} + +/*! + \class QtVirtualKeyboard::HunspellUpdateSuggestionsTask + \internal +*/ + +void HunspellUpdateSuggestionsTask::run() +{ + emit updateSuggestions(wordList, tag); +} + +void HunspellAddWordTask::run() +{ + auto fromUtf16 = QStringEncoder(Hunspell_get_dic_encoding(hunspell)); + if (!fromUtf16.isValid()) + return; + + QString tmpWord; + tmpWord.reserve(64); + for (int i = 0, count = wordList->size(); i < count; ++i) { + const QString word(wordList->wordAt(i)); + if (word.size() < 2) + continue; + Hunspell_add(hunspell, QByteArray { fromUtf16(word) }.constData()); + if (HunspellAddWordTask::alternativeForm(word, tmpWord)) + Hunspell_add(hunspell, QByteArray { fromUtf16(tmpWord) }.constData()); + } +} + +bool HunspellAddWordTask::alternativeForm(const QString &word, QString &alternativeForm) +{ + if (word.size() < 2) + return false; + if (!word.mid(1).isLower()) + return false; + + const QChar initial(word.at(0)); + const QChar newInitial = initial.isUpper() ? initial.toLower() : initial.toUpper(); + if (newInitial == initial) + return false; + + alternativeForm.truncate(0); + alternativeForm.append(word); + alternativeForm[0] = newInitial; + + return true; +} + +void HunspellRemoveWordTask::run() +{ + auto fromUtf16 = QStringEncoder(Hunspell_get_dic_encoding(hunspell)); + if (!fromUtf16.isValid()) + return; + + QString tmpWord; + tmpWord.reserve(64); + for (int i = 0, count = wordList->size(); i < count; ++i) { + const QString word(wordList->wordAt(i)); + if (word.isEmpty()) + continue; + Hunspell_remove(hunspell, QByteArray { fromUtf16(word) }.constData()); + if (HunspellAddWordTask::alternativeForm(word, tmpWord)) + Hunspell_remove(hunspell, QByteArray { fromUtf16(tmpWord) }.constData()); + } +} + +void HunspellLoadWordListTask::run() +{ + wordList->clear(); + + QFile inputFile(filePath); + if (inputFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream inStream(&inputFile); + QString word; + word.reserve(64); + while (inStream.readLineInto(&word)) { + if (!word.isEmpty()) + wordList->appendWord(word); + } + inputFile.close(); + } +} + +void HunspellSaveWordListTask::run() +{ + QFile outputFile(filePath); + if (!QFileInfo::exists(filePath)) + QDir().mkpath(QFileInfo(filePath).absoluteDir().path()); + if (outputFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream outStream(&outputFile); + for (int i = 0, count = wordList->size(); i < count; ++i) { + const QString word(wordList->wordAt(i)); + outStream << word.toUtf8() << '\n'; + } + outputFile.close(); + } +} + +void HunspellFilterWordTask::run() +{ + if (filterList->isEmpty()) + return; + + filterList->rebuildSearchIndex(); + + for (int i = startIndex, count = wordList->size(); i < count;) { + if (filterList->contains(wordList->wordAt(i))) { + wordList->removeWordAt(i); + --count; + } else { + ++i; + } + } +} + +void HunspellBoostWordTask::run() +{ + if (boostList->isEmpty()) + return; + + boostList->rebuildSearchIndex(); + + const QString word(wordList->wordAt(0)); + const QString wordCompletion(boostList->findWordCompletion(word)); + if (!wordCompletion.isEmpty()) { + int from = wordList->indexOfWord(wordCompletion); + if (from != 1) { + int to; + for (to = 1; to < wordList->size() && wordList->wordAt(to).startsWith(word); ++to) + ; + if (from != -1) { + if (to < from) + wordList->moveWord(from, to); + } else { + wordList->insertWord(to, wordCompletion, HunspellWordList::SpellCheckOk); + } + } + } +} + +/*! + \class QtVirtualKeyboard::HunspellWorker + \internal +*/ + +HunspellWorker::HunspellWorker(QObject *parent) : + QThread(parent), + idleSema(), + taskSema(), + taskLock(), + hunspell(nullptr) +{ + abort = false; + qRegisterMetaType<QSharedPointer<HunspellWordList>>("QSharedPointer<HunspellWordList>"); +} + +HunspellWorker::~HunspellWorker() +{ + abort = true; + taskSema.release(1); + wait(); +} + +void HunspellWorker::addTask(QSharedPointer<HunspellTask> task) +{ + if (task) { + QMutexLocker guard(&taskLock); + taskList.append(task); + taskSema.release(); + } +} + +void HunspellWorker::removeAllTasks() +{ + QMutexLocker guard(&taskLock); + taskList.clear(); +} + +void HunspellWorker::waitForAllTasks() +{ + qCDebug(lcHunspell) << "waitForAllTasks enter"; + while (isRunning()) { + idleSema.acquire(); + QMutexLocker guard(&taskLock); + if (taskList.isEmpty()) { + idleSema.release(); + break; + } + idleSema.release(); + } + qCDebug(lcHunspell) << "waitForAllTasks leave"; +} + +void HunspellWorker::run() +{ + QElapsedTimer perf; + while (!abort) { + idleSema.release(); + taskSema.acquire(); + if (abort) + break; + idleSema.acquire(); + QSharedPointer<HunspellTask> currentTask; + { + QMutexLocker guard(&taskLock); + if (!taskList.isEmpty()) { + currentTask = taskList.front(); + taskList.pop_front(); + } + } + if (currentTask) { + QSharedPointer<HunspellLoadDictionaryTask> loadDictionaryTask(currentTask.objectCast<HunspellLoadDictionaryTask>()); + if (loadDictionaryTask) + loadDictionaryTask->hunspellPtr = &hunspell; + else if (hunspell) + currentTask->hunspell = hunspell; + else + continue; + perf.start(); + currentTask->run(); + qCDebug(lcHunspell) << QString(QLatin1String(currentTask->metaObject()->className()) + QLatin1String("::run(): time:")).toLatin1().constData() << perf.elapsed() << "ms"; + } + } + if (hunspell) { + Hunspell_destroy(hunspell); + hunspell = nullptr; + } +} + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE diff --git a/src/plugins/hunspell/module/hunspellworker_p.h b/src/plugins/hunspell/module/hunspellworker_p.h new file mode 100644 index 00000000..24207174 --- /dev/null +++ b/src/plugins/hunspell/module/hunspellworker_p.h @@ -0,0 +1,213 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef HUNSPELLWORKER_P_H +#define HUNSPELLWORKER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QThread> +#include <QSemaphore> +#include <QMutex> +#include <QStringList> +#include <QSharedPointer> +#include <QList> +#include <QLoggingCategory> +#include <QStringDecoder> +#include <QStringEncoder> +#include <hunspell/hunspell.h> +#include "hunspellwordlist_p.h" + +QT_BEGIN_NAMESPACE + +namespace QtVirtualKeyboard { + +Q_DECLARE_LOGGING_CATEGORY(lcHunspell) + +class HunspellTask : public QObject +{ + Q_OBJECT +public: + explicit HunspellTask(QObject *parent = nullptr) : + QObject(parent), + hunspell(nullptr) + {} + + virtual void run() = 0; + + Hunhandle *hunspell; +}; + +class HunspellLoadDictionaryTask : public HunspellTask +{ + Q_OBJECT +public: + explicit HunspellLoadDictionaryTask(const QString &locale, const QStringList &searchPaths); + + void run() override; + +signals: + void completed(bool success); + +public: + Hunhandle **hunspellPtr; + const QString locale; + const QStringList searchPaths; +}; + +class HunspellBuildSuggestionsTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + bool autoCorrect; + + void run() override; + bool spellCheck(const QString &word); + int levenshteinDistance(const QString &s, const QString &t); + QString removeAccentsAndDiacritics(const QString& s); + +private: + QStringDecoder textDecoder; + QStringEncoder textEncoder; +}; + +class HunspellUpdateSuggestionsTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + + void run() override; + +signals: + void updateSuggestions(const QSharedPointer<HunspellWordList> &wordList, int tag); + +public: + int tag; +}; + +class HunspellAddWordTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + + void run() override; + + static bool alternativeForm(const QString &word, QString &alternativeForm); +}; + +class HunspellRemoveWordTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + + void run() override; +}; + +class HunspellLoadWordListTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + QString filePath; + + void run() override; +}; + +class HunspellSaveWordListTask : public HunspellTask +{ + Q_OBJECT +public: + QSharedPointer<HunspellWordList> wordList; + QString filePath; + + void run() override; +}; + +class HunspellFilterWordTask : public HunspellTask +{ + Q_OBJECT +public: + HunspellFilterWordTask() : + HunspellTask(), + startIndex(1) + {} + + QSharedPointer<HunspellWordList> wordList; + QSharedPointer<HunspellWordList> filterList; + int startIndex; + + void run() override; +}; + +class HunspellBoostWordTask : public HunspellTask +{ + Q_OBJECT +public: + HunspellBoostWordTask() : + HunspellTask() + {} + + QSharedPointer<HunspellWordList> wordList; + QSharedPointer<HunspellWordList> boostList; + + void run() override; +}; + +class HunspellWorker : public QThread +{ + Q_OBJECT +public: + explicit HunspellWorker(QObject *parent = nullptr); + ~HunspellWorker(); + + void addTask(QSharedPointer<HunspellTask> task); + void removeAllTasks(); + void waitForAllTasks(); + + template <class X> + void removeAllTasksOfType() { + QMutexLocker guard(&taskLock); + for (int i = 0; i < taskList.size();) { + QSharedPointer<X> task(taskList[i].objectCast<X>()); + if (task) { + qCDebug(lcHunspell) << "Remove task" << QLatin1String(task->metaObject()->className()); + taskList.removeAt(i); + } else { + i++; + } + } + } + +protected: + void run() override; + +private: + void createHunspell(); + +private: + friend class HunspellLoadDictionaryTask; + QList<QSharedPointer<HunspellTask> > taskList; + QSemaphore idleSema; + QSemaphore taskSema; + QMutex taskLock; + Hunhandle *hunspell; + QBasicAtomicInt abort; +}; + +} // namespace QtVirtualKeyboard +QT_END_NAMESPACE + +#endif // HUNSPELLWORKER_P_H diff --git a/src/plugins/hunspell/module/qhunspellinputmethod_global.h b/src/plugins/hunspell/module/qhunspellinputmethod_global.h new file mode 100644 index 00000000..433535c6 --- /dev/null +++ b/src/plugins/hunspell/module/qhunspellinputmethod_global.h @@ -0,0 +1,10 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QHUNSPELLINPUTMETHOD_GLOBAL_H +#define QHUNSPELLINPUTMETHOD_GLOBAL_H + +#include <QtCore/qglobal.h> +#include <QtHunspellInputMethod/qthunspellinputmethodexports.h> + +#endif |