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
5#include "qwasmscreen.h"
6#include "qwasmwindow.h"
8#include <QtCore/private/qwasmsuspendresumecontrol_p.h>
9#include <QtGui/qwindow.h>
10
11#include <sstream>
12
14{
15 QWasmAccessibility::enable();
16}
17
18#if QT_CONFIG(accessibility)
19
20#include <QtGui/private/qaccessiblebridgeutils_p.h>
21
22Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility")
23
24namespace {
25EM_JS(emscripten::EM_VAL, getActiveElement_js, (emscripten::EM_VAL undefHandle), {
26 var activeEl = document.activeElement;
27 while (true) {
28 if (!activeEl) {
29 return undefHandle;
30 } else if (activeEl.shadowRoot) {
31 activeEl = activeEl.shadowRoot.activeElement;
32 } else {
33 return Emval.toHandle(activeEl);
34 }
35 }
36})
37}
38
39// Qt WebAssembly a11y backend
40//
41// This backend implements accessibility support by creating "shadowing" html
42// elements for each Qt UI element. We access the DOM by using Emscripten's
43// val.h API.
44//
45// Currently, html elements are created in response to notifyAccessibilityUpdate
46// events. In addition or alternatively, we could also walk the accessibility tree
47// from setRootObject().
48
49QWasmAccessibility::QWasmAccessibility()
50{
51 s_instance = this;
52
53 if (qEnvironmentVariableIntValue("QT_WASM_ENABLE_ACCESSIBILITY") == 1)
54 enableAccessibility();
55
56 // Register accessibility element event handler
57 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
58 Q_ASSERT(suspendResume);
59 m_eventHandlerIndex = suspendResume->registerEventHandler([this](const emscripten::val event){
60 this->handleEventFromHtmlElement(event);
61 });
62}
63
64QWasmAccessibility::~QWasmAccessibility()
65{
66 // Remove accessibility element event handler
67 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
68 Q_ASSERT(suspendResume);
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 auto describedByContainer = document.call<emscripten::val>("createElement", std::string("div"));
202
203 a11yContainer.call<void>("appendChild", elementContainer);
204 a11yContainer.call<void>("appendChild", describedByContainer);
205
206 return a11yContainer;
207}
208
209emscripten::val QWasmAccessibility::getA11yContainer(QAccessibleInterface *iface)
210{
211 return getA11yContainer(getWindow(iface));
212}
213
214emscripten::val QWasmAccessibility::getDescribedByContainer(QWindow *window)
215{
216 auto a11yContainer = getA11yContainer(window);
217 if (a11yContainer.isUndefined())
218 return emscripten ::val::undefined();
219
220 Q_ASSERT(a11yContainer["childElementCount"].as<unsigned>() == 2);
221 Q_ASSERT(!a11yContainer["children"][1].isUndefined());
222
223 return a11yContainer["children"][1];
224}
225
226emscripten::val QWasmAccessibility::getDescribedByContainer(QAccessibleInterface *iface)
227{
228 return getDescribedByContainer(getWindow(iface));
229}
230
231emscripten::val QWasmAccessibility::getElementContainer(QWindow *window)
232{
233 auto a11yContainer = getA11yContainer(window);
234 if (a11yContainer.isUndefined())
235 return emscripten ::val::undefined();
236
237 Q_ASSERT(a11yContainer["childElementCount"].as<unsigned>() == 2);
238 Q_ASSERT(!a11yContainer["children"][0].isUndefined());
239 return a11yContainer["children"][0];
240}
241
242emscripten::val QWasmAccessibility::getElementContainer(QAccessibleInterface *iface)
243{
244 // Here we skip QWindow nodes, as they are already present. Such nodes
245 // has a parent window of null.
246 //
247 // The next node should return the a11y container.
248 // Further nodes should return the element of the parent.
249 if (!getWindow(iface))
250 return emscripten::val::undefined();
251
252 if (isWindowNode(iface))
253 return emscripten::val::undefined();
254
255 if (isWindowNode(iface->parent()))
256 return getElementContainer(getWindow(iface->parent()));
257
258 // Regular node
259 return getHtmlElement(iface->parent());
260}
261
262QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface)
263{
264 if (!iface)
265 return nullptr;
266
267 QWindow *window = iface->window();
268 // this is needed to add tabs as the window is not available
269 if (!window && iface->parent())
270 window = iface->parent()->window();
271 return window;
272}
273
274emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container)
275{
276 if (container.isUndefined())
277 return emscripten::val::global("document");
278 return container["ownerDocument"];
279}
280
281emscripten::val QWasmAccessibility::getDocument(QAccessibleInterface *iface)
282{
283 return getDocument(getA11yContainer(iface));
284}
285
286void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
287 const std::string &val)
288{
289 if (val != "")
290 element.call<void>("setAttribute", attr, val);
291 else
292 element.call<void>("removeAttribute", attr);
293}
294
295void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr,
296 const char *val)
297{
298 setAttribute(element, attr, std::string(val));
299}
300
301void QWasmAccessibility::setAttribute(emscripten::val element, const std::string &attr, bool val)
302{
303 if (val)
304 element.call<void>("setAttribute", attr, val);
305 else
306 element.call<void>("removeAttribute", attr);
307}
308
309void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property,
310 const std::string &val)
311{
312 element.set(property, val);
313}
314
315void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, const char *val)
316{
317 setProperty(element, property, std::string(val));
318}
319
320void QWasmAccessibility::setProperty(emscripten::val element, const std::string &property, bool val)
321{
322 element.set(property, val);
323}
324
325
326void QWasmAccessibility::addEventListener(emscripten::val element, const char *eventType)
327{
328 element.call<void>("addEventListener", emscripten::val(eventType),
329 QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_eventHandlerIndex),
330 true);
331}
332
333emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface)
334{
335 // Get the html container element for the interface; this depends on which
336 // QScreen it is on. If the interface is not on a screen yet we get an undefined
337 // container, and the code below handles that case as well.
338 emscripten::val container = getElementContainer(iface);
339
340 // Get the correct html document for the container, or fall back
341 // to the global document. TODO: Does using the correct document actually matter?
342 emscripten::val document = getDocument(container);
343
344 // Translate the Qt a11y elemen role into html element type + ARIA role.
345 // Here we can either create <div> elements with a spesific ARIA role,
346 // or create e.g. <button> elements which should have built-in accessibility.
347 emscripten::val element = [this, iface, document] {
348
349 emscripten::val element = emscripten::val::undefined();
350
351 switch (iface->role()) {
352
353 case QAccessible::Button: {
354 element = document.call<emscripten::val>("createElement", std::string("button"));
355 addEventListener(element, "click");
356 } break;
357
358 case QAccessible::CheckBox: {
359 element = document.call<emscripten::val>("createElement", std::string("input"));
360 setAttribute(element, "type", "checkbox");
361 setAttribute(element, "checked", iface->state().checked);
362 setProperty(element, "indeterminate", iface->state().checkStateMixed);
363 addEventListener(element, "change");
364 } break;
365
366 case QAccessible::RadioButton: {
367 element = document.call<emscripten::val>("createElement", std::string("input"));
368 setAttribute(element, "type", "radio");
369 setAttribute(element, "checked", iface->state().checked);
370 setProperty(element, "name", "buttonGroup");
371 addEventListener(element, "change");
372 } break;
373
374 case QAccessible::SpinBox:
375 case QAccessible::Slider: {
376 const auto minValue = iface->valueInterface()->minimumValue().toString().toStdString();
377 const auto maxValue = iface->valueInterface()->maximumValue().toString().toStdString();
378 const auto stepValue =
379 iface->valueInterface()->minimumStepSize().toString().toStdString();
380 const auto value = iface->valueInterface()->currentValue().toString().toStdString();
381 element = document.call<emscripten::val>("createElement", std::string("input"));
382 setAttribute(element,"type", "number");
383 setAttribute(element, "min", minValue);
384 setAttribute(element, "max", maxValue);
385 setAttribute(element, "step", stepValue);
386 setProperty(element, "value", value);
387 } break;
388
389 case QAccessible::PageTabList:{
390 element = document.call<emscripten::val>("createElement", std::string("div"));
391 setAttribute(element, "role", "tablist");
392
393 m_elements[iface] = element;
394
395 for (int i = 0; i < iface->childCount(); ++i)
396 createHtmlElement(iface->child(i));
397
398 } break;
399
400 case QAccessible::PageTab:{
401 const QString text = iface->text(QAccessible::Name);
402 element = document.call<emscripten::val>("createElement", std::string("button"));
403 setAttribute(element, "role", "tab");
404 setAttribute(element, "title", text.toStdString());
405 addEventListener(element, "click");
406 } break;
407
408 case QAccessible::ScrollBar: {
409 const std::string valueString =
410 iface->valueInterface()->currentValue().toString().toStdString();
411 element = document.call<emscripten::val>("createElement", std::string("div"));
412 setAttribute(element, "role", "scrollbar");
413 setAttribute(element, "aria-valuenow", valueString);
414 addEventListener(element, "change");
415 } break;
416
417 case QAccessible::StaticText: {
418 element = document.call<emscripten::val>("createElement", std::string("div"));
419 } break;
420 case QAccessible::Dialog: {
421 element = document.call<emscripten::val>("createElement", std::string("dialog"));
422 }break;
423 case QAccessible::ToolBar:{
424 const QString text = iface->text(QAccessible::Name);
425 element = document.call<emscripten::val>("createElement", std::string("div"));
426 setAttribute(element, "role", "toolbar");
427 setAttribute(element, "title", text.toStdString());
428 addEventListener(element, "click");
429 }break;
430 case QAccessible::MenuItem:
431 case QAccessible::ButtonMenu: {
432 const QString text = iface->text(QAccessible::Name);
433 element = document.call<emscripten::val>("createElement", std::string("button"));
434 setAttribute(element, "role", "menuitem");
435 setAttribute(element, "title", text.toStdString());
436 addEventListener(element, "click");
437 }break;
438 case QAccessible::MenuBar:
439 case QAccessible::PopupMenu: {
440 const QString text = iface->text(QAccessible::Name);
441 element = document.call<emscripten::val>("createElement", std::string("div"));
442 setAttribute(element, "role", "menubar");
443 setAttribute(element, "title", text.toStdString());
444 m_elements[iface] = element;
445
446 for (int i = 0; i < iface->childCount(); ++i) {
447 emscripten::val childElement = createHtmlElement(iface->child(i));
448 setAttribute(childElement, "aria-owns", text.toStdString());
449 }
450 }break;
451 case QAccessible::EditableText: {
452 element = document.call<emscripten::val>("createElement", std::string("input"));
453 setAttribute(element, "type", "text");
454 setAttribute(element, "contenteditable", "true");
455 setAttribute(element, "readonly", iface->state().readOnly);
456 setProperty(element, "inputMode", "text");
457 } break;
458 default:
459 qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role();
460 element = document.call<emscripten::val>("createElement", std::string("div"));
461 }
462
463 addEventListener(element, "focus");
464 return element;
465
466 }();
467
468 m_elements[iface] = element;
469
470 setHtmlElementGeometry(iface);
471 setHtmlElementTextName(iface);
472 setHtmlElementDisabled(iface);
473 setHtmlElementVisibility(iface, !iface->state().invisible);
474 handleIdentifierUpdate(iface);
475 handleDescriptionChanged(iface);
476
477 linkToParent(iface);
478 // Link in child elements
479 for (int i = 0; i < iface->childCount(); ++i) {
480 if (!getHtmlElement(iface->child(i)).isUndefined())
481 linkToParent(iface->child(i));
482 }
483
484 return element;
485}
486
487void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface)
488{
489 Q_UNUSED(iface);
490 qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement";
491}
492
493emscripten::val QWasmAccessibility::getHtmlElement(QAccessibleInterface *iface)
494{
495 auto it = m_elements.find(iface);
496 if (it != m_elements.end())
497 return it.value();
498
499 return emscripten::val::undefined();
500}
501
502void QWasmAccessibility::repairLinks(QAccessibleInterface *iface)
503{
504 // relink any children that are linked to the wrong parent,
505 // This can be caused by a missing ParentChanged event.
506 bool moved = false;
507 for (int i = 0; i < iface->childCount(); ++i) {
508 const auto elementI = getHtmlElement(iface->child(i));
509 const auto containerI = getElementContainer(iface->child(i));
510
511 if (!elementI.isUndefined() &&
512 !containerI.isUndefined() &&
513 !elementI["parentElement"].isUndefined() &&
514 !elementI["parentElement"].isNull() &&
515 elementI["parentElement"] != containerI) {
516 moved = true;
517 break;
518 }
519 }
520 if (moved) {
521 for (int i = 0; i < iface->childCount(); ++i) {
522 const auto elementI = getHtmlElement(iface->child(i));
523 const auto containerI = getElementContainer(iface->child(i));
524 if (!elementI.isUndefined() && !containerI.isUndefined())
525 containerI.call<void>("appendChild", elementI);
526 }
527 }
528}
529
530void QWasmAccessibility::linkToParent(QAccessibleInterface *iface)
531{
532 emscripten::val element = getHtmlElement(iface);
533 emscripten::val container = getElementContainer(iface);
534 if (container.isUndefined() || element.isUndefined())
535 return;
536
537 // Make sure that we don't change the focused element
538 const auto activeElementBefore = emscripten::val::take_ownership(
539 getActiveElement_js(emscripten::val::undefined().as_handle()));
540
541
542 repairLinks(iface->parent());
543
544 emscripten::val next = emscripten::val::undefined();
545 const int thisIndex = iface->parent()->indexOfChild(iface);
546 Q_ASSERT(thisIndex >= 0 && thisIndex < iface->parent()->childCount());
547 for (int i = thisIndex + 1; i < iface->parent()->childCount(); ++i) {
548 const auto elementI = getHtmlElement(iface->parent()->child(i));
549 if (!elementI.isUndefined() &&
550 elementI["parentElement"] == container) {
551 next = elementI;
552 break;
553 }
554 }
555 if (next.isUndefined()) {
556 container.call<void>("appendChild", element);
557 } else {
558 container.call<void>("insertBefore", element, next);
559 }
560
561 const auto activeElementAfter = emscripten::val::take_ownership(
562 getActiveElement_js(emscripten::val::undefined().as_handle()));
563 if (activeElementBefore != activeElementAfter) {
564 if (!activeElementBefore.isUndefined() && !activeElementBefore.isNull())
565 activeElementBefore.call<void>("focus");
566 }
567}
568
569void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible)
570{
571 emscripten::val element = getHtmlElement(iface);
572
573 if (visible) {
574 setAttribute(element, "aria-hidden", false);
575 setAttribute(element, "tabindex", "");
576 } else {
577 setAttribute(element, "aria-hidden", true); // aria-hidden mean completely hidden; maybe some sort of soft-hidden should be used.
578 setAttribute(element, "tabindex", "-1");
579 }
580}
581
582void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface)
583{
584 const emscripten::val element = getHtmlElement(iface);
585
586 QRect windowGeometry = iface->rect();
587 if (iface->parent()) {
588 // Both iface and iface->parent returns geometry in screen coordinates
589 // We only want the relative coordinates, so the coordinate system does
590 // not matter as long as it is the same.
591 const QRect parentRect = iface->parent()->rect();
592 const QRect thisRect = iface->rect();
593 const QRect result(thisRect.topLeft() - parentRect.topLeft(), thisRect.size());
594 windowGeometry = result;
595 } else {
596 // Elements without a parent are not a part of the a11y tree, and don't
597 // have meaningful geometry.
598 Q_ASSERT(!getWindow(iface));
599 }
600 setHtmlElementGeometry(element, windowGeometry);
601}
602
603void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry)
604{
605 // Position the element using "position: absolute" in order to place
606 // it under the corresponding Qt element in the screen.
607 emscripten::val style = element["style"];
608 style.set("position", std::string("absolute"));
609 style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the
610 // screen element, but isn't
611 style.set("left", std::to_string(geometry.x()) + "px");
612 style.set("top", std::to_string(geometry.y()) + "px");
613 style.set("width", std::to_string(geometry.width()) + "px");
614 style.set("height", std::to_string(geometry.height()) + "px");
615}
616
617void QWasmAccessibility::setHtmlElementTextName(QAccessibleInterface *iface)
618{
619 const emscripten::val element = getHtmlElement(iface);
620 const QString name = iface->text(QAccessible::Name);
621 const QString value = iface->text(QAccessible::Value);
622
623 // A <div> cannot contain aria-label
624 if (iface->role() == QAccessible::StaticText)
625 setProperty(element, "innerText", name.toStdString());
626 else if (iface->role() == QAccessible::EditableText)
627 setProperty(element, "value", value.toStdString());
628 else
629 setAttribute(element, "aria-label", name.toStdString());
630}
631
632void QWasmAccessibility::setHtmlElementTextNameLE(QAccessibleInterface *iface)
633{
634 const emscripten::val element = getHtmlElement(iface);
635 QString value = iface->text(QAccessible::Value);
636 setProperty(element, "value", value.toStdString());
637}
638
639void QWasmAccessibility::setHtmlElementFocus(QAccessibleInterface *iface)
640{
641 const auto element = getHtmlElement(iface);
642 element.call<void>("focus");
643}
644
645void QWasmAccessibility::setHtmlElementDisabled(QAccessibleInterface *iface)
646{
647 auto element = getHtmlElement(iface);
648 setAttribute(element, "aria-disabled", iface->state().disabled);
649}
650
651void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event)
652{
653 switch (event->type()) {
654 case QAccessible::NameChanged: {
655 setHtmlElementTextName(event->accessibleInterface());
656 } break;
657 default:
658 qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type();
659 break;
660 }
661}
662
663void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event)
664{
665 switch (event->type()) {
666 case QAccessible::StateChanged: {
667 auto iface = event->accessibleInterface();
668 auto element = getHtmlElement(iface);
669 setAttribute(element, "readonly", iface->state().readOnly);
670 if (iface->state().passwordEdit)
671 setProperty(element, "type", "password");
672 else
673 setProperty(element, "type", "text");
674 } break;
675 case QAccessible::NameChanged: {
676 setHtmlElementTextName(event->accessibleInterface());
677 } break;
678 case QAccessible::ObjectShow:
679 case QAccessible::Focus: {
680 auto iface = event->accessibleInterface();
681 auto element = getHtmlElement(iface);
682 if (!element.isUndefined()) {
683 setAttribute(element, "readonly", iface->state().readOnly);
684 if (iface->state().passwordEdit)
685 setProperty(element, "type", "password");
686 else
687 setProperty(element, "type", "text");
688 }
689 setHtmlElementTextNameLE(iface);
690 } break;
691 case QAccessible::TextRemoved:
692 case QAccessible::TextInserted:
693 case QAccessible::TextCaretMoved: {
694 setHtmlElementTextNameLE(event->accessibleInterface());
695 } break;
696 default:
697 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
698 break;
699 }
700}
701
702void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event)
703{
704 QAccessibleInterface *iface = m_elements.key(event["target"]);
705
706 if (iface == nullptr) {
707 return;
708 } else {
709 QString eventType = QString::fromStdString(event["type"].as<std::string>());
710 const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface);
711
712 if (eventType == "focus") {
713 if (actionNames.contains(QAccessibleActionInterface::setFocusAction()))
714 iface->actionInterface()->doAction(QAccessibleActionInterface::setFocusAction());
715 } else if (actionNames.contains(QAccessibleActionInterface::pressAction())) {
716 iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction());
717 } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) {
718 iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction());
719 }
720 }
721}
722
723void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event)
724{
725 qCDebug(lcQpaAccessibility) << "TODO: implement handleButtonUpdate for event" << event->type();
726}
727
728void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event)
729{
730 switch (event->type()) {
731 case QAccessible::Focus:
732 case QAccessible::NameChanged: {
733 setHtmlElementTextName(event->accessibleInterface());
734 } break;
735 case QAccessible::StateChanged: {
736 QAccessibleInterface *accessible = event->accessibleInterface();
737 const emscripten::val element = getHtmlElement(accessible);
738 setAttribute(element, "checked", accessible->state().checked);
739 setProperty(element, "indeterminate", accessible->state().checkStateMixed);
740 } break;
741 default:
742 qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type();
743 break;
744 }
745}
746void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event)
747{
748 QAccessibleInterface *iface = event->accessibleInterface();
749 QString text = iface->text(QAccessible::Name);
750 QString desc = iface->text(QAccessible::Description);
751 switch (event->type()) {
752 case QAccessible::NameChanged:
753 case QAccessible::StateChanged:{
754 const emscripten::val element = getHtmlElement(iface);
755 setAttribute(element, "title", text.toStdString());
756 } break;
757 default:
758 qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type();
759 break;
760 }
761}
762void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event)
763{
764 QAccessibleInterface *iface = event->accessibleInterface();
765 QString text = iface->text(QAccessible::Name);
766 QString desc = iface->text(QAccessible::Description);
767 switch (event->type()) {
768 case QAccessible::Focus:
769 case QAccessible::NameChanged:
770 case QAccessible::MenuStart ://"TODO: To implement later
771 case QAccessible::StateChanged:{
772 const emscripten::val element = getHtmlElement(iface);
773 setAttribute(element, "title", text.toStdString());
774 } break;
775 case QAccessible::PopupMenuStart: {
776 if (iface->childCount() > 0) {
777 const auto childElement = getHtmlElement(iface->child(0));
778 childElement.call<void>("focus");
779 }
780 } break;
781 default:
782 qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type();
783 break;
784 }
785}
786void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) {
787
788 switch (event->type()) {
789 case QAccessible::NameChanged:
790 case QAccessible::Focus:
791 case QAccessible::DialogStart:
792 case QAccessible::StateChanged: {
793 setHtmlElementTextName(event->accessibleInterface());
794 } break;
795 default:
796 qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type();
797 break;
798 }
799}
800
801void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface)
802{
803 if (!iface)
804 return;
805
806 // We ignore toplevel windows which is categorized
807 // by getWindow(iface->parent()) != getWindow(iface)
808 const QWindow *window1 = getWindow(iface);
809 const QWindow *window0 = (iface->parent()) ? getWindow(iface->parent()) : nullptr;
810
811 if (window1 && window0 == window1) {
812 // Create html element for the interface, sync up properties.
813 bool exists = !getHtmlElement(iface).isUndefined();
814 if (!exists)
815 exists = !createHtmlElement(iface).isUndefined();
816
817 if (exists) {
818 linkToParent(iface);
819 setHtmlElementVisibility(iface, !iface->state().invisible);
820 setHtmlElementGeometry(iface);
821 setHtmlElementTextName(iface);
822 setHtmlElementDisabled(iface);
823 handleIdentifierUpdate(iface);
824 handleDescriptionChanged(iface);
825 }
826 }
827 for (int i = 0; i < iface->childCount(); ++i)
828 populateAccessibilityTree(iface->child(i));
829}
830
831void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event)
832{
833 switch (event->type()) {
834 case QAccessible::Focus:
835 case QAccessible::NameChanged: {
836 setHtmlElementTextName(event->accessibleInterface());
837 } break;
838 case QAccessible::StateChanged: {
839 QAccessibleInterface *accessible = event->accessibleInterface();
840 const emscripten::val element = getHtmlElement(accessible);
841 setAttribute(element, "checked", accessible->state().checked);
842 } break;
843 default:
844 qDebug() << "TODO: implement handleRadioButtonUpdate for event" << event->type();
845 break;
846 }
847}
848
849void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event)
850{
851 switch (event->type()) {
852 case QAccessible::ObjectCreated:
853 case QAccessible::StateChanged: {
854 } break;
855 case QAccessible::Focus:
856 case QAccessible::NameChanged: {
857 setHtmlElementTextName(event->accessibleInterface());
858 } break;
859 case QAccessible::ValueChanged: {
860 QAccessibleInterface *accessible = event->accessibleInterface();
861 const emscripten::val element = getHtmlElement(accessible);
862 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
863 setProperty(element, "value", valueString);
864 } break;
865 default:
866 qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type();
867 break;
868 }
869}
870
871void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event)
872{
873 switch (event->type()) {
874 case QAccessible::ObjectCreated:
875 case QAccessible::StateChanged: {
876 } break;
877 case QAccessible::Focus:
878 case QAccessible::NameChanged: {
879 setHtmlElementTextName(event->accessibleInterface());
880 } break;
881 case QAccessible::ValueChanged: {
882 QAccessibleInterface *accessible = event->accessibleInterface();
883 const emscripten::val element = getHtmlElement(accessible);
884 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
885 setProperty(element, "value", valueString);
886 } break;
887 default:
888 qDebug() << "TODO: implement handleSliderUpdate for event" << event->type();
889 break;
890 }
891}
892
893void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event)
894{
895 switch (event->type()) {
896 case QAccessible::Focus:
897 case QAccessible::NameChanged: {
898 setHtmlElementTextName(event->accessibleInterface());
899 } break;
900 case QAccessible::ValueChanged: {
901 QAccessibleInterface *accessible = event->accessibleInterface();
902 const emscripten::val element = getHtmlElement(accessible);
903 std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString();
904 setAttribute(element, "aria-valuenow", valueString);
905 } break;
906 default:
907 qDebug() << "TODO: implement handleSliderUpdate for event" << event->type();
908 break;
909 }
910
911}
912
913void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event)
914{
915 switch (event->type()) {
916 case QAccessible::NameChanged: {
917 setHtmlElementTextName(event->accessibleInterface());
918 } break;
919 case QAccessible::Focus: {
920 setHtmlElementTextName(event->accessibleInterface());
921 } break;
922 default:
923 qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type();
924 break;
925 }
926}
927
928void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event)
929{
930 switch (event->type()) {
931 case QAccessible::NameChanged: {
932 setHtmlElementTextName(event->accessibleInterface());
933 } break;
934 case QAccessible::Focus: {
935 setHtmlElementTextName(event->accessibleInterface());
936 } break;
937 default:
938 qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type();
939 break;
940 }
941}
942
943void QWasmAccessibility::handleIdentifierUpdate(QAccessibleInterface *iface)
944{
945 const emscripten::val element = getHtmlElement(iface);
946 QString id = iface->text(QAccessible::Identifier).replace(" ", "_");
947 if (id.isEmpty() && iface->role() == QAccessible::PageTabList) {
948 std::ostringstream oss;
949 oss << "tabList_0x" << (void *)iface;
950 id = QString::fromUtf8(oss.str());
951 }
952
953 setAttribute(element, "id", id.toStdString());
954 if (!id.isEmpty()) {
955 if (iface->role() == QAccessible::PageTabList) {
956 for (int i = 0; i < iface->childCount(); ++i) {
957 const auto child = getHtmlElement(iface->child(i));
958 setAttribute(child, "aria-owns", id.toStdString());
959 }
960 }
961 }
962}
963
964void QWasmAccessibility::handleDescriptionChanged(QAccessibleInterface *iface)
965{
966 const auto desc = iface->text(QAccessible::Description).toStdString();
967 auto element = getHtmlElement(iface);
968 auto container = getDescribedByContainer(iface);
969 if (!container.isUndefined()) {
970 std::ostringstream oss;
971 oss << "dbid_" << (void *)iface;
972 auto id = oss.str();
973
974 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
975 if (desc.empty()) {
976 if (!describedBy.isUndefined() && !describedBy.isNull()) {
977 container.call<void>("removeChild", describedBy);
978 }
979 setAttribute(element, "aria-describedby", "");
980 } else {
981 if (describedBy.isUndefined() || describedBy.isNull()) {
982 auto document = getDocument(container);
983 describedBy = document.call<emscripten::val>("createElement", std::string("p"));
984
985 container.call<void>("appendChild", describedBy);
986 }
987 setAttribute(describedBy, "id", id);
988 setAttribute(describedBy, "aria-hidden", true);
989 setAttribute(element, "aria-describedby", id);
990 setProperty(describedBy, "innerText", desc);
991 }
992 }
993}
994
995void QWasmAccessibility::createObject(QAccessibleInterface *iface)
996{
997 if (getHtmlElement(iface).isUndefined())
998 createHtmlElement(iface);
999}
1000
1001void QWasmAccessibility::removeObject(QAccessibleInterface *iface)
1002{
1003 // Do not dereference the object pointer. it might be invalid.
1004 // Do not dereference the iface either, it refers to the object.
1005 // Note: we may remove children, making them have parentElement undefined
1006 // so we need to check for parentElement here. We do assume that removeObject
1007 // is called on all objects, just not in any predefined order.
1008 const auto it = m_elements.find(iface);
1009 if (it != m_elements.end()) {
1010 auto element = it.value();
1011 auto container = getDescribedByContainer(iface);
1012 if (!container.isUndefined()) {
1013 std::ostringstream oss;
1014 oss << "dbid_" << (void *)iface;
1015 auto id = oss.str();
1016 auto describedBy = container.call<emscripten::val>("querySelector", "#" + std::string(id));
1017 if (!describedBy.isUndefined() && !describedBy.isNull() &&
1018 !describedBy["parentElement"].isUndefined() && !describedBy["parentElement"].isNull())
1019 describedBy["parentElement"].call<void>("removeChild", describedBy);
1020 }
1021 if (!element["parentElement"].isUndefined() && !element["parentElement"].isNull())
1022 element["parentElement"].call<void>("removeChild", element);
1023 m_elements.erase(it);
1024 }
1025}
1026
1027void QWasmAccessibility::unlinkParentForChildren(QAccessibleInterface *iface)
1028{
1029 auto element = getHtmlElement(iface);
1030 if (!element.isUndefined()) {
1031 auto oldContainer = element["parentElement"];
1032 auto newContainer = getElementContainer(iface);
1033 if (!oldContainer.isUndefined() &&
1034 !oldContainer.isNull() &&
1035 oldContainer != newContainer) {
1036 oldContainer.call<void>("removeChild", element);
1037 }
1038 }
1039 for (int i = 0; i < iface->childCount(); ++i)
1040 unlinkParentForChildren(iface->child(i));
1041}
1042
1043void QWasmAccessibility::relinkParentForChildren(QAccessibleInterface *iface)
1044{
1045 auto element = getHtmlElement(iface);
1046 if (!element.isUndefined()) {
1047 if (element["parentElement"].isUndefined() ||
1048 element["parentElement"].isNull()) {
1049 linkToParent(iface);
1050 }
1051 }
1052 for (int i = 0; i < iface->childCount(); ++i)
1053 relinkParentForChildren(iface->child(i));
1054}
1055
1056void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event)
1057{
1058 if (!m_accessibilityEnabled)
1059 return;
1060
1061 QAccessibleInterface *iface = event->accessibleInterface();
1062 if (!iface) {
1063 qWarning() << "notifyAccessibilityUpdate with null a11y interface" << event->type() << event->object();
1064 return;
1065 }
1066
1067 // Handle event types that creates/removes objects.
1068 switch (event->type()) {
1069 case QAccessible::ObjectCreated:
1070 // Do nothing, there are too many changes to the interface
1071 // before ObjectShow is called
1072 return;
1073
1074 case QAccessible::ObjectDestroyed:
1075 // The object might be under destruction, and the interface is not valid
1076 // but we can look at the pointer,
1077 removeObject(iface);
1078 return;
1079
1080 case QAccessible::ObjectShow: // We do not get ObjectCreated from widgets, we get ObjectShow
1081 createObject(iface);
1082 break;
1083
1084 case QAccessible::ParentChanged:
1085 unlinkParentForChildren(iface);
1086 relinkParentForChildren(iface);
1087 break;
1088
1089 default:
1090 break;
1091 };
1092
1093 if (getHtmlElement(iface).isUndefined())
1094 return;
1095
1096 // Handle some common event types. See
1097 // https://doc.qt.io/qt-5/qaccessible.html#Event-enum
1098 switch (event->type()) {
1099 case QAccessible::StateChanged: {
1100 QAccessibleStateChangeEvent *stateChangeEvent = (QAccessibleStateChangeEvent *)event;
1101 if (stateChangeEvent->changedStates().disabled)
1102 setHtmlElementDisabled(iface);
1103 } break;
1104
1105 case QAccessible::DescriptionChanged:
1106 handleDescriptionChanged(iface);
1107 return;
1108
1109 case QAccessible::Focus:
1110 // We do not get all callbacks for the geometry
1111 // hence we update here as well.
1112 setHtmlElementGeometry(iface);
1113 setHtmlElementFocus(iface);
1114 break;
1115
1116 case QAccessible::IdentifierChanged:
1117 handleIdentifierUpdate(iface);
1118 return;
1119
1120 case QAccessible::ObjectShow:
1121 linkToParent(iface);
1122 setHtmlElementVisibility(iface, true);
1123
1124 // Sync up properties on show;
1125 setHtmlElementGeometry(iface);
1126 setHtmlElementTextName(iface);
1127 break;
1128
1129 case QAccessible::ObjectHide:
1130 linkToParent(iface);
1131 setHtmlElementVisibility(iface, false);
1132 return;
1133
1134 case QAccessible::LocationChanged:
1135 setHtmlElementGeometry(iface);
1136 return;
1137
1138 // TODO: maybe handle more types here
1139 default:
1140 break;
1141 };
1142
1143 // Switch on interface role, see
1144 // https://doc.qt.io/qt-5/qaccessibleinterface.html#role
1145 switch (iface->role()) {
1146 case QAccessible::StaticText:
1147 handleStaticTextUpdate(event);
1148 break;
1149 case QAccessible::Button:
1150 handleStaticTextUpdate(event);
1151 break;
1152 case QAccessible::CheckBox:
1153 handleCheckBoxUpdate(event);
1154 break;
1155 case QAccessible::EditableText:
1156 handleLineEditUpdate(event);
1157 break;
1158 case QAccessible::Dialog:
1159 handleDialogUpdate(event);
1160 break;
1161 case QAccessible::MenuItem:
1162 case QAccessible::MenuBar:
1163 case QAccessible::PopupMenu:
1164 handleMenuUpdate(event);
1165 break;
1166 case QAccessible::ToolBar:
1167 case QAccessible::ButtonMenu:
1168 handleToolUpdate(event);
1169 case QAccessible::RadioButton:
1170 handleRadioButtonUpdate(event);
1171 break;
1172 case QAccessible::SpinBox:
1173 handleSpinBoxUpdate(event);
1174 break;
1175 case QAccessible::Slider:
1176 handleSliderUpdate(event);
1177 break;
1178 case QAccessible::PageTab:
1179 handlePageTabUpdate(event);
1180 break;
1181 case QAccessible::PageTabList:
1182 handlePageTabListUpdate(event);
1183 break;
1184 case QAccessible::ScrollBar:
1185 handleScrollBarUpdate(event);
1186 break;
1187 default:
1188 qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role();
1189 };
1190}
1191
1192void QWasmAccessibility::setRootObject(QObject *root)
1193{
1194 m_rootObject = root;
1195}
1196
1197void QWasmAccessibility::initialize()
1198{
1199
1200}
1201
1202void QWasmAccessibility::cleanup()
1203{
1204
1205}
1206
1207#endif // QT_CONFIG(accessibility)
void QWasmAccessibilityEnable()