diff options
Diffstat (limited to 'src/plugins/example/hwr/examplehwrinputmethod.cpp')
-rw-r--r-- | src/plugins/example/hwr/examplehwrinputmethod.cpp | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/src/plugins/example/hwr/examplehwrinputmethod.cpp b/src/plugins/example/hwr/examplehwrinputmethod.cpp new file mode 100644 index 00000000..58d1d2c8 --- /dev/null +++ b/src/plugins/example/hwr/examplehwrinputmethod.cpp @@ -0,0 +1,632 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#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 |