blob: a7c0aad9bc9a5b1cf1c992dac29607363dec2b8c [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 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 <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 <QGuiApplication>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickwindow.h>
#include <QtGui/qpa/qplatformintegration.h>
#include <QtGui/private/qguiapplication_p.h>
QT_BEGIN_NAMESPACE
bool operator==(const QInputMethodEvent::Attribute &attribute1, const QInputMethodEvent::Attribute &attribute2)
{
return attribute1.start == attribute2.start &&
attribute1.length == attribute2.length &&
attribute1.type == attribute2.type &&
attribute1.value == attribute2.value;
}
using namespace QtVirtualKeyboard;
const bool QtVirtualKeyboard::QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS = qEnvironmentVariableIsSet("QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS");
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)
#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
, activeNavigationKeys()
#endif
{
}
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();
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;
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);
}
QStringList QVirtualKeyboardInputContextPrivate::inputMethods() const
{
return platformInputContext ? platformInputContext->inputMethods() : QStringList();
}
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;
if (QQuickItem *item = qobject_cast<QQuickItem *>(inputPanel))
item->setZ(std::numeric_limits<qreal>::max());
}
void QVirtualKeyboardInputContextPrivate::hideInputPanel()
{
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.length();
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);
}
}
}
void QVirtualKeyboardInputContextPrivate::onInputItemChanged()
{
if (QObject *item = inputItem()) {
if (QQuickItem *vkbPanel = qobject_cast<QQuickItem*>(inputPanel)) {
if (QQuickItem *quickItem = qobject_cast<QQuickItem*>(item)) {
const QVariant isDesktopPanel = vkbPanel->property("desktopPanel");
/*
For integrated keyboards, make sure it's a sibling to the overlay. The
high z-order will make sure it gets events also during a modal session.
*/
if (isDesktopPanel.isValid() && !isDesktopPanel.toBool())
vkbPanel->setParentItem(quickItem->window()->contentItem());
}
}
} else {
if (!activeKeys.isEmpty()) {
// After losing keyboard focus it is impossible to track pressed keys
activeKeys.clear();
clearState(State::KeyEvent);
}
}
clearState(State::InputMethodClick);
}
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)
{
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;
// fetch
QInputMethodQueryEvent imQueryEvent(Qt::InputMethodQueries(Qt::ImHints |
Qt::ImQueryInput | Qt::ImInputItemClipRectangle));
platformInputContext->sendEvent(&imQueryEvent);
Qt::InputMethodHints inputMethodHints = Qt::InputMethodHints(imQueryEvent.value(Qt::ImHints).toInt());
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->isInputPanelVisible() && (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.length();
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.length()) {
commit();
}
}
clearState(State::InputMethodClick);
break;
case QInputMethod::ContextMenu:
break;
}
}
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);
// 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);
#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
int key = keyEvent->key();
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;
}
}
#endif
// Break composing text since the virtual keyboard does not support hard keyboard events
if (!preeditText.isEmpty())
commit();
}
#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
else if (type == QEvent::ShortcutOverride) {
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;
}
#endif
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 : qAsConst(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.count();
for (int i = 0; i < count; ++i) {
if (attributes.at(i).type == attributeType)
return i;
}
return -1;
}
QT_END_NAMESPACE