// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmevent.h" #include "qwasmkeytranslator.h" #include #include #include QT_BEGIN_NAMESPACE namespace { constexpr std::string_view WebDeadKeyValue = "Dead"; bool isDeadKeyEvent(const char *key) { return qstrncmp(key, WebDeadKeyValue.data(), WebDeadKeyValue.size()) == 0; } Qt::Key getKeyFromCode(const std::string &code) { if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(code.c_str())) return *mapping; static QRegularExpression regex(QString(QStringLiteral(R"re((?:Key|Digit)(\w))re"))); const auto codeQString = QString::fromStdString(code); const auto match = regex.match(codeQString); if (!match.hasMatch()) return Qt::Key_unknown; constexpr size_t CharacterIndex = 1; return static_cast(match.capturedView(CharacterIndex).at(0).toLatin1()); } Qt::Key webKeyToQtKey(const std::string &code, const std::string &key, bool isDeadKey, QFlags modifiers) { if (isDeadKey) { auto mapped = getKeyFromCode(code); switch (mapped) { case Qt::Key_U: return Qt::Key_Dead_Diaeresis; case Qt::Key_E: return Qt::Key_Dead_Acute; case Qt::Key_I: return Qt::Key_Dead_Circumflex; case Qt::Key_N: return Qt::Key_Dead_Tilde; case Qt::Key_QuoteLeft: return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Tilde : Qt::Key_Dead_Grave; case Qt::Key_6: return Qt::Key_Dead_Circumflex; case Qt::Key_Apostrophe: return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Diaeresis : Qt::Key_Dead_Acute; case Qt::Key_AsciiTilde: return Qt::Key_Dead_Tilde; default: return Qt::Key_unknown; } } else if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(key.c_str())) { if (modifiers.testFlag(Qt::ShiftModifier) && (*mapping == Qt::Key::Key_Tab)) *mapping = Qt::Key::Key_Backtab; return *mapping; } // cast to unicode key QString str = QString::fromUtf8(key.c_str()).toUpper(); if (str.length() > 1) return Qt::Key_unknown; QStringIterator i(str); return static_cast(i.next(0)); } QFlags getKeyboardModifiers(const emscripten::val &event) { QFlags keyModifier = Qt::NoModifier; if (event["shiftKey"].as()) keyModifier |= Qt::ShiftModifier; if (event["ctrlKey"].as()) keyModifier |= platform() == Platform::MacOS ? Qt::MetaModifier : Qt::ControlModifier; if (event["altKey"].as()) keyModifier |= Qt::AltModifier; if (event["metaKey"].as()) keyModifier |= platform() == Platform::MacOS ? Qt::ControlModifier : Qt::MetaModifier; if (event["constructor"]["name"].as() == "KeyboardEvent" && event["location"].as() == DOM_KEY_LOCATION_NUMPAD) { keyModifier |= Qt::KeypadModifier; } return keyModifier; } } // namespace Event::Event(EventType type, emscripten::val webEvent) : webEvent(webEvent), type(type) { } bool Event::isTargetedForQtElement() const { // Check event target via composedPath, which returns the true path even // if the browser retargets the event for Qt's shadow DOM container. This // is needed to avoid capturing the pointer in cases where foreign html // elements are embedded inside Qt's shadow DOM. emscripten::val path = webEvent.call("composedPath"); QString topElementClassName = QString::fromEcmaString(path[0]["className"]); return topElementClassName.startsWith("qt-"); // .e.g. qt-window-canvas } KeyEvent::KeyEvent(EventType type, emscripten::val event, QWasmDeadKeySupport *deadKeySupport) : Event(type, event) { const auto code = event["code"].as(); const auto webKey = event["key"].as(); deadKey = isDeadKeyEvent(webKey.c_str()); autoRepeat = event["repeat"].as(); modifiers = getKeyboardModifiers(event); key = webKeyToQtKey(code, webKey, deadKey, modifiers); text = QString::fromUtf8(webKey); // Alt + keypad number -> insert utf-8 character // The individual numbers shall not be inserted but // on some platforms they are if numlock is // activated if ((modifiers & Qt::AltModifier) && (modifiers & Qt::KeypadModifier)) text.clear(); if (text.size() > 1) text.clear(); if (key == Qt::Key_Tab) text = "\t"; deadKeySupport->applyDeadKeyTranslations(this); } MouseEvent::MouseEvent(EventType type, emscripten::val event) : Event(type, event) { mouseButton = MouseEvent::buttonFromWeb(event["button"].as()); mouseButtons = MouseEvent::buttonsFromWeb(event["buttons"].as()); // The current button state (event.buttons) may be out of sync for some PointerDown // events where the "down" state is very brief, for example taps on Apple trackpads. // Qt expects that the current button state is in sync with the event, so we sync // it up here. if (type == EventType::PointerDown) mouseButtons |= mouseButton; localPoint = QPointF(event["offsetX"].as(), event["offsetY"].as()); pointInPage = QPointF(event["pageX"].as(), event["pageY"].as()); pointInViewport = QPointF(event["clientX"].as(), event["clientY"].as()); modifiers = getKeyboardModifiers(event); } PointerEvent::PointerEvent(EventType type, emscripten::val event) : MouseEvent(type, event) { pointerId = event["pointerId"].as(); pointerType = ([type = event["pointerType"].as()]() { if (type == "mouse") return PointerType::Mouse; if (type == "touch") return PointerType::Touch; if (type == "pen") return PointerType::Pen; return PointerType::Other; })(); width = event["width"].as(); height = event["height"].as(); pressure = event["pressure"].as(); tiltX = event["tiltX"].as(); tiltY = event["tiltY"].as(); tangentialPressure = event["tangentialPressure"].as(); twist = event["twist"].as(); isPrimary = event["isPrimary"].as(); } DragEvent::DragEvent(EventType type, emscripten::val event, QWindow *window) : MouseEvent(type, event), dataTransfer(event["dataTransfer"]), targetWindow(window) { dropAction = ([event]() { const std::string effect = event["dataTransfer"]["dropEffect"].as(); if (effect == "copy") return Qt::CopyAction; else if (effect == "move") return Qt::MoveAction; else if (effect == "link") return Qt::LinkAction; return Qt::IgnoreAction; })(); } void DragEvent::cancelDragStart() { Q_ASSERT_X(type == EventType::DragStart, Q_FUNC_INFO, "Only supported for DragStart"); webEvent.call("preventDefault"); } void DragEvent::acceptDragOver() { Q_ASSERT_X(type == EventType::DragOver, Q_FUNC_INFO, "Only supported for DragOver"); webEvent.call("preventDefault"); } void DragEvent::acceptDrop() { Q_ASSERT_X(type == EventType::Drop, Q_FUNC_INFO, "Only supported for Drop"); webEvent.call("preventDefault"); } WheelEvent::WheelEvent(EventType type, emscripten::val event) : MouseEvent(type, event) { deltaMode = ([event]() { const int deltaMode = event["deltaMode"].as(); const auto jsWheelEventType = emscripten::val::global("WheelEvent"); if (deltaMode == jsWheelEventType["DOM_DELTA_PIXEL"].as()) return DeltaMode::Pixel; else if (deltaMode == jsWheelEventType["DOM_DELTA_LINE"].as()) return DeltaMode::Line; return DeltaMode::Page; })(); delta = QPointF(event["deltaX"].as(), event["deltaY"].as()); webkitDirectionInvertedFromDevice = event["webkitDirectionInvertedFromDevice"].as(); } QT_END_NAMESPACE