// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h>
#include <QtVirtualKeyboard/private/platforminputcontext_p.h>
#include <QtVirtualKeyboard/private/settings_p.h>
#include <QtVirtualKeyboard/private/shifthandler_p.h>
#include <QtVirtualKeyboard/private/virtualkeyboarddebug_p.h>
#include <QtVirtualKeyboard/private/enterkeyaction_p.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
#include <QtVirtualKeyboard/qvirtualkeyboardobserver.h>
#include <QtVirtualKeyboard/private/enterkeyactionattachedtype_p.h>
#include <QtVirtualKeyboard/private/virtualkeyboardattachedtype_p.h>
#include <QtVirtualKeyboard/qvirtualkeyboarddictionarymanager.h>

#include <QFile>
#include <QGuiApplication>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickwindow.h>
#include <QtGui/qpa/qplatformintegration.h>
#include <QtGui/private/qguiapplication_p.h>

QT_BEGIN_NAMESPACE

using namespace QtVirtualKeyboard;


bool QtVirtualKeyboard::forceEventsWithoutFocus()
{
    static bool env = qEnvironmentVariableIsSet("QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS");
    return env;
}

QVirtualKeyboardInputContextPrivate::QVirtualKeyboardInputContextPrivate(QVirtualKeyboardInputContext *q_ptr) :
    QObject(nullptr),
    q_ptr(q_ptr),
    platformInputContext(nullptr),
    inputEngine(nullptr),
    _shiftHandler(nullptr),
    keyboardRect(),
    previewRect(),
    _previewVisible(false),
    animating(false),
    _focus(false),
    cursorPosition(0),
    anchorPosition(0),
    forceAnchorPosition(-1),
    _forceCursorPosition(-1),
    inputMethodHints(Qt::ImhNone),
    preeditText(),
    preeditTextAttributes(),
    surroundingText(),
    selectedText(),
    anchorRectangle(),
    cursorRectangle(),
    selectionControlVisible(false),
    anchorRectIntersectsClipRect(false),
    cursorRectIntersectsClipRect(false),
    activeNavigationKeys()
{
}

void QVirtualKeyboardInputContextPrivate::init()
{
    Q_Q(QVirtualKeyboardInputContext);
    QGuiApplicationPrivate *guiApplicationPrivate = QGuiApplicationPrivate::instance();
    QPlatformIntegration *platformIntegration = guiApplicationPrivate->platformIntegration();
    QPlatformInputContext *unknownPlatformInputContext = platformIntegration->inputContext();
    platformInputContext = qobject_cast<PlatformInputContext *>(unknownPlatformInputContext);
    inputEngine = new QVirtualKeyboardInputEngine(q);
    _shiftHandler = new ShiftHandler(q);
    inputEngine->init();
    _shiftHandler->init();
    _shadow.setInputContext(q);
    if (platformInputContext) {
        platformInputContext->setInputContext(q);
        QObject::connect(platformInputContext, &PlatformInputContext::focusObjectChanged, this, &QVirtualKeyboardInputContextPrivate::onInputItemChanged);
        QObject::connect(platformInputContext, &PlatformInputContext::focusObjectChanged, this, &QVirtualKeyboardInputContextPrivate::inputItemChanged);
    }
}

QVirtualKeyboardInputContextPrivate::~QVirtualKeyboardInputContextPrivate()
{
}

bool QVirtualKeyboardInputContextPrivate::focus() const
{
    return _focus;
}

void QVirtualKeyboardInputContextPrivate::setFocus(bool focus)
{
    if (_focus != focus) {
        VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::setFocus():" << focus;
        _focus = focus;
        emit focusChanged();
    }
}

QRectF QVirtualKeyboardInputContextPrivate::keyboardRectangle() const
{
    return keyboardRect;
}

void QVirtualKeyboardInputContextPrivate::setKeyboardRectangle(QRectF rectangle)
{
    if (keyboardRect != rectangle) {
        keyboardRect = rectangle;
        emit keyboardRectangleChanged();
        if (platformInputContext)
            platformInputContext->emitKeyboardRectChanged();
    }
}

QRectF QVirtualKeyboardInputContextPrivate::previewRectangle() const
{
    return previewRect;
}

void QVirtualKeyboardInputContextPrivate::setPreviewRectangle(QRectF rectangle)
{
    if (previewRect != rectangle) {
        previewRect = rectangle;
        emit previewRectangleChanged();
    }
}

bool QVirtualKeyboardInputContextPrivate::previewVisible() const
{
    return _previewVisible;
}

void QVirtualKeyboardInputContextPrivate::setPreviewVisible(bool visible)
{
    if (_previewVisible != visible) {
        _previewVisible = visible;
        emit previewVisibleChanged();
    }
}

QString QVirtualKeyboardInputContextPrivate::locale() const
{
    return platformInputContext ? platformInputContext->locale().name() : QString();
}

void QVirtualKeyboardInputContextPrivate::setLocale(const QString &locale)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::setLocale():" << locale;
    if (!platformInputContext)
        return;
    QLocale newLocale(locale);
    if (newLocale != platformInputContext->locale()) {
        platformInputContext->setLocale(newLocale);
        platformInputContext->setInputDirection(newLocale.textDirection());
        emit localeChanged();
    }
}

QObject *QVirtualKeyboardInputContextPrivate::inputItem() const
{
    return platformInputContext ? platformInputContext->focusObject() : nullptr;
}

ShiftHandler *QVirtualKeyboardInputContextPrivate::shiftHandler() const
{
    return _shiftHandler;
}

ShadowInputContext *QVirtualKeyboardInputContextPrivate::shadow() const
{
    return const_cast<ShadowInputContext *>(&_shadow);
}

void QVirtualKeyboardInputContextPrivate::setKeyboardObserver(QVirtualKeyboardObserver *keyboardObserver)
{
    if (!this->keyboardObserver.isNull())
        return;

    this->keyboardObserver = keyboardObserver;
}

bool QVirtualKeyboardInputContextPrivate::fileExists(const QUrl &fileUrl)
{
    QString fileName;
    if (fileUrl.scheme() == QLatin1String("qrc")) {
        fileName = QLatin1Char(':') + fileUrl.path();
    } else {
        fileName = fileUrl.toLocalFile();
    }
    return !fileName.isEmpty() && QFile::exists(fileName);
}

bool QVirtualKeyboardInputContextPrivate::hasEnterKeyAction(QObject *item) const
{
    return item != nullptr && qmlAttachedPropertiesObject<EnterKeyAction>(item, false);
}

void QVirtualKeyboardInputContextPrivate::registerInputPanel(QObject *inputPanel)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::registerInputPanel():" << inputPanel;
    Q_ASSERT(!this->inputPanel);
    this->inputPanel = inputPanel;
}

void QVirtualKeyboardInputContextPrivate::hideInputPanel()
{
    if (platformInputContext)
        platformInputContext->hideInputPanel();
}

void QVirtualKeyboardInputContextPrivate::updateAvailableLocales(const QStringList &availableLocales)
{
    Settings *settings = Settings::instance();
    if (settings)
        settings->setAvailableLocales(availableLocales);
}

void QVirtualKeyboardInputContextPrivate::forceCursorPosition(int anchorPosition, int cursorPosition)
{
    if (!_shadow.inputItem())
        return;
    if (!platformInputContext->m_visible)
        return;
    if (testState(State::Reselect))
        return;
    if (testState(State::SyncShadowInput))
        return;

    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::forceCursorPosition():" << cursorPosition << "anchorPosition:" << anchorPosition;
    if (!preeditText.isEmpty()) {
        forceAnchorPosition = -1;
        _forceCursorPosition = cursorPosition;
        if (cursorPosition > this->cursorPosition)
            _forceCursorPosition += preeditText.size();
        commit();
    } else {
        forceAnchorPosition = anchorPosition;
        _forceCursorPosition = cursorPosition;
        Q_Q(QVirtualKeyboardInputContext);
        q->setPreeditText(QString());
        if (!inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
                   cursorPosition > 0 && selectedText.isEmpty()) {
            QVirtualKeyboardScopedState reselectState(this, State::Reselect);
            if (inputEngine->reselect(cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor))
                setState(State::InputMethodClick);
        }
    }
}

/*! \internal
    The context private becomes a containment mask for a dimmer opened by a
    modal QQuickPopup. The dimmer eats events, and the virtual keyboard must
    continue to work during modal sessions as well. This implementation lets
    all pointer events within the area of the input panel through.
*/
bool QVirtualKeyboardInputContextPrivate::contains(const QPointF &point) const
{
    bool hit = false;
    if (dimmer && platformInputContext->isInputPanelVisible()) {
        const auto scenePoint = dimmer->mapToScene(point);
        if (keyboardRectangle().contains(scenePoint)) {
            hit = true;
        } else if (QQuickItem *vkbPanel = qobject_cast<QQuickItem*>(inputPanel)) {
            const auto vkbPanelPoint = vkbPanel->mapFromScene(scenePoint);
            if (vkbPanel->contains(vkbPanelPoint))
                hit = true;
        }
    }
    // dimmer doesn't contain points that hit the input panel
    return !hit;
}

KeyboardFunctionKey QVirtualKeyboardInputContextPrivate::keyboardFunctionKey(QtVirtualKeyboard::KeyboardFunction keyboardFunction) const
{
    switch (keyboardFunction) {
    case KeyboardFunction::HideInputPanel:
        return KeyboardFunctionKey::Hide;
    case KeyboardFunction::ChangeLanguage:
        return KeyboardFunctionKey::Language;
    case KeyboardFunction::ToggleHandwritingMode:
        return KeyboardFunctionKey::None;
    }
    return KeyboardFunctionKey::None;
}

void QVirtualKeyboardInputContextPrivate::onInputItemChanged()
{
    QObject *item = inputItem();
    if (item) {
        if (QQuickItem *vkbPanel = qobject_cast<QQuickItem*>(inputPanel)) {
            if (QQuickItem *quickItem = qobject_cast<QQuickItem*>(item)) {
                const QVariant isDesktopPanel = vkbPanel->property("desktopPanel");
                if (isDesktopPanel.isValid() && !isDesktopPanel.toBool()) {
                    // Integrated keyboards used in a Qt Quick Controls UI must continue to
                    // work during a modal session, which is implemented using an overlay
                    // and dimmer item. So, make use of some QQC2 internals to find out if
                    // there is a dimmer, and if so, make ourselves the containment mask
                    // that can let pointer events through to the keyboard.
                    if (QQuickWindow *quickWindow = quickItem->window()) {
                        if (QQuickItem *overlay = quickWindow->property("_q_QQuickOverlay").value<QQuickItem*>()) {
                            if (dimmer && dimmer->containmentMask() == this) {
                                dimmer->setContainmentMask(nullptr);
                                dimmer = nullptr;
                            }
                            if (overlay && overlay->isVisible()) {
                                dimmer = overlay->property("_q_dimmerItem").value<QQuickItem*>();
                                if (dimmer)
                                    dimmer->setContainmentMask(this);
                            }
                        }
                    }
                }
            }
        }
    } else {
        if (!activeKeys.isEmpty()) {
            // After losing keyboard focus it is impossible to track pressed keys
            activeKeys.clear();
            clearState(State::KeyEvent);
        }
    }
    clearState(State::InputMethodClick);

    QStringList extraDictionaries;
    if (item) {
        VirtualKeyboardAttachedType *virtualKeyboardAttachedType = static_cast<VirtualKeyboardAttachedType *>(qmlAttachedPropertiesObject<VirtualKeyboard>(item, false));
        if (virtualKeyboardAttachedType)
            extraDictionaries = virtualKeyboardAttachedType->extraDictionaries();
    }
    QVirtualKeyboardDictionaryManager::instance()->setExtraDictionaries(extraDictionaries);
}

void QVirtualKeyboardInputContextPrivate::sendPreedit(const QString &text, const QList<QInputMethodEvent::Attribute> &attributes, int replaceFrom, int replaceLength)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::sendPreedit()"
#ifdef SENSITIVE_DEBUG
           << text << replaceFrom << replaceLength
#endif
        ;

    bool textChanged = preeditText != text;
    bool attributesChanged = preeditTextAttributes != attributes;

    if (textChanged || attributesChanged) {
        preeditText = text;
        preeditTextAttributes = attributes;

        if (platformInputContext) {
            QInputMethodEvent event(text, attributes);
            const bool replace = replaceFrom != 0 || replaceLength > 0;
            if (replace)
                event.setCommitString(QString(), replaceFrom, replaceLength);

            sendInputMethodEvent(&event);

            // Send also to shadow input if only attributes changed.
            // In this case the update() may not be called, so the shadow
            // input may be out of sync.
            if (_shadow.inputItem() && !replace && !text.isEmpty() &&
                    !textChanged && attributesChanged) {
                VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::sendPreedit(shadow)"
#ifdef SENSITIVE_DEBUG
                       << text << replaceFrom << replaceLength
#endif
                    ;
                event.setAccepted(true);
                QGuiApplication::sendEvent(_shadow.inputItem(), &event);
            }
        }

        if (textChanged) {
            Q_Q(QVirtualKeyboardInputContext);
            emit q->preeditTextChanged();
        }
    }

    if (preeditText.isEmpty())
        preeditTextAttributes.clear();
}

void QVirtualKeyboardInputContextPrivate::sendInputMethodEvent(QInputMethodEvent *event)
{
    if (!platformInputContext)
        return;
    QVirtualKeyboardScopedState inputMethodEventState(this, State::InputMethodEvent);
    platformInputContext->sendEvent(event);
}

void QVirtualKeyboardInputContextPrivate::reset()
{
    inputEngine->reset();
}

void QVirtualKeyboardInputContextPrivate::commit()
{
    inputEngine->update();
}

void QVirtualKeyboardInputContextPrivate::update(Qt::InputMethodQueries queries)
{
    Q_Q(QVirtualKeyboardInputContext);

    // No need to fetch input clip rectangle during animation
    if (!(queries & ~Qt::ImInputItemClipRectangle) && animating)
        return;

    if (!platformInputContext)
        return;

    // fetch
    QInputMethodQueryEvent imQueryEvent(Qt::InputMethodQueries(Qt::ImHints |
                    Qt::ImQueryInput | Qt::ImInputItemClipRectangle));
    platformInputContext->sendEvent(&imQueryEvent);
    Qt::InputMethodHints inputMethodHints = Qt::InputMethodHints(imQueryEvent.value(Qt::ImHints).toInt());
    inputMethodHints |= Settings::instance()->inputMethodHints();
    const int cursorPosition = imQueryEvent.value(Qt::ImCursorPosition).toInt();
    const int anchorPosition = imQueryEvent.value(Qt::ImAnchorPosition).toInt();
    QRectF anchorRectangle;
    QRectF cursorRectangle;
    if (const QGuiApplication *app = qApp) {
        anchorRectangle = app->inputMethod()->anchorRectangle();
        cursorRectangle = app->inputMethod()->cursorRectangle();
    } else {
        anchorRectangle = this->anchorRectangle;
        cursorRectangle = this->cursorRectangle;
    }
    QString surroundingText = imQueryEvent.value(Qt::ImSurroundingText).toString();
    QString selectedText = imQueryEvent.value(Qt::ImCurrentSelection).toString();

    // check against changes
    bool newInputMethodHints = inputMethodHints != this->inputMethodHints;
    bool newSurroundingText = surroundingText != this->surroundingText;
    bool newSelectedText = selectedText != this->selectedText;
    bool newAnchorPosition = anchorPosition != this->anchorPosition;
    bool newCursorPosition = cursorPosition != this->cursorPosition;
    bool newAnchorRectangle = anchorRectangle != this->anchorRectangle;
    bool newCursorRectangle = cursorRectangle != this->cursorRectangle;
    bool selectionControlVisible = platformInputContext->evaluateInputPanelVisible() && (cursorPosition != anchorPosition) && !inputMethodHints.testFlag(Qt::ImhNoTextHandles);
    bool newSelectionControlVisible = selectionControlVisible != this->selectionControlVisible;

    QRectF inputItemClipRect = imQueryEvent.value(Qt::ImInputItemClipRectangle).toRectF();
    QRectF anchorRect = imQueryEvent.value(Qt::ImAnchorRectangle).toRectF();
    QRectF cursorRect = imQueryEvent.value(Qt::ImCursorRectangle).toRectF();

    bool anchorRectIntersectsClipRect = inputItemClipRect.intersects(anchorRect);
    bool newAnchorRectIntersectsClipRect = anchorRectIntersectsClipRect != this->anchorRectIntersectsClipRect;

    bool cursorRectIntersectsClipRect = inputItemClipRect.intersects(cursorRect);
    bool newCursorRectIntersectsClipRect = cursorRectIntersectsClipRect != this->cursorRectIntersectsClipRect;

    // update
    this->inputMethodHints = inputMethodHints;
    this->surroundingText = surroundingText;
    this->selectedText = selectedText;
    this->anchorPosition = anchorPosition;
    this->cursorPosition = cursorPosition;
    this->anchorRectangle = anchorRectangle;
    this->cursorRectangle = cursorRectangle;
    this->selectionControlVisible = selectionControlVisible;
    this->anchorRectIntersectsClipRect = anchorRectIntersectsClipRect;
    this->cursorRectIntersectsClipRect = cursorRectIntersectsClipRect;

    // update input engine
    if ((newSurroundingText || newCursorPosition) &&
            !testState(State::InputMethodEvent)) {
        commit();
    }
    if (newInputMethodHints) {
        reset();
    }

    // notify
    if (newInputMethodHints) {
        emit q->inputMethodHintsChanged();
    }
    if (newSurroundingText) {
        emit q->surroundingTextChanged();
    }
    if (newSelectedText) {
        emit q->selectedTextChanged();
    }
    if (newAnchorPosition) {
        emit q->anchorPositionChanged();
    }
    if (newCursorPosition) {
        emit q->cursorPositionChanged();
    }
    if (newAnchorRectangle) {
        emit q->anchorRectangleChanged();
    }
    if (newCursorRectangle) {
        emit q->cursorRectangleChanged();
    }
    if (newSelectionControlVisible) {
        emit q->selectionControlVisibleChanged();
    }
    if (newAnchorRectIntersectsClipRect) {
        emit q->anchorRectIntersectsClipRectChanged();
    }
    if (newCursorRectIntersectsClipRect) {
        emit q->cursorRectIntersectsClipRectChanged();
    }

    // word reselection
    if (newInputMethodHints || newSurroundingText || newSelectedText)
        clearState(State::InputMethodClick);
    if ((newSurroundingText || newCursorPosition) && !newSelectedText && isEmptyState() &&
            !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
            cursorPosition > 0 && this->selectedText.isEmpty()) {
        QVirtualKeyboardScopedState reselectState(this, State::Reselect);
        if (inputEngine->reselect(cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor))
            setState(State::InputMethodClick);
    }

    if (!testState(State::SyncShadowInput)) {
        QVirtualKeyboardScopedState syncShadowInputState(this, State::SyncShadowInput);
        _shadow.update(queries);
    }
}

void QVirtualKeyboardInputContextPrivate::invokeAction(QInputMethod::Action action, int cursorPosition)
{
    switch (action) {
    case QInputMethod::Click:
        if (isEmptyState()) {
            if (inputEngine->clickPreeditText(cursorPosition))
                break;

            bool reselect = !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) && selectedText.isEmpty() && cursorPosition < preeditText.size();
            if (reselect) {
                QVirtualKeyboardScopedState reselectState(this, State::Reselect);
                _forceCursorPosition = this->cursorPosition + cursorPosition;
                commit();
                inputEngine->reselect(this->cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordBeforeCursor);
            } else if (!preeditText.isEmpty() && cursorPosition == preeditText.size()) {
                commit();
            }
        }
        clearState(State::InputMethodClick);
        break;

    case QInputMethod::ContextMenu:
        break;
    }
}

void QVirtualKeyboardInputContextPrivate::maybeCloseOnReturn()
{
    if (!Settings::instance()->closeOnReturn())
        return;

    const int imHints = QInputMethod::queryFocusObject(Qt::ImHints, QVariant()).toInt();
    if (imHints & Qt::ImhMultiLine)
        return;

    const Qt::EnterKeyType keyType = static_cast<Qt::EnterKeyType>(QInputMethod::queryFocusObject(Qt::ImEnterKeyType, QVariant()).toInt());
    switch (keyType) {
    case Qt::EnterKeyDefault:
    case Qt::EnterKeyDone:
    case Qt::EnterKeyGo:
    case Qt::EnterKeySend:
    case Qt::EnterKeySearch:
        break;
    case Qt::EnterKeyReturn:
    case Qt::EnterKeyNext:
    case Qt::EnterKeyPrevious:
        return;
    }

    if (EnterKeyActionAttachedType *enterKeyActionAttachedType = static_cast<EnterKeyActionAttachedType *>(qmlAttachedPropertiesObject<EnterKeyAction>(inputItem(), false))) {
        const EnterKeyAction::Id enterKeyActionId = static_cast<EnterKeyAction::Id>(enterKeyActionAttachedType->actionId());
        switch (enterKeyActionId) {
        case EnterKeyAction::None:
        case EnterKeyAction::Done:
        case EnterKeyAction::Go:
        case EnterKeyAction::Search:
        case EnterKeyAction::Send:
            break;
        case EnterKeyAction::Next:
            return;
        }
    }

    hideInputPanel();
}

bool QVirtualKeyboardInputContextPrivate::filterEvent(const QEvent *event)
{
    QEvent::Type type = event->type();
    if (type == QEvent::KeyPress || type == QEvent::KeyRelease) {
        const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
        const int key = keyEvent->key();

        // Keep track of pressed keys update key event state
        if (type == QEvent::KeyPress)
            activeKeys += keyEvent->nativeScanCode();
        else if (type == QEvent::KeyRelease)
            activeKeys -= keyEvent->nativeScanCode();

        if (activeKeys.isEmpty())
            clearState(State::KeyEvent);
        else
            setState(State::KeyEvent);

        if (Settings::instance()->arrowKeyNavigationEnabled()) {
            if ((key >= Qt::Key_Left && key <= Qt::Key_Down) || key == Qt::Key_Return) {
                if (type == QEvent::KeyPress && platformInputContext->isInputPanelVisible()) {
                    activeNavigationKeys += key;
                    emit navigationKeyPressed(key, keyEvent->isAutoRepeat());
                    return true;
                } else if (type == QEvent::KeyRelease && activeNavigationKeys.contains(key)) {
                    activeNavigationKeys -= key;
                    emit navigationKeyReleased(key, keyEvent->isAutoRepeat());
                    return true;
                }
            }
        }

        if (type == QEvent::KeyRelease && (key == Qt::Key_Return || key == Qt::Key_Enter)) {
            maybeCloseOnReturn();
        }

        // Break composing text since the virtual keyboard does not support hard keyboard events
        if (!preeditText.isEmpty()) {
            if (type == QEvent::KeyPress && (key == Qt::Key_Delete || key == Qt::Key_Backspace)) {
                reset();
                Q_Q(QVirtualKeyboardInputContext);
                q->clear();
                return true;
            } else {
                commit();
            }
        }
    } else if (type == QEvent::ShortcutOverride) {
        if (Settings::instance()->arrowKeyNavigationEnabled()) {
            const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
            int key = keyEvent->key();
            if ((key >= Qt::Key_Left && key <= Qt::Key_Down) || key == Qt::Key_Return)
                return true;
        }
    }

    return false;
}

void QVirtualKeyboardInputContextPrivate::addSelectionAttribute(QList<QInputMethodEvent::Attribute> &attributes)
{
    if (!testAttribute(attributes, QInputMethodEvent::Selection)) {
        // Convert Cursor attribute to Selection attribute.
        // In this case the cursor is set in pre-edit text, but
        // the cursor is not being forced to specific location.
        if (_forceCursorPosition == -1) {
            int cursorAttributeIndex = findAttribute(preeditTextAttributes, QInputMethodEvent::Cursor);
            if (cursorAttributeIndex != -1 && preeditTextAttributes[cursorAttributeIndex].length > 0)
                _forceCursorPosition = cursorPosition + preeditTextAttributes[cursorAttributeIndex].start;
            forceAnchorPosition = -1;
        }

        if (_forceCursorPosition != -1) {
            if (forceAnchorPosition != -1)
                attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, forceAnchorPosition, _forceCursorPosition - forceAnchorPosition, QVariant()));
            else
                attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, _forceCursorPosition, 0, QVariant()));
        }
    }
    forceAnchorPosition = -1;
    _forceCursorPosition = -1;
}

bool QVirtualKeyboardInputContextPrivate::testAttribute(const QList<QInputMethodEvent::Attribute> &attributes, QInputMethodEvent::AttributeType attributeType) const
{
    for (const QInputMethodEvent::Attribute &attribute : std::as_const(attributes)) {
        if (attribute.type == attributeType)
            return true;
    }
    return false;
}

int QVirtualKeyboardInputContextPrivate::findAttribute(const QList<QInputMethodEvent::Attribute> &attributes, QInputMethodEvent::AttributeType attributeType) const
{
    const int count = attributes.size();
    for (int i = 0; i < count; ++i) {
        if (attributes.at(i).type == attributeType)
            return i;
    }
    return -1;
}

void QVirtualKeyboardInputContextPrivate::updateSelectionControlVisible(bool inputPanelVisible)
{
    Q_Q(QVirtualKeyboardInputContext);
    bool newSelectionControlVisible = inputPanelVisible && (cursorPosition != anchorPosition) && !inputMethodHints.testFlag(Qt::ImhNoTextHandles);
    if (selectionControlVisible != newSelectionControlVisible) {
        selectionControlVisible = newSelectionControlVisible;
        emit q->selectionControlVisibleChanged();
    }
}

QT_END_NAMESPACE
