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
11QT_BEGIN_NAMESPACE
12namespace QtVirtualKeyboard {
13
14Q_LOGGING_CATEGORY(lcPinyin, "qt.virtualkeyboard.pinyin")
15
16class PinyinInputMethodPrivate
17{
18 Q_DECLARE_PUBLIC(PinyinInputMethod)
19
20public:
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
313class ScopedCandidateListUpdate
314{
315 Q_DISABLE_COPY(ScopedCandidateListUpdate)
316public:
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
331private:
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
343PinyinInputMethod::PinyinInputMethod(QObject *parent) :
344 QVirtualKeyboardAbstractInputMethod(parent),
345 d_ptr(new PinyinInputMethodPrivate(this))
346{
347}
348
349PinyinInputMethod::~PinyinInputMethod()
350{
351}
352
353QList<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
364bool 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
375bool PinyinInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
376{
377 Q_UNUSED(textCase);
378 return true;
379}
380
381bool 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
419QList<QVirtualKeyboardSelectionListModel::Type> PinyinInputMethod::selectionLists()
420{
421 return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList;
422}
423
424int PinyinInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
425{
426 Q_UNUSED(type);
427 Q_D(PinyinInputMethod);
428 return d->candidatesCount();
429}
430
431QVariant 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
450void 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
459void PinyinInputMethod::reset()
460{
461 Q_D(PinyinInputMethod);
462 ScopedCandidateListUpdate scopedCandidateListUpdate(d);
463 Q_UNUSED(scopedCandidateListUpdate);
464 d->resetToIdleState();
465}
466
467void 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
477QT_END_NAMESPACE
478

source code of qtvirtualkeyboard/src/plugins/pinyin/pinyininputmethod.cpp