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
9QT_BEGIN_NAMESPACE
10namespace QtVirtualKeyboard {
11
12Q_LOGGING_CATEGORY(lcPinyin, "qt.virtualkeyboard.pinyin")
13
14class PinyinInputMethodPrivate
15{
16 Q_DECLARE_PUBLIC(PinyinInputMethod)
17
18public:
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
311class ScopedCandidateListUpdate
312{
313 Q_DISABLE_COPY(ScopedCandidateListUpdate)
314public:
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
329private:
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
341PinyinInputMethod::PinyinInputMethod(QObject *parent) :
342 QVirtualKeyboardAbstractInputMethod(parent),
343 d_ptr(new PinyinInputMethodPrivate(this))
344{
345}
346
347PinyinInputMethod::~PinyinInputMethod()
348{
349}
350
351QList<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
362bool 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
373bool PinyinInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
374{
375 Q_UNUSED(textCase);
376 return true;
377}
378
379bool 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
417QList<QVirtualKeyboardSelectionListModel::Type> PinyinInputMethod::selectionLists()
418{
419 return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList;
420}
421
422int PinyinInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
423{
424 Q_UNUSED(type);
425 Q_D(PinyinInputMethod);
426 return d->candidatesCount();
427}
428
429QVariant 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
448void 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
457void PinyinInputMethod::reset()
458{
459 Q_D(PinyinInputMethod);
460 ScopedCandidateListUpdate scopedCandidateListUpdate(d);
461 Q_UNUSED(scopedCandidateListUpdate);
462 d->resetToIdleState();
463}
464
465void 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
475QT_END_NAMESPACE
476

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