| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "pinyininputmethod_p.h" |
| #include "pinyindecoderservice_p.h" |
| #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h> |
| #include <QLoggingCategory> |
| |
| QT_BEGIN_NAMESPACE |
| namespace QtVirtualKeyboard { |
| |
| Q_LOGGING_CATEGORY(lcPinyin, "qt.virtualkeyboard.pinyin") |
| |
| class PinyinInputMethodPrivate |
| { |
| Q_DECLARE_PUBLIC(PinyinInputMethod) |
| |
| public: |
| enum State |
| { |
| Idle, |
| Input, |
| Predict |
| }; |
| |
| PinyinInputMethodPrivate(PinyinInputMethod *q_ptr) : |
| q_ptr(q_ptr), |
| inputMode(QVirtualKeyboardInputEngine::InputMode::Pinyin), |
| pinyinDecoderService(PinyinDecoderService::getInstance()), |
| state(Idle), |
| surface(), |
| totalChoicesNum(0), |
| candidatesList(), |
| fixedLen(0), |
| composingStr(), |
| activeCmpsLen(0), |
| finishSelection(true), |
| posDelSpl(-1), |
| isPosInSpl(false) |
| { |
| } |
| |
| void resetToIdleState() |
| { |
| Q_Q(PinyinInputMethod); |
| |
| QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
| |
| // Disable the user dictionary when entering sensitive data |
| if (inputContext) { |
| bool userDictionaryEnabled = !inputContext->inputMethodHints().testFlag(Qt::ImhSensitiveData); |
| if (userDictionaryEnabled != pinyinDecoderService->isUserDictionaryEnabled()) |
| pinyinDecoderService->setUserDictionary(userDictionaryEnabled); |
| } |
| |
| if (state == Idle) |
| return; |
| |
| state = Idle; |
| surface.clear(); |
| fixedLen = 0; |
| finishSelection = true; |
| composingStr.clear(); |
| if (inputContext) |
| inputContext->setPreeditText(QString()); |
| activeCmpsLen = 0; |
| posDelSpl = -1; |
| isPosInSpl = false; |
| |
| resetCandidates(); |
| } |
| |
| bool addSpellingChar(QChar ch, bool reset) |
| { |
| if (reset) { |
| surface.clear(); |
| pinyinDecoderService->resetSearch(); |
| } |
| if (ch == u'\'') { |
| if (surface.isEmpty()) |
| return false; |
| if (surface.endsWith(ch)) |
| return true; |
| } |
| surface.append(ch); |
| return true; |
| } |
| |
| bool removeSpellingChar() |
| { |
| if (surface.isEmpty()) |
| return false; |
| QVector<int> splStart = pinyinDecoderService->spellingStartPositions(); |
| isPosInSpl = (surface.length() <= splStart[fixedLen + 1]); |
| posDelSpl = isPosInSpl ? fixedLen - 1 : surface.length() - 1; |
| return true; |
| } |
| |
| void chooseAndUpdate(int candId) |
| { |
| Q_Q(PinyinInputMethod); |
| |
| if (state == Predict) |
| choosePredictChoice(candId); |
| else |
| chooseDecodingCandidate(candId); |
| |
| if (composingStr.length() > 0) { |
| if ((candId >= 0 || finishSelection) && composingStr.length() == fixedLen) { |
| QString resultStr = getComposingStrActivePart(); |
| tryPredict(); |
| q->inputContext()->commit(resultStr); |
| } else if (state == Idle) { |
| state = Input; |
| } |
| } else { |
| tryPredict(); |
| } |
| } |
| |
| bool chooseAndFinish() |
| { |
| if (state == Predict || !totalChoicesNum) |
| return false; |
| |
| chooseAndUpdate(0); |
| if (state != Predict && totalChoicesNum > 0) |
| chooseAndUpdate(0); |
| |
| return true; |
| } |
| |
| int candidatesCount() |
| { |
| return totalChoicesNum; |
| } |
| |
| QString candidateAt(int index) |
| { |
| if (index < 0 || index >= totalChoicesNum) |
| return QString(); |
| if (index >= candidatesList.size()) { |
| int fetchMore = qMin(index + 20, totalChoicesNum - candidatesList.size()); |
| candidatesList.append(pinyinDecoderService->fetchCandidates(candidatesList.size(), fetchMore, fixedLen)); |
| if (index == 0 && totalChoicesNum == 1) { |
| int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(true); |
| if (surfaceDecodedLen < surface.length()) |
| candidatesList[0] = candidatesList[0] + surface.mid(surfaceDecodedLen).toLower(); |
| } |
| } |
| return index < candidatesList.size() ? candidatesList[index] : QString(); |
| } |
| |
| void chooseDecodingCandidate(int candId) |
| { |
| Q_Q(PinyinInputMethod); |
| Q_ASSERT(state != Predict); |
| |
| int result = 0; |
| if (candId < 0) { |
| if (surface.length() > 0) { |
| if (posDelSpl < 0) { |
| result = pinyinDecoderService->search(surface); |
| } else { |
| result = pinyinDecoderService->deleteSearch(posDelSpl, isPosInSpl, false); |
| posDelSpl = -1; |
| } |
| } |
| } else { |
| if (totalChoicesNum > 1) { |
| result = pinyinDecoderService->chooceCandidate(candId); |
| } else { |
| QString resultStr; |
| if (totalChoicesNum == 1) { |
| QString undecodedStr = candId < candidatesList.length() ? candidatesList.at(candId) : QString(); |
| resultStr = pinyinDecoderService->candidateAt(0).mid(0, fixedLen) + undecodedStr; |
| } |
| resetToIdleState(); |
| if (!resultStr.isEmpty()) |
| q->inputContext()->commit(resultStr); |
| return; |
| } |
| } |
| |
| resetCandidates(); |
| totalChoicesNum = result; |
| |
| surface = pinyinDecoderService->pinyinString(false); |
| QVector<int> splStart = pinyinDecoderService->spellingStartPositions(); |
| QString fullSent = pinyinDecoderService->candidateAt(0); |
| fixedLen = pinyinDecoderService->fixedLength(); |
| composingStr = fullSent.mid(0, fixedLen) + surface.mid(splStart[fixedLen + 1]); |
| activeCmpsLen = composingStr.length(); |
| |
| // Prepare the display string. |
| QString composingStrDisplay; |
| int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(true); |
| if (!surfaceDecodedLen) { |
| composingStrDisplay = composingStr.toLower(); |
| if (!totalChoicesNum) |
| totalChoicesNum = 1; |
| } else { |
| activeCmpsLen = activeCmpsLen - (surface.length() - surfaceDecodedLen); |
| composingStrDisplay = fullSent.mid(0, fixedLen); |
| for (int pos = fixedLen + 1; pos < splStart.size() - 1; pos++) { |
| composingStrDisplay += surface.mid(splStart[pos], splStart[pos + 1] - splStart[pos]).toUpper(); |
| if (splStart[pos + 1] < surfaceDecodedLen) |
| composingStrDisplay += QLatin1String(" "); |
| } |
| if (surfaceDecodedLen < surface.length()) |
| composingStrDisplay += surface.mid(surfaceDecodedLen).toLower(); |
| } |
| q->inputContext()->setPreeditText(composingStrDisplay); |
| |
| finishSelection = splStart.size() == (fixedLen + 2); |
| if (!finishSelection) |
| candidateAt(0); |
| } |
| |
| void choosePredictChoice(int choiceId) |
| { |
| Q_ASSERT(state == Predict); |
| |
| if (choiceId < 0 || choiceId >= totalChoicesNum) |
| return; |
| |
| QString tmp = candidatesList.at(choiceId); |
| |
| resetCandidates(); |
| |
| candidatesList.append(tmp); |
| totalChoicesNum = 1; |
| |
| surface.clear(); |
| fixedLen = tmp.length(); |
| composingStr = tmp; |
| activeCmpsLen = fixedLen; |
| |
| finishSelection = true; |
| } |
| |
| QString getComposingStrActivePart() |
| { |
| return composingStr.mid(0, activeCmpsLen); |
| } |
| |
| void resetCandidates() |
| { |
| candidatesList.clear(); |
| if (totalChoicesNum) { |
| totalChoicesNum = 0; |
| } |
| } |
| |
| void updateCandidateList() |
| { |
| Q_Q(PinyinInputMethod); |
| emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList); |
| emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, |
| totalChoicesNum > 0 && state == PinyinInputMethodPrivate::Input ? 0 : -1); |
| } |
| |
| bool canDoPrediction() |
| { |
| Q_Q(PinyinInputMethod); |
| QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
| return inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin && |
| composingStr.length() == fixedLen && |
| inputContext && |
| !inputContext->inputMethodHints().testFlag(Qt::ImhNoPredictiveText); |
| } |
| |
| void tryPredict() |
| { |
| // Try to get the prediction list. |
| if (canDoPrediction()) { |
| Q_Q(PinyinInputMethod); |
| if (state != Predict) |
| resetToIdleState(); |
| QVirtualKeyboardInputContext *inputContext = q->inputContext(); |
| int cursorPosition = inputContext->cursorPosition(); |
| int historyStart = qMax(0, cursorPosition - 3); |
| QString history = inputContext->surroundingText().mid(historyStart, cursorPosition - historyStart); |
| candidatesList = pinyinDecoderService->predictionList(history); |
| totalChoicesNum = candidatesList.size(); |
| finishSelection = false; |
| state = Predict; |
| } else { |
| resetCandidates(); |
| } |
| |
| if (!candidatesCount()) |
| resetToIdleState(); |
| } |
| |
| PinyinInputMethod *q_ptr; |
| QVirtualKeyboardInputEngine::InputMode inputMode; |
| QPointer<PinyinDecoderService> pinyinDecoderService; |
| State state; |
| QString surface; |
| int totalChoicesNum; |
| QList<QString> candidatesList; |
| int fixedLen; |
| QString composingStr; |
| int activeCmpsLen; |
| bool finishSelection; |
| int posDelSpl; |
| bool isPosInSpl; |
| }; |
| |
| class ScopedCandidateListUpdate |
| { |
| Q_DISABLE_COPY(ScopedCandidateListUpdate) |
| public: |
| inline explicit ScopedCandidateListUpdate(PinyinInputMethodPrivate *d) : |
| d(d), |
| candidatesList(d->candidatesList), |
| totalChoicesNum(d->totalChoicesNum), |
| state(d->state) |
| { |
| } |
| |
| inline ~ScopedCandidateListUpdate() |
| { |
| if (totalChoicesNum != d->totalChoicesNum || state != d->state || candidatesList != d->candidatesList) |
| d->updateCandidateList(); |
| } |
| |
| private: |
| PinyinInputMethodPrivate *d; |
| QList<QString> candidatesList; |
| int totalChoicesNum; |
| PinyinInputMethodPrivate::State state; |
| }; |
| |
| /*! |
| \class QtVirtualKeyboard::PinyinInputMethod |
| \internal |
| */ |
| |
| PinyinInputMethod::PinyinInputMethod(QObject *parent) : |
| QVirtualKeyboardAbstractInputMethod(parent), |
| d_ptr(new PinyinInputMethodPrivate(this)) |
| { |
| } |
| |
| PinyinInputMethod::~PinyinInputMethod() |
| { |
| } |
| |
| QList<QVirtualKeyboardInputEngine::InputMode> PinyinInputMethod::inputModes(const QString &locale) |
| { |
| Q_UNUSED(locale) |
| Q_D(PinyinInputMethod); |
| QList<QVirtualKeyboardInputEngine::InputMode> result; |
| if (d->pinyinDecoderService) |
| result << QVirtualKeyboardInputEngine::InputMode::Pinyin; |
| result << QVirtualKeyboardInputEngine::InputMode::Latin; |
| return result; |
| } |
| |
| bool PinyinInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode) |
| { |
| Q_UNUSED(locale) |
| Q_D(PinyinInputMethod); |
| reset(); |
| if (inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin && !d->pinyinDecoderService) |
| return false; |
| d->inputMode = inputMode; |
| return true; |
| } |
| |
| bool PinyinInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase) |
| { |
| Q_UNUSED(textCase) |
| return true; |
| } |
| |
| bool PinyinInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers) |
| { |
| Q_UNUSED(modifiers) |
| Q_D(PinyinInputMethod); |
| if (d->inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin) { |
| ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
| Q_UNUSED(scopedCandidateListUpdate) |
| if ((key >= Qt::Key_A && key <= Qt::Key_Z) || (key == Qt::Key_Apostrophe)) { |
| if (d->state == PinyinInputMethodPrivate::Predict) |
| d->resetToIdleState(); |
| if (d->addSpellingChar(text.at(0), d->state == PinyinInputMethodPrivate::Idle)) { |
| d->chooseAndUpdate(-1); |
| return true; |
| } |
| } else if (key == Qt::Key_Space) { |
| if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) { |
| d->chooseAndUpdate(0); |
| return true; |
| } |
| } else if (key == Qt::Key_Return) { |
| if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) { |
| QString surface = d->surface; |
| d->resetToIdleState(); |
| inputContext()->commit(surface); |
| return true; |
| } |
| } else if (key == Qt::Key_Backspace) { |
| if (d->removeSpellingChar()) { |
| d->chooseAndUpdate(-1); |
| return true; |
| } |
| } else if (!text.isEmpty()) { |
| d->chooseAndFinish(); |
| } |
| } |
| return false; |
| } |
| |
| QList<QVirtualKeyboardSelectionListModel::Type> PinyinInputMethod::selectionLists() |
| { |
| return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList; |
| } |
| |
| int PinyinInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type) |
| { |
| Q_UNUSED(type) |
| Q_D(PinyinInputMethod); |
| return d->candidatesCount(); |
| } |
| |
| QVariant PinyinInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role) |
| { |
| QVariant result; |
| Q_UNUSED(type) |
| Q_D(PinyinInputMethod); |
| switch (role) { |
| case QVirtualKeyboardSelectionListModel::Role::Display: |
| result = QVariant(d->candidateAt(index)); |
| break; |
| case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength: |
| result.setValue(0); |
| break; |
| default: |
| result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role); |
| break; |
| } |
| return result; |
| } |
| |
| void PinyinInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index) |
| { |
| Q_UNUSED(type) |
| Q_D(PinyinInputMethod); |
| ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
| Q_UNUSED(scopedCandidateListUpdate) |
| d->chooseAndUpdate(index); |
| } |
| |
| void PinyinInputMethod::reset() |
| { |
| Q_D(PinyinInputMethod); |
| ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
| Q_UNUSED(scopedCandidateListUpdate) |
| d->resetToIdleState(); |
| } |
| |
| void PinyinInputMethod::update() |
| { |
| Q_D(PinyinInputMethod); |
| ScopedCandidateListUpdate scopedCandidateListUpdate(d); |
| Q_UNUSED(scopedCandidateListUpdate) |
| d->chooseAndFinish(); |
| d->tryPredict(); |
| } |
| |
| } // namespace QtVirtualKeyboard |
| QT_END_NAMESPACE |