aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/hunspell/module
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/hunspell/module')
-rw-r--r--src/plugins/hunspell/module/CMakeLists.txt53
-rw-r--r--src/plugins/hunspell/module/hunspellinputmethod.cpp350
-rw-r--r--src/plugins/hunspell/module/hunspellinputmethod_p.cpp347
-rw-r--r--src/plugins/hunspell/module/hunspellinputmethod_p.h62
-rw-r--r--src/plugins/hunspell/module/hunspellinputmethod_p_p.h77
-rw-r--r--src/plugins/hunspell/module/hunspellwordlist.cpp308
-rw-r--r--src/plugins/hunspell/module/hunspellwordlist_p.h86
-rw-r--r--src/plugins/hunspell/module/hunspellworker.cpp450
-rw-r--r--src/plugins/hunspell/module/hunspellworker_p.h213
-rw-r--r--src/plugins/hunspell/module/qhunspellinputmethod_global.h10
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