1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 or (at your option) any later version
20 ** approved by the KDE Free Qt Foundation. The licenses are as published by
21 ** the Free Software Foundation and appearing in the file LICENSE.GPL3
22 ** included in the packaging of this file. Please review the following
23 ** information to ensure the GNU General Public License requirements will
24 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25 **
26 ** $QT_END_LICENSE$
27 **
28 ****************************************************************************/
29
30 #include <QtHunspellInputMethod/private/hunspellinputmethod_p_p.h>
31 #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
32 #include <hunspell/hunspell.h>
33 #include <QStringList>
34 #include <QDir>
35 #include <QTextCodec>
36 #include <QtCore/QLibraryInfo>
37 #include <QStandardPaths>
38
39 QT_BEGIN_NAMESPACE
40 namespace QtVirtualKeyboard {
41
42 const int HunspellInputMethodPrivate::userDictionaryMaxSize = 100;
43
44 /*!
45 \class QtVirtualKeyboard::HunspellInputMethodPrivate
46 \internal
47 */
48
HunspellInputMethodPrivate(HunspellInputMethod * q_ptr)49 HunspellInputMethodPrivate::HunspellInputMethodPrivate(HunspellInputMethod *q_ptr) :
50 q_ptr(q_ptr),
51 hunspellWorker(new HunspellWorker()),
52 locale(),
53 wordCompletionPoint(2),
54 ignoreUpdate(false),
55 autoSpaceAllowed(false),
56 dictionaryState(DictionaryNotLoaded),
57 userDictionaryWords(new HunspellWordList(userDictionaryMaxSize)),
58 blacklistedWords(new HunspellWordList(userDictionaryMaxSize)),
59 wordCandidatesUpdateTag(0)
60 {
61 if (hunspellWorker)
62 hunspellWorker->start();
63 }
64
~HunspellInputMethodPrivate()65 HunspellInputMethodPrivate::~HunspellInputMethodPrivate()
66 {
67 }
68
createHunspell(const QString & locale)69 bool HunspellInputMethodPrivate::createHunspell(const QString &locale)
70 {
71 Q_Q(HunspellInputMethod);
72 if (!hunspellWorker)
73 return false;
74 if (this->locale != locale) {
75 clearSuggestionsRelatedTasks();
76 hunspellWorker->waitForAllTasks();
77 QString hunspellDataPath(qEnvironmentVariable("QT_VIRTUALKEYBOARD_HUNSPELL_DATA_PATH"));
78 const QString pathListSep(
79 #if defined(Q_OS_WIN32)
80 QStringLiteral(";")
81 #else
82 QStringLiteral(":")
83 #endif
84 );
85 QStringList searchPaths(hunspellDataPath.split(pathListSep, Qt::SkipEmptyParts));
86 const QStringList defaultPaths = QStringList()
87 << QDir(QLibraryInfo::location(QLibraryInfo::DataPath) + QStringLiteral("/qtvirtualkeyboard/hunspell")).absolutePath()
88 #if !defined(Q_OS_WIN32)
89 << QStringLiteral("/usr/share/hunspell")
90 << QStringLiteral("/usr/share/myspell/dicts")
91 #endif
92 ;
93 for (const QString &defaultPath : defaultPaths) {
94 if (!searchPaths.contains(defaultPath))
95 searchPaths.append(defaultPath);
96 }
97 QSharedPointer<HunspellLoadDictionaryTask> loadDictionaryTask(new HunspellLoadDictionaryTask(locale, searchPaths));
98 QObject::connect(loadDictionaryTask.data(), &HunspellLoadDictionaryTask::completed, q, &HunspellInputMethod::dictionaryLoadCompleted);
99 dictionaryState = HunspellInputMethodPrivate::DictionaryLoading;
100 emit q->selectionListsChanged();
101 hunspellWorker->addTask(loadDictionaryTask);
102 this->locale = locale;
103
104 loadCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
105 addToHunspell(userDictionaryWords);
106 loadCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
107 removeFromHunspell(blacklistedWords);
108 }
109 return true;
110 }
111
reset()112 void HunspellInputMethodPrivate::reset()
113 {
114 if (clearSuggestions(true)) {
115 Q_Q(HunspellInputMethod);
116 emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
117 emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, wordCandidates.index());
118 }
119 autoSpaceAllowed = false;
120 }
121
updateSuggestions()122 bool HunspellInputMethodPrivate::updateSuggestions()
123 {
124 bool wordCandidateListChanged = false;
125 QString word = wordCandidates.wordAt(0);
126 if (!word.isEmpty() && dictionaryState != HunspellInputMethodPrivate::DictionaryNotLoaded) {
127 wordCandidateListChanged = true;
128 if (word.length() >= wordCompletionPoint) {
129 if (hunspellWorker) {
130 QSharedPointer<HunspellWordList> wordList(new HunspellWordList(wordCandidates));
131
132 // Clear obsolete tasks from the worker queue
133 clearSuggestionsRelatedTasks();
134
135 // Build suggestions
136 QSharedPointer<HunspellBuildSuggestionsTask> buildSuggestionsTask(new HunspellBuildSuggestionsTask());
137 buildSuggestionsTask->wordList = wordList;
138 buildSuggestionsTask->autoCorrect = false;
139 hunspellWorker->addTask(buildSuggestionsTask);
140
141 // Filter out blacklisted word (sometimes Hunspell suggests,
142 // e.g. with different text case)
143 QSharedPointer<HunspellFilterWordTask> filterWordTask(new HunspellFilterWordTask());
144 filterWordTask->wordList = wordList;
145 filterWordTask->filterList = blacklistedWords;
146 hunspellWorker->addTask(filterWordTask);
147
148 // Boost words from user dictionary
149 QSharedPointer<HunspellBoostWordTask> boostWordTask(new HunspellBoostWordTask());
150 boostWordTask->wordList = wordList;
151 boostWordTask->boostList = userDictionaryWords;
152 hunspellWorker->addTask(boostWordTask);
153
154 // Update word candidate list
155 QSharedPointer<HunspellUpdateSuggestionsTask> updateSuggestionsTask(new HunspellUpdateSuggestionsTask());
156 updateSuggestionsTask->wordList = wordList;
157 updateSuggestionsTask->tag = ++wordCandidatesUpdateTag;
158 Q_Q(HunspellInputMethod);
159 QObject::connect(updateSuggestionsTask.data(), &HunspellUpdateSuggestionsTask::updateSuggestions, q, &HunspellInputMethod::updateSuggestions);
160 hunspellWorker->addTask(updateSuggestionsTask);
161 }
162 }
163 } else {
164 wordCandidateListChanged = clearSuggestions();
165 }
166 return wordCandidateListChanged;
167 }
168
clearSuggestions(bool clearInputWord)169 bool HunspellInputMethodPrivate::clearSuggestions(bool clearInputWord)
170 {
171 clearSuggestionsRelatedTasks();
172 return clearInputWord ? wordCandidates.clear() : wordCandidates.clearSuggestions();
173 }
174
clearSuggestionsRelatedTasks()175 void HunspellInputMethodPrivate::clearSuggestionsRelatedTasks()
176 {
177 if (hunspellWorker) {
178 hunspellWorker->removeAllTasksOfType<HunspellBuildSuggestionsTask>();
179 hunspellWorker->removeAllTasksOfType<HunspellFilterWordTask>();
180 hunspellWorker->removeAllTasksOfType<HunspellBoostWordTask>();
181 hunspellWorker->removeAllTasksOfType<HunspellUpdateSuggestionsTask>();
182 }
183 }
184
isAutoSpaceAllowed() const185 bool HunspellInputMethodPrivate::isAutoSpaceAllowed() const
186 {
187 Q_Q(const HunspellInputMethod);
188 if (!autoSpaceAllowed)
189 return false;
190 if (q->inputEngine()->inputMode() == QVirtualKeyboardInputEngine::InputMode::Numeric)
191 return false;
192 QVirtualKeyboardInputContext *ic = q->inputContext();
193 if (!ic)
194 return false;
195 Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
196 return !inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) &&
197 !inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly);
198 }
199
isValidInputChar(const QChar & c) const200 bool HunspellInputMethodPrivate::isValidInputChar(const QChar &c) const
201 {
202 if (c.isLetterOrNumber())
203 return true;
204 if (isJoiner(c))
205 return true;
206 if (c.isMark())
207 return true;
208 return false;
209 }
210
isJoiner(const QChar & c) const211 bool HunspellInputMethodPrivate::isJoiner(const QChar &c) const
212 {
213 if (c.isPunct() || c.isSymbol()) {
214 Q_Q(const HunspellInputMethod);
215 QVirtualKeyboardInputContext *ic = q->inputContext();
216 if (ic) {
217 Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
218 if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) || inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly))
219 return QString(QStringLiteral(":/?#[]@!$&'()*+,;=-_.%")).contains(c);
220 }
221 ushort unicode = c.unicode();
222 if (unicode == Qt::Key_Apostrophe || unicode == Qt::Key_Minus)
223 return true;
224 }
225 return false;
226 }
227
customDictionaryLocation(const QString & dictionaryType) const228 QString HunspellInputMethodPrivate::customDictionaryLocation(const QString &dictionaryType) const
229 {
230 if (dictionaryType.isEmpty() || locale.isEmpty())
231 return QString();
232
233 QString location = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
234 if (location.isEmpty())
235 return QString();
236
237 return QStringLiteral("%1/qtvirtualkeyboard/hunspell/%2-%3.txt")
238 .arg(location)
239 .arg(dictionaryType)
240 .arg(locale);
241 }
242
loadCustomDictionary(const QSharedPointer<HunspellWordList> & wordList,const QString & dictionaryType) const243 void HunspellInputMethodPrivate::loadCustomDictionary(const QSharedPointer<HunspellWordList> &wordList,
244 const QString &dictionaryType) const
245 {
246 QSharedPointer<HunspellLoadWordListTask> loadWordsTask(new HunspellLoadWordListTask());
247 loadWordsTask->filePath = customDictionaryLocation(dictionaryType);
248 loadWordsTask->wordList = wordList;
249 hunspellWorker->addTask(loadWordsTask);
250 }
251
saveCustomDictionary(const QSharedPointer<HunspellWordList> & wordList,const QString & dictionaryType) const252 void HunspellInputMethodPrivate::saveCustomDictionary(const QSharedPointer<HunspellWordList> &wordList,
253 const QString &dictionaryType) const
254 {
255 QSharedPointer<HunspellSaveWordListTask> saveWordsTask(new HunspellSaveWordListTask());
256 saveWordsTask->filePath = customDictionaryLocation(dictionaryType);
257 saveWordsTask->wordList = wordList;
258 hunspellWorker->addTask(saveWordsTask);
259 }
260
addToHunspell(const QSharedPointer<HunspellWordList> & wordList) const261 void HunspellInputMethodPrivate::addToHunspell(const QSharedPointer<HunspellWordList> &wordList) const
262 {
263 QSharedPointer<HunspellAddWordTask> addWordTask(new HunspellAddWordTask());
264 addWordTask->wordList = wordList;
265 hunspellWorker->addTask(addWordTask);
266 }
267
removeFromHunspell(const QSharedPointer<HunspellWordList> & wordList) const268 void HunspellInputMethodPrivate::removeFromHunspell(const QSharedPointer<HunspellWordList> &wordList) const
269 {
270 QSharedPointer<HunspellRemoveWordTask> removeWordTask(new HunspellRemoveWordTask());
271 removeWordTask->wordList = wordList;
272 hunspellWorker->addTask(removeWordTask);
273 }
274
removeFromDictionary(const QString & word)275 void HunspellInputMethodPrivate::removeFromDictionary(const QString &word)
276 {
277 if (userDictionaryWords->removeWord(word) > 0) {
278 saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
279 } else if (!blacklistedWords->contains(word)) {
280 blacklistedWords->appendWord(word);
281 saveCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
282 }
283
284 QSharedPointer<HunspellWordList> wordList(new HunspellWordList());
285 wordList->appendWord(word);
286 removeFromHunspell(wordList);
287
288 updateSuggestions();
289 }
290
addToDictionary()291 void HunspellInputMethodPrivate::addToDictionary()
292 {
293 Q_Q(HunspellInputMethod);
294 // This feature is not allowed when dealing with sensitive information
295 const Qt::InputMethodHints inputMethodHints(q->inputContext()->inputMethodHints());
296 const bool userDictionaryEnabled =
297 !inputMethodHints.testFlag(Qt::ImhHiddenText) &&
298 !inputMethodHints.testFlag(Qt::ImhSensitiveData);
299 if (!userDictionaryEnabled)
300 return;
301
302 if (wordCandidates.isEmpty())
303 return;
304
305 QString word;
306 HunspellWordList::Flags wordFlags;
307 const int activeWordIndex = wordCandidates.index();
308 wordCandidates.wordAt(activeWordIndex, word, wordFlags);
309 if (activeWordIndex == 0) {
310 if (blacklistedWords->removeWord(word) > 0) {
311 saveCustomDictionary(blacklistedWords, QLatin1String("blacklist"));
312 } else if (word.length() > 1 && !wordFlags.testFlag(HunspellWordList::SpellCheckOk) && !userDictionaryWords->contains(word)) {
313 userDictionaryWords->appendWord(word);
314 saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
315 } else {
316 // Avoid adding words to Hunspell which are too short or passed spell check
317 return;
318 }
319
320 QSharedPointer<HunspellWordList> wordList(new HunspellWordList());
321 wordList->appendWord(word);
322 addToHunspell(wordList);
323 } else {
324 // Check if found in the user dictionary and move as last in the list.
325 // This way the list is always ordered by use.
326 // If userDictionaryMaxSize is greater than zero the number of words in the
327 // list will be limited to that amount. By pushing last used items to end of
328 // list we can avoid (to certain extent) removing frequently used words.
329 int userDictionaryIndex = userDictionaryWords->indexOfWord(word);
330 if (userDictionaryIndex != -1) {
331 userDictionaryWords->moveWord(userDictionaryIndex, userDictionaryWords->size() - 1);
332 saveCustomDictionary(userDictionaryWords, QLatin1String("userdictionary"));
333 }
334 }
335 }
336
337 } // namespace QtVirtualKeyboard
338 QT_END_NAMESPACE
339