1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickaccessibleattached_p.h"
5
6#if QT_CONFIG(accessibility)
7
8#include <QtQml/qqmlinfo.h>
9
10#include "private/qquickitem_p.h"
11
12QT_BEGIN_NAMESPACE
13
14/*!
15 \qmltype Accessible
16 \nativetype QQuickAccessibleAttached
17 \brief Enables accessibility of QML items.
18
19 \inqmlmodule QtQuick
20 \ingroup qtquick-visual-utility
21 \ingroup accessibility
22
23 This class is part of the \l {Accessibility for Qt Quick Applications}.
24
25 Items the user interacts with or that give information to the user
26 need to expose their information to the accessibility framework.
27 Then assistive tools can make use of that information to enable
28 users to interact with the application in various ways.
29 This enables Qt Quick applications to be used with screen-readers for example.
30
31 The most important properties are \l name, \l description and \l role.
32
33 Example implementation of a simple button:
34 \qml
35 Rectangle {
36 id: myButton
37 Text {
38 id: label
39 text: "next"
40 }
41 Accessible.role: Accessible.Button
42 Accessible.name: label.text
43 Accessible.description: "shows the next page"
44 Accessible.onPressAction: {
45 // do a button click
46 }
47 }
48 \endqml
49 The \l role is set to \c Button to indicate the type of control.
50 \l {Accessible::}{name} is the most important information and bound to the text on the button.
51 The name is a short and consise description of the control and should reflect the visual label.
52 In this case it is not clear what the button does with the name only, so \l description contains
53 an explanation.
54 There is also a signal handler \l {Accessible::pressAction}{Accessible.pressAction} which can be invoked by assistive tools to trigger
55 the button. This signal handler needs to have the same effect as tapping or clicking the button would have.
56
57 \sa Accessibility
58*/
59
60/*!
61 \qmlproperty string QtQuick::Accessible::name
62
63 This property sets an accessible name.
64 For a button for example, this should have a binding to its text.
65 In general this property should be set to a simple and concise
66 but human readable name. Do not include the type of control
67 you want to represent but just the name.
68*/
69
70/*!
71 \qmlproperty string QtQuick::Accessible::description
72
73 This property sets an accessible description.
74 Similar to the name it describes the item. The description
75 can be a little more verbose and tell what the item does,
76 for example the functionality of the button it describes.
77*/
78
79/*!
80 \qmlproperty enumeration QtQuick::Accessible::role
81
82 This flags sets the semantic type of the widget.
83 A button for example would have "Button" as type.
84 The value must be one of \l QAccessible::Role.
85
86 Some roles have special semantics.
87 In order to implement check boxes for example a "checked" property is expected.
88
89 \table
90 \header
91 \li \b {Role}
92 \li \b {Properties and signals}
93 \li \b {Explanation}
94 \row
95 \li All interactive elements
96 \li \l focusable and \l focused
97 \li All elements that the user can interact with should have focusable set to \c true and
98 set \c focus to \c true when they have the focus. This is important even for applications
99 that run on touch-only devices since screen readers often implement a virtual focus that
100 can be moved from item to item.
101 \row
102 \li Button, CheckBox, RadioButton
103 \li \l {Accessible::pressAction}{Accessible.pressAction}
104 \li A button should have a signal handler with the name \c onPressAction.
105 This signal may be emitted by an assistive tool such as a screen-reader.
106 The implementation needs to behave the same as a mouse click or tap on the button.
107 \row
108 \li CheckBox, RadioButton
109 \li \l checkable, \l checked, \l {Accessible::toggleAction}{Accessible.toggleAction}
110
111 \li The check state of the check box. Updated on Press, Check and Uncheck actions.
112 \row
113 \li Slider, SpinBox, Dial, ScrollBar
114 \li \c value, \c minimumValue, \c maximumValue, \c stepSize
115 \li These properties reflect the state and possible values for the elements.
116 \row
117 \li Slider, SpinBox, Dial, ScrollBar
118 \li \l {Accessible::increaseAction}{Accessible.increaseAction}, \l {Accessible::decreaseAction}{Accessible.decreaseAction}
119 \li Actions to increase and decrease the value of the element.
120 \endtable
121*/
122
123/*!
124 \qmlproperty string QtQuick::Accessible::id
125
126 This property sets an identifier for the object.
127 It can be used to provide stable identifiers to UI tests.
128 By default, the identifier is set to the ID of the QML object.
129 If the ID is not set the default of \l QAccessible::Identifier is used.
130*/
131
132/*! \qmlproperty bool QtQuick::Accessible::focusable
133 \brief This property holds whether this item is focusable.
134
135 By default, this property is \c false except for items where the role is one of
136 \c CheckBox, \c RadioButton, \c Button, \c MenuItem, \c PageTab, \c EditableText, \c SpinBox, \c ComboBox,
137 \c Terminal or \c ScrollBar.
138 \sa focused
139*/
140/*! \qmlproperty bool QtQuick::Accessible::focused
141 \brief This property holds whether this item currently has the active focus.
142
143 By default, this property is \c false, but it will return \c true for items that
144 have \l QQuickItem::hasActiveFocus() returning \c true.
145 \sa focusable
146*/
147/*! \qmlproperty bool QtQuick::Accessible::checkable
148 \brief This property holds whether this item is checkable (like a check box or some buttons).
149
150 By default this property is \c false.
151 \sa checked
152*/
153/*! \qmlproperty bool QtQuick::Accessible::checked
154 \brief This property holds whether this item is currently checked.
155
156 By default this property is \c false.
157 \sa checkable
158*/
159/*! \qmlproperty bool QtQuick::Accessible::editable
160 \brief This property holds whether this item has editable text.
161
162 By default this property is \c false.
163*/
164/*! \qmlproperty bool QtQuick::Accessible::searchEdit
165 \brief This property holds whether this item is input for a search query.
166 This property will only affect editable text.
167
168 By default this property is \c false.
169*/
170/*! \qmlproperty bool QtQuick::Accessible::ignored
171 \brief This property holds whether this item should be ignored by the accessibility framework.
172
173 Sometimes an item is part of a group of items that should be treated as one. For example two labels might be
174 visually placed next to each other, but separate items. For accessibility purposes they should be treated as one
175 and thus they are represented by a third invisible item with the right geometry.
176
177 For example a speed display adds "m/s" as a smaller label:
178 \qml
179 Row {
180 Label {
181 id: speedLabel
182 text: "Speed: 5"
183 Accessible.ignored: true
184 }
185 Label {
186 text: qsTr("m/s")
187 Accessible.ignored: true
188 }
189 Accessible.role: Accessible.StaticText
190 Accessible.name: speedLabel.text + " meters per second"
191 }
192 \endqml
193
194 \since 5.4
195 By default this property is \c false.
196*/
197/*! \qmlproperty bool QtQuick::Accessible::multiLine
198 \brief This property holds whether this item has multiple text lines.
199
200 By default this property is \c false.
201*/
202/*! \qmlproperty bool QtQuick::Accessible::readOnly
203 \brief This property indicates that a text field is read only.
204
205 It is relevant when the role is \l QAccessible::EditableText and set to be read-only.
206 By default this property is \c false.
207*/
208/*! \qmlproperty bool QtQuick::Accessible::selected
209 \brief This property holds whether this item is selected.
210
211 By default this property is \c false.
212 \sa selectable
213*/
214/*! \qmlproperty bool QtQuick::Accessible::selectable
215 \brief This property holds whether this item can be selected.
216
217 By default this property is \c false.
218 \sa selected
219*/
220/*! \qmlproperty bool QtQuick::Accessible::pressed
221 \brief This property holds whether this item is pressed (for example a button during a mouse click).
222
223 By default this property is \c false.
224*/
225/*! \qmlproperty bool QtQuick::Accessible::checkStateMixed
226 \brief This property holds whether this item is in the partially checked state.
227
228 By default this property is \c false.
229 \sa checked, checkable
230*/
231/*! \qmlproperty bool QtQuick::Accessible::defaultButton
232 \brief This property holds whether this item is the default button of a dialog.
233
234 By default this property is \c false.
235*/
236/*! \qmlproperty bool QtQuick::Accessible::passwordEdit
237 \brief This property holds whether this item is a password text edit.
238
239 By default this property is \c false.
240*/
241/*! \qmlproperty bool QtQuick::Accessible::selectableText
242 \brief This property holds whether this item contains selectable text.
243
244 By default this property is \c false.
245*/
246
247/*!
248 \qmlsignal QtQuick::Accessible::pressAction()
249
250 This signal is emitted when a press action is received from an assistive tool such as a screen-reader.
251*/
252/*!
253 \qmlsignal QtQuick::Accessible::toggleAction()
254
255 This signal is emitted when a toggle action is received from an assistive tool such as a screen-reader.
256*/
257/*!
258 \qmlsignal QtQuick::Accessible::increaseAction()
259
260 This signal is emitted when a increase action is received from an assistive tool such as a screen-reader.
261*/
262/*!
263 \qmlsignal QtQuick::Accessible::decreaseAction()
264
265 This signal is emitted when a decrease action is received from an assistive tool such as a screen-reader.
266*/
267/*!
268 \qmlsignal QtQuick::Accessible::scrollUpAction()
269
270 This signal is emitted when a scroll up action is received from an assistive tool such as a screen-reader.
271*/
272/*!
273 \qmlsignal QtQuick::Accessible::scrollDownAction()
274
275 This signal is emitted when a scroll down action is received from an assistive tool such as a screen-reader.
276*/
277/*!
278 \qmlsignal QtQuick::Accessible::scrollLeftAction()
279
280 This signal is emitted when a scroll left action is received from an assistive tool such as a screen-reader.
281*/
282/*!
283 \qmlsignal QtQuick::Accessible::scrollRightAction()
284
285 This signal is emitted when a scroll right action is received from an assistive tool such as a screen-reader.
286*/
287/*!
288 \qmlsignal QtQuick::Accessible::previousPageAction()
289
290 This signal is emitted when a previous page action is received from an assistive tool such as a screen-reader.
291*/
292/*!
293 \qmlsignal QtQuick::Accessible::nextPageAction()
294
295 This signal is emitted when a next page action is received from an assistive tool such as a screen-reader.
296*/
297
298QMetaMethod QQuickAccessibleAttached::sigPress;
299QMetaMethod QQuickAccessibleAttached::sigToggle;
300QMetaMethod QQuickAccessibleAttached::sigIncrease;
301QMetaMethod QQuickAccessibleAttached::sigDecrease;
302QMetaMethod QQuickAccessibleAttached::sigScrollUp;
303QMetaMethod QQuickAccessibleAttached::sigScrollDown;
304QMetaMethod QQuickAccessibleAttached::sigScrollLeft;
305QMetaMethod QQuickAccessibleAttached::sigScrollRight;
306QMetaMethod QQuickAccessibleAttached::sigPreviousPage;
307QMetaMethod QQuickAccessibleAttached::sigNextPage;
308
309QQuickAccessibleAttached::QQuickAccessibleAttached(QObject *parent)
310 : QObject(parent), m_role(QAccessible::NoRole)
311{
312 Q_ASSERT(parent);
313 // Enable accessibility for items with accessible content. This also
314 // enables accessibility for the ancestors of such items.
315 auto item = qobject_cast<QQuickItem *>(o: parent);
316 if (item) {
317 item->d_func()->setAccessible();
318 } else {
319 const QLatin1StringView className(QQmlData::ensurePropertyCache(object: parent)->firstCppMetaObject()->className());
320 if (className != QLatin1StringView("QQuickAction")) {
321 qmlWarning(me: parent) << "Accessible must be attached to an Item or an Action";
322 return;
323 }
324 }
325 QAccessibleEvent ev(parent, QAccessible::ObjectCreated);
326 QAccessible::updateAccessibility(event: &ev);
327
328 if (const QMetaObject *pmo = parent->metaObject()) {
329 auto connectPropertyChangeSignal = [parent, pmo, this](
330 const char *propertyName, const char *signalName, int slotIndex)
331 {
332 // basically does this:
333 // if the parent has the property \a propertyName with the associated \a signalName:
334 // connect(parent, signalName, this, slotIndex)
335
336 // Note that we explicitly want to only connect to standard property/signal naming
337 // convention: "value" & "valueChanged"
338 // (e.g. avoid a compound property with e.g. a signal notifier named "updated()")
339 int idxProperty = pmo->indexOfProperty(name: propertyName);
340 if (idxProperty != -1) {
341 const QMetaProperty property = pmo->property(index: idxProperty);
342 const QMetaMethod signal = property.notifySignal();
343 if (signal.name() == signalName)
344 QMetaObject::connect(sender: parent, signal_index: signal.methodIndex(), receiver: this, method_index: slotIndex);
345 }
346 return;
347 };
348 const QMetaObject &smo = staticMetaObject;
349 static const int valueChangedIndex = smo.indexOfSlot(slot: "valueChanged()");
350 connectPropertyChangeSignal("value", "valueChanged", valueChangedIndex);
351
352 static const int cursorPositionChangedIndex = smo.indexOfSlot(slot: "cursorPositionChanged()");
353 connectPropertyChangeSignal("cursorPosition", "cursorPositionChanged",
354 cursorPositionChangedIndex);
355 }
356
357 if (!sigPress.isValid()) {
358 sigPress = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::pressAction);
359 sigToggle = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::toggleAction);
360 sigIncrease = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::increaseAction);
361 sigDecrease = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::decreaseAction);
362 sigScrollUp = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollUpAction);
363 sigScrollDown = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollDownAction);
364 sigScrollLeft = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollLeftAction);
365 sigScrollRight = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollRightAction);
366 sigPreviousPage = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::previousPageAction);
367 sigNextPage= QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::nextPageAction);
368 }
369}
370
371QQuickAccessibleAttached::~QQuickAccessibleAttached()
372{
373}
374
375void QQuickAccessibleAttached::setRole(QAccessible::Role role)
376{
377 if (role != m_role) {
378 m_role = role;
379 Q_EMIT roleChanged();
380 // There is no way to signify role changes at the moment.
381 // QAccessible::updateAccessibility(parent(), 0, QAccessible::);
382
383 switch (role) {
384 case QAccessible::CheckBox:
385 case QAccessible::RadioButton:
386 if (!m_stateExplicitlySet.focusable)
387 m_state.focusable = true;
388 if (!m_stateExplicitlySet.checkable)
389 m_state.checkable = true;
390 break;
391 case QAccessible::Button:
392 case QAccessible::MenuItem:
393 case QAccessible::PageTab:
394 case QAccessible::SpinBox:
395 case QAccessible::ComboBox:
396 case QAccessible::Terminal:
397 case QAccessible::ScrollBar:
398 if (!m_stateExplicitlySet.focusable)
399 m_state.focusable = true;
400 break;
401 case QAccessible::EditableText:
402 if (!m_stateExplicitlySet.editable)
403 m_state.editable = true;
404 if (!m_stateExplicitlySet.focusable)
405 m_state.focusable = true;
406 break;
407 case QAccessible::StaticText:
408 if (!m_stateExplicitlySet.readOnly)
409 m_state.readOnly = true;
410 if (!m_stateExplicitlySet.focusable)
411 m_state.focusable = true;
412 break;
413 default:
414 break;
415 }
416 }
417}
418
419bool QQuickAccessibleAttached::wasNameExplicitlySet() const
420{
421 return m_nameExplicitlySet;
422}
423
424// Allows types to attach an accessible name to an item as a convenience,
425// so long as the user hasn't done so themselves.
426void QQuickAccessibleAttached::setNameImplicitly(const QString &name)
427{
428 setName(name);
429 m_nameExplicitlySet = false;
430}
431
432QQuickAccessibleAttached *QQuickAccessibleAttached::qmlAttachedProperties(QObject *obj)
433{
434 return new QQuickAccessibleAttached(obj);
435}
436
437bool QQuickAccessibleAttached::ignored() const
438{
439 auto item = qobject_cast<QQuickItem *>(o: parent());
440 return item ? !item->d_func()->isAccessible : false;
441}
442
443void QQuickAccessibleAttached::setIgnored(bool ignored)
444{
445 auto item = qobject_cast<QQuickItem *>(o: parent());
446 if (item && this->ignored() != ignored) {
447 item->d_func()->isAccessible = !ignored;
448 emit ignoredChanged();
449 }
450}
451
452bool QQuickAccessibleAttached::doAction(const QString &actionName)
453{
454 QMetaMethod *sig = nullptr;
455 if (actionName == QAccessibleActionInterface::pressAction())
456 sig = &sigPress;
457 else if (actionName == QAccessibleActionInterface::toggleAction())
458 sig = &sigToggle;
459 else if (actionName == QAccessibleActionInterface::increaseAction())
460 sig = &sigIncrease;
461 else if (actionName == QAccessibleActionInterface::decreaseAction())
462 sig = &sigDecrease;
463 else if (actionName == QAccessibleActionInterface::scrollUpAction())
464 sig = &sigScrollUp;
465 else if (actionName == QAccessibleActionInterface::scrollDownAction())
466 sig = &sigScrollDown;
467 else if (actionName == QAccessibleActionInterface::scrollLeftAction())
468 sig = &sigScrollLeft;
469 else if (actionName == QAccessibleActionInterface::scrollRightAction())
470 sig = &sigScrollRight;
471 else if (actionName == QAccessibleActionInterface::previousPageAction())
472 sig = &sigPreviousPage;
473 else if (actionName == QAccessibleActionInterface::nextPageAction())
474 sig = &sigNextPage;
475 if (sig && isSignalConnected(signal: *sig)) {
476 bool ret = false;
477 if (m_proxying)
478 ret = sig->invoke(obj: m_proxying);
479 if (!ret)
480 ret = sig->invoke(obj: this);
481 return ret;
482 }
483 return false;
484}
485
486void QQuickAccessibleAttached::availableActions(QStringList *actions) const
487{
488 if (isSignalConnected(signal: sigPress))
489 actions->append(t: QAccessibleActionInterface::pressAction());
490 if (isSignalConnected(signal: sigToggle))
491 actions->append(t: QAccessibleActionInterface::toggleAction());
492 if (isSignalConnected(signal: sigIncrease))
493 actions->append(t: QAccessibleActionInterface::increaseAction());
494 if (isSignalConnected(signal: sigDecrease))
495 actions->append(t: QAccessibleActionInterface::decreaseAction());
496 if (isSignalConnected(signal: sigScrollUp))
497 actions->append(t: QAccessibleActionInterface::scrollUpAction());
498 if (isSignalConnected(signal: sigScrollDown))
499 actions->append(t: QAccessibleActionInterface::scrollDownAction());
500 if (isSignalConnected(signal: sigScrollLeft))
501 actions->append(t: QAccessibleActionInterface::scrollLeftAction());
502 if (isSignalConnected(signal: sigScrollRight))
503 actions->append(t: QAccessibleActionInterface::scrollRightAction());
504 if (isSignalConnected(signal: sigPreviousPage))
505 actions->append(t: QAccessibleActionInterface::previousPageAction());
506 if (isSignalConnected(signal: sigNextPage))
507 actions->append(t: QAccessibleActionInterface::nextPageAction());
508}
509
510QString QQuickAccessibleAttached::stripHtml(const QString &html)
511{
512#ifndef QT_NO_TEXTHTMLPARSER
513 QTextDocument doc;
514 doc.setHtml(html);
515 return doc.toPlainText();
516#else
517 return html;
518#endif
519}
520
521void QQuickAccessibleAttached::setProxying(QQuickAccessibleAttached *proxying)
522{
523 if (proxying == m_proxying)
524 return;
525
526 const QMetaObject &mo = staticMetaObject;
527 if (m_proxying) {
528 // We disconnect all signals from the proxy into this object
529 auto mo = m_proxying->metaObject();
530 auto propertyCache = QQmlData::ensurePropertyCache(object: m_proxying);
531 for (int signalIndex = propertyCache->signalOffset();
532 signalIndex < propertyCache->signalCount(); ++signalIndex) {
533 const QMetaMethod m = mo->method(index: propertyCache->signal(index: signalIndex)->coreIndex());
534 Q_ASSERT(m.methodType() == QMetaMethod::Signal);
535 if (m.methodType() != QMetaMethod::Signal)
536 continue;
537
538 disconnect(sender: m_proxying, signal: m, receiver: this, member: m);
539 }
540 }
541
542 m_proxying = proxying;
543
544 if (m_proxying) {
545 // We connect all signals from the proxy into this object
546 auto propertyCache = QQmlData::ensurePropertyCache(object: m_proxying);
547 auto mo = m_proxying->metaObject();
548 for (int signalIndex = propertyCache->signalOffset();
549 signalIndex < propertyCache->signalCount(); ++signalIndex) {
550 const QMetaMethod m = mo->method(index: propertyCache->signal(index: signalIndex)->coreIndex());
551 Q_ASSERT(m.methodType() == QMetaMethod::Signal);
552 connect(sender: proxying, signal: m, receiver: this, method: m);
553 }
554 }
555
556 // We check all properties
557 for (int prop = mo.propertyOffset(); prop < mo.propertyCount(); ++prop) {
558 const QMetaProperty p = mo.property(index: prop);
559 if (!p.hasNotifySignal()) {
560 continue;
561 }
562
563 const QMetaMethod signal = p.notifySignal();
564 if (signal.parameterCount() == 0)
565 signal.invoke(obj: this);
566 else
567 signal.invoke(obj: this, Q_ARG(bool, p.read(this).toBool()));
568 }
569}
570
571/*!
572 * \since 6.8
573 * Issues an announcement event with a \a message with politeness \a politeness.
574 *
575 * \sa QAccessibleAnnouncementEvent
576 */
577void QQuickAccessibleAttached::announce(const QString &message, QAccessible::AnnouncementPoliteness politeness)
578{
579 QAccessibleAnnouncementEvent event(parent(), message);
580 event.setPoliteness(politeness);
581 QAccessible::updateAccessibility(event: &event);
582}
583
584QT_END_NAMESPACE
585
586#include "moc_qquickaccessibleattached_p.cpp"
587
588#endif
589

source code of qtdeclarative/src/quick/items/qquickaccessibleattached.cpp