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 "pinyininputmethod_p.h" |
31 | #include "pinyindecoderservice_p.h" |
32 | #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> |
33 | #include <QLoggingCategory> |
34 | |
35 | QT_BEGIN_NAMESPACE |
36 | namespace QtVirtualKeyboard { |
37 | |
38 | Q_LOGGING_CATEGORY(lcPinyin, "qt.virtualkeyboard.pinyin" ) |
39 | |
40 | class PinyinInputMethodPrivate |
41 | { |
42 | Q_DECLARE_PUBLIC(PinyinInputMethod) |
43 | |
44 | public: |
45 | enum State |
46 | { |
47 | Idle, |
48 | Input, |
49 | Predict |
50 | }; |
51 | |
52 | PinyinInputMethodPrivate(PinyinInputMethod *q_ptr) : |
53 | q_ptr(q_ptr), |
54 | inputMode(QVirtualKeyboardInputEngine::InputMode::Pinyin), |
55 | pinyinDecoderService(PinyinDecoderService::getInstance()), |
56 | state(Idle), |
57 | surface(), |
58 | totalChoicesNum(0), |
59 | candidatesList(), |
60 | fixedLen(0), |
61 | composingStr(), |
62 | activeCmpsLen(0), |
63 | finishSelection(true), |
64 | posDelSpl(-1), |
65 | isPosInSpl(false) |
66 | { |
67 | } |
68 | |
69 | void resetToIdleState() |
70 | { |
71 | Q_Q(PinyinInputMethod); |
72 | |
73 | QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
74 | |
75 | // Disable the user dictionary when entering sensitive data |
76 | if (inputContext && pinyinDecoderService) { |
77 | bool userDictionaryEnabled = !inputContext->inputMethodHints().testFlag(flag: Qt::ImhSensitiveData); |
78 | if (userDictionaryEnabled != pinyinDecoderService->isUserDictionaryEnabled()) |
79 | pinyinDecoderService->setUserDictionary(userDictionaryEnabled); |
80 | } |
81 | |
82 | if (state == Idle) |
83 | return; |
84 | |
85 | state = Idle; |
86 | surface.clear(); |
87 | fixedLen = 0; |
88 | finishSelection = true; |
89 | composingStr.clear(); |
90 | if (inputContext) |
91 | inputContext->setPreeditText(text: QString()); |
92 | activeCmpsLen = 0; |
93 | posDelSpl = -1; |
94 | isPosInSpl = false; |
95 | |
96 | resetCandidates(); |
97 | } |
98 | |
99 | bool addSpellingChar(QChar ch, bool reset) |
100 | { |
101 | if (reset) { |
102 | surface.clear(); |
103 | pinyinDecoderService->resetSearch(); |
104 | } |
105 | if (ch == u'\'') { |
106 | if (surface.isEmpty()) |
107 | return false; |
108 | if (surface.endsWith(c: ch)) |
109 | return true; |
110 | } |
111 | surface.append(c: ch); |
112 | return true; |
113 | } |
114 | |
115 | bool removeSpellingChar() |
116 | { |
117 | if (surface.isEmpty()) |
118 | return false; |
119 | QVector<int> splStart = pinyinDecoderService->spellingStartPositions(); |
120 | isPosInSpl = (surface.length() <= splStart[fixedLen + 1]); |
121 | posDelSpl = isPosInSpl ? fixedLen - 1 : surface.length() - 1; |
122 | return true; |
123 | } |
124 | |
125 | void chooseAndUpdate(int candId) |
126 | { |
127 | Q_Q(PinyinInputMethod); |
128 | |
129 | if (state == Predict) |
130 | choosePredictChoice(choiceId: candId); |
131 | else |
132 | chooseDecodingCandidate(candId); |
133 | |
134 | if (composingStr.length() > 0) { |
135 | if ((candId >= 0 || finishSelection) && composingStr.length() == fixedLen) { |
136 | QString resultStr = getComposingStrActivePart(); |
137 | q->inputContext()->commit(text: resultStr); |
138 | tryPredict(); |
139 | } else if (state == Idle) { |
140 | state = Input; |
141 | } |
142 | } else { |
143 | tryPredict(); |
144 | } |
145 | } |
146 | |
147 | bool chooseAndFinish() |
148 | { |
149 | if (state == Predict || !totalChoicesNum) |
150 | return false; |
151 | |
152 | chooseAndUpdate(candId: 0); |
153 | if (state != Predict && totalChoicesNum > 0) |
154 | chooseAndUpdate(candId: 0); |
155 | |
156 | return true; |
157 | } |
158 | |
159 | int candidatesCount() |
160 | { |
161 | return totalChoicesNum; |
162 | } |
163 | |
164 | QString candidateAt(int index) |
165 | { |
166 | if (index < 0 || index >= totalChoicesNum) |
167 | return QString(); |
168 | if (index >= candidatesList.size()) { |
169 | int fetchMore = qMin(a: index + 20, b: totalChoicesNum - candidatesList.size()); |
170 | candidatesList.append(t: pinyinDecoderService->fetchCandidates(index: candidatesList.size(), count: fetchMore, sentFixedLen: fixedLen)); |
171 | if (index == 0 && totalChoicesNum == 1) { |
172 | int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(decoded: true); |
173 | if (surfaceDecodedLen < surface.length()) |
174 | candidatesList[0] = candidatesList[0] + surface.mid(position: surfaceDecodedLen).toLower(); |
175 | } |
176 | } |
177 | return index < candidatesList.size() ? candidatesList[index] : QString(); |
178 | } |
179 | |
180 | void chooseDecodingCandidate(int candId) |
181 | { |
182 | Q_Q(PinyinInputMethod); |
183 | Q_ASSERT(state != Predict); |
184 | |
185 | int result = 0; |
186 | if (candId < 0) { |
187 | if (surface.length() > 0) { |
188 | if (posDelSpl < 0) { |
189 | result = pinyinDecoderService->search(spelling: surface); |
190 | } else { |
191 | result = pinyinDecoderService->deleteSearch(pos: posDelSpl, isPosInSpellingId: isPosInSpl, clearFixedInThisStep: false); |
192 | posDelSpl = -1; |
193 | } |
194 | } |
195 | } else { |
196 | if (totalChoicesNum > 1) { |
197 | result = pinyinDecoderService->chooceCandidate(index: candId); |
198 | } else { |
199 | QString resultStr; |
200 | if (totalChoicesNum == 1) { |
201 | QString undecodedStr = candId < candidatesList.length() ? candidatesList.at(i: candId) : QString(); |
202 | resultStr = pinyinDecoderService->candidateAt(index: 0).mid(position: 0, n: fixedLen) + undecodedStr; |
203 | } |
204 | resetToIdleState(); |
205 | if (!resultStr.isEmpty()) |
206 | q->inputContext()->commit(text: resultStr); |
207 | return; |
208 | } |
209 | } |
210 | |
211 | resetCandidates(); |
212 | totalChoicesNum = result; |
213 | |
214 | surface = pinyinDecoderService->pinyinString(decoded: false); |
215 | QVector<int> splStart = pinyinDecoderService->spellingStartPositions(); |
216 | QString fullSent = pinyinDecoderService->candidateAt(index: 0); |
217 | fixedLen = pinyinDecoderService->fixedLength(); |
218 | composingStr = fullSent.mid(position: 0, n: fixedLen) + surface.mid(position: splStart[fixedLen + 1]); |
219 | activeCmpsLen = composingStr.length(); |
220 | |
221 | // Prepare the display string. |
222 | QString composingStrDisplay; |
223 | int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(decoded: true); |
224 | if (!surfaceDecodedLen) { |
225 | composingStrDisplay = composingStr.toLower(); |
226 | if (!totalChoicesNum) |
227 | totalChoicesNum = 1; |
228 | } else { |
229 | activeCmpsLen = activeCmpsLen - (surface.length() - surfaceDecodedLen); |
230 | composingStrDisplay = fullSent.mid(position: 0, n: fixedLen); |
231 | for (int pos = fixedLen + 1; pos < splStart.size() - 1; pos++) { |
232 | composingStrDisplay += surface.mid(position: splStart[pos], n: splStart[pos + 1] - splStart[pos]); |
233 | if (splStart[pos + 1] < surfaceDecodedLen) |
234 | composingStrDisplay += QLatin1String(" " ); |
235 | } |
236 | if (surfaceDecodedLen < surface.length()) |
237 | composingStrDisplay += surface.mid(position: surfaceDecodedLen); |
238 | } |
239 | q->inputContext()->setPreeditText(text: composingStrDisplay); |
240 | |
241 | finishSelection = splStart.size() == (fixedLen + 2); |
242 | if (!finishSelection) |
243 | candidateAt(index: 0); |
244 | } |
245 | |
246 | void choosePredictChoice(int choiceId) |
247 | { |
248 | Q_ASSERT(state == Predict); |
249 | |
250 | if (choiceId < 0 || choiceId >= totalChoicesNum) |
251 | return; |
252 | |
253 | QString tmp = candidatesList.at(i: choiceId); |
254 | |
255 | resetCandidates(); |
256 | |
257 | candidatesList.append(t: tmp); |
258 | totalChoicesNum = 1; |
259 | |
260 | surface.clear(); |
261 | fixedLen = tmp.length(); |
262 | composingStr = tmp; |
263 | activeCmpsLen = fixedLen; |
264 | |
265 | finishSelection = true; |
266 | } |
267 | |
268 | QString getComposingStrActivePart() |
269 | { |
270 | return composingStr.mid(position: 0, n: activeCmpsLen); |
271 | } |
272 | |
273 | void resetCandidates() |
274 | { |
275 | candidatesList.clear(); |
276 | if (totalChoicesNum) { |
277 | totalChoicesNum = 0; |
278 | } |
279 | } |
280 | |
281 | void updateCandidateList() |
282 | { |
283 | Q_Q(PinyinInputMethod); |
284 | emit q->selectionListChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList); |
285 | emit q->selectionListActiveItemChanged(type: QVirtualKeyboardSelectionListModel::Type::WordCandidateList, |
286 | index: totalChoicesNum > 0 && state == PinyinInputMethodPrivate::Input ? 0 : -1); |
287 | } |
288 | |
289 | bool canDoPrediction() |
290 | { |
291 | Q_Q(PinyinInputMethod); |
292 | QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
293 | return inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin && |
294 | composingStr.length() == fixedLen && |
295 | inputContext && |
296 | !inputContext->inputMethodHints().testFlag(flag: Qt::ImhNoPredictiveText); |
297 | } |
298 | |
299 | void tryPredict() |
300 | { |
301 | // Try to get the prediction list. |
302 | if (canDoPrediction()) { |
303 | Q_Q(PinyinInputMethod); |
304 | if (state != Predict) |
305 | resetToIdleState(); |
306 | QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
307 | int cursorPosition = inputContext->cursorPosition(); |
308 | int historyStart = qMax(a: 0, b: cursorPosition - 3); |
309 | QString history = inputContext->surroundingText().mid(position: historyStart, n: cursorPosition - historyStart); |
310 | candidatesList = pinyinDecoderService->predictionList(history); |
311 | totalChoicesNum = candidatesList.size(); |
312 | finishSelection = false; |
313 | state = Predict; |
314 | } else { |
315 | resetCandidates(); |
316 | } |
317 | |
318 | if (!candidatesCount()) |
319 | resetToIdleState(); |
320 | } |
321 | |
322 | PinyinInputMethod *q_ptr; |
323 | QVirtualKeyboardInputEngine::InputMode inputMode; |
324 | QPointer<PinyinDecoderService> pinyinDecoderService; |
325 | State state; |
326 | QString surface; |
327 | int totalChoicesNum; |
328 | QList<QString> candidatesList; |
329 | int fixedLen; |
330 | QString composingStr; |
331 | int activeCmpsLen; |
332 | bool finishSelection; |
333 | int posDelSpl; |
334 | bool isPosInSpl; |
335 | }; |
336 | |
337 | class ScopedCandidateListUpdate |
338 | { |
339 | Q_DISABLE_COPY(ScopedCandidateListUpdate) |
340 | public: |
341 | inline explicit ScopedCandidateListUpdate(PinyinInputMethodPrivate *d) : |
342 | d(d), |
343 | candidatesList(d->candidatesList), |
344 | totalChoicesNum(d->totalChoicesNum), |
345 | state(d->state) |
346 | { |
347 | } |
348 | |
349 | inline ~ScopedCandidateListUpdate() |
350 | { |
351 | if (totalChoicesNum != d->totalChoicesNum || state != d->state || candidatesList != d->candidatesList) |
352 | d->updateCandidateList(); |
353 | } |
354 | |
355 | private: |
356 | PinyinInputMethodPrivate *d; |
357 | QList<QString> candidatesList; |
358 | int totalChoicesNum; |
359 | PinyinInputMethodPrivate::State state; |
360 | }; |
361 | |
362 | /*! |
363 | \class QtVirtualKeyboard::PinyinInputMethod |
364 | \internal |
365 | */ |
366 | |
367 | PinyinInputMethod::PinyinInputMethod(QObject *parent) : |
368 | QVirtualKeyboardAbstractInputMethod(parent), |
369 | d_ptr(new PinyinInputMethodPrivate(this)) |
370 | { |
371 | } |
372 | |
373 | PinyinInputMethod::~PinyinInputMethod() |
374 | { |
375 | } |
376 | |
377 | QList<QVirtualKeyboardInputEngine::InputMode> PinyinInputMethod::inputModes(const QString &locale) |
378 | { |
379 | Q_UNUSED(locale) |
380 | Q_D(PinyinInputMethod); |
381 | QList<QVirtualKeyboardInputEngine::InputMode> result; |
382 | if (d->pinyinDecoderService) |
383 | result << QVirtualKeyboardInputEngine::InputMode::Pinyin; |
384 | result << QVirtualKeyboardInputEngine::InputMode::Latin; |
385 | return result; |
386 | } |
387 | |
388 | bool PinyinInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode) |
389 | { |
390 | Q_UNUSED(locale) |
391 | Q_D(PinyinInputMethod); |
392 | reset(); |
393 | if (inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin && !d->pinyinDecoderService) |
394 | return false; |
395 | d->inputMode = inputMode; |
396 | return true; |
397 | } |
398 | |
399 | bool PinyinInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase) |
400 | { |
401 | Q_UNUSED(textCase) |
402 | return true; |
403 | } |
404 | |
405 | bool PinyinInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers) |
406 | { |
407 | Q_UNUSED(modifiers) |
408 | Q_D(PinyinInputMethod); |
409 | if (d->inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin) { |
410 | ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
411 | Q_UNUSED(scopedCandidateListUpdate) |
412 | if ((key >= Qt::Key_A && key <= Qt::Key_Z) || (key == Qt::Key_Apostrophe)) { |
413 | if (d->state == PinyinInputMethodPrivate::Predict) |
414 | d->resetToIdleState(); |
415 | if (d->addSpellingChar(ch: text.at(i: 0), reset: d->state == PinyinInputMethodPrivate::Idle)) { |
416 | d->chooseAndUpdate(candId: -1); |
417 | return true; |
418 | } |
419 | } else if (key == Qt::Key_Space) { |
420 | if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) { |
421 | d->chooseAndUpdate(candId: 0); |
422 | return true; |
423 | } |
424 | } else if (key == Qt::Key_Return) { |
425 | if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) { |
426 | QString surface = d->surface; |
427 | d->resetToIdleState(); |
428 | inputContext()->commit(text: surface); |
429 | return true; |
430 | } |
431 | } else if (key == Qt::Key_Backspace) { |
432 | if (d->removeSpellingChar()) { |
433 | d->chooseAndUpdate(candId: -1); |
434 | return true; |
435 | } |
436 | } else if (!text.isEmpty()) { |
437 | d->chooseAndFinish(); |
438 | } |
439 | } |
440 | return false; |
441 | } |
442 | |
443 | QList<QVirtualKeyboardSelectionListModel::Type> PinyinInputMethod::selectionLists() |
444 | { |
445 | return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList; |
446 | } |
447 | |
448 | int PinyinInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type) |
449 | { |
450 | Q_UNUSED(type) |
451 | Q_D(PinyinInputMethod); |
452 | return d->candidatesCount(); |
453 | } |
454 | |
455 | QVariant PinyinInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role) |
456 | { |
457 | QVariant result; |
458 | Q_UNUSED(type) |
459 | Q_D(PinyinInputMethod); |
460 | switch (role) { |
461 | case QVirtualKeyboardSelectionListModel::Role::Display: |
462 | result = QVariant(d->candidateAt(index)); |
463 | break; |
464 | case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength: |
465 | result.setValue(0); |
466 | break; |
467 | default: |
468 | result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role); |
469 | break; |
470 | } |
471 | return result; |
472 | } |
473 | |
474 | void PinyinInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index) |
475 | { |
476 | Q_UNUSED(type) |
477 | Q_D(PinyinInputMethod); |
478 | ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
479 | Q_UNUSED(scopedCandidateListUpdate) |
480 | d->chooseAndUpdate(candId: index); |
481 | } |
482 | |
483 | void PinyinInputMethod::reset() |
484 | { |
485 | Q_D(PinyinInputMethod); |
486 | ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
487 | Q_UNUSED(scopedCandidateListUpdate) |
488 | d->resetToIdleState(); |
489 | } |
490 | |
491 | void PinyinInputMethod::update() |
492 | { |
493 | Q_D(PinyinInputMethod); |
494 | ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
495 | Q_UNUSED(scopedCandidateListUpdate) |
496 | d->chooseAndFinish(); |
497 | d->tryPredict(); |
498 | } |
499 | |
500 | } // namespace QtVirtualKeyboard |
501 | QT_END_NAMESPACE |
502 | |