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 {
143 auto a11yContainer = getA11yContainer(window);
144 auto describedByContainer =
145 getDescribedByContainer(window);
146 auto elementContainer = getElementContainer(window);
147 auto document = getDocument(a11yContainer);
148
149 // Remove all items by replacing the container
150 if (!describedByContainer.isUndefined()) {
151 a11yContainer.call<void>("removeChild", describedByContainer);
152 describedByContainer = document.call<emscripten::val>("createElement", std::string("div"));
153
154 a11yContainer.call<void>("appendChild", elementContainer);
155 a11yContainer.call<void>("appendChild", describedByContainer);
156 }
157 }
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 const auto minValue = iface->valueInterface()->minimumValue().toString().toStdString();
442 const auto maxValue = iface->valueInterface()->maximumValue().toString().toStdString();
443 const auto stepValue =
444 iface->valueInterface()->minimumStepSize().toString().toStdString();
445 const auto value = iface->valueInterface()->currentValue().toString().toStdString();
446 element = document.call<emscripten::val>("createElement", std::string("input"));
447 setAttribute(element,"type", "number");
448 setAttribute(element, "min", minValue);
449 setAttribute(element, "max", maxValue);
450 setAttribute(element, "step", stepValue);
451 setProperty(element, "value", value);
452 addEventListener(iface, element, "change");
453 } break;
454
455 case QAccessible::PageTabList:{
456 element = document.call<emscripten::val>("createElement", std::string("div"));
457 setAttribute(element, "role", "tablist");
458 setHtmlElementOrientation(element, iface);
459
460 m_elements[iface] = element;
461
462 for (int i = 0; i < iface->childCount(); ++i)
463 createHtmlElement(iface->child(i));
464
465 } break;
466
467 case QAccessible::PageTab:{
468 const QString text = iface->text(QAccessible::Name);
469 element = document.call<emscripten::val>("createElement", std::string("button"));
470 setAttribute(element, "role", "tab");
471 setAttribute(element, "title", text.toStdString());
472 addEventListener(iface, element, "click");
473 } break;
474
475 case QAccessible::ScrollBar: {
476 // Events for scrollbars are handled by Qt, but setFocusPolicy must
477 // be called for this to happen.
478 const auto minValue = iface->valueInterface()->minimumValue().toString().toStdString();
479 const auto maxValue = iface->valueInterface()->maximumValue().toString().toStdString();
480 const auto value = iface->valueInterface()->currentValue().toString().toStdString();
481 element = document.call<emscripten::val>("createElement", std::string("div"));
482 setAttribute(element, "tabindex", "0");
483 setAttribute(element, "role", "scrollbar");
484 setAttribute(element, "aria-valuemin", minValue);
485 setAttribute(element, "aria-valuemax", maxValue);
486 setAttribute(element, "aria-valuenow", value);
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", "menubar");
519 setAttribute(element, "title", text.toStdString());
520 setHtmlElementOrientation(element, iface);
521 m_elements[iface] = element;
522
523 for (int i = 0; i < iface->childCount(); ++i) {
524 emscripten::val childElement = createHtmlElement(iface->child(i));
525 setAttribute(childElement, "aria-owns", text.toStdString());
526 }
527 }break;
528 case QAccessible::EditableText: {
529 element = document.call<emscripten::val>("createElement", std::string("input"));
530 setAttribute(element, "type", "text");
531 setAttribute(element, "contenteditable", "true");
532 setAttribute(element, "readonly", iface->state().readOnly);
533 setAttribute(element, "autocorrect", "off");
534 setProperty(element, "inputMode", "text");
535 } break;
536 case QAccessible::List: {
537 element = document.call<emscripten::val>("createElement", std::string("div"));
538 setAttribute(element, "tabindex", "0");
539 setAttribute(element, "role", "list");
540 } break;
541 case QAccessible::ListItem: {
542 element = document.call<emscripten::val>("createElement", std::string("div"));
543 setAttribute(element, "tabindex", "0");
544 setAttribute(element, "role", "listitem");
545 } break;
546 case QAccessible::ProgressBar: {
547 element = document.call<emscripten::val>("createElement", std::string("div"));
548 setAttribute(element, "tabindex", "0");
549 setAttribute(element, "role", "progressbar");
550 setAttribute(element, "aria-live", "polite");
551 if (auto valueInterface = iface->valueInterface()) {
552 const auto name = iface->text(QAccessible::Name).toStdString();
553 const auto min = valueInterface->minimumValue().toInt();
554 const auto max = valueInterface->maximumValue().toInt();
555 const auto cur = valueInterface->currentValue().toInt();
556
557 // from qprogressbar.cpp
558 const auto totalSteps = qint64(max) - min;
559 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
560
561 if (cur <= min)
562 setAttribute(element, "aria-valuenow", "0");
563 else if (cur >= max)
564 setAttribute(element, "aria-valuenow", "100");
565 else
566 setAttribute(element, "aria-valuenow", std::to_string(progress));
567 setAttribute(element, "aria-valuemin", "0");
568 setAttribute(element, "aria-valuemax", "100");
569 setAttribute(element, "aria-label", name);
570 }
571 } break;
572 default:
573 qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role();
574 element = document.call<emscripten::val>("createElement", std::string("div"));
575 }
576
577 addEventListener(iface, element, "focus");
578 return element;
579
580 }();
581
582 m_elements[iface] = element;
583
584 setHtmlElementGeometry(iface);
585 setHtmlElementDisabled(iface);
586 setHtmlElementVisibility(iface, !iface->state().invisible);
587 handleIdentifierUpdate(iface);
588 handleDescriptionChanged(iface);
589 sendEvent(iface, QAccessible::NameChanged);
590
591 linkToParent(iface);
592 // Link in child elements
593 for (int i = 0; i < iface->childCount(); ++i) {
594 if (!getHtmlElement(iface->child(i)).isUndefined())
595 linkToParent(iface->child(i));
596 }
597
598 return element;
599}
600
601void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface)
602{
603 Q_UNUSED(iface);
604 qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement";
605}
606
607emscripten::val QWasmAccessibility::getHtmlElement(QAccessibleInterface *iface)
608{
609 auto it = m_elements.find(iface);
610 if (it != m_elements.end())
611 return it.value();
612
613 return emscripten::val::undefined();
614}
615
616void QWasmAccessibility::repairLinks(QAccessibleInterface *iface)
617{
618 // relink any children that are linked to the wrong parent,
619 // This can be caused by a missing ParentChanged event.
620 bool moved = false;
621 for (int i = 0; i < iface->childCount(); ++i) {
622 const auto elementI = getHtmlElement(iface->child(i));
623 const auto containerI = getElementContainer(iface->child(i));
624
625 if (!elementI.isUndefined() &&
626 !containerI.isUndefined() &&
627 !elementI["parentElement"].isUndefined() &&
628 !elementI["parentElement"].isNull() &&
629 elementI["parentElement"] != containerI) {
630 moved = true;
631 break;
632 }
633 }
634 if (moved) {
635 for (int i = 0; i < iface->childCount(); ++i) {
636 const auto elementI = getHtmlElement(iface->child(i));
637 const auto containerI = getElementContainer(iface->child(i));
638 if (!elementI.isUndefined() && !containerI.isUndefined())
639 containerI.call<void>("appendChild", elementI);
640 }
641 }
642}
643
644void QWasmAccessibility::linkToParent(QAccessibleInterface *iface)
645{
646 emscripten::val element = getHtmlElement(iface);
647 emscripten::val container = getElementContainer(iface);
648
649 if (container.isUndefined() || element.isUndefined())
650 return;
651
652 // Make sure that we don't change the focused element
653 const auto activeElementBefore = emscripten::val::take_ownership(
654 getActiveElement_js(emscripten::val::undefined().as_handle()));
655
656
657 repairLinks(iface->parent());
658
659 emscripten::val next = emscripten::val::undefined();
660 const int thisIndex = iface->parent()->indexOfChild(iface);
661 if (thisIndex >= 0) {
662 Q_ASSERT(thisIndex < iface->parent()->childCount());
663 for (int i = thisIndex + 1; i < iface->parent()->childCount(); ++i) {
664 const auto elementI = getHtmlElement(iface->parent()->child(i));
665 if (!elementI.isUndefined() &&
666 elementI["parentElement"] == container) {
667 next = elementI;
668 break;
669 }
670 }
671 if (next.isUndefined())
672 container.call<void>("appendChild", element);
673 else
674 container.call<void>("insertBefore", element, next);
675 }
676 const auto activeElementAfter = emscripten::val::take_ownership(
677 getActiveElement_js(emscripten::val::undefined().as_handle()));
678 if (activeElementBefore != activeElementAfter) {
679 if (!activeElementBefore.isUndefined() && !activeElementBefore.isNull())
680 activeElementBefore.call<void>("focus");
681 }
682}
683
684void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible)
685{
686 emscripten::val element = getHtmlElement(iface);
687 setAttribute(element, "inert", !visible);
688}
689
690void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface)
691{
692 const emscripten::val element = getHtmlElement(iface);
693
694 QRect windowGeometry = iface->rect();
695 if (iface->parent()) {
696 // Both iface and iface->parent returns geometry in screen coordinates
697 // We only want the relative coordinates, so the coordinate system does
698 // not matter as long as it is the same.
699 const QRect parentRect = iface->parent()->rect();
700 const QRect thisRect = iface->rect();
701 const QRect result(thisRect.topLeft() - parentRect.topLeft(), thisRect.size());
702 windowGeometry = result;
703 } else {
704 // Elements without a parent are not a part of the a11y tree, and don't
705 // have meaningful geometry.
706 Q_ASSERT(!getWindow(iface));
707 }
708 setHtmlElementGeometry(element, windowGeometry);
709}
710
711void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry)
712{
713 // Position the element using "position: absolute" in order to place
714 // it under the corresponding Qt element in the screen.
715 emscripten::val style = element["style"];
716 style.set("position", std::string("absolute"));
717 style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the
718 // screen element, but isn't
719 style.set("left", std::to_string(geometry.x()) + "px");
720 style.set("top", std::to_string(geometry.y()) + "px");
721 style.set("width", std::to_string(geometry.width()) + "px");
722 style.set("height", std::to_string(geometry.height()) + "px");
723}
724
725void QWasmAccessibility::setHtmlElementFocus(QAccessibleInterface *iface)
726{
727 const auto element = getHtmlElement(iface);
728 element.call<void>("focus");
729}
730
731void QWasmAccessibility::setHtmlElementDisabled(QAccessibleInterface *iface)
732{
733 auto element = getHtmlElement(iface);
734 setAttribute(element, "aria-disabled", iface->state().disabled);
735}
736
737void QWasmAccessibility::setHtmlElementOrientation(emscripten::val element, QAccessibleInterface *iface)
738{
739 Q_ASSERT(iface);
740 if (QAccessibleAttributesInterface *attributesIface = iface->attributesInterface()) {
741 const QVariant orientationVariant =
742 attributesIface->attributeValue(QAccessible::Attribute::Orientation);
743 if (orientationVariant.isValid()) {
744 Q_ASSERT(orientationVariant.canConvert<Qt::Orientation>());
745 const Qt::Orientation orientation = orientationVariant.value<Qt::Orientation>();
746 const std::string value = orientation == Qt::Horizontal ? "horizontal" : "vertical";
747 setAttribute(element, "aria-orientation", value);
748 }
749 }
750}
751
752void QWasmAccessibility::handleListItemUpdate(QAccessibleEvent *event)
753{
754 auto iface = event->accessibleInterface();
755
756 switch (event->type()) {
757 case QAccessible::NameChanged: {
758 // ListItem is a div
759 setNamedProperty(iface, "innerText", QAccessible::Name);
760 } break;
761 default:
762 qCDebug(lcQpaAccessibility) << "TODO: implement handleListItemUpdate for event" << event->type();
763 break;
764 }
765}
766
767void QWasmAccessibility::handleDialUpdate(QAccessibleEvent *event)
768{
769 switch (event->type()) {
770 case QAccessible::ObjectCreated:
771 case QAccessible::StateChanged: {
772 } break;
773 case QAccessible::Focus:
774 case QAccessible::NameChanged: {
775 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
776 } break;
777 case QAccessible::ValueChanged: {
778 QAccessibleInterface *accessible = event->accessibleInterface();
779 const emscripten::val element = getHtmlElement(accessible);
780 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
781 setProperty(element, "value", valueString);
782 } break;
783 default:
784 qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type();
785 break;
786 }
787}
788
789void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event)
790{
791 switch (event->type()) {
792 case QAccessible::NameChanged: {
793 // StaticText is a div
794 setNamedProperty(event->accessibleInterface(), "innerText", QAccessible::Name);
795 } break;
796 default:
797 qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type();
798 break;
799 }
800}
801
802void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event)
803{
804 switch (event->type()) {
805 case QAccessible::StateChanged: {
806 auto iface = event->accessibleInterface();
807 auto element = getHtmlElement(iface);
808 setAttribute(element, "readonly", iface->state().readOnly);
809 if (iface->state().passwordEdit)
810 setProperty(element, "type", "password");
811 else
812 setProperty(element, "type", "text");
813 } break;
814 case QAccessible::ValueChanged:
815 case QAccessible::NameChanged: {
816 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
817 } break;
818 case QAccessible::ObjectShow:
819 case QAccessible::Focus: {
820 auto iface = event->accessibleInterface();
821 auto element = getHtmlElement(iface);
822 if (!element.isUndefined()) {
823 setAttribute(element, "readonly", iface->state().readOnly);
824 if (iface->state().passwordEdit)
825 setProperty(element, "type", "password");
826 else
827 setProperty(element, "type", "text");
828 }
829 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
830 } break;
831 case QAccessible::TextRemoved:
832 case QAccessible::TextInserted: {
833 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
834 } break;
835 case QAccessible::TextCaretMoved: {
836 // We lack ValueChanged event, sync on caret move instead
837 setNamedProperty(event->accessibleInterface(), "value", QAccessible::Value);
838 } break;
839 default:
840 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
841 break;
842 }
843}
844
845void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event)
846{
847 if (event["target"].isNull() || event["target"].isUndefined())
848 return;
849
850 if (event["target"]["data-qta11yinterface"].isNull() || event["target"]["data-qta11yinterface"].isUndefined())
851 return;
852
853 auto iface = reinterpret_cast<QAccessibleInterface *>(event["target"]["data-qta11yinterface"].as<size_t>());
854 if (m_elements.find(iface) == m_elements.end())
855 return;
856
857 const auto eventType = QString::fromStdString(event["type"].as<std::string>());
858 const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
859 bool handled = false;
860
861 if (eventType == "blur") {
862 ;
863 } else if (eventType == "keydown") {
864 ;
865 } else if (eventType == "keyup") {
866 ;
867 } else if (eventType == "focus") {
868 if (actionNames.contains(QAccessibleActionInterface::setFocusAction()))
869 iface->actionInterface()->doAction(QAccessibleActionInterface::setFocusAction());
870 } else if (eventType == "click") {
871 handled = true;
872 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
873 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
874 else if (actionNames.contains(QAccessibleActionInterface::showMenuAction()))
875 iface->actionInterface()->doAction(QAccessibleActionInterface::showMenuAction());
876 } else if (eventType == "change") {
877 handled = true;
878 // Checkboxes can have both toggle and press, in this case it is
879 // important to invoke press to handle tristate correctly
880 if (actionNames.contains(QAccessibleActionInterface::pressAction()))
881 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
882 else if (actionNames.contains(QAccessibleActionInterface::toggleAction()))
883 iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction());
884 } else {
885 qWarning() << " Unknown event" << eventType;
886 }
887
888 if (handled) {
889 event.call<void>("preventDefault");
890 event.call<void>("stopPropagation");
891 }
892}
893
894void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event)
895{
896 switch (event->type()) {
897 case QAccessible::Focus:
898 case QAccessible::NameChanged: {
899 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
900 } break;
901 default:
902 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
903 break;
904 }
905}
906
907void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event)
908{
909 switch (event->type()) {
910 case QAccessible::Focus:
911 case QAccessible::NameChanged: {
912 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
913 } break;
914 case QAccessible::StateChanged: {
915 QAccessibleInterface *accessible = event->accessibleInterface();
916 const emscripten::val element = getHtmlElement(accessible);
917 setAttribute(element, "checked", accessible->state().checked);
918 setProperty(element, "indeterminate", accessible->state().checkStateMixed);
919 } break;
920 default:
921 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
922 break;
923 }
924}
925
926void QWasmAccessibility::handleGroupBoxUpdate(QAccessibleEvent *event)
927{
928 QAccessibleInterface *iface = event->accessibleInterface();
929
930 emscripten::val parent = getHtmlElement(iface);
931 emscripten::val label = parent["children"][0];
932 emscripten::val checkbox = emscripten::val::undefined();
933 if (iface->role() == QAccessible::CheckBox)
934 checkbox = parent["children"][1];
935
936 switch (event->type()) {
937 case QAccessible::Focus:
938 case QAccessible::NameChanged: {
939 setProperty(label, "innerText", iface->text(QAccessible::Name).toStdString());
940 if (!checkbox.isUndefined())
941 setAttribute(checkbox, "aria-label", iface->text(QAccessible::Name).toStdString());
942 } break;
943 case QAccessible::StateChanged: {
944 if (!checkbox.isUndefined()) {
945 setAttribute(checkbox, "checked", iface->state().checked);
946 setProperty(checkbox, "indeterminate", iface->state().checkStateMixed);
947 }
948 } break;
949 default:
950 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
951 break;
952 }
953}
954
955void QWasmAccessibility::handleSwitchUpdate(QAccessibleEvent *event)
956{
957 switch (event->type()) {
958 case QAccessible::Focus:
959 case QAccessible::NameChanged: {
960 /* A switch is like a button in this regard */
961 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
962 } break;
963 case QAccessible::StateChanged: {
964 QAccessibleInterface *accessible = event->accessibleInterface();
965 const emscripten::val element = getHtmlElement(accessible);
966 if (accessible->state().checked)
967 setAttribute(element, "aria-checked", "true");
968 else
969 setAttribute(element, "aria-checked", "false");
970 } break;
971 default:
972 qCDebug(lcQpaAccessibility) << "TODO: implement handleSwitchUpdate for event" << event->type();
973 break;
974 }
975}
976
977void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event)
978{
979 QAccessibleInterface *iface = event->accessibleInterface();
980 QString text = iface->text(QAccessible::Name);
981 QString desc = iface->text(QAccessible::Description);
982 switch (event->type()) {
983 case QAccessible::NameChanged:
984 case QAccessible::StateChanged:{
985 const emscripten::val element = getHtmlElement(iface);
986 setAttribute(element, "title", text.toStdString());
987 } break;
988 default:
989 qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type();
990 break;
991 }
992}
993void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event)
994{
995 QAccessibleInterface *iface = event->accessibleInterface();
996 QString text = iface->text(QAccessible::Name);
997 QString desc = iface->text(QAccessible::Description);
998 switch (event->type()) {
999 case QAccessible::Focus:
1000 case QAccessible::NameChanged:
1001 case QAccessible::MenuStart ://"TODO: To implement later
1002 case QAccessible::StateChanged:{
1003 const emscripten::val element = getHtmlElement(iface);
1004 setAttribute(element, "title", text.toStdString());
1005 } break;
1006 case QAccessible::PopupMenuStart: {
1007 if (iface->childCount() > 0) {
1008 const auto childElement = getHtmlElement(iface->child(0));
1009 childElement.call<void>("focus");
1010 }
1011 } break;
1012 default:
1013 qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type();
1014 break;
1015 }
1016}
1017void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) {
1018
1019 switch (event->type()) {
1020 case QAccessible::NameChanged:
1021 case QAccessible::Focus:
1022 case QAccessible::DialogStart:
1023 case QAccessible::StateChanged: {
1024 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1025 } break;
1026 default:
1027 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
1028 break;
1029 }
1030}
1031
1032void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface)
1033{
1034 if (!iface)
1035 return;
1036
1037 // We ignore toplevel windows which is categorized
1038 // by getWindow(iface->parent()) != getWindow(iface)
1039 const QWindow *window1 = getWindow(iface);
1040 const QWindow *window0 = (iface->parent()) ? getWindow(iface->parent()) : nullptr;
1041
1042 if (window1 && window0 == window1) {
1043 // Create html element for the interface, sync up properties.
1044 bool exists = !getHtmlElement(iface).isUndefined();
1045 if (!exists)
1046 exists = !createHtmlElement(iface).isUndefined();
1047
1048 if (exists) {
1049 linkToParent(iface);
1050 setHtmlElementVisibility(iface, !iface->state().invisible);
1051 setHtmlElementGeometry(iface);
1052 setHtmlElementDisabled(iface);
1053 handleIdentifierUpdate(iface);
1054 handleDescriptionChanged(iface);
1055 sendEvent(iface, QAccessible::NameChanged);
1056 }
1057 }
1058 for (int i = 0; i < iface->childCount(); ++i)
1059 populateAccessibilityTree(iface->child(i));
1060}
1061
1062void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event)
1063{
1064 switch (event->type()) {
1065 case QAccessible::Focus:
1066 case QAccessible::NameChanged: {
1067 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1068 } break;
1069 case QAccessible::StateChanged: {
1070 QAccessibleInterface *accessible = event->accessibleInterface();
1071 const emscripten::val element = getHtmlElement(accessible);
1072 setAttribute(element, "checked", accessible->state().checked);
1073 } break;
1074 default:
1075 qDebug() << "TODO: implement handleRadioButtonUpdate for event" << event->type();
1076 break;
1077 }
1078}
1079
1080void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event)
1081{
1082 switch (event->type()) {
1083 case QAccessible::ObjectCreated:
1084 case QAccessible::StateChanged: {
1085 } break;
1086 case QAccessible::Focus:
1087 case QAccessible::NameChanged: {
1088 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1089 } break;
1090 case QAccessible::ValueChanged: {
1091 QAccessibleInterface *accessible = event->accessibleInterface();
1092 const emscripten::val element = getHtmlElement(accessible);
1093 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
1094 setProperty(element, "value", valueString);
1095 } break;
1096 default:
1097 qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type();
1098 break;
1099 }
1100}
1101
1102void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event)
1103{
1104 switch (event->type()) {
1105 case QAccessible::ObjectCreated:
1106 case QAccessible::StateChanged: {
1107 } break;
1108 case QAccessible::Focus:
1109 case QAccessible::NameChanged: {
1110 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1111 } break;
1112 case QAccessible::ValueChanged: {
1113 QAccessibleInterface *accessible = event->accessibleInterface();
1114 const emscripten::val element = getHtmlElement(accessible);
1115 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
1116 setProperty(element, "value", valueString);
1117 } break;
1118 default:
1119 qDebug() << "TODO: implement handleSliderUpdate for event" << event->type();
1120 break;
1121 }
1122}
1123
1124void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event)
1125{
1126 switch (event->type()) {
1127 case QAccessible::Focus:
1128 case QAccessible::NameChanged: {
1129 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1130 } break;
1131 case QAccessible::ValueChanged: {
1132 QAccessibleInterface *accessible = event->accessibleInterface();
1133 const emscripten::val element = getHtmlElement(accessible);
1134 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
1135 setAttribute(element, "aria-valuenow", valueString);
1136 } break;
1137 default:
1138 qDebug() << "TODO: implement handleSliderUpdate for event" << event->type();
1139 break;
1140 }
1141
1142}
1143
1144void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event)
1145{
1146 switch (event->type()) {
1147 case QAccessible::NameChanged: {
1148 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1149 } break;
1150 case QAccessible::Focus: {
1151 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1152 } break;
1153 default:
1154 qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type();
1155 break;
1156 }
1157}
1158
1159void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event)
1160{
1161 switch (event->type()) {
1162 case QAccessible::NameChanged: {
1163 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1164 } break;
1165 case QAccessible::Focus: {
1166 setNamedAttribute(event->accessibleInterface(), "aria-label", QAccessible::Name);
1167 } break;
1168 default:
1169 qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type();
1170 break;
1171 }
1172}
1173
1174void QWasmAccessibility::handleIdentifierUpdate(QAccessibleInterface *iface)
1175{
1176 const emscripten::val element = getHtmlElement(iface);
1177 QString id = iface->text(QAccessible::Identifier).replace(" ", "_");
1178 if (id.isEmpty() && iface->role() == QAccessible::PageTabList) {
1179 std::ostringstream oss;
1180 oss << "tabList_0x" << (void *)iface;
1181 id = QString::fromUtf8(oss.str());
1182 }
1183
1184 setAttribute(element, "id", id.toStdString());
1185 if (!id.isEmpty()) {
1186 if (iface->role() == QAccessible::PageTabList) {
1187 for (int i = 0; i < iface->childCount(); ++i) {
1188 const auto child = getHtmlElement(iface->child(i));
1189 setAttribute(child, "aria-owns", id.toStdString());
1190 }
1191 }
1192 }
1193}
1194
1195void QWasmAccessibility::handleDescriptionChanged(QAccessibleInterface *iface)
1196{
1197 const auto desc = iface->text(QAccessible::Description).toStdString();
1198 auto element = getHtmlElement(iface);
1199 auto container = getDescribedByContainer(iface);
1200 if (!container.isUndefined()) {
1201 std::ostringstream oss;
1202 oss << "dbid_" << (void *)iface;
1203 auto id = oss.str();
1204
1205 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
1206 if (desc.empty()) {
1207 if (!describedBy.isUndefined() && !describedBy.isNull()) {
1208 container.call<void>("removeChild", describedBy);
1209 }
1210 setAttribute(element, "aria-describedby", "");
1211 } else {
1212 if (describedBy.isUndefined() || describedBy.isNull()) {
1213 auto document = getDocument(container);
1214 describedBy = document.call<emscripten::val>("createElement", std::string("p"));
1215
1216 container.call<void>("appendChild", describedBy);
1217 }
1218 setAttribute(describedBy, "id", id);
1219 setAttribute(describedBy, "inert", true);
1220 setAttribute(element, "aria-describedby", id);
1221 setProperty(describedBy, "innerText", desc);
1222 }
1223 }
1224}
1225
1226void QWasmAccessibility::handleAnnouncement(QAccessibleInterface *iface,
1227 QAccessibleAnnouncementEvent *announcementEvent)
1228{
1229 emscripten::val element = getHtmlElement(iface);
1230 if (element["ariaNotify"].isUndefined())
1231 return;
1232
1233 const std::string message = announcementEvent->message().toStdString();
1234 const std::string prio(announcementEvent->politeness()
1235 == QAccessible::AnnouncementPoliteness::Assertive
1236 ? "high"
1237 : "normal");
1238 emscripten::val options = emscripten::val::object();
1239 options.set("priority", prio);
1240 element.call<void>("ariaNotify", message, options);
1241}
1242
1243void QWasmAccessibility::handleProgressBarUpdate(QAccessibleEvent *event)
1244{
1245 auto iface = event->accessibleInterface();
1246 auto valueInterface = iface->valueInterface();
1247 auto element = getHtmlElement(iface);
1248
1249 switch (event->type()) {
1250 case QAccessible::Focus:
1251 case QAccessible::NameChanged: {
1252 const auto name = iface->text(QAccessible::Name).toStdString();
1253 setAttribute(element, "aria-label", name);
1254 } // fallthrough
1255 case QAccessible::ValueChanged: {
1256 if (valueInterface) {
1257 const auto min = valueInterface->minimumValue().toInt();
1258 const auto max = valueInterface->maximumValue().toInt();
1259 const auto cur = valueInterface->currentValue().toInt();
1260
1261 // from qprogressbar.cpp
1262 const auto totalSteps = qint64(max) - min;
1263 const auto progress = (totalSteps == 0) ? 0 : static_cast<int>((qint64(cur) - min) * 100.0 / totalSteps);
1264
1265 if (cur <= min)
1266 setAttribute(element, "aria-valuenow", "0");
1267 else if (cur >= max)
1268 setAttribute(element, "aria-valuenow", "100");
1269 else
1270 setAttribute(element, "aria-valuenow", std::to_string(progress));
1271 }
1272 } break;
1273 default: {
1274 } break;
1275 } /* end-switch */
1276}
1277
1278void QWasmAccessibility::createObject(QAccessibleInterface *iface)
1279{
1280 if (getHtmlElement(iface).isUndefined())
1281 createHtmlElement(iface);
1282}
1283
1284void QWasmAccessibility::removeObject(QAccessibleInterface *iface)
1285{
1286 // Do not dereference the object pointer. it might be invalid.
1287 // Do not dereference the iface either, it refers to the object.
1288 // Note: we may remove children, making them have parentElement undefined
1289 // so we need to check for parentElement here. We do assume that removeObject
1290 // is called on all objects, just not in any predefined order.
1291 const auto it = m_elements.find(iface);
1292 if (it != m_elements.end()) {
1293 auto element = it.value();
1294 auto container = getDescribedByContainer(iface);
1295 if (!container.isUndefined()) {
1296 std::ostringstream oss;
1297 oss << "dbid_" << (void *)iface;
1298 auto id = oss.str();
1299 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
1300 if (!describedBy.isUndefined() && !describedBy.isNull() &&
1301 !describedBy["parentElement"].isUndefined() && !describedBy["parentElement"].isNull())
1302 describedBy["parentElement"].call<void>("removeChild", describedBy);
1303 }
1304 if (!element["parentElement"].isUndefined() && !element["parentElement"].isNull())
1305 element["parentElement"].call<void>("removeChild", element);
1306 m_elements.erase(it);
1307 }
1308}
1309
1310void QWasmAccessibility::unlinkParentForChildren(QAccessibleInterface *iface)
1311{
1312 auto element = getHtmlElement(iface);
1313 if (!element.isUndefined()) {
1314 auto oldContainer = element["parentElement"];
1315 auto newContainer = getElementContainer(iface);
1316 if (!oldContainer.isUndefined() &&
1317 !oldContainer.isNull() &&
1318 oldContainer != newContainer) {
1319 oldContainer.call<void>("removeChild", element);
1320 }
1321 }
1322 for (int i = 0; i < iface->childCount(); ++i)
1323 unlinkParentForChildren(iface->child(i));
1324}
1325
1326void QWasmAccessibility::relinkParentForChildren(QAccessibleInterface *iface)
1327{
1328 auto element = getHtmlElement(iface);
1329 if (!element.isUndefined()) {
1330 if (element["parentElement"].isUndefined() ||
1331 element["parentElement"].isNull()) {
1332 linkToParent(iface);
1333 }
1334 }
1335 for (int i = 0; i < iface->childCount(); ++i)
1336 relinkParentForChildren(iface->child(i));
1337}
1338
1339void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event)
1340{
1341 if (handleUpdateByEventType(event))
1342 handleUpdateByInterfaceRole(event);
1343}
1344
1345bool QWasmAccessibility::handleUpdateByEventType(QAccessibleEvent *event)
1346{
1347 if (!m_accessibilityEnabled)
1348 return false;
1349
1350 QAccessibleInterface *iface = event->accessibleInterface();
1351 if (!iface) {
1352 qWarning() << "handleUpdateByEventType with null a11y interface" << event->type() << event->object();
1353 return false;
1354 }
1355
1356 // Handle event types that creates/removes objects.
1357 switch (event->type()) {
1358 case QAccessible::ObjectCreated:
1359 // Do nothing, there are too many changes to the interface
1360 // before ObjectShow is called
1361 return false;
1362
1363 case QAccessible::ObjectDestroyed:
1364 // The object might be under destruction, and the interface is not valid
1365 // but we can look at the pointer,
1366 removeObject(iface);
1367 return false;
1368
1369 case QAccessible::ObjectShow: // We do not get ObjectCreated from widgets, we get ObjectShow
1370 createObject(iface);
1371 break;
1372
1373 case QAccessible::ParentChanged:
1374 unlinkParentForChildren(iface);
1375 relinkParentForChildren(iface);
1376 break;
1377
1378 default:
1379 break;
1380 };
1381
1382 if (getHtmlElement(iface).isUndefined())
1383 return false;
1384
1385 // Handle some common event types. See
1386 // https://doc.qt.io/qt-6/qaccessible.html#Event-enum
1387 switch (event->type()) {
1388 case QAccessible::Announcement:
1389 handleAnnouncement(iface, static_cast<QAccessibleAnnouncementEvent *>(event));
1390 return false;
1391
1392 case QAccessible::StateChanged: {
1393 QAccessibleStateChangeEvent *stateChangeEvent = (QAccessibleStateChangeEvent *)event;
1394 if (stateChangeEvent->changedStates().disabled)
1395 setHtmlElementDisabled(iface);
1396 } break;
1397
1398 case QAccessible::DescriptionChanged:
1399 handleDescriptionChanged(iface);
1400 return false;
1401
1402 case QAccessible::Focus:
1403 // We do not get all callbacks for the geometry
1404 // hence we update here as well.
1405 setHtmlElementGeometry(iface);
1406 setHtmlElementFocus(iface);
1407 break;
1408
1409 case QAccessible::IdentifierChanged:
1410 handleIdentifierUpdate(iface);
1411 return false;
1412
1413 case QAccessible::ObjectShow:
1414 linkToParent(iface);
1415 setHtmlElementVisibility(iface, true);
1416
1417 // Sync up properties on show;
1418 setHtmlElementGeometry(iface);
1419 sendEvent(iface, QAccessible::NameChanged);
1420 break;
1421
1422 case QAccessible::ObjectHide:
1423 // Do not call linkToParent, only set visibility to false
1424 setHtmlElementVisibility(iface, false);
1425 return false;
1426
1427 case QAccessible::LocationChanged:
1428 setHtmlElementGeometry(iface);
1429 return false;
1430
1431 // TODO: maybe handle more types here
1432 default:
1433 break;
1434 };
1435
1436 return true;
1437}
1438
1439void QWasmAccessibility::handleUpdateByInterfaceRole(QAccessibleEvent *event)
1440{
1441 if (!m_accessibilityEnabled)
1442 return;
1443
1444 QAccessibleInterface *iface = event->accessibleInterface();
1445 if (!iface) {
1446 qWarning() << "handleUpdateByInterfaceRole with null a11y interface" << event->type() << event->object();
1447 return;
1448 }
1449
1450 // Switch on interface role, see
1451 // https://doc.qt.io/qt-5/qaccessibleinterface.html#role
1452 switch (iface->role()) {
1453 case QAccessible::List:
1454 break;
1455 case QAccessible::ListItem:
1456 handleListItemUpdate(event);
1457 break;
1458 case QAccessible::StaticText:
1459 handleStaticTextUpdate(event);
1460 break;
1461 case QAccessible::Button:
1462 handleButtonUpdate(event);
1463 break;
1464 case QAccessible::CheckBox:
1465 if (iface->childCount() > 0)
1466 handleGroupBoxUpdate(event);
1467 else
1468 handleCheckBoxUpdate(event);
1469 break;
1470 case QAccessible::Switch:
1471 handleSwitchUpdate(event);
1472 break;
1473 case QAccessible::EditableText:
1474 handleLineEditUpdate(event);
1475 break;
1476 case QAccessible::Dialog:
1477 handleDialogUpdate(event);
1478 break;
1479 case QAccessible::MenuItem:
1480 case QAccessible::MenuBar:
1481 case QAccessible::PopupMenu:
1482 handleMenuUpdate(event);
1483 break;
1484 case QAccessible::ToolBar:
1485 case QAccessible::ButtonMenu:
1486 handleToolUpdate(event);
1487 case QAccessible::RadioButton:
1488 handleRadioButtonUpdate(event);
1489 break;
1490 case QAccessible::Dial:
1491 handleDialUpdate(event);
1492 break;
1493 case QAccessible::SpinBox:
1494 handleSpinBoxUpdate(event);
1495 break;
1496 case QAccessible::Slider:
1497 handleSliderUpdate(event);
1498 break;
1499 case QAccessible::PageTab:
1500 handlePageTabUpdate(event);
1501 break;
1502 case QAccessible::PageTabList:
1503 handlePageTabListUpdate(event);
1504 break;
1505 case QAccessible::ScrollBar:
1506 handleScrollBarUpdate(event);
1507 break;
1508 case QAccessible::Grouping:
1509 handleGroupBoxUpdate(event);
1510 break;
1511 case QAccessible::ProgressBar:
1512 handleProgressBarUpdate(event);
1513 break;
1514 default:
1515 qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role();
1516 };
1517}
1518
1519void QWasmAccessibility::setRootObject(QObject *root)
1520{
1521 m_rootObject = root;
1522}
1523
1524void QWasmAccessibility::initialize()
1525{
1526
1527}
1528
1529void QWasmAccessibility::cleanup()
1530{
1531
1532}
1533
1534#endif // QT_CONFIG(accessibility)
void QWasmAccessibilityEnable()