| 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 |  |