458 lines
20 KiB
C++
458 lines
20 KiB
C++
// Copyright (C) 2019 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
#include "qwasminputcontext.h"
|
|
#include "qwasmwindow.h"
|
|
|
|
#include <QRectF>
|
|
#include <QLoggingCategory>
|
|
#include <qguiapplication.h>
|
|
#include <qwindow.h>
|
|
#include <qpa/qplatforminputcontext.h>
|
|
#include <qpa/qwindowsysteminterface.h>
|
|
#include <QClipboard>
|
|
#include <QtGui/qtextobject.h>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
Q_LOGGING_CATEGORY(qLcQpaWasmInputContext, "qt.qpa.wasm.inputcontext")
|
|
|
|
using namespace qstdweb;
|
|
|
|
static void inputCallback(emscripten::val event)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "isComposing : " << event["isComposing"].as<bool>();
|
|
|
|
QString inputStr = (event["data"] != emscripten::val::null()
|
|
&& event["data"] != emscripten::val::undefined()) ?
|
|
QString::fromStdString(event["data"].as<std::string>()) : QString();
|
|
|
|
QWasmInputContext *wasmInput =
|
|
reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>());
|
|
|
|
emscripten::val inputType = event["inputType"];
|
|
if (inputType != emscripten::val::null()
|
|
&& inputType != emscripten::val::undefined()) {
|
|
const auto inputTypeString = inputType.as<std::string>();
|
|
// There are many inputTypes for InputEvent
|
|
// https://www.w3.org/TR/input-events-1/
|
|
// Some of them should be implemented here later.
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType : " << inputTypeString;
|
|
if (!inputTypeString.compare("deleteContentBackward")) {
|
|
QWindowSystemInterface::handleKeyEvent(0,
|
|
QEvent::KeyPress,
|
|
Qt::Key_Backspace,
|
|
Qt::NoModifier);
|
|
QWindowSystemInterface::handleKeyEvent(0,
|
|
QEvent::KeyRelease,
|
|
Qt::Key_Backspace,
|
|
Qt::NoModifier);
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} 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);
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} else if (!inputTypeString.compare("insertCompositionText")) {
|
|
qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr;
|
|
wasmInput->insertPreedit();
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} else if (!inputTypeString.compare("insertReplacementText")) {
|
|
qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr;
|
|
//auto ranges = event.call<emscripten::val>("getTargetRanges");
|
|
//qCDebug(qLcQpaWasmInputContext) << ranges["length"].as<int>();
|
|
// WA For Korean IME
|
|
// insertReplacementText should have targetRanges but
|
|
// Safari cannot have it and just it seems to be supposed
|
|
// to replace previous input.
|
|
wasmInput->insertText(inputStr, true);
|
|
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} else if (!inputTypeString.compare("deleteCompositionText")) {
|
|
wasmInput->setPreeditString("", 0);
|
|
wasmInput->insertPreedit();
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} else if (!inputTypeString.compare("insertFromComposition")) {
|
|
wasmInput->setPreeditString(inputStr, 0);
|
|
wasmInput->insertPreedit();
|
|
event.call<void>("stopImmediatePropagation");
|
|
return;
|
|
} else if (!inputTypeString.compare("insertText")) {
|
|
wasmInput->insertText(inputStr);
|
|
event.call<void>("stopImmediatePropagation");
|
|
} else if (!inputTypeString.compare("insertFromPaste")) {
|
|
wasmInput->insertText(QGuiApplication::clipboard()->text());
|
|
event.call<void>("stopImmediatePropagation");
|
|
// 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";
|
|
}
|
|
}
|
|
}
|
|
|
|
static void compositionEndCallback(emscripten::val event)
|
|
{
|
|
const auto inputStr = QString::fromStdString(event["data"].as<std::string>());
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr;
|
|
|
|
QWasmInputContext *wasmInput =
|
|
reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>());
|
|
|
|
if (wasmInput->preeditString().isEmpty())
|
|
return;
|
|
|
|
if (inputStr != wasmInput->preeditString()) {
|
|
qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO
|
|
<< "Composition string" << inputStr
|
|
<< "is differ from" << wasmInput->preeditString();
|
|
}
|
|
wasmInput->commitPreeditAndClear();
|
|
}
|
|
|
|
static void compositionStartCallback(emscripten::val event)
|
|
{
|
|
Q_UNUSED(event);
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
|
|
// Do nothing when starting composition
|
|
}
|
|
|
|
/*
|
|
// Test implementation
|
|
static void beforeInputCallback(emscripten::val event)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
|
|
auto ranges = event.call<emscripten::val>("getTargetRanges");
|
|
auto length = ranges["length"].as<int>();
|
|
for (auto i = 0; i < length; i++) {
|
|
qCDebug(qLcQpaWasmInputContext) << ranges.call<emscripten::val>("get", i)["startOffset"].as<int>();
|
|
qCDebug(qLcQpaWasmInputContext) << ranges.call<emscripten::val>("get", i)["endOffset"].as<int>();
|
|
}
|
|
}
|
|
*/
|
|
|
|
static void compositionUpdateCallback(emscripten::val event)
|
|
{
|
|
const auto compositionStr = QString::fromStdString(event["data"].as<std::string>());
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr;
|
|
|
|
QWasmInputContext *wasmInput =
|
|
reinterpret_cast<QWasmInputContext *>(event["target"]["data-qinputcontext"].as<quintptr>());
|
|
|
|
// WA for IOS.
|
|
// Not sure now because I cannot test it anymore.
|
|
// int replaceSize = 0;
|
|
// emscripten::val win = emscripten::val::global("window");
|
|
// emscripten::val sel = win.call<emscripten::val>("getSelection");
|
|
// if (sel != emscripten::val::null()
|
|
// && sel != emscripten::val::undefined()
|
|
// && sel["rangeCount"].as<int>() > 0) {
|
|
// QInputMethodQueryEvent queryEvent(Qt::ImQueryAll);
|
|
// QCoreApplication::sendEvent(QGuiApplication::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();
|
|
//
|
|
// const QString &selectedStr = QString::fromUtf8(sel.call<emscripten::val>("toString").as<std::string>());
|
|
// const auto &preeditStr = wasmInput->preeditString();
|
|
// qCDebug(qLcQpaWasmInputContext) << "Selection.type : " << sel["type"].as<std::string>();
|
|
// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Selected: " << selectedStr;
|
|
// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "PreeditString: " << preeditStr;
|
|
// if (!sel["type"].as<std::string>().compare("Range")) {
|
|
// QString surroundingTextBeforeCursor = queryEvent.value(Qt::ImTextBeforeCursor).toString();
|
|
// if (surroundingTextBeforeCursor.endsWith(selectedStr)) {
|
|
// replaceSize = selectedStr.size();
|
|
// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Current Preedit: " << preeditStr << replaceSize;
|
|
// }
|
|
// }
|
|
// emscripten::val range = sel.call<emscripten::val>("getRangeAt", 0);
|
|
// qCDebug(qLcQpaWasmInputContext) << "Range.startOffset : " << range["startOffset"].as<int>();
|
|
// qCDebug(qLcQpaWasmInputContext) << "Range.endOffset : " << range["endOffset"].as<int>();
|
|
// }
|
|
//
|
|
// wasmInput->setPreeditString(compositionStr, replaceSize);
|
|
wasmInput->setPreeditString(compositionStr, 0);
|
|
}
|
|
|
|
static void copyCallback(emscripten::val event)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
QString inputStr = clipboard->text();
|
|
qCDebug(qLcQpaWasmInputContext) << "QClipboard : " << inputStr;
|
|
event["clipboardData"].call<void>("setData",
|
|
emscripten::val("text/plain"),
|
|
inputStr.toStdString());
|
|
event.call<void>("preventDefault");
|
|
event.call<void>("stopImmediatePropagation");
|
|
}
|
|
|
|
static void cutCallback(emscripten::val event)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
QString inputStr = clipboard->text();
|
|
qCDebug(qLcQpaWasmInputContext) << "QClipboard : " << inputStr;
|
|
event["clipboardData"].call<void>("setData",
|
|
emscripten::val("text/plain"),
|
|
inputStr.toStdString());
|
|
event.call<void>("preventDefault");
|
|
event.call<void>("stopImmediatePropagation");
|
|
}
|
|
|
|
static void pasteCallback(emscripten::val event)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
|
|
emscripten::val clipboardData = event["clipboardData"].call<emscripten::val>("getData", emscripten::val("text/plain"));
|
|
QString clipboardStr = QString::fromStdString(clipboardData.as<std::string>());
|
|
qCDebug(qLcQpaWasmInputContext) << "wasm clipboard : " << clipboardStr;
|
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
|
if (clipboard->text() != clipboardStr)
|
|
clipboard->setText(clipboardStr);
|
|
|
|
// propagate to input event (insertFromPaste)
|
|
}
|
|
|
|
EMSCRIPTEN_BINDINGS(wasminputcontext_module) {
|
|
function("qtCompositionEndCallback", &compositionEndCallback);
|
|
function("qtCompositionStartCallback", &compositionStartCallback);
|
|
function("qtCompositionUpdateCallback", &compositionUpdateCallback);
|
|
function("qtInputCallback", &inputCallback);
|
|
//function("qtBeforeInputCallback", &beforeInputCallback);
|
|
|
|
function("qtCopyCallback", ©Callback);
|
|
function("qtCutCallback", &cutCallback);
|
|
function("qtPasteCallback", &pasteCallback);
|
|
}
|
|
|
|
QWasmInputContext::QWasmInputContext()
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
emscripten::val document = emscripten::val::global("document");
|
|
// This 'input' can be an issue to handle multiple lines,
|
|
// 'textarea' can be used instead.
|
|
m_inputElement = document.call<emscripten::val>("createElement", std::string("input"));
|
|
m_inputElement.set("type", "text");
|
|
m_inputElement.set("contenteditable","true");
|
|
|
|
m_inputElement["style"].set("position", "absolute");
|
|
m_inputElement["style"].set("left", 0);
|
|
m_inputElement["style"].set("top", 0);
|
|
m_inputElement["style"].set("opacity", 0);
|
|
m_inputElement["style"].set("display", "");
|
|
m_inputElement["style"].set("z-index", -2);
|
|
|
|
m_inputElement.set("data-qinputcontext",
|
|
emscripten::val(quintptr(reinterpret_cast<void *>(this))));
|
|
emscripten::val body = document["body"];
|
|
body.call<void>("appendChild", m_inputElement);
|
|
|
|
m_inputElement.call<void>("addEventListener", std::string("compositionstart"),
|
|
emscripten::val::module_property("qtCompositionStartCallback"),
|
|
emscripten::val(false));
|
|
m_inputElement.call<void>("addEventListener", std::string("compositionupdate"),
|
|
emscripten::val::module_property("qtCompositionUpdateCallback"),
|
|
emscripten::val(false));
|
|
m_inputElement.call<void>("addEventListener", std::string("compositionend"),
|
|
emscripten::val::module_property("qtCompositionEndCallback"),
|
|
emscripten::val(false));
|
|
m_inputElement.call<void>("addEventListener", std::string("input"),
|
|
emscripten::val::module_property("qtInputCallback"),
|
|
emscripten::val(false));
|
|
//m_inputElement.call<void>("addEventListener", std::string("beforeinput"),
|
|
// emscripten::val::module_property("qtBeforeInputCallback"),
|
|
// emscripten::val(false));
|
|
|
|
// Clipboard for InputContext
|
|
m_inputElement.call<void>("addEventListener", std::string("cut"),
|
|
emscripten::val::module_property("qtCutCallback"),
|
|
emscripten::val(false));
|
|
m_inputElement.call<void>("addEventListener", std::string("copy"),
|
|
emscripten::val::module_property("qtCopyCallback"),
|
|
emscripten::val(false));
|
|
m_inputElement.call<void>("addEventListener", std::string("paste"),
|
|
emscripten::val::module_property("qtPasteCallback"),
|
|
emscripten::val(false));
|
|
}
|
|
|
|
QWasmInputContext::~QWasmInputContext()
|
|
{
|
|
}
|
|
|
|
void QWasmInputContext::update(Qt::InputMethodQueries queries)
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries;
|
|
|
|
QPlatformInputContext::update(queries);
|
|
}
|
|
|
|
void QWasmInputContext::showInputPanel()
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
m_visibleInputPanel = true;
|
|
|
|
updateInputElement();
|
|
}
|
|
|
|
void QWasmInputContext::updateInputElement()
|
|
{
|
|
// Mobile devices can dismiss keyboard/IME and focus is still on input.
|
|
// Successive clicks on the same input should open the keyboard/IME.
|
|
|
|
// If there is no focus object, or no visible input panel, remove focus
|
|
const QWindow *focusWindow = QGuiApplication::focusWindow();
|
|
if (!m_focusObject || !focusWindow || !m_visibleInputPanel || !m_inputMethodAccepted) {
|
|
m_inputElement["style"].set("left", "0px");
|
|
m_inputElement["style"].set("top", "0px");
|
|
m_inputElement["style"].set("width", "1px");
|
|
m_inputElement["style"].set("height", "1px");
|
|
m_inputElement.set("value", "");
|
|
|
|
m_inputElement.call<void>("blur");
|
|
if (focusWindow && focusWindow->handle())
|
|
((QWasmWindow *)(focusWindow->handle()))->focus();
|
|
|
|
return;
|
|
}
|
|
|
|
Q_ASSERT(focusWindow);
|
|
Q_ASSERT(m_focusObject);
|
|
Q_ASSERT(m_visibleInputPanel);
|
|
Q_ASSERT(m_inputMethodAccepted);
|
|
|
|
// Set the geometry
|
|
QPoint globalPos;
|
|
const QRect cursorRectangle = QPlatformInputContext::cursorRectangle().toRect();
|
|
if (cursorRectangle.isValid()) {
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "cursorRectangle: " << cursorRectangle;
|
|
globalPos = focusWindow->mapToGlobal(cursorRectangle.topLeft());
|
|
if (globalPos.x() > 0) globalPos.setX(globalPos.x() - 1);
|
|
if (globalPos.y() > 0) globalPos.setY(globalPos.y() - 1);
|
|
}
|
|
|
|
const auto styleLeft = std::to_string(globalPos.x()) + "px";
|
|
const auto styleTop = std::to_string(globalPos.y()) + "px";
|
|
m_inputElement["style"].set("left", styleLeft);
|
|
m_inputElement["style"].set("top", styleTop);
|
|
m_inputElement["style"].set("width", "1px");
|
|
m_inputElement["style"].set("height", "1px");
|
|
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << QRectF::fromDOMRect(m_inputElement.call<emscripten::val>("getBoundingClientRect"));
|
|
|
|
// Set the text input
|
|
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());
|
|
|
|
m_inputElement.call<void>("focus");
|
|
}
|
|
|
|
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_inputMethodAccepted = (object && inputMethodAccepted());
|
|
m_focusObject = object;
|
|
updateInputElement();
|
|
QPlatformInputContext::setFocusObject(object);
|
|
}
|
|
|
|
void QWasmInputContext::hideInputPanel()
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO;
|
|
m_visibleInputPanel = false;
|
|
|
|
// hide only if m_focusObject does not exist
|
|
if (!m_focusObject)
|
|
updateInputElement();
|
|
}
|
|
|
|
void QWasmInputContext::setPreeditString(QString preeditStr, int replaceSize)
|
|
{
|
|
m_preeditString = preeditStr;
|
|
m_replaceSize = replaceSize;
|
|
}
|
|
|
|
void QWasmInputContext::insertPreedit()
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
|
|
|
|
QList<QInputMethodEvent::Attribute> attributes;
|
|
{
|
|
QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor,
|
|
m_preeditString.length(),
|
|
1);
|
|
attributes.append(attr_cursor);
|
|
|
|
QTextCharFormat format;
|
|
format.setFontUnderline(true);
|
|
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
|
QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat,
|
|
0,
|
|
m_preeditString.length(), format);
|
|
attributes.append(attr_format);
|
|
}
|
|
|
|
QInputMethodEvent e(m_preeditString, attributes);
|
|
if (m_replaceSize > 0)
|
|
e.setCommitString("", -m_replaceSize, m_replaceSize);
|
|
QCoreApplication::sendEvent(m_focusObject, &e);
|
|
}
|
|
|
|
void QWasmInputContext::commitPreeditAndClear()
|
|
{
|
|
qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString;
|
|
|
|
if (m_preeditString.isEmpty())
|
|
return;
|
|
QInputMethodEvent e;
|
|
e.setCommitString(m_preeditString);
|
|
m_preeditString.clear();
|
|
QCoreApplication::sendEvent(m_focusObject, &e);
|
|
}
|
|
|
|
void QWasmInputContext::insertText(QString inputStr, bool replace)
|
|
{
|
|
Q_UNUSED(replace);
|
|
if (!inputStr.isEmpty()) {
|
|
const int replaceLen = 0;
|
|
QInputMethodEvent e;
|
|
e.setCommitString(inputStr, -replaceLen, replaceLen);
|
|
QCoreApplication::sendEvent(m_focusObject, &e);
|
|
}
|
|
}
|
|
|
|
QT_END_NAMESPACE
|