// 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())) { 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)); } } // namespace namespace KeyboardModifier { template <> QFlags getForEvent( const EmscriptenKeyboardEvent& event) { return internal::Helper::getModifierForEvent(event) | (event.location == DOM_KEY_LOCATION_NUMPAD ? Qt::KeypadModifier : Qt::NoModifier); } } // namespace KeyboardModifier Event::Event(EventType type, emscripten::val webEvent) : webEvent(webEvent), type(type) { } Event::~Event() = default; Event::Event(const Event &other) = default; Event::Event(Event &&other) = default; Event &Event::operator=(const Event &other) = default; Event &Event::operator=(Event &&other) = default; KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event) { const auto code = event["code"].as(); const auto webKey = event["key"].as(); deadKey = isDeadKeyEvent(webKey.c_str()); modifiers = KeyboardModifier::getForEvent(event); key = webKeyToQtKey(code, webKey, deadKey, modifiers); text = QString::fromUtf8(webKey); if (text.size() > 1) text.clear(); if (key == Qt::Key_Tab) text = "\t"; } KeyEvent::~KeyEvent() = default; KeyEvent::KeyEvent(const KeyEvent &other) = default; KeyEvent::KeyEvent(KeyEvent &&other) = default; KeyEvent &KeyEvent::operator=(const KeyEvent &other) = default; KeyEvent &KeyEvent::operator=(KeyEvent &&other) = default; std::optional KeyEvent::fromWebWithDeadKeyTranslation(emscripten::val event, QWasmDeadKeySupport *deadKeySupport) { const auto eventType = ([&event]() -> std::optional { const auto eventTypeString = event["type"].as(); if (eventTypeString == "keydown") return EventType::KeyDown; else if (eventTypeString == "keyup") return EventType::KeyUp; return std::nullopt; })(); if (!eventType) return std::nullopt; auto result = KeyEvent(*eventType, event); deadKeySupport->applyDeadKeyTranslations(&result); return result; } 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 = KeyboardModifier::getForEvent(event); } MouseEvent::~MouseEvent() = default; MouseEvent::MouseEvent(const MouseEvent &other) = default; MouseEvent::MouseEvent(MouseEvent &&other) = default; MouseEvent &MouseEvent::operator=(const MouseEvent &other) = default; MouseEvent &MouseEvent::operator=(MouseEvent &&other) = default; 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(); } PointerEvent::~PointerEvent() = default; PointerEvent::PointerEvent(const PointerEvent &other) = default; PointerEvent::PointerEvent(PointerEvent &&other) = default; PointerEvent &PointerEvent::operator=(const PointerEvent &other) = default; PointerEvent &PointerEvent::operator=(PointerEvent &&other) = default; std::optional PointerEvent::fromWeb(emscripten::val event) { const auto eventType = ([&event]() -> std::optional { const auto eventTypeString = event["type"].as(); if (eventTypeString == "pointermove") return EventType::PointerMove; else if (eventTypeString == "pointerup") return EventType::PointerUp; else if (eventTypeString == "pointerdown") return EventType::PointerDown; else if (eventTypeString == "pointerenter") return EventType::PointerEnter; else if (eventTypeString == "pointerleave") return EventType::PointerLeave; return std::nullopt; })(); if (!eventType) return std::nullopt; return PointerEvent(*eventType, event); } 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; })(); } DragEvent::~DragEvent() = default; DragEvent::DragEvent(const DragEvent &other) = default; DragEvent::DragEvent(DragEvent &&other) = default; DragEvent &DragEvent::operator=(const DragEvent &other) = default; DragEvent &DragEvent::operator=(DragEvent &&other) = default; std::optional DragEvent::fromWeb(emscripten::val event, QWindow *targetWindow) { const auto eventType = ([&event]() -> std::optional { const auto eventTypeString = event["type"].as(); if (eventTypeString == "dragend") return EventType::DragEnd; if (eventTypeString == "dragover") return EventType::DragOver; if (eventTypeString == "dragstart") return EventType::DragStart; if (eventTypeString == "drop") return EventType::Drop; return std::nullopt; })(); if (!eventType) return std::nullopt; return DragEvent(*eventType, event, targetWindow); } 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(); } WheelEvent::~WheelEvent() = default; WheelEvent::WheelEvent(const WheelEvent &other) = default; WheelEvent::WheelEvent(WheelEvent &&other) = default; WheelEvent &WheelEvent::operator=(const WheelEvent &other) = default; WheelEvent &WheelEvent::operator=(WheelEvent &&other) = default; std::optional WheelEvent::fromWeb(emscripten::val event) { const auto eventType = ([&event]() -> std::optional { const auto eventTypeString = event["type"].as(); if (eventTypeString == "wheel") return EventType::Wheel; return std::nullopt; })(); if (!eventType) return std::nullopt; return WheelEvent(*eventType, event); } QT_END_NAMESPACE