blob: 4651ac1f96349037125f92b6014bb227ebd3c5f8 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtHunspellInputMethod/private/hunspellinputmethod_p_p.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
#include <hunspell/hunspell.h>
#include <QStringList>
#include <QDir>
#include <QTextCodec>
#include <QtCore/QLibraryInfo>
#include <QStandardPaths>
QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {
const int HunspellInputMethodPrivate::userDictionaryMaxSize = 100;
/*!
\class QtVirtualKeyboard::HunspellInputMethodPrivate
\internal
*/
HunspellInputMethodPrivate::HunspellInputMethodPrivate(HunspellInputMethod *q_ptr) :
q_ptr(q_ptr),
hunspellWorker(new HunspellWorker()),
locale(),
wordCompletionPoint(2),
ignoreUpdate(false),
autoSpaceAllowed(false),
dictionaryState(DictionaryNotLoaded),
userDictionaryWords(new HunspellWordList(userDictionaryMaxSize)),
blacklistedWords(new HunspellWordList(userDictionaryMaxSize)),
wordCandidatesUpdateTag(0)
{
if (hunspellWorker)
hunspellWorker->start();
}
HunspellInputMethodPrivate::~HunspellInputMethodPrivate()
{
}
bool HunspellInputMethodPrivate::createHunspell(const QString &locale)
{
Q_Q(HunspellInputMethod);
if (!hunspellWorker)
return false;
if (this->locale != locale) {
clearSuggestionsRelatedTasks();
hunspellWorker->waitForAllTasks();
QString hunspellDataPath(qEnvironmentVariable("QT_VIRTUALKEYBOARD_HUNSPELL_DATA_PATH"));
const QString pathListSep(
#if defined(Q_OS_WIN32)
QStringLiteral(";")
#else
QStringLiteral(":")
#endif
);
QStringList searchPaths(hunspellDataPath.split(pathListSep, QString::SkipEmptyParts));
const QStringList defaultPaths = QStringList()
<< QDir(QLibraryInfo::location(QLibraryInfo::DataPath) + QStringLiteral("/qtvirtualkeyboard/hunspell")).absolutePath()
#if !defined(Q_OS_WIN32)
<< QStringLiteral("/usr/share/hunspell")
<< QStringLiteral("/usr/share/myspell/dicts")
#endif
;
for (const QString &defaultPath : defaultPaths) {
if (!searchPaths.contains(defaultPath))
searchPaths.append(defaultPath);
}
QSharedPointer<HunspellLoadDictionaryTask> loadDictionaryTask(new HunspellLoadDictionaryTask(locale, searchPaths));
QObject::connect(loadDictionaryTask.data(), &HunspellLoadDictionaryTask::completed, q, &HunspellInputMethod::dictionaryLoadCompleted);
dictionaryState = HunspellInputMethodPrivate::DictionaryLoading;
emit q->selectionListsChanged();
hunspellWorker->addTask(loadDictionaryTask);
this->locale = locale;
loadCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
addToHunspell(userDictionaryWords);
loadCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
removeFromHunspell(blacklistedWords);
}
return true;
}
void HunspellInputMethodPrivate::reset()
{
if (clearSuggestions(true)) {
Q_Q(HunspellInputMethod);
emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, wordCandidates.index());
}
autoSpaceAllowed = false;
}
bool HunspellInputMethodPrivate::updateSuggestions()
{
bool wordCandidateListChanged = false;
QString word = wordCandidates.wordAt(0);
if (!word.isEmpty() && dictionaryState != HunspellInputMethodPrivate::DictionaryNotLoaded) {
wordCandidateListChanged = true;
if (word.length() >= wordCompletionPoint) {
if (hunspellWorker) {
QSharedPointer<HunspellWordList> wordList(new HunspellWordList(wordCandidates));
// Clear obsolete tasks from the worker queue
clearSuggestionsRelatedTasks();
// Build suggestions
QSharedPointer<HunspellBuildSuggestionsTask> buildSuggestionsTask(new HunspellBuildSuggestionsTask());
buildSuggestionsTask->wordList = wordList;
buildSuggestionsTask->autoCorrect = false;
hunspellWorker->addTask(buildSuggestionsTask);
// Filter out blacklisted word (sometimes Hunspell suggests,
// e.g. with different text case)
QSharedPointer<HunspellFilterWordTask> filterWordTask(new HunspellFilterWordTask());
filterWordTask->wordList = wordList;
filterWordTask->filterList = blacklistedWords;
hunspellWorker->addTask(filterWordTask);
// Boost words from user dictionary
QSharedPointer<HunspellBoostWordTask> boostWordTask(new HunspellBoostWordTask());
boostWordTask->wordList = wordList;
boostWordTask->boostList = userDictionaryWords;
hunspellWorker->addTask(boostWordTask);
// Update word candidate list
QSharedPointer<HunspellUpdateSuggestionsTask> updateSuggestionsTask(new HunspellUpdateSuggestionsTask());
updateSuggestionsTask->wordList = wordList;
updateSuggestionsTask->tag = ++wordCandidatesUpdateTag;
Q_Q(HunspellInputMethod);
QObject::connect(updateSuggestionsTask.data(), &HunspellUpdateSuggestionsTask::updateSuggestions, q, &HunspellInputMethod::updateSuggestions);
hunspellWorker->addTask(updateSuggestionsTask);
}
}
} else {
wordCandidateListChanged = clearSuggestions();
}
return wordCandidateListChanged;
}
bool HunspellInputMethodPrivate::clearSuggestions(bool clearInputWord)
{
clearSuggestionsRelatedTasks();
return clearInputWord ? wordCandidates.clear() : wordCandidates.clearSuggestions();
}
void HunspellInputMethodPrivate::clearSuggestionsRelatedTasks()
{
if (hunspellWorker) {
hunspellWorker->removeAllTasksOfType<HunspellBuildSuggestionsTask>();
hunspellWorker->removeAllTasksOfType<HunspellFilterWordTask>();
hunspellWorker->removeAllTasksOfType<HunspellBoostWordTask>();
hunspellWorker->removeAllTasksOfType<HunspellUpdateSuggestionsTask>();
}
}
bool HunspellInputMethodPrivate::isAutoSpaceAllowed() const
{
Q_Q(const HunspellInputMethod);
if (!autoSpaceAllowed)
return false;
if (q->inputEngine()->inputMode() == QVirtualKeyboardInputEngine::InputMode::Numeric)
return false;
QVirtualKeyboardInputContext *ic = q->inputContext();
if (!ic)
return false;
Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
return !inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) &&
!inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly);
}
bool HunspellInputMethodPrivate::isValidInputChar(const QChar &c) const
{
if (c.isLetterOrNumber())
return true;
if (isJoiner(c))
return true;
if (c.isMark())
return true;
return false;
}
bool HunspellInputMethodPrivate::isJoiner(const QChar &c) const
{
if (c.isPunct() || c.isSymbol()) {
Q_Q(const HunspellInputMethod);
QVirtualKeyboardInputContext *ic = q->inputContext();
if (ic) {
Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) || inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly))
return QString(QStringLiteral(":/?#[]@!$&'()*+,;=-_.%")).contains(c);
}
ushort unicode = c.unicode();
if (unicode == Qt::Key_Apostrophe || unicode == Qt::Key_Minus)
return true;
}
return false;
}
QString HunspellInputMethodPrivate::customDictionaryLocation(const QString &dictionaryType) const
{
if (dictionaryType.isEmpty() || locale.isEmpty())
return QString();
QString location = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
if (location.isEmpty())
return QString();
return QStringLiteral("%1/qtvirtualkeyboard/hunspell/%2-%3.txt")
.arg(location)
.arg(dictionaryType)
.arg(locale);
}
void HunspellInputMethodPrivate::loadCustomDictionary(const QSharedPointer<HunspellWordList> &wordList,
const QString &dictionaryType) const
{
QSharedPointer<HunspellLoadWordListTask> loadWordsTask(new HunspellLoadWordListTask());
loadWordsTask->filePath = customDictionaryLocation(dictionaryType);
loadWordsTask->wordList = wordList;
hunspellWorker->addTask(loadWordsTask);
}
void HunspellInputMethodPrivate::saveCustomDictionary(const QSharedPointer<HunspellWordList> &wordList,
const QString &dictionaryType) const
{
QSharedPointer<HunspellSaveWordListTask> saveWordsTask(new HunspellSaveWordListTask());
saveWordsTask->filePath = customDictionaryLocation(dictionaryType);
saveWordsTask->wordList = wordList;
hunspellWorker->addTask(saveWordsTask);
}
void HunspellInputMethodPrivate::addToHunspell(const QSharedPointer<HunspellWordList> &wordList) const
{
QSharedPointer<HunspellAddWordTask> addWordTask(new HunspellAddWordTask());
addWordTask->wordList = wordList;
hunspellWorker->addTask(addWordTask);
}
void HunspellInputMethodPrivate::removeFromHunspell(const QSharedPointer<HunspellWordList> &wordList) const
{
QSharedPointer<HunspellRemoveWordTask> removeWordTask(new HunspellRemoveWordTask());
removeWordTask->wordList = wordList;
hunspellWorker->addTask(removeWordTask);
}
void HunspellInputMethodPrivate::removeFromDictionary(const QString &word)
{
if (userDictionaryWords->removeWord(word) > 0) {
saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
} else if (!blacklistedWords->contains(word)) {
blacklistedWords->appendWord(word);
saveCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
}
QSharedPointer<HunspellWordList> wordList(new HunspellWordList());
wordList->appendWord(word);
removeFromHunspell(wordList);
updateSuggestions();
}
void HunspellInputMethodPrivate::addToDictionary()
{
Q_Q(HunspellInputMethod);
// This feature is not allowed when dealing with sensitive information
const Qt::InputMethodHints inputMethodHints(q->inputContext()->inputMethodHints());
const bool userDictionaryEnabled =
!inputMethodHints.testFlag(Qt::ImhHiddenText) &&
!inputMethodHints.testFlag(Qt::ImhSensitiveData);
if (!userDictionaryEnabled)
return;
if (wordCandidates.isEmpty())
return;
QString word;
HunspellWordList::Flags wordFlags;
const int activeWordIndex = wordCandidates.index();
wordCandidates.wordAt(activeWordIndex, word, wordFlags);
if (activeWordIndex == 0) {
if (blacklistedWords->removeWord(word) > 0) {
saveCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
} else if (word.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