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