Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasmaccessibility.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6#include "qwasmscreen.h"
7#include "qwasmwindow.h"
9#include <QtCore/private/qwasmsuspendresumecontrol_p.h>
10#include <QtGui/qwindow.h>
11
12#include <sstream>
13
15{
16 QWasmAccessibility::enable();
17}
18
19#if QT_CONFIG(accessibility)
20
21#include <QtGui/private/qaccessiblebridgeutils_p.h>
22
23Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility")
24
25namespace {
26EM_JS(emscripten::EM_VAL, getActiveElement_js, (emscripten::EM_VAL undefHandle), {
27 var activeEl = document.activeElement;
28 while (true) {
29 if (!activeEl) {
30 return undefHandle;
31 } else if (activeEl.shadowRoot) {
32 activeEl = activeEl.shadowRoot.activeElement;
33 } else {
34 return Emval.toHandle(activeEl);
35 }
36 }
37})
38}
39
40// Qt WebAssembly a11y backend
41//
42// This backend implements accessibility support by creating "shadowing" html
43// elements for each Qt UI element. We access the DOM by using Emscripten's
44// val.h API.
45//
46// Currently, html elements are created in response to notifyAccessibilityUpdate
47// events. In addition or alternatively, we could also walk the accessibility tree
48// from setRootObject().
49
50QWasmAccessibility::QWasmAccessibility()
51{
52 s_instance = this;
53
54 if (qEnvironmentVariableIntValue("QT_WASM_ENABLE_ACCESSIBILITY") == 1)
55 enableAccessibility();
56
57 // Register accessibility element event handler
58 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
59
60 m_eventHandlerIndex = suspendResume->registerEventHandler([this](const emscripten::val event){
61 this->handleEventFromHtmlElement(event);
62 });
63}
64
65QWasmAccessibility::~QWasmAccessibility()
66{
67 // Remove accessibility element event handler
68 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
69 suspendResume->removeEventHandler(m_eventHandlerIndex);
70
71 s_instance = nullptr;
72}
73
74QWasmAccessibility *QWasmAccessibility::s_instance = nullptr;
75
76QWasmAccessibility* QWasmAccessibility::get()
77{
78 return s_instance;
79}
80
81void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window)
82{
83 get()->addAccessibilityEnableButtonImpl(window);
84}
85
86void QWasmAccessibility::onShowWindow(QWindow *window)
87{
88 get()->onShowWindowImpl(window);
89}
90
91void QWasmAccessibility::onRemoveWindow(QWindow *window)
92{
93 get()->onRemoveWindowImpl(window);
94}
95
96bool QWasmAccessibility::isEnabled()
97{
98 return get()->m_accessibilityEnabled;
99}
100void QWasmAccessibility::enable()
101{
102 if (!isEnabled())
103 get()->enableAccessibility();
104}
105
106void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window)
107{
108 if (m_accessibilityEnabled)
109 return;
110
111 emscripten::val container = getElementContainer(window);
112 emscripten::val document = getDocument(container);
113 emscripten::val button = document.call<emscripten::val>("createElement", std::string("button"));
114 setProperty(button, "innerText", "Enable Screen Reader");
115 button["classList"].call<void>("add", emscripten::val("hidden-visually-read-by-screen-reader"));
116 container.call<void>("appendChild", button);
117
118 auto enableContext = std::make_tuple(button, std::make_unique<qstdweb::EventCallback>
119 (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); }));
120 m_enableButtons.insert(std::make_pair(window, std::move(enableContext)));
121}
122
123void QWasmAccessibility::onShowWindowImpl(QWindow *window)
124{
125 if (!m_accessibilityEnabled)
126 return;
127 populateAccessibilityTree(window->accessibleRoot());
128}
129
130void QWasmAccessibility::onRemoveWindowImpl(QWindow *window)
131{
132 {
133 const auto it = m_enableButtons.find(window);
134 if (it != m_enableButtons.end()) {
135 // Remove button
136 auto [element, callback] = it->second;
137 Q_UNUSED(callback);
138 element["parentElement"].call<void>("removeChild", element);
139 m_enableButtons.erase(it);
140 }
141 }
142 if (!m_accessibilityEnabled)
143 return;
144
145 auto a11yContainer = getA11yContainer(window);
146 if (a11yContainer.isUndefined())
147 return;
148
149 // Forget the elements of the window, they go away with its container.
150 for (auto it = m_elements.begin(); it != m_elements.end();) {
151 if (a11yContainer.call<bool>("contains", it.value()))
152 it = m_elements.erase(it);
153 else
154 ++it;
155 }
156 getElementContainer(window).call<void>("replaceChildren");
157 getDescribedByContainer(window).call<void>("replaceChildren");
158}
159
160void QWasmAccessibility::enableAccessibility()
161{
162 // Enable accessibility. Remove all "enable" buttons and populate the
163 // accessibility tree for each window.
164
165 Q_ASSERT(!m_accessibilityEnabled);
166 m_accessibilityEnabled = true;
167 setActive(true);
168 for (const auto& [key, value] : m_enableButtons) {
169 const auto &[element, callback] = value;
170 Q_UNUSED(callback);
171 if (auto wasmWindow = QWasmWindow::fromWindow(key))
172 wasmWindow->onAccessibilityEnable();
173 onShowWindowImpl(key);
174 element["parentElement"].call<void>("removeChild", element);
175 }
176 m_enableButtons.clear();
177}
178
179bool QWasmAccessibility::isWindowNode(QAccessibleInterface *iface)
180{
181 return (iface && !getWindow(iface->parent()) && getWindow(iface));
182}
183
184emscripten::val QWasmAccessibility::getA11yContainer(QWindow *window)
185{
186 const auto wasmWindow = QWasmWindow::fromWindow(window);
187 if (!wasmWindow)
188 return emscripten::val::undefined();
189
190 auto a11yContainer = wasmWindow->a11yContainer();
191 if (a11yContainer["childElementCount"].as<unsigned>() == 2)
192 return a11yContainer;
193
194 Q_ASSERT(a11yContainer["childElementCount"].as<unsigned>() == 0);
195
196 const auto document = getDocument(a11yContainer);
197 if (document.isUndefined())
198 return emscripten::val::undefined();
199
200 auto elementContainer = document.call<emscripten::val>("createElement", std::string("div"));
201 elementContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-elements-container"));
202 auto describedByContainer = document.call<emscripten::val>("createElement", std::string("div"));
203 describedByContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-describedby-container"));
204
205 a11yContainer.call<void>("appendChild", elementContainer);
206 a11yContainer.call<void>("appendChild", describedByContainer);
207
208 return a11yContainer;
209}
210
211emscripten::val QWasmAccessibility::getA11yContainer(QAccessibleInterface *iface)
212{
213 return getA11yContainer(getWindow(iface));
214}
215
216emscripten::val QWasmAccessibility::getDescribedByContainer(QWindow *window)
217{
218 auto a11yContainer = getA11yContainer(window);
219 if (a11yContainer.isUndefined())
220 return emscripten ::val::undefined();
221
222 Q_ASSERT(a11yContainer["childElementCount"].as<unsigned>() == 2);
223 Q_ASSERT(!a11yContainer["children"][1].isUndefined());
224
225 return a11yContainer["children"][1];
226}
227
228emscripten::val QWasmAccessibility::getDescribedByContainer(QAccessibleInterface *iface)
229{
230 return getDescribedByContainer(getWindow(iface));
231}
232
233emscripten::val QWasmAccessibility::getElementContainer(QWindow *window)
234{
235 auto a11yContainer = getA11yContainer(window);
236 if (a11yContainer.isUndefined())
237 return emscripten ::val::undefined();
238
239 Q_ASSERT(a11yContainer["childElementCount"].as<unsigned>() == 2);
240 Q_ASSERT(!a11yContainer["children"][0].isUndefined());
241 return a11yContainer["children"][0];
242}
243
244emscripten::val QWasmAccessibility::getElementContainer(QAccessibleInterface *iface)
245{
246 // Here we skip QWindow nodes, as they are already present. Such nodes
247 // has a parent window of null.
248 //
249 // The next node should return the a11y container.
250 // Further nodes should return the element of the parent.
251 if (!getWindow(iface))
252 return emscripten::val::undefined();
253
254 if (isWindowNode(iface))
255 return emscripten::val::undefined();
256
257 if (isWindowNode(iface->parent()))
258 return getElementContainer(getWindow(iface->parent()));
259
260 // Regular node
261 return getHtmlElement(iface->parent());
262}
263
264QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface)
265{
266 if (!iface)
267 return nullptr;
268
269 QWindow *window = iface->window();
270 // this is needed to add tabs as the window is not available
271 if (!window && iface->parent())
272 window = iface->parent()->window();
273 return window;
274}
275
276emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container)
277{
278 if (container.isUndefined())
279 return emscripten::val::global("document");
280 return container["ownerDocument"];
281}
282
283emscripten::val QWasmAccessibility::getDocument(QAccessibleInterface *iface)
284{
285 return getDocument(getA11yContainer(iface));
286}
287
288void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
289 const std::string &val)
290{
291 if (val != "")
292 element.call<void>("setAttribute", attr, val);
293 else
294 element.call<void>("removeAttribute", attr);
295}
296
297void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
298 const char *val)
299{
300 setAttribute(element, attr, std::string(val));
301}
302
303void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, bool val)
304{
305 if (val)
306 element.call<void>("setAttribute", attr, val);
307 else
308 element.call<void>("removeAttribute", attr);
309}
310
311void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property,
312 const std::string &val)
313{
314 element.set(property, val);
315}
316
317void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, const char *val)
318{
319 setProperty(element, property, std::string(val));
320}
321
322void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, bool val)
323{
324 element.set(property, val);
325}
326
327void QWasmAccessibility::setNamedAttribute(QAccessibleInterface *iface, const std::string &attribute, QAccessible::Text text)
328{
329 const emscripten::val element = getHtmlElement(iface);
330 setAttribute(element, attribute, iface->text(text).toStdString());
331}
332void QWasmAccessibility::setNamedProperty(QAccessibleInterface *iface, const std::string &property, QAccessible::Text text)
333{
334 const emscripten::val element = getHtmlElement(iface);
335 setProperty(element, property, iface->text(text).toStdString());
336}
337
338void QWasmAccessibility::addEventListener(QAccessibleInterface *iface, emscripten::val element, const char *eventType)
339{
340 element.set("data-qta11yinterface", reinterpret_cast<size_t>(iface));
341 element.call<void>("addEventListener", emscripten::val(eventType),
342 QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_eventHandlerIndex),
343 true);
344}
345
346void QWasmAccessibility::sendEvent(QAccessibleInterface *iface, QAccessible::Event eventType)
347{
348 if (iface->object()) {
349 QAccessibleEvent event(iface->object(), eventType);
350 handleUpdateByInterfaceRole(&event);
351 } else {
352 QAccessibleEvent event(iface, eventType);
353 handleUpdateByInterfaceRole(&event);
354 }
355}
356
357emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface)
358{
359 // Get the html container element for the interface; this depends on which
360 // QScreen it is on. If the interface is not on a screen yet we get an undefined
361 // container, and the code below handles that case as well.
362 emscripten::val container = getElementContainer(iface);
363
364 // Get the correct html document for the container, or fall back
365 // to the global document. TODO: Does using the correct document actually matter?
366 emscripten::val document = getDocument(container);
367
368 // Translate the Qt a11y elemen role into html element type + ARIA role.
369 // Here we can either create <div> elements with a spesific ARIA role,
370 // or create e.g. <button> elements which should have built-in accessibility.
371 emscripten::val element = [this, iface, document] {
372
373 emscripten::val element = emscripten::val::undefined();
374
375 switch (iface->role()) {
376
377 case QAccessible::Button: {
378 element = document.call<emscripten::val>("createElement", std::string("button"));
379 addEventListener(iface, element, "click");
380 } break;
381
382 case QAccessible::Grouping:
383 case QAccessible::CheckBox: {
384 // Three cases:
385 // 1) role=CheckBox, childCount() == 0 -> Checkbox
386 // 2) role=CheckBox, childCount() > 0 -> GroupBox w/checkbox
387 // 3) role=Grouping -> GroupBox w/label
388
389 emscripten::val checkbox = emscripten::val::undefined();
390 if (iface->role() == QAccessible::CheckBox) {
391 checkbox = document.call<emscripten::val>("createElement", std::string("input"));
392 setAttribute(checkbox, "type", "checkbox");
393 setAttribute(checkbox, "checked", iface->state().checked);
394 setProperty(checkbox, "indeterminate", iface->state().checkStateMixed);
395 addEventListener(iface, checkbox, "change");
396 }
397
398 if (iface->childCount() > 0 || iface->role() == QAccessible::Grouping) {
399 auto label = document.call<emscripten::val>("createElement", std::string("span"));
400
401 const std::string id = QString::asprintf("lbid%p", iface).toStdString();
402 setAttribute(label, "id", id);
403
404 element = document.call<emscripten::val>("createElement", std::string("div"));
405 element.call<void>("appendChild", label);
406
407 setAttribute(element, "role", "group");
408 setAttribute(element, "aria-labelledby", id);
409
410 if (!checkbox.isUndefined()) {
411 element.call<void>("appendChild", checkbox);
412 addEventListener(iface, checkbox, "focus");
413 }
414 } else {
415 element = checkbox;
416 }
417 } break;
418
419 case QAccessible::Switch: {
420 element = document.call<emscripten::val>("createElement", std::string("button"));
421 setAttribute(element, "type", "button");
422 setAttribute(element, "role", "switch");
423 if (iface->state().checked)
424 setAttribute(element, "aria-checked", "true");
425 else
426 setAttribute(element, "aria-checked", "false");
427 addEventListener(iface, element, "click");
428 } break;
429
430 case QAccessible::RadioButton: {
431 element = document.call<emscripten::val>("createElement", std::string("input"));
432 setAttribute(element, "type", "radio");
433 setAttribute(element, "checked", iface->state().checked);
434 setProperty(element, "name", "buttonGroup");
435 addEventListener(iface, element, "change");
436 } break;
437
438 case QAccessible::Dial:
439 case QAccessible::SpinBox:
440 case QAccessible::Slider: {
441 element = document.call<emscripten::val>("createElement", std::string("input"));
442 setAttribute(element,"type", "number");
443 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
444 setAttribute(element, "min", valueInterface->minimumValue().toString().toStdString());
445 setAttribute(element, "max", valueInterface->maximumValue().toString().toStdString());
446 setAttribute(element, "step",
447 valueInterface->minimumStepSize().toString().toStdString());
448 setProperty(element, "value", valueInterface->currentValue().toString().toStdString());
449 }
450 addEventListener(iface, element, "change");
451 } break;
452
453 case QAccessible::PageTabList:{
454 element = document.call<emscripten::val>("createElement", std::string("div"));
455 setAttribute(element, "role", "tablist");
456 setHtmlElementOrientation(element, iface);
457
458 m_elements[iface] = element;
459
460 for (int i = 0; i < iface->childCount(); ++i)
461 createHtmlElement(iface->child(i));
462
463 } break;
464
465 case QAccessible::PageTab:{
466 const QString text = iface->text(QAccessible::Name);
467 element = document.call<emscripten::val>("createElement", std::string("button"));
468 setAttribute(element, "role", "tab");
469 setAttribute(element, "title", text.toStdString());
470 addEventListener(iface, element, "click");
471 } break;
472
473 case QAccessible::ScrollBar: {
474 // Events for scrollbars are handled by Qt, but setFocusPolicy must
475 // be called for this to happen.
476 element = document.call<emscripten::val>("createElement", std::string("div"));
477 setAttribute(element, "tabindex", "0");
478 setAttribute(element, "role", "scrollbar");
479 if (QAccessibleValueInterface *valueInterface = iface->valueInterface()) {
480 setAttribute(element, "aria-valuemin",
481 valueInterface->minimumValue().toString().toStdString());
482 setAttribute(element, "aria-valuemax",
483 valueInterface->maximumValue().toString().toStdString());
484 setAttribute(element, "aria-valuenow",
485 valueInterface->currentValue().toString().toStdString());
486 }
487 setHtmlElementOrientation(element, iface);
488 addEventListener(iface, element, "change");
489 } break;
490
491 case QAccessible::StaticText: {
492 element = document.call<emscripten::val>("createElement", std::string("div"));
493 } break;
494 case QAccessible::Dialog: {
495 element = document.call<emscripten::val>("createElement", std::string("dialog"));
496 }break;
497 case QAccessible::ToolBar:{
498 const QString text = iface->text(QAccessible::Name);
499 element = document.call<emscripten::val>("createElement", std::string("div"));
500 setAttribute(element, "role", "toolbar");
501 setAttribute(element, "title", text.toStdString());
502 setHtmlElementOrientation(element, iface);
503 addEventListener(iface, element, "click");
504 }break;
505 case QAccessible::MenuItem:
506 case QAccessible::ButtonDropDown:
507 case QAccessible::ButtonMenu: {
508 const QString text = iface->text(QAccessible::Name);
509 element = document.call<emscripten::val>("createElement", std::string("button"));
510 setAttribute(element, "role", "menuitem");
511 setAttribute(element, "title", text.toStdString());
512 addEventListener(iface, element, "click");
513 }break;
514 case QAccessible::MenuBar:
515 case QAccessible::PopupMenu: {
516 const QString text = iface->text(QAccessible::Name);
517 element = document.call<emscripten::val>("createElement", std::string("div"));
518 setAttribute(element, "role",
519 iface->role() == QAccessible::PopupMenu ? "menu" : "menubar");
520 setAttribute(element, "title", text.toStdString());
521 setHtmlElementOrientation(element, iface);
522 m_elements[iface] = element;
523
524 // The items are nested in the menu element, which already expresses
525 // ownership. aria-owns takes element ids, not the menu title.
526 for (int i = 0; i < iface->childCount(); ++i)
527 createHtmlElement(iface->child(i));
528 }break;
529 case QAccessible::EditableText: {
530 element = document.call<emscripten::val>("createElement", std::string("input"));
531 setAttribute(element, "type", "text");
532 setAttribute(element, "contenteditable", "true");
533 setAttribute(element, "readonly", iface->state().readOnly);
534 setAttribute(element, "autocorrect", "off");
535 setProperty(element, "inputMode", "text");
536 } break;
537 case QAccessible::List: {
538 element = document.call<emscripten::val>("createElement", std::string("div"));
539 setAttribute(element, "tabindex", "0");
540 setAttribute(element, "role", "list");
541 } break;
542 case QAccessible::ListItem: {
543 element = document.call<emscripten::val>("createElement", std::string("div"));
544 setAttribute(element, "tabindex", "0");
545 setAttribute(element, "role", "listitem");
546 } break;
547 case QAccessible::ProgressBar: {
548 element = document.call<emscripten::val>("createElement", std::string("div"));
549 setAttribute(element, "tabindex", "0");
550 setAttribute(element, "role", "progressbar");
551 setAttribute(element, "aria-live", "polite");
552 if (auto valueInterface = iface->valueInterface()) {
553 const auto name = iface->text(QAccessible::Name).toStdString();
554 const auto min = valueInterface->minimumValue().toInt();
555 const auto max = valueInterface->maximumValue().toInt();
556 const auto cur = valueInterface->currentValue().toInt();
557
558 // from qprogressbar.cpp
559 const auto totalSteps = qint64(max) - min;
560 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
561
562 if (cur <= min)
563 setAttribute(element, "aria-valuenow", "0");
564 else if (cur >= max)
565 setAttribute(element, "aria-valuenow", "100");
566 else
567 setAttribute(element, "aria-valuenow", std::to_string(progress));
568 setAttribute(element, "aria-valuemin", "0");
569 setAttribute(element, "aria-valuemax", "100");
570 setAttribute(element, "aria-label", name);
571 }
572 } break;
573 default:
574 qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role();
575 element = document.call<emscripten::val>("createElement", std::string("div"));
576 }
577
578 addEventListener(iface, element, "focus");
579 return element;
580
581 }();
582
583 m_elements[iface] = element;
584
585 setHtmlElementGeometry(iface);
586 setHtmlElementDisabled(iface);
587 setHtmlElementVisibility(iface, !iface->state().invisible);
588 handleIdentifierUpdate(iface);
589 handleDescriptionChanged(iface);
590 sendEvent(iface, QAccessible::NameChanged);
591
592 linkToParent(iface);
593 // Link in child elements
594 for (int i = 0; i < iface->childCount(); ++i) {
595 if (!getHtmlElement(iface->child(i)).isUndefined())
596 linkToParent(iface->child(i));
597 }
598
599 return element;
600}
601
602void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface)
603{
604 Q_UNUSED(iface);
605 qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement";
606}
607
608emscripten::val QWasmAccessibility::getHtmlElement(QAccessibleInterface *iface)
609{
610 auto it = m_elements.find(iface);
611 if (it != m_elements.end())
612 return it.value();
613
614 return emscripten::val::undefined();
615}
616
617void QWasmAccessibility::repairLinks(QAccessibleInterface *iface)
618{
619 // relink any children that are linked to the wrong parent,
620 // This can be caused by a missing ParentChanged event.
621 bool moved = false;
622 for (int i = 0; i < iface->childCount(); ++i) {
623 const auto elementI = getHtmlElement(iface->child(i));
624 const auto containerI = getElementContainer(iface->child(i));
625
626 if (!elementI.isUndefined() &&
627 !containerI.isUndefined() &&
628 !elementI["parentElement"].isUndefined() &&
629 !elementI["parentElement"].isNull() &&
630 elementI["parentElement"] != containerI) {
631 moved = true;
632 break;
633 }
634 }
635 if (moved) {
636 for (int i = 0; i < iface->childCount(); ++i) {
637 const auto elementI = getHtmlElement(iface->child(i));
638 const auto containerI = getElementContainer(iface->child(i));
639 if (!elementI.isUndefined() && !containerI.isUndefined())
640 containerI.call<void>("appendChild", elementI);
641 }
642 }
643}
644
645void QWasmAccessibility::linkToParent(QAccessibleInterface *iface)
646{
647 emscripten::val element = getHtmlElement(iface);
648 emscripten::val container = getElementContainer(iface);
649
650 if (container.isUndefined() || element.isUndefined())
651 return;
652
653 // Make sure that we don't change the focused element
654 const auto activeElementBefore = emscripten::val::take_ownership(
655 getActiveElement_js(emscripten::val::undefined().as_handle()));
656
657
658 repairLinks(iface->parent());
659
660 emscripten::val next = emscripten::val::undefined();
661 const int thisIndex = iface->parent()->indexOfChild(iface);
662 if (thisIndex >= 0) {
663 Q_ASSERT(thisIndex < iface->parent()->childCount());
664 for (int i = thisIndex + 1; i < iface->parent()->childCount(); ++i) {
665 const auto elementI = getHtmlElement(iface->parent()->child(i));
666 if (!elementI.isUndefined() &&
667 elementI["parentElement"] == container) {
668 next = elementI;
669 break;
670 }
671 }
672 if (next.isUndefined())
673 container.call<void>("appendChild", element);
674 else
675 container.call<void>("insertBefore", element, next);
676 }
677 const auto activeElementAfter = emscripten::val::take_ownership(
678 getActiveElement_js(emscripten::val::undefined().as_handle()));
679 if (activeElementBefore != activeElementAfter) {
680 if (!activeElementBefore.isUndefined() && !activeElementBefore.isNull())
681 activeElementBefore.call<void>("focus");
682 }
683}
684
685void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible)
686{
687 emscripten::val element = getHtmlElement(iface);
688 setAttribute(element, "inert", !visible);
689}
690
691void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface)
692{
693 const emscripten::val element = getHtmlElement(iface);
694
695 QRect windowGeometry = iface->rect();
696 if (iface->parent()) {
697 // Both iface and iface->parent returns geometry in screen coordinates
698 // We only want the relative coordinates, so the coordinate system does
699 // not matter as long as it is the same.
700 const QRect parentRect = iface->parent()->rect();
701 const QRect thisRect = iface->rect();
702 const QRect result(thisRect.topLeft() - parentRect.topLeft(), thisRect.size());
703 windowGeometry = result;
704 } else {
705 // Elements without a parent are not a part of the a11y tree, and don't
706 // have meaningful geometry.
707 Q_ASSERT(!getWindow(iface));
708 }
709 setHtmlElementGeometry(element, windowGeometry);
710}
711
712void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry)
713{
714 // Position the element using "position: absolute" in order to place
715 // it under the corresponding Qt element in the screen.
716 emscripten::val style = element["style"];
717 style.set("position", std::string("absolute"));
718 style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the
719 // screen element, but isn't
720 style.set("left", std::to_string(geometry.x()) + "px");
721 style.set("top", std::to_string(geometry.y()) + "px");
722 style.set("width", std::to_string(geometry.width()) + "px");
723 style.set("height", std::to_string(geometry.height()) + "px");
724}
725
726void QWasmAccessibility::setHtmlElementFocus(QAccessibleInterface *iface)
727{
728 const auto element = getHtmlElement(iface);
729 element.call<void>("focus");
730}
731
732void QWasmAccessibility::setHtmlElementDisabled(QAccessibleInterface *iface)
733{
734 auto element = getHtmlElement(iface);
735 setAttribute(element, "aria-disabled", iface->state().disabled);
736}
737
738void QWasmAccessibility::setHtmlElementOrientation(emscripten::val element, QAccessibleInterface *iface)
739{
740 Q_ASSERT(iface);
741 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
742 const QVariant orientationVariant =
743 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
744 if (orientationVariant.isValid()) {
745 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
746 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
747 const std::string value = orientation == Qt::Horizontal ? "horizontal" : "vertical";
748 setAttribute(element, "aria-orientation", value);
749 }
750 }
751}
752
753void QWasmAccessibility::handleListItemUpdate(QAccessibleEvent *event)
754{
755 auto iface = event->accessibleInterface();
756
757 switch (event->type()) {
758 case QAccessible::NameChanged: {
759 // ListItem is a div
760 setNamedProperty(iface, "innerText", QAccessible::Name);
761 } break;
762 default:
763 qCDebug(lcQpaAccessibility) << "TODO: implement handleListItemUpdate for event" << event->type();
764 break;
765 }
766}
767
768void QWasmAccessibility::handleDialUpdate(QAccessibleEvent *event)
769{
770 switch (event->type()) {
771 case QAccessible::ObjectCreated:
772 case QAccessible::StateChanged: {
773 } break;
774 case QAccessible::Focus:
775 case QAccessible::NameChanged: {
776 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
777 } break;
778 case QAccessible::ValueChanged: {
779 QAccessibleInterface *accessible = event->accessibleInterface();
780 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
781 setProperty(getHtmlElement(accessible), "value",
782 valueInterface->currentValue().toString().toStdString());
783 }
784 } break;
785 default:
786 qCDebug(lcQpaAccessibility) << "TODO: implement handleDialUpdate for event" << event->type();
787 break;
788 }
789}
790
791void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event)
792{
793 switch (event->type()) {
794 case QAccessible::NameChanged: {
795 // StaticText is a div
796 setNamedProperty(event->accessibleInterface(), "innerText", QAccessible::Name);
797 } break;
798 default:
799 qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type();
800 break;
801 }
802}
803
804void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event)
805{
806 switch (event->type()) {
807 case QAccessible::StateChanged: {
808 auto iface = event->accessibleInterface();
809 auto element = getHtmlElement(iface);
810 setAttribute(element, "readonly", iface->state().readOnly);
811 if (iface->state().passwordEdit)
812 setProperty(element, "type", "password");
813 else
814 setProperty(element, "type", "text");
815 } break;
816 case QAccessible::ValueChanged:
817 case QAccessible::NameChanged: {
818 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
819 } break;
820 case QAccessible::ObjectShow:
821 case QAccessible::Focus: {
822 auto iface = event->accessibleInterface();
823 auto element = getHtmlElement(iface);
824 if (!element.isUndefined()) {
825 setAttribute(element, "readonly", iface->state().readOnly);
826 if (iface->state().passwordEdit)
827 setProperty(element, "type", "password");
828 else
829 setProperty(element, "type", "text");
830 }
831 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
832 } break;
833 case QAccessible::TextRemoved:
834 case QAccessible::TextInserted: {
835 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
836 } break;
837 case QAccessible::TextCaretMoved: {
838 // We lack ValueChanged event, sync on caret move instead
839 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
840 } break;
841 default:
842 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
843 break;
844 }
845}
846
847void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event)
848{
849 if (event["target"].isNull() || event["target"].isUndefined())
850 return;
851
852 if (event["target"]["data-qta11yinterface"].isNull() || event["target"]["data-qta11yinterface"].isUndefined())
853 return;
854
855 auto iface = reinterpret_cast<QAccessibleInterface *>(event["target"]["data-qta11yinterface"].as<size_t>());
856 if (m_elements.find(iface) == m_elements.end())
857 return;
858
859 const auto eventType = QString::fromStdString(event["type"].as<std::string>());
860 const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
861 bool handled = false;
862
863 if (eventType == "blur") {
864 ;
865 } else if (eventType == "keydown") {
866 ;
867 } else if (eventType == "keyup") {
868 ;
869 } else if (eventType == "focus") {
870 if (actionNames.contains(QAccessibleActionInterface::setFocusAction()))
871 iface->actionInterface()->doAction(QAccessibleActionInterface::setFocusAction());
872 } else if (eventType == "click") {
873 handled = true;
874 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
875 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
876 else if (actionNames.contains(QAccessibleActionInterface::showMenuAction()))
877 iface->actionInterface()->doAction(QAccessibleActionInterface::showMenuAction());
878 } else if (eventType == "change") {
879 handled = true;
880 // Checkboxes can have both toggle and press, in this case it is
881 // important to invoke press to handle tristate correctly
882 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
883 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
884 else if (actionNames.contains(QAccessibleActionInterface::toggleAction()))
885 iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction());
886 } else {
887 qCDebug(lcQpaAccessibility) << "Unknown event" << eventType;
888 }
889
890 if (handled) {
891 event.call<void>("preventDefault");
892 event.call<void>("stopPropagation");
893 }
894}
895
896void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event)
897{
898 switch (event->type()) {
899 case QAccessible::Focus:
900 case QAccessible::NameChanged: {
901 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
902 } break;
903 default:
904 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
905 break;
906 }
907}
908
909void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event)
910{
911 switch (event->type()) {
912 case QAccessible::Focus:
913 case QAccessible::NameChanged: {
914 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
915 } break;
916 case QAccessible::StateChanged: {
917 QAccessibleInterface *accessible = event->accessibleInterface();
918 const emscripten::val element = getHtmlElement(accessible);
919 setAttribute(element, "checked", accessible->state().checked);
920 setProperty(element, "indeterminate", accessible->state().checkStateMixed);
921 } break;
922 default:
923 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
924 break;
925 }
926}
927
928void QWasmAccessibility::handleGroupBoxUpdate(QAccessibleEvent *event)
929{
930 QAccessibleInterface *iface = event->accessibleInterface();
931
932 emscripten::val parent = getHtmlElement(iface);
933 emscripten::val label = parent["children"][0];
934 emscripten::val checkbox = emscripten::val::undefined();
935 if (iface->role() == QAccessible::CheckBox)
936 checkbox = parent["children"][1];
937
938 switch (event->type()) {
939 case QAccessible::Focus:
940 case QAccessible::NameChanged: {
941 setProperty(label, "innerText", iface->text(QAccessible::Name).toStdString());
942 if (!checkbox.isUndefined())
943 setAttribute(checkbox, "aria-label", iface->text(QAccessible::Name).toStdString());
944 } break;
945 case QAccessible::StateChanged: {
946 if (!checkbox.isUndefined()) {
947 setAttribute(checkbox, "checked", iface->state().checked);
948 setProperty(checkbox, "indeterminate", iface->state().checkStateMixed);
949 }
950 } break;
951 default:
952 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
953 break;
954 }
955}
956
957void QWasmAccessibility::handleSwitchUpdate(QAccessibleEvent *event)
958{
959 switch (event->type()) {
960 case QAccessible::Focus:
961 case QAccessible::NameChanged: {
962 /* A switch is like a button in this regard */
963 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
964 } break;
965 case QAccessible::StateChanged: {
966 QAccessibleInterface *accessible = event->accessibleInterface();
967 const emscripten::val element = getHtmlElement(accessible);
968 if (accessible->state().checked)
969 setAttribute(element, "aria-checked", "true");
970 else
971 setAttribute(element, "aria-checked", "false");
972 } break;
973 default:
974 qCDebug(lcQpaAccessibility) << "TODO: implement handleSwitchUpdate for event" << event->type();
975 break;
976 }
977}
978
979void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event)
980{
981 QAccessibleInterface *iface = event->accessibleInterface();
982 QString text = iface->text(QAccessible::Name);
983 QString desc = iface->text(QAccessible::Description);
984 switch (event->type()) {
985 case QAccessible::NameChanged:
986 case QAccessible::StateChanged:{
987 const emscripten::val element = getHtmlElement(iface);
988 setAttribute(element, "title", text.toStdString());
989 } break;
990 default:
991 qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type();
992 break;
993 }
994}
995void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event)
996{
997 QAccessibleInterface *iface = event->accessibleInterface();
998 QString text = iface->text(QAccessible::Name);
999 QString desc = iface->text(QAccessible::Description);
1000 switch (event->type()) {
1001 case QAccessible::Focus:
1002 case QAccessible::NameChanged:
1003 case QAccessible::MenuStart ://"TODO: To implement later
1004 case QAccessible::StateChanged:{
1005 const emscripten::val element = getHtmlElement(iface);
1006 setAttribute(element, "title", text.toStdString());
1007 } break;
1008 case QAccessible::PopupMenuStart: {
1009 if (iface->childCount() > 0) {
1010 const auto childElement = getHtmlElement(iface->child(0));
1011 childElement.call<void>("focus");
1012 }
1013 } break;
1014 default:
1015 qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type();
1016 break;
1017 }
1018}
1019void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) {
1020
1021 switch (event->type()) {
1022 case QAccessible::NameChanged:
1023 case QAccessible::Focus:
1024 case QAccessible::DialogStart:
1025 case QAccessible::StateChanged: {
1026 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1027 } break;
1028 default:
1029 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
1030 break;
1031 }
1032}
1033
1034void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface)
1035{
1036 if (!iface)
1037 return;
1038
1039 // We ignore toplevel windows which is categorized
1040 // by getWindow(iface->parent()) != getWindow(iface)
1041 const QWindow *window1 = getWindow(iface);
1042 const QWindow *window0 = (iface->parent()) ? getWindow(iface->parent()) : nullptr;
1043
1044 if (window1 && window0 == window1) {
1045 // Create html element for the interface, sync up properties.
1046 bool exists = !getHtmlElement(iface).isUndefined();
1047 if (!exists)
1048 exists = !createHtmlElement(iface).isUndefined();
1049
1050 if (exists) {
1051 linkToParent(iface);
1052 setHtmlElementVisibility(iface, !iface->state().invisible);
1053 setHtmlElementGeometry(iface);
1054 setHtmlElementDisabled(iface);
1055 handleIdentifierUpdate(iface);
1056 handleDescriptionChanged(iface);
1057 sendEvent(iface, QAccessible::NameChanged);
1058 }
1059 }
1060 for (int i = 0; i < iface->childCount(); ++i)
1061 populateAccessibilityTree(iface->child(i));
1062}
1063
1064void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event)
1065{
1066 switch (event->type()) {
1067 case QAccessible::Focus:
1068 case QAccessible::NameChanged: {
1069 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1070 } break;
1071 case QAccessible::StateChanged: {
1072 QAccessibleInterface *accessible = event->accessibleInterface();
1073 const emscripten::val element = getHtmlElement(accessible);
1074 setAttribute(element, "checked", accessible->state().checked);
1075 } break;
1076 default:
1077 qCDebug(lcQpaAccessibility) << "TODO: implement handleRadioButtonUpdate for event" << event->type();
1078 break;
1079 }
1080}
1081
1082void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event)
1083{
1084 switch (event->type()) {
1085 case QAccessible::ObjectCreated:
1086 case QAccessible::StateChanged: {
1087 } break;
1088 case QAccessible::Focus:
1089 case QAccessible::NameChanged: {
1090 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1091 } break;
1092 case QAccessible::ValueChanged: {
1093 QAccessibleInterface *accessible = event->accessibleInterface();
1094 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1095 setProperty(getHtmlElement(accessible), "value",
1096 valueInterface->currentValue().toString().toStdString());
1097 }
1098 } break;
1099 default:
1100 qCDebug(lcQpaAccessibility) << "TODO: implement handleSpinBoxUpdate for event" << event->type();
1101 break;
1102 }
1103}
1104
1105void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event)
1106{
1107 switch (event->type()) {
1108 case QAccessible::ObjectCreated:
1109 case QAccessible::StateChanged: {
1110 } break;
1111 case QAccessible::Focus:
1112 case QAccessible::NameChanged: {
1113 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1114 } break;
1115 case QAccessible::ValueChanged: {
1116 QAccessibleInterface *accessible = event->accessibleInterface();
1117 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1118 setProperty(getHtmlElement(accessible), "value",
1119 valueInterface->currentValue().toString().toStdString());
1120 }
1121 } break;
1122 default:
1123 qCDebug(lcQpaAccessibility) << "TODO: implement handleSliderUpdate for event" << event->type();
1124 break;
1125 }
1126}
1127
1128void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event)
1129{
1130 switch (event->type()) {
1131 case QAccessible::Focus:
1132 case QAccessible::NameChanged: {
1133 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1134 } break;
1135 case QAccessible::ValueChanged: {
1136 QAccessibleInterface *accessible = event->accessibleInterface();
1137 if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) {
1138 setAttribute(getHtmlElement(accessible), "aria-valuenow",
1139 valueInterface->currentValue().toString().toStdString());
1140 }
1141 } break;
1142 default:
1143 qCDebug(lcQpaAccessibility) << "TODO: implement handleScrollBarUpdate for event" << event->type();
1144 break;
1145 }
1146}
1147
1148void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event)
1149{
1150 switch (event->type()) {
1151 case QAccessible::NameChanged: {
1152 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1153 } break;
1154 case QAccessible::Focus: {
1155 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1156 } break;
1157 default:
1158 qCDebug(lcQpaAccessibility) << "TODO: implement handlePageTabUpdate for event" << event->type();
1159 break;
1160 }
1161}
1162
1163void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event)
1164{
1165 switch (event->type()) {
1166 case QAccessible::NameChanged: {
1167 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1168 } break;
1169 case QAccessible::Focus: {
1170 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1171 } break;
1172 default:
1173 qCDebug(lcQpaAccessibility) << "TODO: implement handlePageTabListUpdate for event" << event->type();
1174 break;
1175 }
1176}
1177
1178void QWasmAccessibility::handleIdentifierUpdate(QAccessibleInterface *iface)
1179{
1180 // The tabs are nested in the tab list element, so they need no aria-owns
1181 // pointing back at it, which would also be the wrong direction.
1182 const emscripten::val element = getHtmlElement(iface);
1183 const QString id = iface->text(QAccessible::Identifier).replace(" ", "_");
1184 setAttribute(element, "id", id.toStdString());
1185}
1186
1187void QWasmAccessibility::handleDescriptionChanged(QAccessibleInterface *iface)
1188{
1189 const auto desc = iface->text(QAccessible::Description).toStdString();
1190 auto element = getHtmlElement(iface);
1191 auto container = getDescribedByContainer(iface);
1192 if (!container.isUndefined()) {
1193 std::ostringstream oss;
1194 oss << "dbid_" << (void *)iface;
1195 auto id = oss.str();
1196
1197 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
1198 if (desc.empty()) {
1199 if (!describedBy.isUndefined() && !describedBy.isNull()) {
1200 container.call<void>("removeChild", describedBy);
1201 }
1202 setAttribute(element, "aria-describedby", "");
1203 } else {
1204 if (describedBy.isUndefined() || describedBy.isNull()) {
1205 auto document = getDocument(container);
1206 describedBy = document.call<emscripten::val>("createElement", std::string("p"));
1207
1208 container.call<void>("appendChild", describedBy);
1209 }
1210 setAttribute(describedBy, "id", id);
1211 setAttribute(describedBy, "inert", true);
1212 setAttribute(element, "aria-describedby", id);
1213 setProperty(describedBy, "innerText", desc);
1214 }
1215 }
1216}
1217
1218void QWasmAccessibility::handleAnnouncement(QAccessibleInterface *iface,
1219 QAccessibleAnnouncementEvent *announcementEvent)
1220{
1221 emscripten::val element = getHtmlElement(iface);
1222 if (element["ariaNotify"].isUndefined())
1223 return;
1224
1225 const std::string message = announcementEvent->message().toStdString();
1226 const std::string prio(announcementEvent->politeness()
1227 == QAccessible::AnnouncementPoliteness::Assertive
1228 ? "high"
1229 : "normal");
1230 emscripten::val options = emscripten::val::object();
1231 options.set("priority", prio);
1232 element.call<void>("ariaNotify", message, options);
1233}
1234
1235void QWasmAccessibility::handleProgressBarUpdate(QAccessibleEvent *event)
1236{
1237 auto iface = event->accessibleInterface();
1238 auto valueInterface = iface->valueInterface();
1239 auto element = getHtmlElement(iface);
1240
1241 switch (event->type()) {
1242 case QAccessible::Focus:
1243 case QAccessible::NameChanged: {
1244 const auto name = iface->text(QAccessible::Name).toStdString();
1245 setAttribute(element, "aria-label", name);
1246 } // fallthrough
1247 case QAccessible::ValueChanged: {
1248 if (valueInterface) {
1249 const auto min = valueInterface->minimumValue().toInt();
1250 const auto max = valueInterface->maximumValue().toInt();
1251 const auto cur = valueInterface->currentValue().toInt();
1252
1253 // from qprogressbar.cpp
1254 const auto totalSteps = qint64(max) - min;
1255 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
1256
1257 if (cur <= min)
1258 setAttribute(element, "aria-valuenow", "0");
1259 else if (cur >= max)
1260 setAttribute(element, "aria-valuenow", "100");
1261 else
1262 setAttribute(element, "aria-valuenow", std::to_string(progress));
1263 }
1264 } break;
1265 default: {
1266 } break;
1267 } /* end-switch */
1268}
1269
1270void QWasmAccessibility::createObject(QAccessibleInterface *iface)
1271{
1272 if (getHtmlElement(iface).isUndefined())
1273 createHtmlElement(iface);
1274}
1275
1276void QWasmAccessibility::removeObject(QAccessibleInterface *iface)
1277{
1278 // Do not dereference the object pointer. it might be invalid.
1279 // Do not dereference the iface either, it refers to the object.
1280 // Note: we may remove children, making them have parentElement undefined
1281 // so we need to check for parentElement here. We do assume that removeObject
1282 // is called on all objects, just not in any predefined order.
1283 const auto it = m_elements.find(iface);
1284 if (it != m_elements.end()) {
1285 auto element = it.value();
1286 auto container = getDescribedByContainer(iface);
1287 if (!container.isUndefined()) {
1288 std::ostringstream oss;
1289 oss << "dbid_" << (void *)iface;
1290 auto id = oss.str();
1291 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
1292 if (!describedBy.isUndefined() && !describedBy.isNull() &&
1293 !describedBy["parentElement"].isUndefined() && !describedBy["parentElement"].isNull())
1294 describedBy["parentElement"].call<void>("removeChild", describedBy);
1295 }
1296 if (!element["parentElement"].isUndefined() && !element["parentElement"].isNull())
1297 element["parentElement"].call<void>("removeChild", element);
1298 m_elements.erase(it);
1299 }
1300}
1301
1302void QWasmAccessibility::unlinkParentForChildren(QAccessibleInterface *iface)
1303{
1304 auto element = getHtmlElement(iface);
1305 if (!element.isUndefined()) {
1306 auto oldContainer = element["parentElement"];
1307 auto newContainer = getElementContainer(iface);
1308 if (!oldContainer.isUndefined() &&
1309 !oldContainer.isNull() &&
1310 oldContainer != newContainer) {
1311 oldContainer.call<void>("removeChild", element);
1312 }
1313 }
1314 for (int i = 0; i < iface->childCount(); ++i)
1315 unlinkParentForChildren(iface->child(i));
1316}
1317
1318void QWasmAccessibility::relinkParentForChildren(QAccessibleInterface *iface)
1319{
1320 auto element = getHtmlElement(iface);
1321 if (!element.isUndefined()) {
1322 if (element["parentElement"].isUndefined() ||
1323 element["parentElement"].isNull()) {
1324 linkToParent(iface);
1325 }
1326 }
1327 for (int i = 0; i < iface->childCount(); ++i)
1328 relinkParentForChildren(iface->child(i));
1329}
1330
1331void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event)
1332{
1333 if (handleUpdateByEventType(event))
1334 handleUpdateByInterfaceRole(event);
1335}
1336
1337bool QWasmAccessibility::handleUpdateByEventType(QAccessibleEvent *event)
1338{
1339 if (!m_accessibilityEnabled)
1340 return false;
1341
1342 QAccessibleInterface *iface = event->accessibleInterface();
1343 if (!iface) {
1344 qWarning() << "handleUpdateByEventType with null a11y interface" << event->type() << event->object();
1345 return false;
1346 }
1347
1348 // Handle event types that creates/removes objects.
1349 switch (event->type()) {
1350 case QAccessible::ObjectCreated:
1351 // Do nothing, there are too many changes to the interface
1352 // before ObjectShow is called
1353 return false;
1354
1355 case QAccessible::ObjectDestroyed:
1356 // The object might be under destruction, and the interface is not valid
1357 // but we can look at the pointer,
1358 removeObject(iface);
1359 return false;
1360
1361 case QAccessible::ObjectShow: // We do not get ObjectCreated from widgets, we get ObjectShow
1362 createObject(iface);
1363 break;
1364
1365 case QAccessible::ParentChanged:
1366 unlinkParentForChildren(iface);
1367 relinkParentForChildren(iface);
1368 break;
1369
1370 default:
1371 break;
1372 };
1373
1374 if (getHtmlElement(iface).isUndefined())
1375 return false;
1376
1377 // Handle some common event types. See
1378 // https://doc.qt.io/qt-6/qaccessible.html#Event-enum
1379 switch (event->type()) {
1380 case QAccessible::Announcement:
1381 handleAnnouncement(iface, static_cast<QAccessibleAnnouncementEvent *>(event));
1382 return false;
1383
1384 case QAccessible::StateChanged: {
1385 QAccessibleStateChangeEvent *stateChangeEvent = (QAccessibleStateChangeEvent *)event;
1386 if (stateChangeEvent->changedStates().disabled)
1387 setHtmlElementDisabled(iface);
1388 } break;
1389
1390 case QAccessible::DescriptionChanged:
1391 handleDescriptionChanged(iface);
1392 return false;
1393
1394 case QAccessible::Focus:
1395 // We do not get all callbacks for the geometry
1396 // hence we update here as well.
1397 setHtmlElementGeometry(iface);
1398 setHtmlElementFocus(iface);
1399 break;
1400
1401 case QAccessible::IdentifierChanged:
1402 handleIdentifierUpdate(iface);
1403 return false;
1404
1405 case QAccessible::ObjectShow:
1406 linkToParent(iface);
1407 setHtmlElementVisibility(iface, true);
1408
1409 // Sync up properties on show;
1410 setHtmlElementGeometry(iface);
1411 sendEvent(iface, QAccessible::NameChanged);
1412 break;
1413
1414 case QAccessible::ObjectHide:
1415 // Do not call linkToParent, only set visibility to false
1416 setHtmlElementVisibility(iface, false);
1417 return false;
1418
1419 case QAccessible::LocationChanged:
1420 setHtmlElementGeometry(iface);
1421 return false;
1422
1423 // TODO: maybe handle more types here
1424 default:
1425 break;
1426 };
1427
1428 return true;
1429}
1430
1431void QWasmAccessibility::handleUpdateByInterfaceRole(QAccessibleEvent *event)
1432{
1433 if (!m_accessibilityEnabled)
1434 return;
1435
1436 QAccessibleInterface *iface = event->accessibleInterface();
1437 if (!iface) {
1438 qWarning() << "handleUpdateByInterfaceRole with null a11y interface" << event->type() << event->object();
1439 return;
1440 }
1441
1442 // Switch on interface role, see
1443 // https://doc.qt.io/qt-5/qaccessibleinterface.html#role
1444 switch (iface->role()) {
1445 case QAccessible::List:
1446 break;
1447 case QAccessible::ListItem:
1448 handleListItemUpdate(event);
1449 break;
1450 case QAccessible::StaticText:
1451 handleStaticTextUpdate(event);
1452 break;
1453 case QAccessible::Button:
1454 handleButtonUpdate(event);
1455 break;
1456 case QAccessible::CheckBox:
1457 if (iface->childCount() > 0)
1458 handleGroupBoxUpdate(event);
1459 else
1460 handleCheckBoxUpdate(event);
1461 break;
1462 case QAccessible::Switch:
1463 handleSwitchUpdate(event);
1464 break;
1465 case QAccessible::EditableText:
1466 handleLineEditUpdate(event);
1467 break;
1468 case QAccessible::Dialog:
1469 handleDialogUpdate(event);
1470 break;
1471 case QAccessible::MenuItem:
1472 case QAccessible::MenuBar:
1473 case QAccessible::PopupMenu:
1474 handleMenuUpdate(event);
1475 break;
1476 case QAccessible::ToolBar:
1477 case QAccessible::ButtonMenu:
1478 handleToolUpdate(event);
1479 break;
1480 case QAccessible::RadioButton:
1481 handleRadioButtonUpdate(event);
1482 break;
1483 case QAccessible::Dial:
1484 handleDialUpdate(event);
1485 break;
1486 case QAccessible::SpinBox:
1487 handleSpinBoxUpdate(event);
1488 break;
1489 case QAccessible::Slider:
1490 handleSliderUpdate(event);
1491 break;
1492 case QAccessible::PageTab:
1493 handlePageTabUpdate(event);
1494 break;
1495 case QAccessible::PageTabList:
1496 handlePageTabListUpdate(event);
1497 break;
1498 case QAccessible::ScrollBar:
1499 handleScrollBarUpdate(event);
1500 break;
1501 case QAccessible::Grouping:
1502 handleGroupBoxUpdate(event);
1503 break;
1504 case QAccessible::ProgressBar:
1505 handleProgressBarUpdate(event);
1506 break;
1507 default:
1508 qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role();
1509 };
1510}
1511
1512void QWasmAccessibility::setRootObject(QObject *root)
1513{
1514 m_rootObject = root;
1515}
1516
1517void QWasmAccessibility::initialize()
1518{
1519
1520}
1521
1522void QWasmAccessibility::cleanup()
1523{
1524
1525}
1526
1527#endif // QT_CONFIG(accessibility)
void QWasmAccessibilityEnable()