Revision control
++ // Copyright (C) 2023 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "examplehwrinputmethod_p.h"
++ #include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
++ #include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
++ #include <QtVirtualKeyboard/qvirtualkeyboardtrace.h>
++ #include <QtVirtualKeyboard/private/handwritinggesturerecognizer_p.h>
++ #include <QtVirtualKeyboard/private/settings_p.h>
++ #include <QtVirtualKeyboard/private/qvirtualkeyboardabstractinputmethod_p.h>
++ #include <QCryptographicHash>
++ #include <QRandomGenerator>
++ #include <QLoggingCategory>
++ #include <QLocale>
++
++ QT_BEGIN_NAMESPACE
++ namespace QtVirtualKeyboard {
++
++ Q_LOGGING_CATEGORY(lcExampleHwr, "qt.virtualkeyboard.example.hwr")
++
++ class ExampleHwrInputMethodPrivate : public QVirtualKeyboardAbstractInputMethodPrivate
++ {
++ public:
++ Q_DECLARE_PUBLIC(ExampleHwrInputMethod)
++
++ ExampleHwrInputMethodPrivate(ExampleHwrInputMethod *q_ptr) :
++ QVirtualKeyboardAbstractInputMethodPrivate(),
++ q_ptr(q_ptr)
++ {
++ }
++
++ bool setInputMode(const QLocale &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
++ {
++ Q_UNUSED(locale);
++ finishRecognition();
++ this->inputMode = inputMode;
++ return true;
++ }
++
++ QByteArray getContext(QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
++ const QVariantMap &traceCaptureDeviceInfo,
++ const QVariantMap &traceScreenInfo) const
++ {
++ QCryptographicHash hash(QCryptographicHash::Md5);
++
++ hash.addData(QByteArrayView(reinterpret_cast<const char *>(&patternRecognitionMode), sizeof(patternRecognitionMode)));
++
++ QByteArray mapData;
++ QDataStream ds(&mapData, QIODevice::WriteOnly);
++ ds << traceCaptureDeviceInfo;
++ ds << traceScreenInfo;
++ hash.addData(mapData);
++
++ return hash.result();
++ }
++
++ void setContext(QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
++ const QVariantMap &traceCaptureDeviceInfo,
++ const QVariantMap &traceScreenInfo,
++ const QByteArray &context)
++ {
++ Q_UNUSED(patternRecognitionMode);
++ Q_UNUSED(traceScreenInfo);
++ if (context == currentContext)
++ return;
++ currentContext = context;
++
++ qCDebug(lcExampleHwr) << "setContext:" << QLatin1String((context.toHex()));
++
++ // Finish recognition, but preserve current input
++ const int dpi = traceCaptureDeviceInfo.value(QLatin1String("dpi"), 96).toInt();
++ static const int INSTANT_GESTURE_WIDTH_THRESHOLD_MM = 25;
++ gestureWidthThreshold = qRound(INSTANT_GESTURE_WIDTH_THRESHOLD_MM / 25.4 * dpi);
++
++ gestureRecognizer.setDpi(dpi);
++ }
++
++ QVirtualKeyboardTrace *traceBegin(
++ int traceId, QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
++ const QVariantMap &traceCaptureDeviceInfo, const QVariantMap &traceScreenInfo)
++ {
++ // The result id follows the trace id so that the (previous)
++ // results completed during the handwriting can be rejected.
++ resultId = traceId;
++
++ QByteArray context = getContext(patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo);
++ if (context != currentContext) {
++ setContext(patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo, context);
++ }
++
++ Q_Q(ExampleHwrInputMethod);
++ QVirtualKeyboardTrace *trace = new QVirtualKeyboardTrace(q);
++ traceList.append(trace);
++
++ return trace;
++ }
++
++ void traceEnd(QVirtualKeyboardTrace *trace)
++ {
++ if (trace->isCanceled()) {
++ traceList.removeOne(trace);
++ delete trace;
++ } else if (handleGesture()) {
++ finishRecognition();
++ return;
++ }
++ if (!traceList.isEmpty()) {
++ if (countActiveTraces() == 0)
++ restartRecognition();
++ }
++ }
++
++ int countActiveTraces() const
++ {
++ int count = 0;
++ for (QVirtualKeyboardTrace *trace : std::as_const(traceList)) {
++ if (!trace->isFinal())
++ count++;
++ }
++ return count;
++ }
++
++ void clearTraces()
++ {
++ qDeleteAll(traceList);
++ traceList.clear();
++ }
++
++ bool applyGesture(const QChar &gesture)
++ {
++ Q_Q(ExampleHwrInputMethod);
++ QVirtualKeyboardInputContext *ic = q->inputContext();
++ switch (gesture.unicode()) {
++ case '\b':
++ return ic->inputEngine()->virtualKeyClick(Qt::Key_Backspace, QString(), Qt::NoModifier);
++ case '\r':
++ return ic->inputEngine()->virtualKeyClick(Qt::Key_Return, QLatin1String("\n"), Qt::NoModifier);
++ case ' ':
++ return ic->inputEngine()->virtualKeyClick(Qt::Key_Space, QLatin1String(" "), Qt::NoModifier);
++ default:
++ return false;
++ }
++ }
++
++ bool handleGesture()
++ {
++ if (countActiveTraces() > 0)
++ return false;
++
++ QVariantMap gesture(gestureRecognizer.recognize(traceList.mid(traceList.size() - 1, 1)));
++ if (gesture.isEmpty())
++ return false;
++
++ qCDebug(lcExampleHwr) << "handleGesture:" << gesture;
++
++ if (gesture[QLatin1String("type")].toString() == QLatin1String("swipe")) {
++
++ static const int SWIPE_ANGLE_THRESHOLD = 15; // degrees +-
++
++ qreal swipeLength = gesture[QLatin1String("length")].toReal();
++ if (swipeLength >= gestureWidthThreshold) {
++
++ Q_Q(ExampleHwrInputMethod);
++ QVirtualKeyboardInputContext *ic = q->inputContext();
++ if (!ic)
++ return false;
++
++ qreal swipeAngle = gesture[QLatin1String("angle_degrees")].toReal();
++ int swipeTouchCount = gesture[QLatin1String("touch_count")].toInt();
++
++ // Swipe left
++ if (swipeAngle <= 180 + SWIPE_ANGLE_THRESHOLD && swipeAngle >= 180 - SWIPE_ANGLE_THRESHOLD) {
++ if (swipeTouchCount == 1) {
++ // Single swipe: backspace
++ ic->inputEngine()->virtualKeyClick(Qt::Key_Backspace, QString(), Qt::NoModifier);
++ return true;
++ }
++ return false;
++ }
++
++ // Swipe right
++ if (swipeAngle <= SWIPE_ANGLE_THRESHOLD || swipeAngle >= 360 - SWIPE_ANGLE_THRESHOLD) {
++ if (swipeTouchCount == 1) {
++ // Single swipe: space
++ ic->inputEngine()->virtualKeyClick(Qt::Key_Space, QLatin1String(" "), Qt::NoModifier);
++ return true;
++ }
++ return false;
++ }
++
++ // Swipe up
++ if (swipeAngle <= 270 + SWIPE_ANGLE_THRESHOLD && swipeAngle >= 270 - SWIPE_ANGLE_THRESHOLD) {
++ if (swipeTouchCount == 1) {
++ // Single swipe: toggle input mode
++ select();
++ if (!(ic->inputMethodHints() & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly))) {
++ QList<int> inputModes = ic->inputEngine()->inputModes();
++ // Filter out duplicate numeric mode (in favor of Numeric)
++ int indexOfNumericInputMode = inputModes.indexOf(static_cast<int>(QVirtualKeyboardInputEngine::InputMode::Numeric));
++ int indexOfDialableInputMode = inputModes.indexOf(static_cast<int>(QVirtualKeyboardInputEngine::InputMode::Dialable));
++ if (indexOfNumericInputMode != -1 && indexOfDialableInputMode != -1)
++ inputModes.removeAt(inputMode != QVirtualKeyboardInputEngine::InputMode::Dialable ?
++ indexOfDialableInputMode :
++ indexOfNumericInputMode);
++ if (inputModes.size() > 1) {
++ int inputModeIndex = inputModes.indexOf(static_cast<int>(inputMode)) + 1;
++ if (inputModeIndex >= inputModes.size())
++ inputModeIndex = 0;
++ ic->inputEngine()->setInputMode(static_cast<QVirtualKeyboardInputEngine::InputMode>(inputModes.at(inputModeIndex)));
++ }
++ }
++ return true;
++ }
++ }
++ }
++ }
++
++ return false;
++ }
++
++ bool isValidInputChar(const QChar &c) const
++ {
++ if (c.isLetterOrNumber())
++ return true;
++ if (isJoiner(c))
++ return true;
++ return false;
++ }
++
++ bool isJoiner(const QChar &c) const
++ {
++ if (c.isPunct() || c.isSymbol()) {
++ Q_Q(const ExampleHwrInputMethod);
++ QVirtualKeyboardInputContext *ic = q->inputContext();
++ if (ic) {
++ Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
++ if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) || inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly))
++ return QString(QStringLiteral(":/?#[]@!$&'()*+,;=-_.%")).contains(c);
++ }
++ ushort unicode = c.unicode();
++ if (unicode == Qt::Key_Apostrophe || unicode == Qt::Key_Minus)
++ return true;
++ }
++ return false;
++ }
++
++ void restartRecognition()
++ {
++ qCDebug(lcExampleHwr) << "restartRecognition";
++
++ resetResultTimer(Settings::instance()->hwrTimeoutForAlphabetic());
++ }
++
++ bool finishRecognition(bool emitSelectionListChanged = true)
++ {
++ qCDebug(lcExampleHwr) << "finishRecognition";
++ bool result = !traceList.isEmpty();
++
++ stopResultTimer();
++ clearTraces();
++
++ if (!wordCandidates.isEmpty()) {
++ wordCandidates.clear();
++ activeWordIndex = -1;
++ if (emitSelectionListChanged) {
++ Q_Q(ExampleHwrInputMethod);
++ emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
++ emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
++ }
++ result = true;
++ }
++
++ return result;
++ }
++
++ void select(int index = -1)
++ {
++ Q_Q(ExampleHwrInputMethod);
++ QVirtualKeyboardInputContext *ic = q->inputContext();
++ if (!ic)
++ return;
++
++ if (!wordCandidates.isEmpty())
++ ic->commit(wordCandidates.at(index != -1 ? index : 0));
++
++ finishRecognition();
++ }
++
++ void processResult()
++ {
++ qCDebug(lcExampleHwr) << "processResult";
++
++ Q_Q(ExampleHwrInputMethod);
++ QVirtualKeyboardInputContext *ic = q->inputContext();
++ if (!ic)
++ return;
++
++ QStringList newWordCandidates;
++ QString word = !wordCandidates.isEmpty() ? wordCandidates.at(0) : QString();
++ switch (inputMode) {
++ case QVirtualKeyboardInputEngine::InputMode::Latin:
++ appendRandomChar(word);
++ break;
++ case QVirtualKeyboardInputEngine::InputMode::Numeric:
++ case QVirtualKeyboardInputEngine::InputMode::Dialable:
++ appendRandomDigit(word);
++ break;
++ default:
++ break;
++ }
++ newWordCandidates.append(word);
++ activeWordIndex = 0;
++ wordCandidates = newWordCandidates;
++ qCDebug(lcExampleHwr) << "wordCandidates" << wordCandidates;
++ ic->setPreeditText(word);
++
++ emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
++ emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
++ }
++
++ static void appendRandomChar(QString& word)
++ {
++ word.append(QChar('a' + QRandomGenerator::global()->bounded(26)));
++ }
++
++ static void appendRandomDigit(QString& word)
++ {
++ word.append(QChar('0' + QRandomGenerator::global()->bounded(10)));
++ }
++
++ void resetResultTimer(int interval = 500)
++ {
++ qCDebug(lcExampleHwr) << "resetResultTimer:" << interval;
++ Q_Q(ExampleHwrInputMethod);
++ stopResultTimer();
++ resultTimer = q->startTimer(interval);
++ }
++
++ void stopResultTimer()
++ {
++ if (resultTimer) {
++ qCDebug(lcExampleHwr) << "stopResultTimer";
++ Q_Q(ExampleHwrInputMethod);
++ q->killTimer(resultTimer);
++ resultTimer = 0;
++ }
++ }
++
++ ExampleHwrInputMethod *q_ptr = nullptr;
++ QVirtualKeyboardInputEngine::InputMode inputMode = QVirtualKeyboardInputEngine::InputMode::Latin;
++ QByteArray currentContext;
++ int gestureWidthThreshold = 0;
++ int resultId = 0;
++ int lastResultId = 0;
++ int resultTimer = 0;
++ QList<QVirtualKeyboardTrace *> traceList;
++ HandwritingGestureRecognizer gestureRecognizer;
++ QStringList wordCandidates;
++ int activeWordIndex = -1;
++ };
++
++ /*!
++ \class QtVirtualKeyboard::ExampleHwrInputMethod
++ \internal
++ */
++
++ ExampleHwrInputMethod::ExampleHwrInputMethod(QObject *parent) :
++ QVirtualKeyboardAbstractInputMethod(*new ExampleHwrInputMethodPrivate(this), parent)
++ {
++ }
++
++ ExampleHwrInputMethod::~ExampleHwrInputMethod()
++ {
++ }
++
++ QList<QVirtualKeyboardInputEngine::InputMode> ExampleHwrInputMethod::inputModes(const QString &locale)
++ {
++ Q_UNUSED(locale);
++ QList<QVirtualKeyboardInputEngine::InputMode> availableInputModes = {
++ QVirtualKeyboardInputEngine::InputMode::Latin,
++ QVirtualKeyboardInputEngine::InputMode::Numeric,
++ };
++ return availableInputModes;
++ }
++
++ bool ExampleHwrInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
++ {
++ Q_D(ExampleHwrInputMethod);
++ d->select();
++ return d->setInputMode(QLocale(locale), inputMode);
++ }
++
++ bool ExampleHwrInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
++ {
++ Q_UNUSED(textCase);
++ return true;
++ }
++
++ bool ExampleHwrInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
++ {
++ Q_UNUSED(modifiers);
++ Q_D(ExampleHwrInputMethod);
++ switch (key) {
++ case Qt::Key_Enter:
++ case Qt::Key_Return:
++ case Qt::Key_Tab:
++ case Qt::Key_Space:
++ d->select();
++ update();
++ break;
++
++ case Qt::Key_Backspace:
++ {
++ QVirtualKeyboardInputContext *ic = inputContext();
++ QString preeditText = ic->preeditText();
++ if (preeditText.length() > 1) {
++ preeditText.chop(1);
++ ic->setPreeditText(preeditText);
++ emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
++ emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
++ return true;
++ } else {
++ bool result = !preeditText.isEmpty();
++ if (result)
++ ic->clear();
++ d->finishRecognition();
++ return result;
++ }
++ }
++
++ default:
++ if (text.length() > 0) {
++ QVirtualKeyboardInputContext *ic = inputContext();
++ QString preeditText = ic->preeditText();
++ QChar c = text.at(0);
++ bool addToWord = d->isValidInputChar(c) && (!preeditText.isEmpty() || !d->isJoiner(c));
++ if (addToWord) {
++ preeditText.append(text);
++ ic->setPreeditText(preeditText);
++ d->finishRecognition(false);
++ d->wordCandidates.append(preeditText);
++ d->activeWordIndex = 0;
++ emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
++ emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
++ return true;
++ } else {
++ ic->commit();
++ d->finishRecognition();
++ }
++ break;
++ }
++ }
++ return false;
++ }
++
++ void ExampleHwrInputMethod::reset()
++ {
++ Q_D(ExampleHwrInputMethod);
++ qCDebug(lcExampleHwr) << "reset";
++ d->finishRecognition();
++ }
++
++ void ExampleHwrInputMethod::update()
++ {
++ Q_D(ExampleHwrInputMethod);
++ qCDebug(lcExampleHwr) << "update";
++ d->select();
++ }
++
++ QList<QVirtualKeyboardSelectionListModel::Type> ExampleHwrInputMethod::selectionLists()
++ {
++ return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList;
++ }
++
++ int ExampleHwrInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
++ {
++ Q_UNUSED(type);
++ Q_D(ExampleHwrInputMethod);
++ return d->wordCandidates.size();
++ }
++
++ QVariant ExampleHwrInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role)
++ {
++ QVariant result;
++ Q_D(ExampleHwrInputMethod);
++ switch (role) {
++ case QVirtualKeyboardSelectionListModel::Role::Display:
++ result = QVariant(d->wordCandidates.at(index));
++ break;
++ case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength:
++ result.setValue(0);
++ break;
++ case QVirtualKeyboardSelectionListModel::Role::Dictionary:
++ {
++ QVirtualKeyboardSelectionListModel::DictionaryType dictionaryType =
++ QVirtualKeyboardSelectionListModel::DictionaryType::Default;
++ result = QVariant(static_cast<int>(dictionaryType));
++ break;
++ }
++ case QVirtualKeyboardSelectionListModel::Role::CanRemoveSuggestion:
++ result = QVariant(false);
++ break;
++ default:
++ result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role);
++ break;
++ }
++ return result;
++ }
++
++ void ExampleHwrInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
++ {
++ Q_UNUSED(type);
++ Q_D(ExampleHwrInputMethod);
++ d->select(index);
++ }
++
++ QList<QVirtualKeyboardInputEngine::PatternRecognitionMode> ExampleHwrInputMethod::patternRecognitionModes() const
++ {
++ return QList<QVirtualKeyboardInputEngine::PatternRecognitionMode>()
++ << QVirtualKeyboardInputEngine::PatternRecognitionMode::Handwriting;
++ }
++
++ QVirtualKeyboardTrace *ExampleHwrInputMethod::traceBegin(
++ int traceId, QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
++ const QVariantMap &traceCaptureDeviceInfo, const QVariantMap &traceScreenInfo)
++ {
++ Q_D(ExampleHwrInputMethod);
++ return d->traceBegin(traceId, patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo);
++ }
++
++ bool ExampleHwrInputMethod::traceEnd(QVirtualKeyboardTrace *trace)
++ {
++ Q_D(ExampleHwrInputMethod);
++ d->traceEnd(trace);
++ return true;
++ }
++
++ bool ExampleHwrInputMethod::reselect(int cursorPosition, const QVirtualKeyboardInputEngine::ReselectFlags &reselectFlags)
++ {
++ Q_D(ExampleHwrInputMethod);
++
++ QVirtualKeyboardInputContext *ic = inputContext();
++ if (!ic)
++ return false;
++
++ const int maxLength = 32;
++ const QString surroundingText = ic->surroundingText();
++ int replaceFrom = 0;
++ QString stringStart;
++
++ if (cursorPosition > surroundingText.length())
++ return false;
++
++ if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordBeforeCursor)) {
++ for (int i = cursorPosition - 1; i >= 0 && stringStart.length() < maxLength; --i) {
++ QChar c = surroundingText.at(i);
++ if (!d->isValidInputChar(c))
++ break;
++ stringStart.insert(0, c);
++ --replaceFrom;
++ }
++
++ while (replaceFrom < 0 && d->isJoiner(stringStart.at(0))) {
++ stringStart.remove(0, 1);
++ ++replaceFrom;
++ }
++ }
++
++ if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == 0) {
++ stringStart.clear();
++ return false;
++ }
++
++ if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAfterCursor)) {
++ for (int i = cursorPosition; i < surroundingText.length() && stringStart.length() < maxLength; ++i) {
++ QChar c = surroundingText.at(i);
++ if (!d->isValidInputChar(c))
++ break;
++ stringStart.append(c);
++ }
++
++ while (replaceFrom > -stringStart.length()) {
++ int lastPos = stringStart.length() - 1;
++ if (!d->isJoiner(stringStart.at(lastPos)))
++ break;
++ stringStart.remove(lastPos, 1);
++ }
++ }
++
++ if (stringStart.isEmpty())
++ return false;
++
++ if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == -stringStart.length() && stringStart.length() < maxLength) {
++ stringStart.clear();
++ return false;
++ }
++
++ if (d->isJoiner(stringStart.at(0))) {
++ stringStart.clear();
++ return false;
++ }
++
++ if (d->isJoiner(stringStart.at(stringStart.length() - 1))) {
++ stringStart.clear();
++ return false;
++ }
++
++ ic->setPreeditText(stringStart, QList<QInputMethodEvent::Attribute>(), replaceFrom, stringStart.length());
++ d->activeWordIndex = 0;
++ d->wordCandidates = {stringStart};
++ emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
++ emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
++
++ return true;
++ }
++
++ void ExampleHwrInputMethod::timerEvent(QTimerEvent *timerEvent)
++ {
++ Q_D(ExampleHwrInputMethod);
++ int timerId = timerEvent->timerId();
++ qCDebug(lcExampleHwr) << "timerEvent():" << timerId;
++ if (timerId == d->resultTimer) {
++ if (!d->countActiveTraces()) {
++ d->stopResultTimer();
++ d->processResult();
++ d->clearTraces();
++ }
++ }
++ }
++
++ } // namespace QtVirtualKeyboard
++ QT_END_NAMESPACE