diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp index eb3e30b1409..cb1e3f135c0 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.cpp +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -3,6 +3,7 @@ #include "qwasmaccessibility.h" #include "qwasmscreen.h" +#include "qwasmwindow.h" #include @@ -18,33 +19,105 @@ Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") // events. In addition or alternatively, we could also walk the accessibility tree // from setRootObject(). +namespace { +QWasmWindow *asWasmWindow(QWindow *window) +{ + return static_cast(window->handle()); +} +} // namespace QWasmAccessibility::QWasmAccessibility() { + s_instance = this; } QWasmAccessibility::~QWasmAccessibility() { + s_instance = nullptr; +} +QWasmAccessibility *QWasmAccessibility::s_instance = nullptr; + +QWasmAccessibility* QWasmAccessibility::get() +{ + return s_instance; +} + +void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window) +{ + get()->addAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::removeAccessibilityEnableButton(QWindow *window) +{ + get()->removeAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window) +{ + if (m_accessibilityEnabled) + return; + + emscripten::val container = getContainer(window); + emscripten::val document = getDocument(container); + emscripten::val button = document.call("createElement", std::string("button")); + button.set("innerText", std::string("Enable Screen Reader")); + container.call("appendChild", button); + + emscripten::val style = button["style"]; + style.set("width", "100%"); + style.set("height", "100%"); + + auto enableContext = std::make_tuple(button, std::make_unique + (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); })); + m_enableButtons.insert(std::make_pair(window, std::move(enableContext))); +} + +void QWasmAccessibility::removeAccessibilityEnableButtonImpl(QWindow *window) +{ + auto it = m_enableButtons.find(window); + if (it == m_enableButtons.end()) + return; + + // Remove button + auto [element, callback] = it->second; + Q_UNUSED(callback); + element["parentElement"].call("removeChild", element); + m_enableButtons.erase(it); +} + +void QWasmAccessibility::enableAccessibility() +{ + // Enable accessibility globally for the applicaton. Remove all "enable" + // buttons and populate the accessibility tree, starting from the root object. + + Q_ASSERT(!m_accessibilityEnabled); + m_accessibilityEnabled = true; + for (const auto& [key, value] : m_enableButtons) { + const auto &[element, callback] = value; + Q_UNUSED(key); + Q_UNUSED(callback); + element["parentElement"].call("removeChild", element); + } + m_enableButtons.clear(); + populateAccessibilityTree(QAccessible::queryAccessibleInterface(m_rootObject)); +} + +emscripten::val QWasmAccessibility::getContainer(QWindow *window) +{ + return window ? asWasmWindow(window)->a11yContainer() : emscripten::val::undefined(); } emscripten::val QWasmAccessibility::getContainer(QAccessibleInterface *iface) { - // Get to QWasmScreen::container(), return undefined element if unable to - QWindow *window = iface->window(); - if (!window) - return emscripten::val::undefined(); - QWasmScreen *screen = QWasmScreen::get(window->screen()); - if (!screen) - return emscripten::val::undefined(); - return screen->element(); + return getContainer(iface->window()); } emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container) { if (container.isUndefined()) - return emscripten::val::undefined(); + return emscripten::val::global("document"); return container["ownerDocument"]; } @@ -62,7 +135,7 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac // Get the correct html document for the container, or fall back // to the global document. TODO: Does using the correct document actually matter? - emscripten::val document = container.isUndefined() ? emscripten::val::global("document") : getDocument(container); + emscripten::val document = getDocument(container); // Translate the Qt a11y elemen role into html element type + ARIA role. // Here we can either create
elements with a spesific ARIA role, @@ -136,14 +209,13 @@ void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, b void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface) { emscripten::val element = ensureHtmlElement(iface); - setHtmlElementGeometry(iface, element); + setHtmlElementGeometry(element, iface->rect()); } -void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface, emscripten::val element) +void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry) { // Position the element using "position: absolute" in order to place // it under the corresponding Qt element in the screen. - QRect geometry = iface->rect(); emscripten::val style = element["style"]; style.set("position", std::string("absolute")); style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the @@ -190,8 +262,27 @@ void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event) } } +void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface) +{ + if (!iface) + return; + + // Create html element for the interface, sync up properties. + ensureHtmlElement(iface); + const bool visible = !iface->state().invisible; + setHtmlElementVisibility(iface, visible); + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + + for (int i = 0; i < iface->childCount(); ++i) + populateAccessibilityTree(iface->child(i)); +} + void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) { + if (!m_accessibilityEnabled) + return; + QAccessibleInterface *iface = event->accessibleInterface(); if (!iface) { qWarning() << "notifyAccessibilityUpdate with null a11y interface" ; @@ -237,11 +328,9 @@ void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) }; } -void QWasmAccessibility::setRootObject(QObject *o) +void QWasmAccessibility::setRootObject(QObject *root) { - qCDebug(lcQpaAccessibility) << "setRootObject" << o; - QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(o); - Q_UNUSED(iface) + m_rootObject = root; } void QWasmAccessibility::initialize() diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.h b/src/plugins/platforms/wasm/qwasmaccessibility.h index 6d5330c5605..2c76f41c003 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.h +++ b/src/plugins/platforms/wasm/qwasmaccessibility.h @@ -5,11 +5,14 @@ #define QWASMACCESIBILITY_H #include +#include #include #include #include +#include + Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility) class QWasmAccessibility : public QPlatformAccessibility @@ -18,6 +21,17 @@ public: QWasmAccessibility(); ~QWasmAccessibility(); + static QWasmAccessibility* get(); + + static void addAccessibilityEnableButton(QWindow *window); + static void removeAccessibilityEnableButton(QWindow *window); + +private: + void addAccessibilityEnableButtonImpl(QWindow *window); + void removeAccessibilityEnableButtonImpl(QWindow *window); + void enableAccessibility(); + + static emscripten::val getContainer(QWindow *window); static emscripten::val getContainer(QAccessibleInterface *iface); static emscripten::val getDocument(const emscripten::val &container); static emscripten::val getDocument(QAccessibleInterface *iface); @@ -27,19 +41,24 @@ public: emscripten::val ensureHtmlElement(QAccessibleInterface *iface); void setHtmlElementVisibility(QAccessibleInterface *iface, bool visible); void setHtmlElementGeometry(QAccessibleInterface *iface); - void setHtmlElementGeometry(QAccessibleInterface *iface, emscripten::val element); + void setHtmlElementGeometry(emscripten::val element, QRect geometry); void setHtmlElementTextName(QAccessibleInterface *iface); void handleStaticTextUpdate(QAccessibleEvent *event); void handleButtonUpdate(QAccessibleEvent *event); void handleCheckBoxUpdate(QAccessibleEvent *event); + void populateAccessibilityTree(QAccessibleInterface *iface); void notifyAccessibilityUpdate(QAccessibleEvent *event) override; void setRootObject(QObject *o) override; void initialize() override; void cleanup() override; private: + static QWasmAccessibility *s_instance; + QObject *m_rootObject = nullptr; + bool m_accessibilityEnabled = false; + std::map>> m_enableButtons; QHash m_elements; }; diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 4b923087dbe..b9320f4a254 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -16,6 +16,7 @@ #include "qwasmevent.h" #include "qwasmeventdispatcher.h" #include "qwasmstring.h" +#include "qwasmaccessibility.h" #include #include @@ -470,6 +471,7 @@ QWasmWindow::~QWasmWindow() m_compositor->removeWindow(this); if (m_requestAnimationFrameId > -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); + QWasmAccessibility::removeAccessibilityEnableButton(window()); } void QWasmWindow::destroy() @@ -505,6 +507,11 @@ void QWasmWindow::initialize() setWindowIcon(window()->icon()); m_normalGeometry = rect; QPlatformWindow::setGeometry(m_normalGeometry); + + // Add accessibility-enable button. The user can activate this + // button to opt-in to accessibility. + if (window()->isTopLevel()) + QWasmAccessibility::addAccessibilityEnableButton(window()); } QWasmScreen *QWasmWindow::platformScreen() const