// 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 "qwasmaccessibility.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) { emscripten::val inputType = event["inputType"]; if (inputType.isNull() || inputType.isUndefined()) return; const auto inputTypeString = inputType.as(); // also may be dataTransfer // containing rich text emscripten::val inputData = event["data"]; QString inputStr = (!inputData.isNull() && !inputData.isUndefined()) ? QString::fromEcmaString(inputData) : QString(); // 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")) { QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); QCoreApplication::sendEvent(m_focusObject, &queryEvent); int cursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt(); int deleteLength = rangesPair.second - rangesPair.first; int deleteFrom = -1; if (cursorPosition >= rangesPair.first) { deleteFrom = -(cursorPosition - rangesPair.first); } QInputMethodEvent e; e.setCommitString(QString(), deleteFrom, deleteLength); QCoreApplication::sendEvent(m_focusObject, &e); rangesPair.first = 0; rangesPair.second = 0; 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) << "insertCompositionText : " << inputStr; event.call("stopImmediatePropagation"); QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); QCoreApplication::sendEvent(m_focusObject, &queryEvent); int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt() ; int replaceIndex = (qCursorPosition - rangesPair.first); int replaceLength = rangesPair.second - rangesPair.first; setPreeditString(inputStr, replaceIndex); insertPreedit(replaceLength); rangesPair.first = 0; rangesPair.second = 0; event.call("stopImmediatePropagation"); return; } else if (!inputTypeString.compare("insertReplacementText")) { // the previous input string up to the space, needs replaced with this // used on iOS when continuing composition after focus change // there's no range given qCDebug(qLcQpaWasmInputContext) << "insertReplacementText >>>>" << "inputString : " << inputStr; emscripten::val ranges = event.call("getTargetRanges"); m_preeditString.clear(); std::string elementString = m_inputElement["value"].as(); QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); QCoreApplication::sendEvent(m_focusObject, &queryEvent); QString textFieldString = queryEvent.value(Qt::ImTextBeforeCursor).toString(); int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt(); if (rangesPair.first != 0 || rangesPair.second != 0) { int replaceIndex = (qCursorPosition - rangesPair.first); int replaceLength = rangesPair.second - rangesPair.first; replaceText(inputStr, -replaceIndex, replaceLength); rangesPair.first = 0; rangesPair.second = 0; } else { int spaceIndex = textFieldString.lastIndexOf(' ') + 1; int replaceIndex = (qCursorPosition - spaceIndex); replaceText(inputStr, -replaceIndex, replaceIndex); } event.call("stopImmediatePropagation"); return; } else if (!inputTypeString.compare("deleteCompositionText")) { setPreeditString("", 0); insertPreedit(); event.call("stopImmediatePropagation"); return; } else if (!inputTypeString.compare("insertFromComposition")) { setPreeditString(inputStr, 0); insertPreedit(); event.call("stopImmediatePropagation"); return; } else if (!inputTypeString.compare("insertText")) { if ((rangesPair.first != 0 || rangesPair.second != 0) && rangesPair.first != rangesPair.second) { QInputMethodQueryEvent queryEvent(Qt::ImQueryAll); QCoreApplication::sendEvent(m_focusObject, &queryEvent); int qCursorPosition = queryEvent.value(Qt::ImCursorPosition).toInt(); int replaceIndex = (qCursorPosition - rangesPair.first); int replaceLength = rangesPair.second - rangesPair.first; replaceText(inputStr, -replaceIndex, replaceLength); rangesPair.first = 0; rangesPair.second = 0; } else { insertText(inputStr); } event.call("stopImmediatePropagation"); #if QT_CONFIG(clipboard) } else if (!inputTypeString.compare("insertFromPaste")) { 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::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, 0); } void QWasmInputContext::beforeInputCallback(emscripten::val event) { emscripten::val ranges = event.call("getTargetRanges"); auto length = ranges["length"].as(); for (auto i = 0; i < length; i++) { emscripten::val range = ranges[i]; qCDebug(qLcQpaWasmInputContext) << "startOffset" << range["startOffset"].as(); qCDebug(qLcQpaWasmInputContext) << "endOffset" << range["endOffset"].as(); rangesPair.first = range["startOffset"].as(); rangesPair.second = range["endOffset"].as(); } } QWasmInputContext::QWasmInputContext() { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; } QWasmInputContext::~QWasmInputContext() { } void QWasmInputContext::update(Qt::InputMethodQueries queries) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << queries; if ((queries & Qt::ImEnabled) && (inputMethodAccepted() != m_inputMethodAccepted)) { if (m_focusObject && !m_preeditString.isEmpty()) commitPreeditAndClear(); updateInputElement(); } QPlatformInputContext::update(queries); } void QWasmInputContext::showInputPanel() { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; // Note: showInputPanel not necessarily called, we shall // still accept input if we have a focus object and // inputMethodAccepted(). updateInputElement(); } void QWasmInputContext::updateGeometry() { if (QWasmAccessibility::isEnabled()) return; if (m_inputElement.isNull()) return; const QWindow *focusWindow = QGuiApplication::focusWindow(); if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) { m_inputElement["style"].set("left", "0px"); m_inputElement["style"].set("top", "0px"); } else { Q_ASSERT(focusWindow); Q_ASSERT(m_focusObject); Q_ASSERT(m_inputMethodAccepted); 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"); m_inputElement["style"].set("height", "1px"); } } void QWasmInputContext::updateInputElement() { m_inputMethodAccepted = inputMethodAccepted(); if (QWasmAccessibility::isEnabled()) return; // Mobile devices can dismiss keyboard/IME and focus is still on input. // Successive clicks on the same input should open the keyboard/IME. updateGeometry(); // If there is no focus object, or no visible input panel, remove focus QWasmWindow *focusWindow = QWasmWindow::fromWindow(QGuiApplication::focusWindow()); if (!m_focusObject || !focusWindow || !m_inputMethodAccepted) { if (!m_inputElement.isNull()) { m_inputElement.set("value", ""); m_inputElement.set("inputMode", std::string("none")); } if (focusWindow) { focusWindow->focus(); } else { if (!m_inputElement.isNull()) m_inputElement.call("blur"); } m_inputElement = emscripten::val::null(); return; } Q_ASSERT(focusWindow); Q_ASSERT(m_focusObject); Q_ASSERT(m_inputMethodAccepted); m_inputElement = focusWindow->inputElement(); 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()); QInputMethodQueryEvent query((Qt::InputMethodQueries(Qt::ImHints))); QCoreApplication::sendEvent(m_focusObject, &query); if (Qt::InputMethodHints(query.value(Qt::ImHints).toInt()).testFlag(Qt::ImhHiddenText)) m_inputElement.set("type", "password"); else m_inputElement.set("type", "text"); m_inputElement.set("inputMode", std::string("text")); 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_focusObject = object; updateInputElement(); QPlatformInputContext::setFocusObject(object); } void QWasmInputContext::hideInputPanel() { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO; // hide only if m_focusObject does not exist if (!m_focusObject) updateInputElement(); } void QWasmInputContext::setPreeditString(QString preeditStr, int replaceSize) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << preeditStr << replaceSize; m_preeditString = preeditStr; m_replaceIndex = replaceSize; } void QWasmInputContext::insertPreedit(int replaceLength) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << m_preeditString; if (replaceLength == 0) replaceLength = m_preeditString.length(); QList 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); if (m_replaceIndex > 0) e.setCommitString("", -m_replaceIndex, replaceLength); 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); } void QWasmInputContext::insertText(QString inputStr, bool replace) { // commitString qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replace; Q_UNUSED(replace); if (!inputStr.isEmpty()) { const int replaceLen = 0; QInputMethodEvent e; e.setCommitString(inputStr, -replaceLen, replaceLen); QCoreApplication::sendEvent(m_focusObject, &e); } } /* This will replace the text in the focusobject at replaceFrom position, and replaceSize length with the text in inputStr. */ void QWasmInputContext::replaceText(QString inputStr, int replaceFrom, int replaceSize) { qCDebug(qLcQpaWasmInputContext) << Q_FUNC_INFO << inputStr << replaceFrom << replaceSize; QList attributes; { QInputMethodEvent::Attribute attr_cursor(QInputMethodEvent::Cursor, 0, // start 1); // length attributes.append(attr_cursor); QTextCharFormat format; format.setFontUnderline(true); format.setUnderlineStyle(QTextCharFormat::SingleUnderline); QInputMethodEvent::Attribute attr_format(QInputMethodEvent::TextFormat, 0, replaceSize, format); attributes.append(attr_format); } QInputMethodEvent e1(QString(), attributes); e1.setCommitString(inputStr, replaceFrom, replaceSize); QCoreApplication::sendEvent(m_focusObject, &e1); m_preeditString.clear(); } QT_END_NAMESPACE