diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 1aa3ffa5b36..2d00d9f723a 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmclipboard.h" +#include "qwasminputcontext.h" #include "qwasmdom.h" #include "qwasmevent.h" #include "qwasmwindow.h" @@ -47,6 +48,10 @@ static void commonCopyEvent(val event) static void qClipboardCutTo(val event) { + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput && wasmInput->usingTextInput()) + return; + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( @@ -58,6 +63,10 @@ static void qClipboardCutTo(val event) static void qClipboardCopyTo(val event) { + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput && wasmInput->usingTextInput()) + return; + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard QWindowSystemInterface::handleKeyEvent( @@ -68,6 +77,10 @@ static void qClipboardCopyTo(val event) static void qClipboardPasteTo(val event) { + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput && wasmInput->usingTextInput()) + return; + event.call("preventDefault"); // prevent browser from handling drop event QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp index ae72e7b7f99..f8dbd86a193 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -1,161 +1,432 @@ // Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only -#include - #include "qwasminputcontext.h" -#include "qwasmintegration.h" -#include "qwasmplatform.h" + #include -#include -#include "qwasmscreen.h" +#include #include #include -#include +#include #include +#include QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcQpaWasmInputContext, "qt.qpa.wasm.inputcontext") + using namespace qstdweb; static void inputCallback(emscripten::val event) { - // android sends all the characters typed since the user started typing in this element - int length = event["target"]["value"]["length"].as(); - if (length <= 0) - return; + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "isComposing : " << event["isComposing"].as(); - emscripten::val _incomingCharVal = event["data"]; - if (_incomingCharVal != emscripten::val::undefined() && _incomingCharVal != emscripten::val::null()) { + QString inputStr = (event["data"] != emscripten::val::null() + && event["data"] != emscripten::val::undefined()) ? + QString::fromStdString(event["data"].as()) : QString(); - QString str = QString::fromStdString(_incomingCharVal.as()); - QWasmInputContext *wasmInput = - reinterpret_cast(event["target"]["data-qinputcontext"].as()); - wasmInput->inputStringChanged(str, EMSCRIPTEN_EVENT_KEYDOWN, wasmInput); + QWasmInputContext *wasmInput = + reinterpret_cast(event["target"]["data-qinputcontext"].as()); + + emscripten::val inputType = event["inputType"]; + if (inputType != emscripten::val::null() + && inputType != emscripten::val::undefined()) { + const auto inputTypeString = inputType.as(); + // 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("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("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertCompositionText")) { + qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; + wasmInput->insertPreedit(); + event.call("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertReplacementText")) { + qCDebug(qLcQpaWasmInputContext) << "inputString : " << inputStr; + //auto ranges = event.call("getTargetRanges"); + //qCDebug(qLcQpaWasmInputContext) << ranges["length"].as(); + // 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("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("deleteCompositionText")) { + wasmInput->setPreeditString("", 0); + wasmInput->insertPreedit(); + event.call("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertFromComposition")) { + wasmInput->setPreeditString(inputStr, 0); + wasmInput->insertPreedit(); + event.call("stopImmediatePropagation"); + return; + } else if (!inputTypeString.compare("insertText")) { + wasmInput->insertText(inputStr); + event.call("stopImmediatePropagation"); + } else if (!inputTypeString.compare("insertFromPaste")) { + wasmInput->insertText(QGuiApplication::clipboard()->text()); + event.call("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() << "\" is not supported in Qt yet"; + } } - // this clears the input string, so backspaces do not send a character - // but stops suggestions - event["target"].set("value", ""); } -EMSCRIPTEN_BINDINGS(clipboard_module) { - function("qtInputContextCallback", &inputCallback); +static void compositionEndCallback(emscripten::val event) +{ + const auto inputStr = QString::fromStdString(event["data"].as()); + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr; + + QWasmInputContext *wasmInput = + reinterpret_cast(event["target"]["data-qinputcontext"].as()); + + 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("getTargetRanges"); + auto length = ranges["length"].as(); + for (auto i = 0; i < length; i++) { + qCDebug(qLcQpaWasmInputContext) << ranges.call("get", i)["startOffset"].as(); + qCDebug(qLcQpaWasmInputContext) << ranges.call("get", i)["endOffset"].as(); + } +} +*/ + +static void compositionUpdateCallback(emscripten::val event) +{ + const auto compositionStr = QString::fromStdString(event["data"].as()); + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << compositionStr; + + QWasmInputContext *wasmInput = + reinterpret_cast(event["target"]["data-qinputcontext"].as()); + + // 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("getSelection"); +// if (sel != emscripten::val::null() +// && sel != emscripten::val::undefined() +// && sel["rangeCount"].as() > 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("toString").as()); +// const auto &preeditStr = wasmInput->preeditString(); +// qCDebug(qLcQpaWasmInputContext) << "Selection.type : " << sel["type"].as(); +// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "Selected: " << selectedStr; +// qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "PreeditString: " << preeditStr; +// if (!sel["type"].as().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("getRangeAt", 0); +// qCDebug(qLcQpaWasmInputContext) << "Range.startOffset : " << range["startOffset"].as(); +// qCDebug(qLcQpaWasmInputContext) << "Range.endOffset : " << range["endOffset"].as(); +// } +// +// 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("setData", + emscripten::val("text/plain"), + inputStr.toStdString()); + event.call("preventDefault"); + event.call("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("setData", + emscripten::val("text/plain"), + inputStr.toStdString()); + event.call("preventDefault"); + event.call("stopImmediatePropagation"); +} + +static void pasteCallback(emscripten::val event) +{ + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; + + emscripten::val clipboardData = event["clipboardData"].call("getData", emscripten::val("text/plain")); + QString clipboardStr = QString::fromStdString(clipboardData.as()); + 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("createElement", std::string("input")); m_inputElement.set("type", "text"); - m_inputElement.set("style", "position:absolute;left:-1000px;top:-1000px"); // offscreen m_inputElement.set("contenteditable","true"); - if (platform() == Platform::MacOS || platform() == Platform::iOS) { - auto callback = [=](emscripten::val) { - m_inputElement["parentElement"].call("removeChild", m_inputElement); - inputPanelIsOpen = false; - }; - m_blurEventHandler.reset(new EventCallback(m_inputElement, "blur", callback)); + m_inputElement["style"].set("position", "absolute"); + m_inputElement["style"].set("opacity", 0); + m_inputElement["style"].set("display", ""); + m_inputElement["style"].set("z-index", -2); - } else { + m_inputElement.set("data-qinputcontext", + emscripten::val(quintptr(reinterpret_cast(this)))); + emscripten::val body = document["body"]; + body.call("appendChild", m_inputElement); - const std::string inputType = platform() == Platform::Windows ? "textInput" : "input"; + m_inputElement.call("addEventListener", std::string("compositionstart"), + emscripten::val::module_property("qtCompositionStartCallback"), + emscripten::val(false)); + m_inputElement.call("addEventListener", std::string("compositionupdate"), + emscripten::val::module_property("qtCompositionUpdateCallback"), + emscripten::val(false)); + m_inputElement.call("addEventListener", std::string("compositionend"), + emscripten::val::module_property("qtCompositionEndCallback"), + emscripten::val(false)); + m_inputElement.call("addEventListener", std::string("input"), + emscripten::val::module_property("qtInputCallback"), + emscripten::val(false)); + //m_inputElement.call("addEventListener", std::string("beforeinput"), + // emscripten::val::module_property("qtBeforeInputCallback"), + // emscripten::val(false)); - document.call("addEventListener", inputType, - emscripten::val::module_property("qtInputContextCallback"), - emscripten::val(false)); - m_inputElement.set("data-qinputcontext", - emscripten::val(quintptr(reinterpret_cast(this)))); - emscripten::val body = document["body"]; - body.call("appendChild", m_inputElement); - } - - QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, - &QWasmInputContext::focusWindowChanged); + // Clipboard for InputContext + m_inputElement.call("addEventListener", std::string("cut"), + emscripten::val::module_property("qtCutCallback"), + emscripten::val(false)); + m_inputElement.call("addEventListener", std::string("copy"), + emscripten::val::module_property("qtCopyCallback"), + emscripten::val(false)); + m_inputElement.call("addEventListener", std::string("paste"), + emscripten::val::module_property("qtPasteCallback"), + emscripten::val(false)); } QWasmInputContext::~QWasmInputContext() { - if (platform() == Platform::Android || platform() == Platform::Windows) - emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); -} - -void QWasmInputContext::focusWindowChanged(QWindow *focusWindow) -{ - m_focusWindow = focusWindow; -} - -emscripten::val QWasmInputContext::inputHandlerElementForFocusedWindow() -{ - if (!m_focusWindow) - return emscripten::val::undefined(); - return static_cast(m_focusWindow->handle())->inputHandlerElement(); } void QWasmInputContext::update(Qt::InputMethodQueries queries) { + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries; + QPlatformInputContext::update(queries); } void QWasmInputContext::showInputPanel() { - if (platform() == Platform::Windows - && !inputPanelIsOpen) { // call this only once for win32 - m_inputElement.call("focus"); - return; - } - // this is called each time the keyboard is touched + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; - // Add the input element as a child of the screen for the - // currently focused window and give it focus. The browser - // will not display the input element, but mobile browsers - // should display the virtual keyboard. Key events will be - // captured by the keyboard event handler installed on the - // screen element. - - if (platform() == Platform::MacOS // keep for compatibility - || platform() == Platform::iOS - || platform() == Platform::Windows) { - emscripten::val inputWrapper = inputHandlerElementForFocusedWindow(); - if (inputWrapper.isUndefined()) - return; - inputWrapper.call("appendChild", m_inputElement); - } + if (!inputMethodAccepted()) + return; m_inputElement.call("focus"); - inputPanelIsOpen = true; + m_usingTextInput = true; + + QWindow *window = QGuiApplication::focusWindow(); + if (!window || !m_focusObject) + return; + + const QRect cursorRectangle = QPlatformInputContext::cursorRectangle().toRect(); + if (!cursorRectangle.isValid()) + return; + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "cursorRectangle: " << cursorRectangle; + const QPoint &globalPos = window->mapToGlobal(cursorRectangle.topLeft()); + 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); + + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << QRectF::fromDOMRect(m_inputElement.call("getBoundingClientRect")); + + 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()); +} + +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(); + + if (inputMethodAccepted()) { + m_inputElement.call("focus"); + m_usingTextInput = true; + + m_focusObject = object; + } else { + m_inputElement.call("blur"); + m_usingTextInput = false; + + m_focusObject = nullptr; + } + QPlatformInputContext::setFocusObject(object); } void QWasmInputContext::hideInputPanel() { - if (QWasmIntegration::get()->touchPoints < 1) - return; - m_inputElement.call("blur"); - inputPanelIsOpen = false; + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; + + // hide only if m_focusObject does not exist + if (!m_focusObject) { + m_inputElement.call("blur"); + m_usingTextInput = false; + } } -void QWasmInputContext::inputStringChanged(QString &inputString, int eventType, QWasmInputContext *context) +void QWasmInputContext::setPreeditString(QString preeditStr, int replaceSize) { - Q_UNUSED(context) - QKeySequence keys = QKeySequence::fromString(inputString); - Qt::Key thisKey = keys[0].key(); - - // synthesize this keyevent as android is not normal - if (inputString.size() > 2 && (thisKey < Qt::Key_F35 - || thisKey > Qt::Key_Back)) { - inputString.clear(); - } - if (inputString == QStringLiteral("Escape")) { - thisKey = Qt::Key_Escape; - inputString.clear(); - } else if (thisKey == Qt::Key(0)) { - thisKey = Qt::Key_Return; - } - - QWindowSystemInterface::handleKeyEvent( - 0, eventType == EMSCRIPTEN_EVENT_KEYDOWN ? QEvent::KeyPress : QEvent::KeyRelease, - thisKey, keys[0].keyboardModifiers(), - eventType == EMSCRIPTEN_EVENT_KEYDOWN ? inputString : QStringLiteral("")); + m_preeditString = preeditStr; + m_replaceSize = replaceSize; } +void QWasmInputContext::insertPreedit() +{ + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString; + + QList 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 diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 10dd1a09506..f7683e8e96e 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -6,7 +6,6 @@ #include -#include #include #include #include @@ -14,6 +13,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(qLcQpaWasmInputContext) + class QWasmInputContext : public QPlatformInputContext { Q_DISABLE_COPY(QWasmInputContext) @@ -28,19 +29,24 @@ public: void hideInputPanel() override; bool isValid() const override { return true; } - void focusWindowChanged(QWindow *focusWindow); - void inputStringChanged(QString &, int eventType, QWasmInputContext *context); + const QString preeditString() { return m_preeditString; } + void setPreeditString(QString preeditStr, int replaceSize); + void insertPreedit(); + void commitPreeditAndClear(); emscripten::val m_inputElement = emscripten::val::null(); + void insertText(QString inputStr, bool replace = false); + + bool usingTextInput() { return m_usingTextInput; } + void setUsingTextInput(bool enable) { m_usingTextInput = enable; } + void setFocusObject(QObject *object) override; + private: - emscripten::val inputHandlerElementForFocusedWindow(); + QString m_preeditString; + int m_replaceSize = 0; - bool m_inputPanelVisible = false; - - QPointer m_focusWindow; - std::unique_ptr m_blurEventHandler; - std::unique_ptr m_inputEventHandler; - bool inputPanelIsOpen = false; + bool m_usingTextInput = false; + QObject *m_focusObject = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index f5cc3e2eee6..e4373749d82 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -157,8 +157,6 @@ QWasmIntegration::~QWasmIntegration() delete m_fontDb; delete m_desktopServices; - if (m_platformInputContext) - delete m_platformInputContext; #if QT_CONFIG(accessibility) delete m_accessibility; #endif @@ -226,13 +224,13 @@ QPlatformOpenGLContext *QWasmIntegration::createPlatformOpenGLContext(QOpenGLCon void QWasmIntegration::initialize() { auto icStrs = QPlatformInputContextFactory::requested(); - if (icStrs.isEmpty() && touchPoints < 1) - return; - - if (!icStrs.isEmpty()) + if (!icStrs.isEmpty()) { m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); - else + m_wasmInputContext = nullptr; + } else { m_inputContext.reset(new QWasmInputContext()); + m_wasmInputContext = static_cast(m_inputContext.data()); + } } QPlatformInputContext *QWasmIntegration::inputContext() const diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index 870bd0d16b4..99ccf333fb2 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -61,13 +61,13 @@ public: #endif void initialize() override; QPlatformInputContext *inputContext() const override; + QWasmInputContext *wasmInputContext() const { return m_wasmInputContext; } #if QT_CONFIG(draganddrop) QPlatformDrag *drag() const override; #endif QWasmClipboard *getWasmClipboard() { return m_clipboard; } - QWasmInputContext *getWasmInputContext() { return m_platformInputContext; } static QWasmIntegration *get() { return s_instance; } void setContainerElements(emscripten::val elementArray); @@ -100,7 +100,7 @@ private: mutable QScopedPointer m_inputContext; static QWasmIntegration *s_instance; - mutable QWasmInputContext *m_platformInputContext = nullptr; + QWasmInputContext *m_wasmInputContext = nullptr; #if QT_CONFIG(draganddrop) std::unique_ptr m_drag; diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 99e9bb22f1b..524853b7692 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -30,6 +30,7 @@ #include #include +#include QT_BEGIN_NAMESPACE @@ -106,6 +107,15 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_flags = window()->flags(); const auto pointerCallback = std::function([this](emscripten::val event) { + const auto eventTypeString = event["type"].as(); + + // Ideally it should not be happened but + // it takes place sometime with some reason + // without compositionend. + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput && !wasmInput->preeditString().isEmpty()) + wasmInput->commitPreeditAndClear(); + if (processPointer(*PointerEvent::fromWeb(event))) event.call("preventDefault"); }); @@ -121,25 +131,74 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, event.call("preventDefault"); }); + const auto keyCallbackForInputContext = std::function([this](emscripten::val event) { + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput) { + const auto keyString = QString::fromStdString(event["key"].as()); + qCDebug(qLcQpaWasmInputContext) << "Key callback" << keyString << keyString.size(); + + if (keyString == "Unidentified") { + // Android makes a bunch of KeyEvents as "Unidentified" + // They will be processed just in InputContext. + return; + } else if (event["ctrlKey"].as() + || event["altKey"].as() + || event["metaKey"].as()) { + if (processKeyForInputContext(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call("preventDefault"); + event.call("stopImmediatePropagation"); + return; + } else if (keyString.size() != 1) { + if (!wasmInput->preeditString().isEmpty()) { + if (keyString == "Process" || keyString == "Backspace") { + // processed by InputContext + // "Process" should be handled by InputContext but + // QWasmInputContext's function is incomplete now + // so, there will be some exceptions here. + return; + } else if (keyString != "Shift" + && keyString != "Meta" + && keyString != "Alt" + && keyString != "Control" + && !keyString.startsWith("Arrow")) { + wasmInput->commitPreeditAndClear(); + } + } + } else if (wasmInput->inputMethodAccepted()) { + // processed in inputContext with skipping processKey + return; + } + } + + qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent"; + if (processKeyForInputContext(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call("preventDefault"); + event.call("stopImmediatePropagation"); + }); + const auto keyCallback = std::function([this](emscripten::val event) { + qCDebug(qLcQpaWasmInputContext) << "processKey as KeyEvent"; if (processKey(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) event.call("preventDefault"); event.call("stopPropagation"); }); - emscripten::val keyFocusWindow; - if (QWasmInputContext *wasmContext = - qobject_cast(QWasmIntegration::get()->inputContext())) { - // if there is an touchscreen input context, - // use that window for key input - keyFocusWindow = wasmContext->m_inputElement; - } else { - keyFocusWindow = m_qtWindow; + QWasmInputContext *wasmInput = QWasmIntegration::get()->wasmInputContext(); + if (wasmInput) { + m_keyDownCallbackForInputContext = + std::make_unique(wasmInput->m_inputElement, + "keydown", + keyCallbackForInputContext); + m_keyUpCallbackForInputContext = + std::make_unique(wasmInput->m_inputElement, + "keyup", + keyCallbackForInputContext); } m_keyDownCallback = - std::make_unique(keyFocusWindow, "keydown", keyCallback); - m_keyUpCallback = std::make_unique(keyFocusWindow, "keyup", keyCallback); + std::make_unique(m_qtWindow, "keydown", keyCallback); + m_keyUpCallback =std::make_unique(m_qtWindow, "keyup", keyCallback); + setParent(parent()); } @@ -356,8 +415,6 @@ void QWasmWindow::raise() { bringToTop(); invalidate(); - if (QWasmIntegration::get()->inputContext()) - m_canvas.call("focus"); } void QWasmWindow::lower() @@ -507,6 +564,29 @@ bool QWasmWindow::processKey(const KeyEvent &event) : result; } +bool QWasmWindow::processKeyForInputContext(const KeyEvent &event) +{ + qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; + Q_ASSERT(event.type == EventType::KeyDown || event.type == EventType::KeyUp); + + QKeySequence keySeq(event.modifiers | event.key); + + if (keySeq == QKeySequence::Paste) { + // Process it in pasteCallback and inputCallback + return false; + } + + const auto result = QWindowSystemInterface::handleKeyEvent( + 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, + event.modifiers, event.text); + + // Copy/Cut callback required to copy qtClipboard to system clipboard + if (keySeq == QKeySequence::Copy || keySeq == QKeySequence::Cut) + return false; + + return result; +} + bool QWasmWindow::processPointer(const PointerEvent &event) { if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index ab0dc68e839..480ad6de721 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -35,6 +35,8 @@ struct PointerEvent; class QWasmDeadKeySupport; struct WheelEvent; +Q_DECLARE_LOGGING_CATEGORY(qLcQpaWasmInputContext) + class QWasmWindow final : public QPlatformWindow, public QWasmWindowTreeNode, public QNativeInterface::Private::QWasmWindow @@ -122,6 +124,7 @@ private: void commitParent(QWasmWindowTreeNode *parent); bool processKey(const KeyEvent &event); + bool processKeyForInputContext(const KeyEvent &event); bool processPointer(const PointerEvent &event); bool processWheel(const WheelEvent &event); @@ -146,6 +149,8 @@ private: std::unique_ptr m_keyDownCallback; std::unique_ptr m_keyUpCallback; + std::unique_ptr m_keyDownCallbackForInputContext; + std::unique_ptr m_keyUpCallbackForInputContext; std::unique_ptr m_pointerLeaveCallback; std::unique_ptr m_pointerEnterCallback;