diff options
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) |