Revision control
Copy as Markdown
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qwasminputcontext.h"
#include "qwasmwindow.h"
#include "qwasmaccessibility.h"
#include <QRectF>
#include <QLoggingCategory>
#include <qguiapplication.h>
#include <qwindow.h>
#include <qpa/qplatforminputcontext.h>
#include <qpa/qwindowsysteminterface.h>
#if QT_CONFIG(clipboard)
#include <QClipboard>
#endif
#include <QtGui/qtextobject.h>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(qLcQpaWasmInputContext, "qt.qpa.wasm.inputcontext")
using namespace qstdweb;
void QWasmInputContext::inputCallback(emscripten::val event)
{
emscripten::val inputType = event["inputType"];
if (inputType.isNull() || inputType.isUndefined())
return;
const auto inputTypeString = inputType.as<std::string>();
emscripten::val inputData = event["data"];
QString inputStr = (!inputData.isNull() && !inputData.isUndefined())
? QString::fromEcmaString(inputData) : QString();
// also do dataTransfer
// containing rich text
emscripten::val dataTr = event["dataTransfer"];
if (!dataTr.isUndefined() && !dataTr.isNull()) {
qCDebug(qLcQpaWasmInputContext) << "inputEvent dataTransfer" << inputStr;
inputStr = QString::fromStdString(dataTr.
call<emscripten::val>("getData",
std::string("text")).as<std::string>());
}
if (m_ignoreNextInput) {
m_ignoreNextInput = false;
return;
}
// There are many inputTypes for InputEvent
// Some of them should be implemented here later.
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString;
if (!inputTypeString.compare("deleteContentBackward")
|| !inputTypeString.compare("deleteContent")) {
// deleteContent is direction-agnostic. WebKit emits it for generic
// editing deletions, and embedded virtual keyboards drive backspace
// this way, so treat it as a backward delete.
const ResolvedRange resolved = resolveRange(getFirstRange());
const int deleteFrom = resolved.replaceFrom <= 0 ? resolved.replaceFrom : -1;
commitText(QString(), deleteFrom, resolved.replaceLength);
} else if (!inputTypeString.compare("deleteContentForward")) {
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::NoModifier);
} else if (!inputTypeString.compare("deleteWordBackward")) {
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Backspace, Qt::ControlModifier);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Backspace, Qt::ControlModifier);
} else if (!inputTypeString.compare("deleteWordForward")) {
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Delete, Qt::ControlModifier);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Delete, Qt::ControlModifier);
} else if (!inputTypeString.compare("insertParagraph")) {
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier);
} else if (!inputTypeString.compare("insertLineBreak")) {
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_Return, Qt::ShiftModifier);
QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_Return, Qt::ShiftModifier);
} else if (!inputTypeString.compare("insertCompositionText")) {
qCDebug(qLcQpaWasmInputContext) << "insertCompositionText : " << inputStr;
if (event["isComposing"].as<bool>()) {
const CompositionRange range = getFirstRange();
setPreeditString(inputStr);
insertPreedit(range.end - range.start);
}
} else if (!inputTypeString.compare("insertReplacementText")) {
qCDebug(qLcQpaWasmInputContext) << "insertReplacementText >>>>" << "inputString : " << inputStr;
const CompositionRange range = getFirstRange();
if (range.isValid) {
const ResolvedRange resolved = resolveRange(range);
commitText(inputStr, resolved.replaceFrom, resolved.replaceLength);
} else {
QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
QCoreApplication::sendEvent(m_focusObject, &queryEvent);
int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
QString textFieldString = queryEvent.value(Qt::ImTextBeforeCursor).toString();
int spaceIndex = textFieldString.lastIndexOf(' ') + 1;
int replaceIndex = (qCursorPosition - spaceIndex);
commitText(inputStr, -replaceIndex, replaceIndex);
}
} else if (!inputTypeString.compare("deleteCompositionText")) {
setPreeditString("");
insertPreedit();
} else if (!inputTypeString.compare("insertFromComposition")) {
setPreeditString(inputStr);
insertPreedit();
} else if (!inputTypeString.compare("insertText")) {
const CompositionRange range = getFirstRange();
if (range.isValid && range.start != range.end) {
const ResolvedRange resolved = resolveRange(range);
commitText(inputStr, resolved.replaceFrom, resolved.replaceLength);
} else {
commitText(inputStr);
}
} else if (!inputTypeString.compare("insertFromPaste")) {
commitText(inputStr);
// These can be supported here,
// But now, keyCallback in QWasmWindow
// will take them as exceptions.
//} else if (!inputTypeString.compare("deleteByCut")) {
} else {
qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" <<
inputType.as<std::string>() << "\" is not supported in Qt yet";
}
// Prevent the event from propagting to any listeners outside of the Qt window
event.call<void>("stopPropagation");
// The composition target range set by the preceding "beforeinput" event has
// been consumed by this input event; clear it so a later input event without
// a fresh beforeinput cannot reuse a stale range.
m_compositionRange = emscripten::val::null();
}
void QWasmInputContext::compositionEndCallback(emscripten::val event)
{
const auto inputStr = QString::fromEcmaString(event["data"]);
if (preeditString().isEmpty()) // we get final results from inputCallback
return;
if (inputStr != preeditString()) {
qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO
<< "Composition string" << inputStr
<< "is differ from" << preeditString();
}
commitPreeditAndClear();
}
void QWasmInputContext::compositionStartCallback(emscripten::val event)
{
Q_UNUSED(event);
// Do nothing when starting composition
}
void QWasmInputContext::compositionUpdateCallback(emscripten::val event)
{
const auto compositionStr = QString::fromEcmaString(event["data"]);
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr;
setPreeditString(compositionStr);
}
void QWasmInputContext::beforeInputCallback(emscripten::val event)
{
m_compositionRange = event.call<emscripten::val>("getTargetRanges");
if (const CompositionRange range = getFirstRange(); range.isValid) {
qCDebug(qLcQpaWasmInputContext) << "startOffset" << range.start;
qCDebug(qLcQpaWasmInputContext) << "endOffset" << range.end;
}
}
// Returns the first of the target ranges from the most recent "beforeinput"
// event (InputEvent.getTargetRanges(), a possibly empty list of StaticRanges).
QWasmInputContext::CompositionRange QWasmInputContext::getFirstRange() const
{
CompositionRange range;
if (!m_compositionRange.isNull() && !m_compositionRange.isUndefined()
&& m_compositionRange["length"].as<int>() > 0) {
emscripten::val first = m_compositionRange[0];
range.start = first["startOffset"].as<int>();
range.end = first["endOffset"].as<int>();
range.isValid = true;
}
return range;
}
// Resolves a composition range against the current input cursor position for
// the focus object. Returns (replaceFrom, replaceLength) which can be passed
// to QInputMethodEvent, where replaceFrom is an offset from the cursor position.
QWasmInputContext::ResolvedRange QWasmInputContext::resolveRange(const CompositionRange &range) const
{
QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
QCoreApplication::sendEvent(m_focusObject, &queryEvent);
const int cursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt();
return { range.start - cursorPosition, range.end - range.start };
}
QWasmInputContext::QWasmInputContext()
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
}
QWasmInputContext::~QWasmInputContext()
{
}
void QWasmInputContext::update(Qt::InputMethodQueries queries)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries;
if (m_inputElement.isNull())
return;
if (queries & Qt::ImInputItemClipRectangle) {
const QRect inputItemRectangle = QPlatformInputContext::inputItemRectangle().toRect();
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "propagating inputItemRectangle:" << inputItemRectangle;
m_inputElement["style"].set("left", std::to_string(inputItemRectangle.x()) + "px");
m_inputElement["style"].set("top", std::to_string(inputItemRectangle.y()) + "px");
m_inputElement["style"].set("width", "1px"); // "1px" to avoid overflowing window layout FIXME: fix layout
m_inputElement["style"].set("height", "1px");
}
}
void QWasmInputContext::showInputPanel()
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
// Note: showInputPanel not necessarily called, we also
// need to make sure the input panel is shown in response
// to setFocusObject()
if (!m_inputElement.isNull())
focusOnFocusWindow(QWasmWindow::InputFocus);
}
void QWasmInputContext::hideInputPanel()
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
// hide only if m_focusObject does not exist
if (m_focusObject)
return;
focusOnFocusWindow(QWasmWindow::WindowFocus);
}
void QWasmInputContext::setFocusObject(QObject *object)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << object << inputMethodAccepted();
// Commit the previous composition before change m_focusObject
if (m_focusObject && !m_preeditString.isEmpty())
commitPreeditAndClear();
m_focusObject = object;
// Handle the case where the current focus object does not accept input
// (becuse it is e.g. a button, or is null)
if (!inputMethodAccepted()) {
// "park" the input element now that it is unused
if (!m_inputElement.isNull()) {
m_inputElement["style"].set("left", "0px");
m_inputElement["style"].set("top", "0px");
m_inputElement = emscripten::val::null();
}
// Set focus to the focus helper for the current focus window. This moves focus
// away from the input element which will hide virtual keyboards.
focusOnFocusWindow(QWasmWindow::WindowFocus);
} else {
// Make the input element's properties match the current focus obj
m_inputElement = QWasmWindow::focusedWindowInputElement();
if (!m_inputElement.isNull()) {
updateInputElement();
// Set foucs to the input element. This shows (and re-shows) the virtual
// keyboard on mobile devices.
focusOnFocusWindow(QWasmWindow::InputFocus);
}
}
}
void QWasmInputContext::updateInputElement()
{
if (QWasmAccessibility::isEnabled())
return;
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
QCoreApplication::sendEvent(m_focusObject, &queryEvent);
qCDebug(qLcQpaWasmInputContext) << "Qt surrounding text: " << queryEvent.value(Qt::ImSurroundingText).toString();
qCDebug(qLcQpaWasmInputContext) << "Qt current selection: " << queryEvent.value(Qt::ImCurrentSelection).toString();
qCDebug(qLcQpaWasmInputContext) << "Qt text before cursor: " << queryEvent.value(Qt::ImTextBeforeCursor).toString();
qCDebug(qLcQpaWasmInputContext) << "Qt text after cursor: " << queryEvent.value(Qt::ImTextAfterCursor).toString();
qCDebug(qLcQpaWasmInputContext) << "Qt cursor position: " << queryEvent.value(Qt::ImCursorPosition).toInt();
qCDebug(qLcQpaWasmInputContext) << "Qt anchor position: " << queryEvent.value(Qt::ImAnchorPosition).toInt();
m_inputElement.set("value", queryEvent.value(Qt::ImSurroundingText).toString().toStdString());
m_inputElement.set("selectionStart", queryEvent.value(Qt::ImAnchorPosition).toUInt());
m_inputElement.set("selectionEnd", queryEvent.value(Qt::ImCursorPosition).toUInt());
Qt::InputMethodHints imHints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toInt());
// Set input mode, use "password" for ImhHiddenText to prevent unwanted interactions with
// spell checking and auto-complete.
if (imHints & Qt::ImhHiddenText)
m_inputElement.set("type", "password");
else
m_inputElement.set("type", "text");
// Change inputmode to suit Qt imhints
std::string inMode = "text";
if (imHints & Qt::ImhDigitsOnly)
inMode = "numeric";
if (imHints & Qt::ImhFormattedNumbersOnly)
inMode = "decimal";
if (imHints & Qt::ImhDialableCharactersOnly)
inMode = "tel";
if (imHints & Qt::ImhEmailCharactersOnly)
inMode = "email";
if (imHints & Qt::ImhUrlCharactersOnly)
inMode = "url";
// FIXME: handle search mode?
m_inputElement.set("inputMode", inMode);
}
// Moves native focus to the focus helper (WindowFocus) or the input element
// (InputFocus) of the currently focused window. InputFocus shows the virtual
// keyboard on mobile; WindowFocus moves focus off the input element and hides it.
void QWasmInputContext::focusOnFocusWindow(QWasmWindow::FocusTarget target)
{
if (QWasmWindow *focusWindow = QWasmWindow::focusWindow())
focusWindow->focus(target);
}
void QWasmInputContext::setPreeditString(QString preeditStr)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << preeditStr;
m_preeditString = preeditStr;
}
void QWasmInputContext::insertPreedit(int replaceLength)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
if (replaceLength == 0)
replaceLength = m_preeditString.length();
QList<QInputMethodEvent::Attribute> attributes;
{
QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
0,
1);
attributes.append(attr_cursor);
QTextCharFormat format;
format.setFontUnderline(true);
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
0,
replaceLength, format);
attributes.append(attr_format);
}
QInputMethodEvent e(m_preeditString, attributes);
QCoreApplication::sendEvent(m_focusObject, &e);
}
void QWasmInputContext::commitPreeditAndClear()
{
if (m_preeditString.isEmpty())
return;
QInputMethodEvent e;
e.setCommitString(m_preeditString);
m_preeditString.clear();
QCoreApplication::sendEvent(m_focusObject, &e);
}
// Commits text to the focus object, optionally replacing replaceLength
// characters starting replaceFrom positions relative to the cursor.
void QWasmInputContext::commitText(const QString &text, int replaceFrom, int replaceLength)
{
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << text << replaceFrom << replaceLength;
QInputMethodEvent e;
e.setCommitString(text, replaceFrom, replaceLength);
QCoreApplication::sendEvent(m_focusObject, &e);
m_preeditString.clear();
}
QT_END_NAMESPACE