// 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 #include #include #include #include #include #if QT_CONFIG(clipboard) #include #endif #include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcQpaWasmInputContext, "qt.qpa.wasm.inputcontext") using namespace qstdweb; void QWasmInputContext::inputCallback(emscripten::val event) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << "isComposing : " << event["isComposing"].as(); QString inputStr = (event["data"] != emscripten::val::null() && event["data"] != emscripten::val::undefined()) ? QString::fromStdString(event["data"].as()) : QString(); 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"); #if QT_CONFIG(clipboard) } 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")) { #endif } else { qCWarning(qLcQpaWasmInputContext) << Q_FUNC_INFO << "inputType \"" << inputType.as() << "\" is not supported in Qt yet"; } } } void QWasmInputContext::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(); } void QWasmInputContext::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(); } } */ void QWasmInputContext::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); } #if QT_CONFIG(clipboard) 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) } #endif // QT_CONFIG(clipboard) 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("contenteditable","true"); m_inputElement.call("setAttribute", std::string("aria-hidden"), std::string("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(this)))); emscripten::val body = document["body"]; body.call("appendChild", m_inputElement); m_inputCallback = QWasmEventHandler(m_inputElement, "input", QWasmInputContext::inputCallback); m_compositionEndCallback = QWasmEventHandler(m_inputElement, "compositionend", QWasmInputContext::compositionEndCallback); m_compositionStartCallback = QWasmEventHandler(m_inputElement, "compositionstart", QWasmInputContext::compositionStartCallback); m_compositionUpdateCallback = QWasmEventHandler(m_inputElement, "compositionupdate", QWasmInputContext::compositionUpdateCallback); #if QT_CONFIG(clipboard) // Clipboard for InputContext m_clipboardCut = QWasmEventHandler(m_inputElement, "cut", cutCallback); m_clipboardCopy = QWasmEventHandler(m_inputElement, "copy", copyCallback); m_clipboardPaste = QWasmEventHandler(m_inputElement, "paste", pasteCallback); #endif } 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("blur"); if (QWasmWindow *wasmwindow = QWasmWindow::fromWindow(focusWindow)) wasmwindow->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("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("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 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