aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJarkko Koivikko <jarkko.koivikko@code-q.fi>2018-08-07 12:01:39 +0300
committerJarkko Koivikko <jarkko.koivikko@code-q.fi>2018-08-16 13:31:39 +0000
commite803aec1ea21fd00e13b9535a4b536cc43c26ee4 (patch)
tree7cdcfcebb4de0cf3651aeea56637e335b46f9a18
parentcf69f8603e3a1fee24f79d1b446b5ea717e2cf7d (diff)
Add user dictionary and learning for Hunspell
This change adds user dictionary and learning function for Hunspell. Learning happens when the user selects the first candidate from the word candidate list (or presses the space key while the first candidate is selected) and the word does not exist in the default dictionary. User can remove a word from user dictionary by long pressing an item on the word candidate list (and selecting from the menu). This also allows the user to block words originating from the system dictionary. The Hunspell user dictionary is hard limited to 100 words. However, when user enters a word again (which exists in the user dictionary), it will be prioritized in the dictionary, so the most frequent words stay in the dictionary. The dictionaries are language and locale specific and are stored in: QStandardPaths::GenericConfigLocation + "/qtvirtualkeyboard/hunspell" The dictionaries are UTF-8 encoded text files and contain one word per line. The same directory also contains the blacklist files (which also exist per language and locale). These files contain words which are blacklisted from the default dictionary. [ChangeLog] Added user dictionary and learning for Hunspell Change-Id: Ib0fc70f7eaa14ec49b74000144a75c59313ac0fb Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
-rw-r--r--src/plugin/plugin.cpp2
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod.cpp167
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.cpp197
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.h4
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p_p.h19
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellworker.cpp527
-rw-r--r--src/plugins/hunspell/hunspellinputmethod/hunspellworker_p.h161
-rw-r--r--src/plugins/lipi-toolkit/plugin/lipiinputmethod.cpp1
-rw-r--r--src/styles/SelectionListItem.qml7
-rw-r--r--src/virtualkeyboard/abstractinputmethod.cpp17
-rw-r--r--src/virtualkeyboard/abstractinputmethod.h1
-rw-r--r--src/virtualkeyboard/content/components/Keyboard.qml245
-rw-r--r--src/virtualkeyboard/content/components/PopupList.qml (renamed from src/virtualkeyboard/content/components/LanguagePopupList.qml)18
-rw-r--r--src/virtualkeyboard/content/components/WordCandidatePopupList.qml24
-rw-r--r--src/virtualkeyboard/content/content.qrc2
-rw-r--r--src/virtualkeyboard/selectionlistmodel.cpp24
-rw-r--r--src/virtualkeyboard/selectionlistmodel.h12
-rw-r--r--tests/auto/inputpanel/data/inputpanel/inputpanel.qml57
-rw-r--r--tests/auto/inputpanel/data/tst_inputpanel.qml73
-rw-r--r--tests/auto/inputpanel/tst_inputpanel.cpp15
20 files changed, 1343 insertions, 230 deletions
diff --git a/src/plugin/plugin.cpp b/src/plugin/plugin.cpp
index 4841f53b..956bafbd 100644
--- a/src/plugin/plugin.cpp
+++ b/src/plugin/plugin.cpp
@@ -191,7 +191,7 @@ QPlatformInputContext *QVirtualKeyboardPlugin::create(const QString &system, con
qmlRegisterType(QUrl(componentsPath + QLatin1String("TraceInputArea.qml")), pluginUri, 2, 0, "TraceInputArea");
qmlRegisterType(QUrl(componentsPath + QLatin1String("TraceInputKey.qml")), pluginUri, 2, 0, "TraceInputKey");
qmlRegisterType(QUrl(componentsPath + QLatin1String("WordCandidatePopupList.qml")), pluginUri, 2, 0, "WordCandidatePopupList");
- qmlRegisterType(QUrl(componentsPath + QLatin1String("LanguagePopupList.qml")), pluginUri, 2, 1, "LanguagePopupList");
+ qmlRegisterType(QUrl(componentsPath + QLatin1String("PopupList.qml")), pluginUri, 2, 3, "PopupList");
qmlRegisterType(QUrl(componentsPath + QLatin1String("SelectionControl.qml")), pluginUri, 2, 1, "SelectionControl");
qmlRegisterType(QUrl(componentsPath + QLatin1String("InputModeKey.qml")), pluginUri, 2, 3, "InputModeKey");
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod.cpp b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod.cpp
index d437f8fe..ee8d31e3 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod.cpp
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod.cpp
@@ -108,16 +108,24 @@ bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::Keyboar
update();
break;
case Qt::Key_Backspace:
- if (!d->word.isEmpty()) {
- d->word.remove(d->word.length() - 1, 1);
- ic->setPreeditText(d->word);
- if (d->updateSuggestions()) {
- emit selectionListChanged(SelectionListModel::WordCandidateList);
- emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->activeWordIndex);
+ {
+ QString word = d->wordCandidates.wordAt(0);
+ if (!word.isEmpty()) {
+ word.remove(word.length() - 1, 1);
+ ic->setPreeditText(word);
+ if (!word.isEmpty()) {
+ d->wordCandidates.updateWord(0, word);
+ if (d->updateSuggestions()) {
+ emit selectionListChanged(SelectionListModel::WordCandidateList);
+ emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->wordCandidates.index());
+ }
+ } else {
+ d->reset();
}
accept = true;
}
break;
+ }
default:
if (inputMethodHints.testFlag(Qt::ImhNoPredictiveText))
break;
@@ -127,10 +135,11 @@ bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::Keyboar
}
if (text.length() > 0) {
QChar c = text.at(0);
- bool addToWord = d->isValidInputChar(c) && (!d->word.isEmpty() || !d->isJoiner(c));
+ QString word = d->wordCandidates.wordAt(0);
+ bool addToWord = d->isValidInputChar(c) && (!word.isEmpty() || !d->isJoiner(c));
if (addToWord) {
/* Automatic space insertion. */
- if (d->word.isEmpty()) {
+ if (word.isEmpty()) {
QString surroundingText = ic->surroundingText();
int cursorPosition = ic->cursorPosition();
/* Rules for automatic space insertion:
@@ -144,7 +153,7 @@ bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::Keyboar
if (!lastChar.isSpace() &&
lastChar != Qt::Key_Minus &&
d->isAutoSpaceAllowed()) {
- ic->commit(" ");
+ ic->commit(QLatin1String(" "));
}
}
}
@@ -152,21 +161,22 @@ bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::Keyboar
pre-edit text. The update is triggered if the text editor has
a selection which the pre-edit text will replace.
*/
- d->ignoreUpdate = d->word.isEmpty();
- d->word.append(text);
- ic->setPreeditText(d->word);
+ d->ignoreUpdate = word.isEmpty();
+ word.append(text);
+ d->wordCandidates.updateWord(0, word);
+ ic->setPreeditText(word);
d->ignoreUpdate = false;
if (d->updateSuggestions()) {
emit selectionListChanged(SelectionListModel::WordCandidateList);
- emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->activeWordIndex);
+ emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->wordCandidates.index());
}
accept = true;
} else if (text.length() > 1) {
- bool addSpace = !d->word.isEmpty() || d->autoSpaceAllowed;
+ bool addSpace = !word.isEmpty() || d->autoSpaceAllowed;
update();
d->autoSpaceAllowed = true;
if (addSpace && d->isAutoSpaceAllowed())
- ic->commit(" ");
+ ic->commit(QLatin1String(" "));
ic->commit(text);
d->autoSpaceAllowed = addSpace;
accept = true;
@@ -185,7 +195,10 @@ bool HunspellInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::Keyboar
QList<SelectionListModel::Type> HunspellInputMethod::selectionLists()
{
Q_D(const HunspellInputMethod);
- Qt::InputMethodHints inputMethodHints = inputContext()->inputMethodHints();
+ InputContext *ic = inputContext();
+ if (!ic)
+ return QList<SelectionListModel::Type>();
+ Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded || inputMethodHints.testFlag(Qt::ImhNoPredictiveText) || inputMethodHints.testFlag(Qt::ImhHiddenText))
return QList<SelectionListModel::Type>();
return QList<SelectionListModel::Type>() << SelectionListModel::WordCandidateList;
@@ -195,7 +208,7 @@ int HunspellInputMethod::selectionListItemCount(SelectionListModel::Type type)
{
Q_UNUSED(type)
Q_D(HunspellInputMethod);
- return d->wordCandidates.count();
+ return d->wordCandidates.size();
}
QVariant HunspellInputMethod::selectionListData(SelectionListModel::Type type, int index, int role)
@@ -204,15 +217,28 @@ QVariant HunspellInputMethod::selectionListData(SelectionListModel::Type type, i
Q_D(HunspellInputMethod);
switch (role) {
case SelectionListModel::DisplayRole:
- result = QVariant(d->wordCandidates.at(index));
+ result = QVariant(d->wordCandidates.wordAt(index));
break;
case SelectionListModel::WordCompletionLengthRole:
{
- const QString wordCandidate(d->wordCandidates.at(index));
- int wordCompletionLength = wordCandidate.length() - d->word.length();
- result.setValue((wordCompletionLength > 0 && wordCandidate.startsWith(d->word)) ? wordCompletionLength : 0);
+ const QString wordCandidate(d->wordCandidates.wordAt(index));
+ const QString word(d->wordCandidates.wordAt(0));
+ int wordCompletionLength = wordCandidate.length() - word.length();
+ result.setValue((wordCompletionLength > 0 && wordCandidate.startsWith(word)) ? wordCompletionLength : 0);
break;
}
+ case SelectionListModel::DictionaryTypeRole:
+ {
+ const QString wordCandidate(d->wordCandidates.wordAt(index));
+ SelectionListModel::DictionaryType dictionaryType =
+ d->userDictionaryWords && d->userDictionaryWords->contains(wordCandidate) ?
+ SelectionListModel::UserDictionary : SelectionListModel::DefaultDictionary;
+ result = QVariant(static_cast<int>(dictionaryType));
+ break;
+ }
+ case SelectionListModel::CanRemoveSuggestionRole:
+ result.setValue(index > 0 && d->wordCandidates.wordFlagsAt(index).testFlag(HunspellWordList::SpellCheckOk));
+ break;
default:
result = AbstractInputMethod::selectionListData(type, index, role);
break;
@@ -224,16 +250,33 @@ void HunspellInputMethod::selectionListItemSelected(SelectionListModel::Type typ
{
Q_UNUSED(type)
Q_D(HunspellInputMethod);
- QString finalWord = d->wordCandidates.at(index);
+ d->wordCandidates.setIndex(index);
+ d->addToDictionary();
+ QString finalWord = d->wordCandidates.wordAt(index);
reset();
inputContext()->commit(finalWord);
d->autoSpaceAllowed = true;
}
+bool HunspellInputMethod::selectionListRemoveItem(SelectionListModel::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 InputEngine::ReselectFlags &reselectFlags)
{
Q_D(HunspellInputMethod);
- Q_ASSERT(d->word.isEmpty());
+ QString word(d->wordCandidates.wordAt(0));
+ Q_ASSERT(word.isEmpty());
if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded)
return false;
@@ -250,61 +293,54 @@ bool HunspellInputMethod::reselect(int cursorPosition, const InputEngine::Resele
QChar c = surroundingText.at(i);
if (!d->isValidInputChar(c))
break;
- d->word.insert(0, c);
+ word.insert(0, c);
--replaceFrom;
}
- while (replaceFrom < 0 && d->isJoiner(d->word.at(0))) {
- d->word.remove(0, 1);
+ while (replaceFrom < 0 && d->isJoiner(word.at(0))) {
+ word.remove(0, 1);
++replaceFrom;
}
}
- if (reselectFlags.testFlag(InputEngine::WordAtCursor) && replaceFrom == 0) {
- d->word.clear();
+ if (reselectFlags.testFlag(InputEngine::WordAtCursor) && replaceFrom == 0)
return false;
- }
if (reselectFlags.testFlag(InputEngine::WordAfterCursor)) {
for (int i = cursorPosition; i < surroundingText.length(); ++i) {
QChar c = surroundingText.at(i);
if (!d->isValidInputChar(c))
break;
- d->word.append(c);
+ word.append(c);
}
- while (replaceFrom > -d->word.length()) {
- int lastPos = d->word.length() - 1;
- if (!d->isJoiner(d->word.at(lastPos)))
+ while (replaceFrom > -word.length()) {
+ int lastPos = word.length() - 1;
+ if (!d->isJoiner(word.at(lastPos)))
break;
- d->word.remove(lastPos, 1);
+ word.remove(lastPos, 1);
}
}
- if (d->word.isEmpty())
+ if (word.isEmpty())
return false;
- if (reselectFlags.testFlag(InputEngine::WordAtCursor) && replaceFrom == -d->word.length()) {
- d->word.clear();
+ if (reselectFlags.testFlag(InputEngine::WordAtCursor) && replaceFrom == -word.length())
return false;
- }
- if (d->isJoiner(d->word.at(0))) {
- d->word.clear();
+ if (d->isJoiner(word.at(0)))
return false;
- }
- if (d->isJoiner(d->word.at(d->word.length() - 1))) {
- d->word.clear();
+ if (d->isJoiner(word.at(word.length() - 1)))
return false;
- }
- ic->setPreeditText(d->word, QList<QInputMethodEvent::Attribute>(), replaceFrom, d->word.length());
+ d->wordCandidates.updateWord(0, word);
+ ic->setPreeditText(word, QList<QInputMethodEvent::Attribute>(), replaceFrom, word.length());
d->autoSpaceAllowed = false;
if (d->updateSuggestions()) {
emit selectionListChanged(SelectionListModel::WordCandidateList);
- emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->activeWordIndex);
+ emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->wordCandidates.index());
}
return true;
@@ -321,37 +357,50 @@ void HunspellInputMethod::update()
Q_D(HunspellInputMethod);
if (d->ignoreUpdate)
return;
- if (!d->word.isEmpty()) {
- QString finalWord = d->hasSuggestions() ? d->wordCandidates.at(d->activeWordIndex) : d->word;
- d->reset();
- inputContext()->commit(finalWord);
+
+ QString finalWord;
+ if (!d->wordCandidates.isEmpty()) {
+ d->addToDictionary();
+ finalWord = d->wordCandidates.wordAt(d->wordCandidates.index());
}
+ d->reset();
+ inputContext()->commit(finalWord);
d->autoSpaceAllowed = false;
}
-void HunspellInputMethod::updateSuggestions(const QStringList &wordList, int activeWordIndex)
+void HunspellInputMethod::updateSuggestions(const QSharedPointer<HunspellWordList> &wordList, int tag)
{
Q_D(HunspellInputMethod);
if (d->dictionaryState == HunspellInputMethodPrivate::DictionaryNotLoaded) {
+ qCDebug(lcHunspell) << "updateSuggestions: skip (dictionary not loaded)";
update();
return;
}
- d->wordCandidates.clear();
- d->wordCandidates.append(wordList);
- // Make sure the exact match is up-to-date
- if (!d->word.isEmpty() && !d->wordCandidates.isEmpty() && d->wordCandidates.at(0) != d->word)
- d->wordCandidates.replace(0, d->word);
- d->activeWordIndex = activeWordIndex;
+ if (d->wordCandidatesUpdateTag != tag) {
+ qCDebug(lcHunspell) << "updateSuggestions: skip tag" << tag << "current" << d->wordCandidatesUpdateTag;
+ return;
+ }
+ QString word(d->wordCandidates.wordAt(0));
+ d->wordCandidates = *wordList;
+ if (d->wordCandidates.wordAt(0).compare(word) != 0)
+ d->wordCandidates.updateWord(0, word);
emit selectionListChanged(SelectionListModel::WordCandidateList);
- emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->activeWordIndex);
+ emit selectionListActiveItemChanged(SelectionListModel::WordCandidateList, d->wordCandidates.index());
}
void HunspellInputMethod::dictionaryLoadCompleted(bool success)
{
Q_D(HunspellInputMethod);
+ InputContext *ic = inputContext();
+ if (!ic)
+ return;
+
+ QList<SelectionListModel::Type> oldSelectionLists = selectionLists();
d->dictionaryState = success ? HunspellInputMethodPrivate::DictionaryReady :
HunspellInputMethodPrivate::DictionaryNotLoaded;
- emit selectionListsChanged();
+ QList<SelectionListModel::Type> newSelectionLists = selectionLists();
+ if (oldSelectionLists != newSelectionLists)
+ emit selectionListsChanged();
}
} // namespace QtVirtualKeyboard
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.cpp b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.cpp
index 3a6777b2..2027a987 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.cpp
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.cpp
@@ -34,10 +34,13 @@
#include <QDir>
#include <QTextCodec>
#include <QtCore/QLibraryInfo>
+#include <QStandardPaths>
QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {
+const int HunspellInputMethodPrivate::userDictionaryMaxSize = 100;
+
/*!
\class QtVirtualKeyboard::HunspellInputMethodPrivate
\internal
@@ -47,13 +50,13 @@ HunspellInputMethodPrivate::HunspellInputMethodPrivate(HunspellInputMethod *q_pt
q_ptr(q_ptr),
hunspellWorker(new HunspellWorker()),
locale(),
- word(),
- wordCandidates(),
- activeWordIndex(-1),
wordCompletionPoint(2),
ignoreUpdate(false),
autoSpaceAllowed(false),
- dictionaryState(DictionaryNotLoaded)
+ dictionaryState(DictionaryNotLoaded),
+ userDictionaryWords(new HunspellWordList(userDictionaryMaxSize)),
+ blacklistedWords(new HunspellWordList(userDictionaryMaxSize)),
+ wordCandidatesUpdateTag(0)
{
if (hunspellWorker)
hunspellWorker->start();
@@ -69,7 +72,8 @@ bool HunspellInputMethodPrivate::createHunspell(const QString &locale)
if (!hunspellWorker)
return false;
if (this->locale != locale) {
- hunspellWorker->removeAllTasks();
+ clearSuggestionsRelatedTasks();
+ hunspellWorker->waitForAllTasks();
QString hunspellDataPath(qEnvironmentVariable("QT_VIRTUALKEYBOARD_HUNSPELL_DATA_PATH"));
const QString pathListSep(
#if defined(Q_OS_WIN32)
@@ -96,55 +100,65 @@ bool HunspellInputMethodPrivate::createHunspell(const QString &locale)
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()) {
+ if (clearSuggestions(true)) {
Q_Q(HunspellInputMethod);
emit q->selectionListChanged(SelectionListModel::WordCandidateList);
- emit q->selectionListActiveItemChanged(SelectionListModel::WordCandidateList, activeWordIndex);
+ emit q->selectionListActiveItemChanged(SelectionListModel::WordCandidateList, wordCandidates.index());
}
- word.clear();
autoSpaceAllowed = false;
}
bool HunspellInputMethodPrivate::updateSuggestions()
{
bool wordCandidateListChanged = false;
+ QString word = wordCandidates.wordAt(0);
if (!word.isEmpty() && dictionaryState != HunspellInputMethodPrivate::DictionaryNotLoaded) {
- if (hunspellWorker)
- hunspellWorker->removeAllTasksExcept<HunspellLoadDictionaryTask>();
- if (wordCandidates.isEmpty()) {
- wordCandidates.append(word);
- activeWordIndex = 0;
- wordCandidateListChanged = true;
- } else if (wordCandidates.at(0) != word) {
- wordCandidates.replace(0, word);
- activeWordIndex = 0;
- wordCandidateListChanged = true;
- }
+ wordCandidateListChanged = true;
if (word.length() >= wordCompletionPoint) {
if (hunspellWorker) {
- QSharedPointer<HunspellWordList> wordList(new HunspellWordList());
+ QSharedPointer<HunspellWordList> wordList(new HunspellWordList(wordCandidates));
+
+ // Clear obsolete tasks from the worker queue
+ clearSuggestionsRelatedTasks();
+
+ // Build suggestions
QSharedPointer<HunspellBuildSuggestionsTask> buildSuggestionsTask(new HunspellBuildSuggestionsTask());
- buildSuggestionsTask->word = word;
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;
Q_Q(HunspellInputMethod);
- q->connect(updateSuggestionsTask.data(), SIGNAL(updateSuggestions(QStringList, int)), SLOT(updateSuggestions(QStringList, int)));
+ QObject::connect(updateSuggestionsTask.data(), &HunspellUpdateSuggestionsTask::updateSuggestions, q, &HunspellInputMethod::updateSuggestions);
hunspellWorker->addTask(updateSuggestionsTask);
}
- } else if (wordCandidates.length() > 1) {
- wordCandidates.clear();
- wordCandidates.append(word);
- activeWordIndex = 0;
- wordCandidateListChanged = true;
}
} else {
wordCandidateListChanged = clearSuggestions();
@@ -152,20 +166,20 @@ bool HunspellInputMethodPrivate::updateSuggestions()
return wordCandidateListChanged;
}
-bool HunspellInputMethodPrivate::clearSuggestions()
+bool HunspellInputMethodPrivate::clearSuggestions(bool clearInputWord)
{
- if (hunspellWorker)
- hunspellWorker->removeAllTasksExcept<HunspellLoadDictionaryTask>();
- if (wordCandidates.isEmpty())
- return false;
- wordCandidates.clear();
- activeWordIndex = -1;
- return true;
+ clearSuggestionsRelatedTasks();
+ return clearInputWord ? wordCandidates.clear() : wordCandidates.clearSuggestions();
}
-bool HunspellInputMethodPrivate::hasSuggestions() const
+void HunspellInputMethodPrivate::clearSuggestionsRelatedTasks()
{
- return !wordCandidates.isEmpty();
+ if (hunspellWorker) {
+ hunspellWorker->removeAllTasksOfType<HunspellBuildSuggestionsTask>();
+ hunspellWorker->removeAllTasksOfType<HunspellFilterWordTask>();
+ hunspellWorker->removeAllTasksOfType<HunspellBoostWordTask>();
+ hunspellWorker->removeAllTasksOfType<HunspellUpdateSuggestionsTask>();
+ }
}
bool HunspellInputMethodPrivate::isAutoSpaceAllowed() const
@@ -211,5 +225,114 @@ bool HunspellInputMethodPrivate::isJoiner(const QChar &c) const
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.length() > 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"));
+ }
+ }
+}
+
} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.h b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.h
index 68a4e702..bb9548c0 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.h
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p.h
@@ -47,6 +47,7 @@ QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {
class HunspellInputMethodPrivate;
+class HunspellWordList;
class HunspellInputMethod : public AbstractInputMethod
{
@@ -68,6 +69,7 @@ public:
int selectionListItemCount(SelectionListModel::Type type);
QVariant selectionListData(SelectionListModel::Type type, int index, int role);
void selectionListItemSelected(SelectionListModel::Type type, int index);
+ bool selectionListRemoveItem(SelectionListModel::Type type, int index);
bool reselect(int cursorPosition, const InputEngine::ReselectFlags &reselectFlags);
@@ -75,7 +77,7 @@ public:
void update();
protected Q_SLOTS:
- void updateSuggestions(const QStringList &wordList, int activeWordIndex);
+ void updateSuggestions(const QSharedPointer<HunspellWordList> &wordList, int tag);
void dictionaryLoadCompleted(bool success);
protected:
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p_p.h b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p_p.h
index ebf202fd..8bb75f69 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p_p.h
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellinputmethod_p_p.h
@@ -64,22 +64,31 @@ public:
bool createHunspell(const QString &locale);
void reset();
bool updateSuggestions();
- bool clearSuggestions();
- bool hasSuggestions() const;
+ 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();
HunspellInputMethod *q_ptr;
QScopedPointer<HunspellWorker> hunspellWorker;
QString locale;
- QString word;
- QStringList wordCandidates;
- int activeWordIndex;
+ HunspellWordList wordCandidates;
int wordCompletionPoint;
bool ignoreUpdate;
bool autoSpaceAllowed;
DictionaryState dictionaryState;
+ QSharedPointer<HunspellWordList> userDictionaryWords;
+ QSharedPointer<HunspellWordList> blacklistedWords;
+ int wordCandidatesUpdateTag;
+ static const int userDictionaryMaxSize;
};
} // namespace QtVirtualKeyboard
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellworker.cpp b/src/plugins/hunspell/hunspellinputmethod/hunspellworker.cpp
index 46cbf49c..6387ee16 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellworker.cpp
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellworker.cpp
@@ -28,17 +28,308 @@
****************************************************************************/
#include <QtHunspellInputMethod/private/hunspellworker_p.h>
-#include <QLoggingCategory>
#include <QVector>
#include <QTextCodec>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTime>
+#include <QFile>
+#include <QDir>
+#include <QtAlgorithms>
QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {
-Q_DECLARE_LOGGING_CATEGORY(lcHunspell)
+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.length() > bestMatch.length() &&
+ word.length() < wordB.length() &&
+ 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; });
+}
/*!
\class QtVirtualKeyboard::HunspellTask
@@ -69,9 +360,6 @@ void HunspellLoadDictionaryTask::run()
qCDebug(lcHunspell) << "HunspellLoadDictionaryTask::run(): locale:" << locale;
- QTime perf;
- perf.start();
-
if (*hunspellPtr) {
Hunspell_destroy(*hunspellPtr);
*hunspellPtr = nullptr;
@@ -97,13 +385,11 @@ void HunspellLoadDictionaryTask::run()
by the QTextCodec.
*/
if (!QTextCodec::codecForName(Hunspell_get_dic_encoding(*hunspellPtr))) {
- qCWarning(lcHunspell) << "The Hunspell dictionary" << dicPath << "cannot be used because it uses an unknown text codec" << QString(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;
}
}
-
- qCDebug(lcHunspell) << "HunspellLoadDictionaryTask::run(): time:" << perf.elapsed() << "ms";
} else {
qCWarning(lcHunspell) << "Hunspell dictionary is missing for" << locale << ". Search paths" << searchPaths;
}
@@ -118,11 +404,11 @@ void HunspellLoadDictionaryTask::run()
void HunspellBuildSuggestionsTask::run()
{
- QTime perf;
- perf.start();
+ if (wordList->isEmpty())
+ return;
- wordList->list.append(word);
- wordList->index = 0;
+ wordList->clearSuggestions();
+ QString word = wordList->wordAt(0);
/* Select text codec based on the dictionary encoding.
Hunspell_get_dic_encoding() should always return at least
@@ -138,34 +424,45 @@ void HunspellBuildSuggestionsTask::run()
/* Collect word candidates from the Hunspell suggestions.
Insert word completions in the beginning of the list.
*/
- const int firstWordCompletionIndex = wordList->list.length();
+ const int firstWordCompletionIndex = wordList->size();
int lastWordCompletionIndex = firstWordCompletionIndex;
bool suggestCapitalization = false;
for (int i = 0; i < n; i++) {
QString wordCandidate(textCodec->toUnicode(slst[i]));
- wordCandidate.replace(QChar(0x2019), '\'');
- if (wordCandidate.compare(word) != 0) {
- QString normalizedWordCandidate = removeAccentsAndDiacritics(wordCandidate);
- /* Prioritize word Capitalization */
- if (!suggestCapitalization && !wordCandidate.compare(word, Qt::CaseInsensitive)) {
- wordList->list.insert(1, wordCandidate);
+ 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.startsWith(word) ||
- wordCandidate.contains(QChar('\''))) {
- wordList->list.insert(lastWordCompletionIndex++, wordCandidate);
- } else {
- wordList->list.append(wordCandidate);
}
+ /* Prioritize word completions, missing punctuation or missing accents */
+ } else if ((normalizedWordCandidate.length() > word.length() &&
+ 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->list.length(); i++) {
- if (QString(wordList->list.at(i)).replace(" ", "").compare(word) == 0) {
+ 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->list.move(i, lastWordCompletionIndex);
+ wordList->moveWord(i, lastWordCompletionIndex);
}
lastWordCompletionIndex++;
}
@@ -178,21 +475,28 @@ void HunspellBuildSuggestionsTask::run()
which may be suboptimal for the purpose, but gives some clue
how much the suggested word differs from the given word.
*/
- if (autoCorrect && wordList->list.length() > 1 && (!spellCheck(word) || suggestCapitalization)) {
- if (lastWordCompletionIndex > firstWordCompletionIndex || levenshteinDistance(word, wordList->list.at(firstWordCompletionIndex)) < 3)
- wordList->index = firstWordCompletionIndex;
+ 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);
- qCDebug(lcHunspell) << "HunspellBuildSuggestionsTask::run(): time:" << perf.elapsed() << "ms";
+ 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, textCodec->fromUnicode(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("[0-9]")))
+ if (word.contains(QRegularExpression(QLatin1Literal("[0-9]"))))
return true;
return Hunspell_spell(hunspell, textCodec->fromUnicode(word).constData()) != 0;
}
@@ -243,7 +547,140 @@ QString HunspellBuildSuggestionsTask::removeAccentsAndDiacritics(const QString&
void HunspellUpdateSuggestionsTask::run()
{
- emit updateSuggestions(wordList->list, wordList->index);
+ emit updateSuggestions(wordList, tag);
+}
+
+void HunspellAddWordTask::run()
+{
+ const QTextCodec *textCodec;
+ textCodec = QTextCodec::codecForName(Hunspell_get_dic_encoding(hunspell));
+ if (!textCodec)
+ return;
+
+ QString tmpWord;
+ tmpWord.reserve(64);
+ for (int i = 0, count = wordList->size(); i < count; ++i) {
+ const QString word(wordList->wordAt(i));
+ if (word.length() < 2)
+ continue;
+ Hunspell_add(hunspell, textCodec->fromUnicode(word).constData());
+ if (HunspellAddWordTask::alternativeForm(word, tmpWord))
+ Hunspell_add(hunspell, textCodec->fromUnicode(tmpWord).constData());
+ }
+}
+
+bool HunspellAddWordTask::alternativeForm(const QString &word, QString &alternativeForm)
+{
+ if (word.length() < 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()
+{
+ const QTextCodec *textCodec;
+ textCodec = QTextCodec::codecForName(Hunspell_get_dic_encoding(hunspell));
+ if (!textCodec)
+ 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, textCodec->fromUnicode(word).constData());
+ if (HunspellAddWordTask::alternativeForm(word, tmpWord))
+ Hunspell_remove(hunspell, textCodec->fromUnicode(tmpWord).constData());
+ }
+}
+
+void HunspellLoadWordListTask::run()
+{
+ wordList->clear();
+
+ QFile inputFile(filePath);
+ if (inputFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QTextStream inStream(&inputFile);
+ inStream.setCodec(QTextCodec::codecForName("UTF-8"));
+ 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);
+ outStream.setCodec(QTextCodec::codecForName("UTF-8"));
+ 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);
+ }
+ }
+ }
}
/*!
@@ -253,11 +690,13 @@ void HunspellUpdateSuggestionsTask::run()
HunspellWorker::HunspellWorker(QObject *parent) :
QThread(parent),
+ idleSema(),
taskSema(),
taskLock(),
hunspell(nullptr)
{
abort = false;
+ qRegisterMetaType<QSharedPointer<HunspellWordList>>("QSharedPointer<HunspellWordList>");
}
HunspellWorker::~HunspellWorker()
@@ -282,12 +721,30 @@ void HunspellWorker::removeAllTasks()
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()
{
+ QTime perf;
while (!abort) {
+ idleSema.release();
taskSema.acquire();
if (abort)
break;
+ idleSema.acquire();
QSharedPointer<HunspellTask> currentTask;
{
QMutexLocker guard(&taskLock);
@@ -304,7 +761,9 @@ void HunspellWorker::run()
currentTask->hunspell = hunspell;
else
continue;
+ perf.start();
currentTask->run();
+ qCDebug(lcHunspell) << QString(QLatin1String(currentTask->metaObject()->className()) + "::run(): time:").toLatin1().constData() << perf.elapsed() << "ms";
}
}
if (hunspell) {
diff --git a/src/plugins/hunspell/hunspellinputmethod/hunspellworker_p.h b/src/plugins/hunspell/hunspellinputmethod/hunspellworker_p.h
index 1fe61bba..5ffb34a9 100644
--- a/src/plugins/hunspell/hunspellinputmethod/hunspellworker_p.h
+++ b/src/plugins/hunspell/hunspellinputmethod/hunspellworker_p.h
@@ -46,6 +46,8 @@
#include <QMutex>
#include <QStringList>
#include <QSharedPointer>
+#include <QVector>
+#include <QLoggingCategory>
#include <hunspell/hunspell.h>
QT_BEGIN_NAMESPACE
@@ -53,6 +55,65 @@ class QTextCodec;
namespace QtVirtualKeyboard {
+Q_DECLARE_LOGGING_CATEGORY(lcHunspell)
+
+class 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;
+ QVector<Flags> _flags;
+ QVector<int> _searchIndex;
+ int _index;
+ int _limit;
+};
+
class HunspellTask : public QObject
{
Q_OBJECT
@@ -84,24 +145,11 @@ public:
const QStringList searchPaths;
};
-class HunspellWordList
-{
-public:
- HunspellWordList() :
- list(),
- index(-1)
- {}
-
- QStringList list;
- int index;
-};
-
class HunspellBuildSuggestionsTask : public HunspellTask
{
Q_OBJECT
const QTextCodec *textCodec;
public:
- QString word;
QSharedPointer<HunspellWordList> wordList;
bool autoCorrect;
@@ -120,7 +168,80 @@ public:
void run();
signals:
- void updateSuggestions(const QStringList &wordList, int activeWordIndex);
+ void updateSuggestions(const QSharedPointer<HunspellWordList> &wordList, int tag);
+
+public:
+ int tag;
+};
+
+class HunspellAddWordTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ QSharedPointer<HunspellWordList> wordList;
+
+ void run();
+
+ static bool alternativeForm(const QString &word, QString &alternativeForm);
+};
+
+class HunspellRemoveWordTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ QSharedPointer<HunspellWordList> wordList;
+
+ void run();
+};
+
+class HunspellLoadWordListTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ QSharedPointer<HunspellWordList> wordList;
+ QString filePath;
+
+ void run();
+};
+
+class HunspellSaveWordListTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ QSharedPointer<HunspellWordList> wordList;
+ QString filePath;
+
+ void run();
+};
+
+class HunspellFilterWordTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ HunspellFilterWordTask() :
+ HunspellTask(),
+ startIndex(1)
+ {}
+
+ QSharedPointer<HunspellWordList> wordList;
+ QSharedPointer<HunspellWordList> filterList;
+ int startIndex;
+
+ void run();
+};
+
+class HunspellBoostWordTask : public HunspellTask
+{
+ Q_OBJECT
+public:
+ HunspellBoostWordTask() :
+ HunspellTask()
+ {}
+
+ QSharedPointer<HunspellWordList> wordList;
+ QSharedPointer<HunspellWordList> boostList;
+
+ void run();
};
class HunspellWorker : public QThread
@@ -132,16 +253,19 @@ public:
void addTask(QSharedPointer<HunspellTask> task);
void removeAllTasks();
+ void waitForAllTasks();
template <class X>
- void removeAllTasksExcept() {
+ void removeAllTasksOfType() {
QMutexLocker guard(&taskLock);
for (int i = 0; i < taskList.size();) {
QSharedPointer<X> task(taskList[i].objectCast<X>());
- if (!task)
+ if (task) {
+ qCDebug(lcHunspell) << "Remove task" << QLatin1String(task->metaObject()->className());
taskList.removeAt(i);
- else
+ } else {
i++;
+ }
}
}
@@ -154,6 +278,7 @@ private:
private:
friend class HunspellLoadDictionaryTask;
QList<QSharedPointer<HunspellTask> > taskList;
+ QSemaphore idleSema;
QSemaphore taskSema;
QMutex taskLock;
Hunhandle *hunspell;
@@ -163,4 +288,6 @@ private:
} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
+Q_DECLARE_METATYPE(QSharedPointer<QT_PREPEND_NAMESPACE(QtVirtualKeyboard)::HunspellWordList>);
+
#endif // HUNSPELLWORKER_P_H
diff --git a/src/plugins/lipi-toolkit/plugin/lipiinputmethod.cpp b/src/plugins/lipi-toolkit/plugin/lipiinputmethod.cpp
index 49fadf24..86877623 100644
--- a/src/plugins/lipi-toolkit/plugin/lipiinputmethod.cpp
+++ b/src/plugins/lipi-toolkit/plugin/lipiinputmethod.cpp
@@ -274,6 +274,7 @@ public:
// Double swipe: commit word, or insert space
cancelRecognition();
#ifdef HAVE_HUNSPELL
+ int activeWordIndex = wordCandidates.index();
if (activeWordIndex != -1) {
q->selectionListItemSelected(SelectionListModel::WordCandidateList, activeWordIndex);
return;
diff --git a/src/styles/SelectionListItem.qml b/src/styles/SelectionListItem.qml
index f3c9fdc9..3419b7e2 100644
--- a/src/styles/SelectionListItem.qml
+++ b/src/styles/SelectionListItem.qml
@@ -27,7 +27,7 @@
**
****************************************************************************/
-import QtQuick 2.0
+import QtQuick 2.11
/*!
\qmltype SelectionListItem
@@ -60,5 +60,10 @@ Item {
selectionListItem.ListView.view.currentIndex = index
selectionListItem.ListView.view.model.selectItem(index)
}
+ onPressAndHold: {
+ if (index === -1)
+ return
+ selectionListItem.ListView.view.longPressItem(index)
+ }
}
}
diff --git a/src/virtualkeyboard/abstractinputmethod.cpp b/src/virtualkeyboard/abstractinputmethod.cpp
index 7c51f437..622e6333 100644
--- a/src/virtualkeyboard/abstractinputmethod.cpp
+++ b/src/virtualkeyboard/abstractinputmethod.cpp
@@ -165,6 +165,13 @@ void AbstractInputMethod::selectionListItemSelected(SelectionListModel::Type typ
Q_UNUSED(index)
}
+bool AbstractInputMethod::selectionListRemoveItem(SelectionListModel::Type type, int index)
+{
+ Q_UNUSED(type)
+ Q_UNUSED(index)
+ return false;
+}
+
/*!
\since QtQuick.VirtualKeyboard 2.0
@@ -305,7 +312,7 @@ bool AbstractInputMethod::clickPreeditText(int cursorPosition)
/*!
\fn QVariant QtVirtualKeyboard::AbstractInputMethod::selectionListData(SelectionListModel::Type type, int index, int role)
- Returns item data for the selection list identified by \a type. The \a role
+ Returns item data for the selection list identified by \a type. The \a \l {QtVirtualKeyboard::SelectionListModel::Role}{role}
parameter specifies which data is requested. The \a index parameter is a
zero based index into the list.
*/
@@ -318,6 +325,14 @@ bool AbstractInputMethod::clickPreeditText(int cursorPosition)
*/
/*!
+ \fn bool QtVirtualKeyboard::AbstractInputMethod::selectionListRemoveItem(SelectionListModel::Type type, int index)
+
+ This method is called when an item at \a index must be removed from dictionary.
+ The selection list is identified by the \a type parameter.
+ The function returns \c true if the word was successfully removed.
+*/
+
+/*!
\fn void QtVirtualKeyboard::AbstractInputMethod::selectionListChanged(int type)
The input method emits this signal when the contents of the selection list
diff --git a/src/virtualkeyboard/abstractinputmethod.h b/src/virtualkeyboard/abstractinputmethod.h
index ed272cc9..8689d84d 100644
--- a/src/virtualkeyboard/abstractinputmethod.h
+++ b/src/virtualkeyboard/abstractinputmethod.h
@@ -60,6 +60,7 @@ public:
virtual int selectionListItemCount(SelectionListModel::Type type);
virtual QVariant selectionListData(SelectionListModel::Type type, int index, int role);
virtual void selectionListItemSelected(SelectionListModel::Type type, int index);
+ virtual bool selectionListRemoveItem(SelectionListModel::Type type, int index);
virtual QList<InputEngine::PatternRecognitionMode> patternRecognitionModes() const;
virtual Trace *traceBegin(int traceId, InputEngine::PatternRecognitionMode patternRecognitionMode,
diff --git a/src/virtualkeyboard/content/components/Keyboard.qml b/src/virtualkeyboard/content/components/Keyboard.qml
index 8485d3c2..9bc04ad7 100644
--- a/src/virtualkeyboard/content/components/Keyboard.qml
+++ b/src/virtualkeyboard/content/components/Keyboard.qml
@@ -30,7 +30,7 @@
import QtQuick 2.0
import QtQuick.Layouts 1.0
import QtQuick.Window 2.2
-import QtQuick.VirtualKeyboard 2.2
+import QtQuick.VirtualKeyboard 2.3
import QtQuick.VirtualKeyboard.Styles 2.1
import QtQuick.VirtualKeyboard.Settings 2.2
import Qt.labs.folderlistmodel 2.0
@@ -184,6 +184,10 @@ Item {
}
break
}
+ if (wordCandidateContextMenu.active) {
+ hideWordCandidateContextMenu()
+ break
+ }
if (wordCandidateView.count) {
if (wordCandidateView.currentIndex > 0) {
wordCandidateView.decrementCurrentIndex()
@@ -230,6 +234,14 @@ Item {
alternativeKeys.close()
keyboardInputArea.setActiveKey(null)
keyboardInputArea.navigateToNextKey(0, 0, false)
+ } else if (wordCandidateContextMenu.active) {
+ if (wordCandidateContextMenuList.currentIndex > 0) {
+ wordCandidateContextMenuList.decrementCurrentIndex()
+ } else if (wordCandidateContextMenuList.keyNavigationWraps && wordCandidateContextMenuList.count > 1) {
+ wordCandidateContextMenuList.currentIndex = wordCandidateContextMenuList.count - 1
+ } else {
+ hideWordCandidateContextMenu()
+ }
} else if (keyboard.navigationModeActive && !keyboardInputArea.initialKey && wordCandidateView.count) {
keyboardInputArea.navigateToNextKey(0, 0, false)
initialKey = keyboardInputArea.initialKey
@@ -262,6 +274,10 @@ Item {
}
break
}
+ if (wordCandidateContextMenu.active) {
+ hideWordCandidateContextMenu()
+ break
+ }
if (wordCandidateView.count) {
if (wordCandidateView.currentIndex + 1 < wordCandidateView.count) {
wordCandidateView.incrementCurrentIndex()
@@ -308,6 +324,16 @@ Item {
alternativeKeys.close()
keyboardInputArea.setActiveKey(null)
keyboardInputArea.navigateToNextKey(0, 0, false)
+ } else if (wordCandidateContextMenu.active) {
+ if (wordCandidateContextMenuList.currentIndex + 1 < wordCandidateContextMenuList.count) {
+ wordCandidateContextMenuList.incrementCurrentIndex()
+ } else if (wordCandidateContextMenuList.keyNavigationWraps && wordCandidateContextMenuList.count > 1) {
+ wordCandidateContextMenuList.currentIndex = 0
+ } else {
+ hideWordCandidateContextMenu()
+ keyboardInputArea.setActiveKey(null)
+ keyboardInputArea.navigateToNextKey(0, 0, false)
+ }
} else if (keyboard.navigationModeActive && !keyboardInputArea.initialKey && wordCandidateView.count) {
keyboardInputArea.navigateToNextKey(0, 0, false)
initialKey = keyboardInputArea.initialKey
@@ -343,10 +369,10 @@ Item {
keyboardInputArea.setActiveKey(keyboardInputArea.initialKey)
keyboardInputArea.press(keyboardInputArea.initialKey, true)
}
- } else if (wordCandidateView.count > 0) {
- wordCandidateView.model.selectItem(wordCandidateView.currentIndex)
- if (!InputContext.preeditText.length)
- keyboardInputArea.navigateToNextKey(0, 1, true)
+ } else if (!wordCandidateContextMenu.active && wordCandidateView.count > 0) {
+ if (!isAutoRepeat) {
+ pressAndHoldTimer.restart()
+ }
}
break
default:
@@ -361,13 +387,26 @@ Item {
languagePopupList.model.selectItem(languagePopupList.currentIndex)
break
}
- if (!languagePopupListActive && !alternativeKeys.active && keyboard.activeKey && !isAutoRepeat) {
+ if (isAutoRepeat)
+ break
+ if (!languagePopupListActive && !alternativeKeys.active && !wordCandidateContextMenu.active && keyboard.activeKey) {
keyboardInputArea.release(keyboard.activeKey)
pressAndHoldTimer.stop()
alternativeKeys.close()
keyboardInputArea.setActiveKey(null)
if (!languagePopupListActive && keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
keyboardInputArea.navigateToNextKey(0, 0, false)
+ } else if (wordCandidateContextMenu.active) {
+ if (!wordCandidateContextMenu.openedByNavigationKeyLongPress) {
+ wordCandidateContextMenu.selectCurrentItem()
+ keyboardInputArea.navigateToNextKey(0, 0, false)
+ } else {
+ wordCandidateContextMenu.openedByNavigationKeyLongPress = false
+ }
+ } else if (!wordCandidateContextMenu.active && wordCandidateView.count > 0) {
+ wordCandidateView.model.selectItem(wordCandidateView.currentIndex)
+ if (!InputContext.preeditText.length)
+ keyboardInputArea.navigateToNextKey(0, 1, true)
}
break
default:
@@ -444,6 +483,9 @@ Item {
keyboardInputArea.initialKey = null
if (keyboardInputArea.navigationCursor !== Qt.point(-1, -1))
keyboardInputArea.navigateToNextKey(0, 0, false)
+ } else if (!wordCandidateContextMenu.active) {
+ wordCandidateContextMenu.show(wordCandidateView.currentIndex)
+ wordCandidateContextMenu.openedByNavigationKeyLongPress = true
}
}
}
@@ -507,6 +549,8 @@ Item {
return languagePopupList.highlightItem
} else if (alternativeKeys.listView.count > 0) {
return alternativeKeys.listView.highlightItem
+ } else if (wordCandidateContextMenu.active) {
+ return wordCandidateContextMenuList.highlightItem
} else if (wordCandidateView.count > 0) {
return wordCandidateView.highlightItem
}
@@ -620,6 +664,7 @@ Item {
if (empty)
wordCandidateViewAutoHideTimer.restart()
wordCandidateView.empty = empty
+ keyboard.hideWordCandidateContextMenu()
}
}
Connections {
@@ -664,6 +709,10 @@ Item {
}
}
}
+
+ function longPressItem(index) {
+ return keyboard.showWordCandidateContextMenu(index)
+ }
}
Item {
@@ -1012,6 +1061,7 @@ Item {
}
Item {
+ id: languagePopup
z: 1
anchors.fill: parent
@@ -1021,13 +1071,19 @@ Item {
enabled: languagePopupList.enabled
}
- LanguagePopupList {
+ PopupList {
id: languagePopupList
+ objectName: "languagePopupList"
z: 2
anchors.left: parent.left
anchors.top: parent.top
enabled: false
model: languageListModel
+ delegate: keyboard.style ? keyboard.style.languageListDelegate : null
+ highlight: keyboard.style ? keyboard.style.languageListHighlight : defaultHighlight
+ add: keyboard.style ? keyboard.style.languageListAdd : null
+ remove: keyboard.style ? keyboard.style.languageListRemove : null
+ background: keyboard.style ? keyboard.style.languageListBackground : null
property rect previewRect: Qt.rect(keyboard.x + languagePopupList.x,
keyboard.y + languagePopupList.y,
languagePopupList.width,
@@ -1058,42 +1114,165 @@ Item {
}
}
}
- }
- function showLanguagePopup(parentItem, customLayoutsOnly) {
- if (!languagePopupList.enabled) {
- var locales = keyboard.listLocales(customLayoutsOnly, parent.externalLanguageSwitchEnabled)
- if (parent.externalLanguageSwitchEnabled) {
- var currentIndex = 0
+ function show(locales, parentItem, customLayoutsOnly) {
+ if (!languagePopupList.enabled) {
+ languageListModel.clear()
for (var i = 0; i < locales.length; i++) {
- if (locales[i] === keyboard.locale) {
- currentIndex = i
- break
- }
+ languageListModel.append({localeName: locales[i].name, displayName: locales[i].locale.nativeLanguageName, localeIndex: locales[i].index})
+ if (locales[i].index === keyboard.localeIndex)
+ languagePopupList.currentIndex = i
}
- parent.externalLanguageSwitch(locales, currentIndex)
- return
+ languagePopupList.positionViewAtIndex(languagePopupList.currentIndex, ListView.Center)
+ languagePopupList.anchors.leftMargin = Qt.binding(function() {return Math.round(keyboard.mapFromItem(parentItem, (parentItem.width - languagePopupList.width) / 2, 0).x)})
+ languagePopupList.anchors.topMargin = Qt.binding(function() {return Math.round(keyboard.mapFromItem(parentItem, 0, -languagePopupList.height).y)})
+ }
+ languagePopupList.enabled = true
+ }
+
+ function hide() {
+ if (languagePopupList.enabled) {
+ languagePopupList.enabled = false
+ languagePopupList.anchors.leftMargin = undefined
+ languagePopupList.anchors.topMargin = undefined
+ languageListModel.clear()
}
- languageListModel.clear()
- for (i = 0; i < locales.length; i++) {
- languageListModel.append({localeName: locales[i].name, displayName: locales[i].locale.nativeLanguageName, localeIndex: locales[i].index})
- if (locales[i].index === keyboard.localeIndex)
- languagePopupList.currentIndex = i
+ }
+ }
+
+ function showLanguagePopup(parentItem, customLayoutsOnly) {
+ var locales = keyboard.listLocales(customLayoutsOnly, parent.externalLanguageSwitchEnabled)
+ if (parent.externalLanguageSwitchEnabled) {
+ var currentIndex = 0
+ for (var i = 0; i < locales.length; i++) {
+ if (locales[i] === keyboard.locale) {
+ currentIndex = i
+ break
+ }
}
- languagePopupList.positionViewAtIndex(languagePopupList.currentIndex, ListView.Center)
- languagePopupList.anchors.leftMargin = Qt.binding(function() {return Math.round(keyboard.mapFromItem(parentItem, (parentItem.width - languagePopupList.width) / 2, 0).x)})
- languagePopupList.anchors.topMargin = Qt.binding(function() {return Math.round(keyboard.mapFromItem(parentItem, 0, -languagePopupList.height).y)})
+ parent.externalLanguageSwitch(locales, currentIndex)
+ return
}
- languagePopupList.enabled = true
+ languagePopup.show(locales, parentItem, customLayoutsOnly)
}
function hideLanguagePopup() {
- if (languagePopupList.enabled) {
- languagePopupList.enabled = false
- languagePopupList.anchors.leftMargin = undefined
- languagePopupList.anchors.topMargin = undefined
- languageListModel.clear()
+ languagePopup.hide()
+ }
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: keyboard.parent.parent ? keyboard.parent.parent.height : Screen.height
+ onPressed: keyboard.hideWordCandidateContextMenu()
+ enabled: wordCandidateContextMenuList.enabled
+ }
+
+ Item {
+ id: wordCandidateContextMenu
+ objectName: "wordCandidateContextMenu"
+ z: 1
+ anchors.fill: parent
+ property int previousWordCandidateIndex: -1
+ readonly property bool active: wordCandidateContextMenuList.visible
+ property bool openedByNavigationKeyLongPress
+
+ PopupList {
+ id: wordCandidateContextMenuList
+ objectName: "wordCandidateContextMenuList"
+ z: 2
+ anchors.left: parent.left
+ anchors.top: parent.top
+ enabled: false
+ model: wordCandidateContextMenuListModel
+ property rect previewRect: Qt.rect(keyboard.x + wordCandidateContextMenuList.x,
+ keyboard.y + wordCandidateContextMenuList.y,
+ wordCandidateContextMenuList.width,
+ wordCandidateContextMenuList.height)
}
+
+ ListModel {
+ id: wordCandidateContextMenuListModel
+
+ function selectItem(index) {
+ wordCandidateContextMenu.previousWordCandidateIndex = -1
+ wordCandidateContextMenuList.currentIndex = index
+ keyboard.soundEffect.play(wordCandidateContextMenuList.currentItem.soundEffect)
+ switch (get(index).action) {
+ case "remove":
+ wordCandidateView.model.removeItem(wordCandidateView.currentIndex)
+ break
+ }
+ keyboard.hideWordCandidateContextMenu()
+ }
+ }
+
+ function show(wordCandidateIndex) {
+ if (wordCandidateContextMenu.enabled)
+ wordCandidateContextMenu.hide()
+
+ wordCandidateContextMenuListModel.clear()
+
+ var canRemoveSuggestion = wordCandidateView.model.dataAt(wordCandidateIndex, SelectionListModel.CanRemoveSuggestionRole)
+ if (canRemoveSuggestion) {
+ var dictionaryType = wordCandidateView.model.dataAt(wordCandidateIndex, SelectionListModel.DictionaryTypeRole)
+ var removeItemText;
+ switch (dictionaryType) {
+ case SelectionListModel.UserDictionary:
+ //~ VirtualKeyboard Context menu for word suggestion if it can be removed from the user dictionary.
+ removeItemText = qsTr("Remove from dictionary")
+ break
+ case SelectionListModel.DefaultDictionary:
+ // Fallthrough
+ default:
+ //~ VirtualKeyboard Context menu for word suggestion if it can be removed from the default dictionary.
+ removeItemText = qsTr("Block word")
+ break
+ }
+ wordCandidateContextMenuListModel.append({action: "remove", display: removeItemText, wordCompletionLength: 0})
+ }
+
+ if (wordCandidateContextMenuListModel.count === 0)
+ return
+
+ previousWordCandidateIndex = wordCandidateView.currentIndex
+ wordCandidateView.currentIndex = wordCandidateIndex
+
+ wordCandidateContextMenuList.anchors.leftMargin = Qt.binding(function() {
+ var leftBorder = Math.round(wordCandidateView.mapFromItem(wordCandidateView.currentItem, (wordCandidateView.currentItem.width - wordCandidateContextMenuList.width) / 2, 0).x)
+ var rightBorder = Math.round(wordCandidateContextMenuList.parent.width - wordCandidateContextMenuList.width)
+ return Math.min(leftBorder, rightBorder)
+ })
+
+ wordCandidateContextMenuList.enabled = true
+ }
+
+ function hide() {
+ if (wordCandidateContextMenuList.enabled) {
+ if (previousWordCandidateIndex !== -1) {
+ wordCandidateView.currentIndex = previousWordCandidateIndex
+ previousWordCandidateIndex = -1
+ }
+ wordCandidateContextMenuList.enabled = false
+ wordCandidateContextMenuList.anchors.leftMargin = undefined
+ wordCandidateContextMenuListModel.clear()
+ }
+ openedByNavigationKeyLongPress = false
+ }
+
+ function selectCurrentItem() {
+ if (active && wordCandidateContextMenuList.currentIndex !== -1)
+ wordCandidateContextMenuListModel.selectItem(wordCandidateContextMenuList.currentIndex)
+ }
+ }
+
+ function showWordCandidateContextMenu(wordCandidateIndex) {
+ wordCandidateContextMenu.show(wordCandidateIndex)
+ }
+
+ function hideWordCandidateContextMenu() {
+ wordCandidateContextMenu.hide()
}
function updateInputMethod() {
diff --git a/src/virtualkeyboard/content/components/LanguagePopupList.qml b/src/virtualkeyboard/content/components/PopupList.qml
index 2c8b8c99..dcd02ee1 100644
--- a/src/virtualkeyboard/content/components/LanguagePopupList.qml
+++ b/src/virtualkeyboard/content/components/PopupList.qml
@@ -28,15 +28,14 @@
****************************************************************************/
import QtQuick 2.0
-import QtQuick.VirtualKeyboard 2.1
+import QtQuick.VirtualKeyboard 2.3
ListView {
- id: languagePopupList
- objectName: "languagePopupList"
-
property int maxVisibleItems: 5
readonly property int preferredVisibleItems: count < maxVisibleItems ? count : maxVisibleItems
readonly property real contentWidth: contentItem.childrenRect.width
+ property alias background: popupListBackground.sourceComponent
+ property alias defaultHighlight: defaultHighlight
clip: true
visible: enabled && count > 0
@@ -44,12 +43,12 @@ ListView {
height: currentItem ? currentItem.height * preferredVisibleItems + (spacing * preferredVisibleItems - 1) : 0
orientation: ListView.Vertical
snapMode: ListView.SnapToItem
- delegate: keyboard.style.languageListDelegate
- highlight: keyboard.style.languageListHighlight ? keyboard.style.languageListHighlight : defaultHighlight
+ delegate: keyboard.style.popupListDelegate
+ highlight: keyboard.style.popupListHighlight ? keyboard.style.popupListHighlight : defaultHighlight
highlightMoveDuration: 0
highlightResizeDuration: 0
- add: keyboard.style.languageListAdd
- remove: keyboard.style.languageListRemove
+ add: keyboard.style.popupListAdd
+ remove: keyboard.style.popupListRemove
keyNavigationWraps: true
onCurrentItemChanged: if (currentItem) keyboard.soundEffect.register(currentItem.soundEffect)
@@ -60,7 +59,8 @@ ListView {
}
Loader {
- sourceComponent: keyboard.style.languageListBackground
+ id: popupListBackground
+ sourceComponent: keyboard.style.popupListBackground
anchors.fill: parent
z: -1
}
diff --git a/src/virtualkeyboard/content/components/WordCandidatePopupList.qml b/src/virtualkeyboard/content/components/WordCandidatePopupList.qml
index 7740cbf9..e255142a 100644
--- a/src/virtualkeyboard/content/components/WordCandidatePopupList.qml
+++ b/src/virtualkeyboard/content/components/WordCandidatePopupList.qml
@@ -28,12 +28,11 @@
****************************************************************************/
import QtQuick 2.0
-import QtQuick.VirtualKeyboard 2.1
+import QtQuick.VirtualKeyboard 2.3
-ListView {
+PopupList {
id: wordCandidatePopupList
- property int maxVisibleItems: 5
readonly property int preferredVisibleItems: {
if (!currentItem)
return 0
@@ -43,13 +42,10 @@ ListView {
--result
return result
}
- readonly property real contentWidth: contentItem.childrenRect.width
readonly property bool flipVertical: currentItem &&
Qt.inputMethod.cursorRectangle.y + (Qt.inputMethod.cursorRectangle.height / 2) > (parent.height / 2) &&
Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height + (currentItem.height * 2) > parent.height
- clip: true
- visible: enabled && count > 0
height: currentItem ? currentItem.height * preferredVisibleItems + (spacing * preferredVisibleItems - 1) : 0
Binding {
target: wordCandidatePopupList
@@ -66,19 +62,9 @@ ListView {
value: Math.round(wordCandidatePopupList.flipVertical ? Qt.inputMethod.cursorRectangle.y - wordCandidatePopupList.height : Qt.inputMethod.cursorRectangle.y + Qt.inputMethod.cursorRectangle.height)
when: wordCandidatePopupList.visible
}
- orientation: ListView.Vertical
- snapMode: ListView.SnapToItem
- delegate: keyboard.style.popupListDelegate
- highlight: keyboard.style.popupListHighlight ? keyboard.style.popupListHighlight : null
- highlightMoveDuration: 0
- highlightResizeDuration: 0
- add: keyboard.style.popupListAdd
- remove: keyboard.style.popupListRemove
- keyNavigationWraps: true
model: enabled ? InputContext.inputEngine.wordCandidateListModel : null
onContentWidthChanged: viewResizeTimer.restart()
- onCurrentItemChanged: if (currentItem) keyboard.soundEffect.register(currentItem.soundEffect)
Timer {
id: viewResizeTimer
@@ -92,10 +78,4 @@ ListView {
onActiveItemChanged: wordCandidatePopupList.currentIndex = index
onItemSelected: if (wordCandidatePopupList.currentItem) keyboard.soundEffect.play(wordCandidatePopupList.currentItem.soundEffect)
}
-
- Loader {
- sourceComponent: keyboard.style.popupListBackground
- anchors.fill: parent
- z: -1
- }
}
diff --git a/src/virtualkeyboard/content/content.qrc b/src/virtualkeyboard/content/content.qrc
index d29dbe33..1e6392f4 100644
--- a/src/virtualkeyboard/content/content.qrc
+++ b/src/virtualkeyboard/content/content.qrc
@@ -27,7 +27,7 @@
<file>components/TraceInputArea.qml</file>
<file>components/HandwritingModeKey.qml</file>
<file>components/WordCandidatePopupList.qml</file>
- <file>components/LanguagePopupList.qml</file>
+ <file>components/PopupList.qml</file>
<file>components/SelectionControl.qml</file>
<file>components/ShadowInputControl.qml</file>
<file>components/InputModeKey.qml</file>
diff --git a/src/virtualkeyboard/selectionlistmodel.cpp b/src/virtualkeyboard/selectionlistmodel.cpp
index 1f8f9db9..33ec8de9 100644
--- a/src/virtualkeyboard/selectionlistmodel.cpp
+++ b/src/virtualkeyboard/selectionlistmodel.cpp
@@ -118,6 +118,22 @@ public:
the completion part expressed as the
number of characters counted from the
end of the string.
+ \value DictionaryTypeRole
+ An integer specifying \ l {QtVirtualKeyboard::SelectionListModel::DictionaryType}{dictionary type}.
+ \value CanRemoveSuggestionRole
+ A boolean value indicating if the word candidate
+ can be removed from dictionary.
+*/
+
+/*!
+ \enum QtVirtualKeyboard::SelectionListModel::DictionaryType
+
+ This enum specifies the dictionary type of a word.
+
+ \value DefaultDictionary
+ The word candidate is from the default dictionary.
+ \value UserDictionary
+ The word candidate is from the user dictionary.
*/
SelectionListModel::SelectionListModel(QObject *parent) :
@@ -226,6 +242,14 @@ void SelectionListModel::selectItem(int index)
}
}
+void SelectionListModel::removeItem(int index)
+{
+ Q_D(SelectionListModel);
+ if (index >= 0 && index < d->rowCount && d->dataSource) {
+ d->dataSource->selectionListRemoveItem(d->type, index);
+ }
+}
+
/*!
* \internal
*/
diff --git a/src/virtualkeyboard/selectionlistmodel.h b/src/virtualkeyboard/selectionlistmodel.h
index ec9b84e3..22d0ed3d 100644
--- a/src/virtualkeyboard/selectionlistmodel.h
+++ b/src/virtualkeyboard/selectionlistmodel.h
@@ -56,11 +56,19 @@ public:
enum Role
{
DisplayRole = Qt::DisplayRole,
- WordCompletionLengthRole = Qt::UserRole + 1
+ WordCompletionLengthRole = Qt::UserRole + 1,
+ DictionaryTypeRole,
+ CanRemoveSuggestionRole,
+ };
+ enum DictionaryType
+ {
+ DefaultDictionary = 0,
+ UserDictionary
};
Q_ENUM(Type)
Q_ENUM(Role)
+ Q_ENUM(DictionaryType)
~SelectionListModel();
void setDataSource(AbstractInputMethod *dataSource, Type type);
@@ -72,6 +80,7 @@ public:
int count() const;
Q_INVOKABLE void selectItem(int index);
+ Q_INVOKABLE void removeItem(int index);
Q_INVOKABLE QVariant dataAt(int index, int role = Qt::DisplayRole) const;
Q_SIGNALS:
@@ -92,5 +101,6 @@ QT_END_NAMESPACE
Q_DECLARE_METATYPE(QT_PREPEND_NAMESPACE(QtVirtualKeyboard)::SelectionListModel::Type)
Q_DECLARE_METATYPE(QT_PREPEND_NAMESPACE(QtVirtualKeyboard)::SelectionListModel::Role)
+Q_DECLARE_METATYPE(QT_PREPEND_NAMESPACE(QtVirtualKeyboard)::SelectionListModel::DictionaryType)
#endif // SELECTIONLISTMODEL_H
diff --git a/tests/auto/inputpanel/data/inputpanel/inputpanel.qml b/tests/auto/inputpanel/data/inputpanel/inputpanel.qml
index a467fb15..c3682b8a 100644
--- a/tests/auto/inputpanel/data/inputpanel/inputpanel.qml
+++ b/tests/auto/inputpanel/data/inputpanel/inputpanel.qml
@@ -67,6 +67,7 @@ InputPanel {
naviationHighlight.widthAnimation.running ||
naviationHighlight.heightAnimation.running
readonly property var wordCandidateView: Utils.findChildByProperty(keyboard, "objectName", "wordCandidateView", null)
+ readonly property var wordCandidateContextMenu: Utils.findChildByProperty(keyboard, "objectName", "wordCandidateContextMenu", null)
readonly property var shadowInputControl: Utils.findChildByProperty(keyboard, "objectName", "shadowInputControl", null)
readonly property var shadowInput: Utils.findChildByProperty(keyboard, "objectName", "shadowInput", null)
readonly property var selectionControl: Utils.findChildByProperty(inputPanel, "objectName", "selectionControl", null)
@@ -178,6 +179,12 @@ InputPanel {
}
SignalSpy {
+ id: wordCandidateContextMenuActiveSpy
+ target: wordCandidateContextMenu
+ signalName: "onActiveChanged"
+ }
+
+ SignalSpy {
id: shiftStateSpy
target: InputContext
signalName: "onShiftChanged"
@@ -530,6 +537,14 @@ InputPanel {
InputContext.shiftHandler.toggleShift()
}
+ function setShift(shift) {
+ InputContext.shift = shift
+ }
+
+ function setCapsLock(capsLock) {
+ InputContext.capsLock = capsLock
+ }
+
function style() {
return VirtualKeyboardSettings.styleName
}
@@ -583,6 +598,48 @@ InputPanel {
return true
}
+ function selectionListCurrentIndex() {
+ return inputPanel.wordCandidateView.currentIndex
+ }
+
+ function selectionListSuggestionIsFromUserDictionary() {
+ if (!inputPanel.wordCandidateView.currentItem)
+ return false
+ var dictionaryType = inputPanel.wordCandidateView.model.dataAt(inputPanel.wordCandidateView.currentIndex, SelectionListModel.DictionaryTypeRole)
+ return dictionaryType !== undefined && dictionaryType === SelectionListModel.UserDictionary
+ }
+
+ function openWordCandidateContextMenu() {
+ if (!inputPanel.wordCandidateView.currentItem)
+ return false
+ testcase.wait(200)
+ wordCandidateContextMenuActiveSpy.clear()
+ testcase.mousePress(inputPanel.wordCandidateView.currentItem)
+ wordCandidateContextMenuActiveSpy.wait()
+ testcase.mouseRelease(inputPanel.wordCandidateView.currentItem)
+ return wordCandidateContextMenu.active
+ }
+
+ function selectItemFromWordCandidateContextMenu(index) {
+ if (!inputPanel.wordCandidateView.currentItem)
+ return false
+ if (!wordCandidateContextMenu.active)
+ return false
+ var wordCandidateContextMenuList = Utils.findChildByProperty(keyboard, "objectName", "wordCandidateContextMenuList", null)
+ if (wordCandidateContextMenuList.currentIndex !== index) {
+ wordCandidateContextMenuList.currentIndex = index
+ testcase.waitForRendering(inputPanel)
+ }
+ if (!wordCandidateContextMenuList.currentItem)
+ return false
+ var itemPos = inputPanel.mapFromItem(wordCandidateContextMenuList.currentItem,
+ wordCandidateContextMenuList.currentItem.width / 2,
+ wordCandidateContextMenuList.currentItem.height / 2)
+ testcase.mouseClick(inputPanel, itemPos.x, itemPos.y, Qt.LeftButton, 0, 20)
+ testcase.waitForRendering(inputPanel)
+ return true
+ }
+
function setHandwritingMode(enabled) {
if (inputPanel.keyboard.handwritingMode !== enabled) {
if (!enabled || inputPanel.keyboard.isHandwritingAvailable())
diff --git a/tests/auto/inputpanel/data/tst_inputpanel.qml b/tests/auto/inputpanel/data/tst_inputpanel.qml
index 83c97593..e2efe8b0 100644
--- a/tests/auto/inputpanel/data/tst_inputpanel.qml
+++ b/tests/auto/inputpanel/data/tst_inputpanel.qml
@@ -1820,19 +1820,14 @@ Rectangle {
skip("Prediction/spell correction not enabled")
for (var len = 1; len <= 5; ++len) {
- inputPanel.wordCandidateListChangedSpy.clear()
inputPanel.virtualKeyClick("z")
- waitForRendering(inputPanel)
- if (len >= 3) {
- if (data.wclAutoCommitWord)
- tryVerify(function() { return inputPanel.wordCandidateView.model.count === 0 }, 500)
- else
- wait(500)
+ if (len >= 2) {
+ inputPanel.wordCandidateListChangedSpy.clear()
+ inputPanel.wordCandidateListChangedSpy.wait()
if (inputPanel.wordCandidateView.model.count <= 1)
break
}
}
- waitForRendering(inputPanel)
if (data.wclAutoCommitWord)
compare(inputPanel.wordCandidateView.model.count, 0)
@@ -2018,5 +2013,67 @@ Rectangle {
compare(inputPanel.shadowInput.text, "")
}
+ function test_userDictionary_data() {
+ return [
+ { inputSequence: ['a','s','d','f'], initShift: false },
+ { inputSequence: ['a','s','d'], initShift: false, expectedSuggestion: "asdf", suggestionIsFromUserDictionary: true },
+ { inputSequence: ['a','s','d'], initShift: true, expectedSuggestion: "Asdf", suggestionIsFromUserDictionary: true },
+ //
+ { inputSequence: ['s','d','f','a'], initShift: true },
+ { inputSequence: ['s','d','f'], initShift: true, expectedSuggestion: "Sdfa", suggestionIsFromUserDictionary: true },
+ { inputSequence: ['s','d','f'], initShift: false, expectedSuggestion: "sdfa", suggestionIsFromUserDictionary: true, removeSuggestion: true },
+ //
+ { inputSequence: ['d','f','a','s'], initCapsLock: true },
+ { inputSequence: ['d','f','a'], initCapsLock: true, expectedSuggestion: "DFAS", suggestionIsFromUserDictionary: true },
+ { inputSequence: ['d','f','a'], initShift: false, unexpectedSuggestion: "dfas", suggestionIsFromUserDictionary: true },
+ //
+ { inputSequence: ['f','a','s','d'], initShift: false, initInputMethodHints: Qt.ImhSensitiveData },
+ { inputSequence: ['f','a','s'], initShift: false, unexpectedSuggestion: "fasd" },
+ { inputSequence: ['f','a','s'], initShift: true, unexpectedSuggestion: "Fasd"},
+ //
+ { initLocale: "en_GB", inputSequence: "windo", expectedSuggestion: "Window", suggestionIsFromUserDictionary: false, removeSuggestion: true },
+ { initLocale: "en_GB", inputSequence: "window", },
+ { initLocale: "en_GB", inputSequence: "windo", expectedSuggestion: "Window", suggestionIsFromUserDictionary: false },
+ ]
+ }
+
+ function test_userDictionary(data) {
+ prepareTest(data, true)
+
+ if (!inputPanel.wordCandidateListVisibleHint)
+ skip("Prediction/spell correction not enabled")
+
+ if (data.hasOwnProperty("initShift"))
+ inputPanel.setShift(data.initShift)
+ if (data.hasOwnProperty("initCapsLock"))
+ inputPanel.setCapsLock(data.initCapsLock)
+
+ for (var inputIndex in data.inputSequence)
+ inputPanel.virtualKeyClick(data.inputSequence[inputIndex])
+
+ if (data.hasOwnProperty("expectedSuggestion")) {
+ tryVerify(function() {return inputPanel.selectionListSearchSuggestion(data.expectedSuggestion)}, 1000, "The expected spell correction suggestion \"%1\" was not found".arg(data.expectedSuggestion))
+ verify(inputPanel.selectionListCurrentIndex() > 0)
+ if (data.hasOwnProperty("suggestionIsFromUserDictionary"))
+ compare(inputPanel.selectionListSuggestionIsFromUserDictionary(), data.suggestionIsFromUserDictionary)
+ if (data.hasOwnProperty("removeSuggestion") && data.removeSuggestion) {
+ verify(inputPanel.openWordCandidateContextMenu())
+ inputPanel.wordCandidateListChangedSpy.clear()
+ verify(inputPanel.selectItemFromWordCandidateContextMenu(0))
+ inputPanel.wordCandidateListChangedSpy.wait()
+ tryVerify(function() {return !inputPanel.selectionListSearchSuggestion(data.expectedSuggestion)}, 1000, "An unexpected spell correction suggestion \"%1\" was found".arg(data.unexpectedSuggestion))
+ } else {
+ inputPanel.selectionListSelectCurrentItem()
+ }
+ } else if (data.hasOwnProperty("unexpectedSuggestion")) {
+ var oldIndex = inputPanel.selectionListCurrentIndex()
+ tryVerify(function() {return !inputPanel.selectionListSearchSuggestion(data.unexpectedSuggestion)}, 1000, "An unexpected spell correction suggestion \"%1\" was found".arg(data.unexpectedSuggestion))
+ compare(inputPanel.selectionListCurrentIndex(), oldIndex)
+ } else {
+ inputPanel.selectionListSelectCurrentItem()
+ }
+
+ Qt.inputMethod.reset()
+ }
}
}
diff --git a/tests/auto/inputpanel/tst_inputpanel.cpp b/tests/auto/inputpanel/tst_inputpanel.cpp
index 8b84f67b..409383c7 100644
--- a/tests/auto/inputpanel/tst_inputpanel.cpp
+++ b/tests/auto/inputpanel/tst_inputpanel.cpp
@@ -29,7 +29,22 @@
#include <QtQuickTest/quicktest.h>
#include <QByteArray>
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <QDir>
static bool s_configEnv = qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));
+static bool initStandardPaths() {
+ QStandardPaths::setTestModeEnabled(true);
+ auto configLocations = QStringList()
+ << QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/qtvirtualkeyboard"
+ << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/qtvirtualkeyboard";
+ for (const QString &configLocation : configLocations) {
+ if (configLocation != "/qtvirtualkeyboard")
+ QDir(configLocation).removeRecursively();
+ }
+ return true;
+}
+static bool s_initStandardPaths = initStandardPaths();
QUICK_TEST_MAIN(inputpanel)