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