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