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 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | /*! |
15 | \qmltype Accessible |
16 | \instantiates 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 | /*! \qmlproperty bool QtQuick::Accessible::focusable |
124 | \brief This property holds whether this item is focusable. |
125 | |
126 | By default, this property is \c false except for items where the role is one of |
127 | \c CheckBox, \c RadioButton, \c Button, \c MenuItem, \c PageTab, \c EditableText, \c SpinBox, \c ComboBox, |
128 | \c Terminal or \c ScrollBar. |
129 | \sa focused |
130 | */ |
131 | /*! \qmlproperty bool QtQuick::Accessible::focused |
132 | \brief This property holds whether this item currently has the active focus. |
133 | |
134 | By default, this property is \c false, but it will return \c true for items that |
135 | have \l QQuickItem::hasActiveFocus() returning \c true. |
136 | \sa focusable |
137 | */ |
138 | /*! \qmlproperty bool QtQuick::Accessible::checkable |
139 | \brief This property holds whether this item is checkable (like a check box or some buttons). |
140 | |
141 | By default this property is \c false. |
142 | \sa checked |
143 | */ |
144 | /*! \qmlproperty bool QtQuick::Accessible::checked |
145 | \brief This property holds whether this item is currently checked. |
146 | |
147 | By default this property is \c false. |
148 | \sa checkable |
149 | */ |
150 | /*! \qmlproperty bool QtQuick::Accessible::editable |
151 | \brief This property holds whether this item has editable text. |
152 | |
153 | By default this property is \c false. |
154 | */ |
155 | /*! \qmlproperty bool QtQuick::Accessible::searchEdit |
156 | \brief This property holds whether this item is input for a search query. |
157 | This property will only affect editable text. |
158 | |
159 | By default this property is \c false. |
160 | */ |
161 | /*! \qmlproperty bool QtQuick::Accessible::ignored |
162 | \brief This property holds whether this item should be ignored by the accessibility framework. |
163 | |
164 | Sometimes an item is part of a group of items that should be treated as one. For example two labels might be |
165 | visually placed next to each other, but separate items. For accessibility purposes they should be treated as one |
166 | and thus they are represented by a third invisible item with the right geometry. |
167 | |
168 | For example a speed display adds "m/s" as a smaller label: |
169 | \qml |
170 | Row { |
171 | Label { |
172 | id: speedLabel |
173 | text: "Speed: 5" |
174 | Accessible.ignored: true |
175 | } |
176 | Label { |
177 | text: qsTr("m/s") |
178 | Accessible.ignored: true |
179 | } |
180 | Accessible.role: Accessible.StaticText |
181 | Accessible.name: speedLabel.text + " meters per second" |
182 | } |
183 | \endqml |
184 | |
185 | \since 5.4 |
186 | By default this property is \c false. |
187 | */ |
188 | /*! \qmlproperty bool QtQuick::Accessible::multiLine |
189 | \brief This property holds whether this item has multiple text lines. |
190 | |
191 | By default this property is \c false. |
192 | */ |
193 | /*! \qmlproperty bool QtQuick::Accessible::readOnly |
194 | \brief This property indicates that a text field is read only. |
195 | |
196 | It is relevant when the role is \l QAccessible::EditableText and set to be read-only. |
197 | By default this property is \c false. |
198 | */ |
199 | /*! \qmlproperty bool QtQuick::Accessible::selected |
200 | \brief This property holds whether this item is selected. |
201 | |
202 | By default this property is \c false. |
203 | \sa selectable |
204 | */ |
205 | /*! \qmlproperty bool QtQuick::Accessible::selectable |
206 | \brief This property holds whether this item can be selected. |
207 | |
208 | By default this property is \c false. |
209 | \sa selected |
210 | */ |
211 | /*! \qmlproperty bool QtQuick::Accessible::pressed |
212 | \brief This property holds whether this item is pressed (for example a button during a mouse click). |
213 | |
214 | By default this property is \c false. |
215 | */ |
216 | /*! \qmlproperty bool QtQuick::Accessible::checkStateMixed |
217 | \brief This property holds whether this item is in the partially checked state. |
218 | |
219 | By default this property is \c false. |
220 | \sa checked, checkable |
221 | */ |
222 | /*! \qmlproperty bool QtQuick::Accessible::defaultButton |
223 | \brief This property holds whether this item is the default button of a dialog. |
224 | |
225 | By default this property is \c false. |
226 | */ |
227 | /*! \qmlproperty bool QtQuick::Accessible::passwordEdit |
228 | \brief This property holds whether this item is a password text edit. |
229 | |
230 | By default this property is \c false. |
231 | */ |
232 | /*! \qmlproperty bool QtQuick::Accessible::selectableText |
233 | \brief This property holds whether this item contains selectable text. |
234 | |
235 | By default this property is \c false. |
236 | */ |
237 | |
238 | /*! |
239 | \qmlsignal QtQuick::Accessible::pressAction() |
240 | |
241 | This signal is emitted when a press action is received from an assistive tool such as a screen-reader. |
242 | */ |
243 | /*! |
244 | \qmlsignal QtQuick::Accessible::toggleAction() |
245 | |
246 | This signal is emitted when a toggle action is received from an assistive tool such as a screen-reader. |
247 | */ |
248 | /*! |
249 | \qmlsignal QtQuick::Accessible::increaseAction() |
250 | |
251 | This signal is emitted when a increase action is received from an assistive tool such as a screen-reader. |
252 | */ |
253 | /*! |
254 | \qmlsignal QtQuick::Accessible::decreaseAction() |
255 | |
256 | This signal is emitted when a decrease action is received from an assistive tool such as a screen-reader. |
257 | */ |
258 | /*! |
259 | \qmlsignal QtQuick::Accessible::scrollUpAction() |
260 | |
261 | This signal is emitted when a scroll up action is received from an assistive tool such as a screen-reader. |
262 | */ |
263 | /*! |
264 | \qmlsignal QtQuick::Accessible::scrollDownAction() |
265 | |
266 | This signal is emitted when a scroll down action is received from an assistive tool such as a screen-reader. |
267 | */ |
268 | /*! |
269 | \qmlsignal QtQuick::Accessible::scrollLeftAction() |
270 | |
271 | This signal is emitted when a scroll left action is received from an assistive tool such as a screen-reader. |
272 | */ |
273 | /*! |
274 | \qmlsignal QtQuick::Accessible::scrollRightAction() |
275 | |
276 | This signal is emitted when a scroll right action is received from an assistive tool such as a screen-reader. |
277 | */ |
278 | /*! |
279 | \qmlsignal QtQuick::Accessible::previousPageAction() |
280 | |
281 | This signal is emitted when a previous page action is received from an assistive tool such as a screen-reader. |
282 | */ |
283 | /*! |
284 | \qmlsignal QtQuick::Accessible::nextPageAction() |
285 | |
286 | This signal is emitted when a next page action is received from an assistive tool such as a screen-reader. |
287 | */ |
288 | |
289 | QMetaMethod QQuickAccessibleAttached::sigPress; |
290 | QMetaMethod QQuickAccessibleAttached::sigToggle; |
291 | QMetaMethod QQuickAccessibleAttached::sigIncrease; |
292 | QMetaMethod QQuickAccessibleAttached::sigDecrease; |
293 | QMetaMethod QQuickAccessibleAttached::sigScrollUp; |
294 | QMetaMethod QQuickAccessibleAttached::sigScrollDown; |
295 | QMetaMethod QQuickAccessibleAttached::sigScrollLeft; |
296 | QMetaMethod QQuickAccessibleAttached::sigScrollRight; |
297 | QMetaMethod QQuickAccessibleAttached::sigPreviousPage; |
298 | QMetaMethod QQuickAccessibleAttached::sigNextPage; |
299 | |
300 | QQuickAccessibleAttached::QQuickAccessibleAttached(QObject *parent) |
301 | : QObject(parent), m_role(QAccessible::NoRole) |
302 | { |
303 | Q_ASSERT(parent); |
304 | if (!item()) { |
305 | qmlWarning(me: parent) << "Accessible must be attached to an Item" ; |
306 | return; |
307 | } |
308 | |
309 | // Enable accessibility for items with accessible content. This also |
310 | // enables accessibility for the ancestors of souch items. |
311 | item()->d_func()->setAccessible(); |
312 | QAccessibleEvent ev(item(), QAccessible::ObjectCreated); |
313 | QAccessible::updateAccessibility(event: &ev); |
314 | |
315 | if (const QMetaObject *pmo = parent->metaObject()) { |
316 | auto connectPropertyChangeSignal = [parent, pmo, this]( |
317 | const char *propertyName, const char *signalName, int slotIndex) |
318 | { |
319 | // basically does this: |
320 | // if the parent has the property \a propertyName with the associated \a signalName: |
321 | // connect(parent, signalName, this, slotIndex) |
322 | |
323 | // Note that we explicitly want to only connect to standard property/signal naming |
324 | // convention: "value" & "valueChanged" |
325 | // (e.g. avoid a compound property with e.g. a signal notifier named "updated()") |
326 | int idxProperty = pmo->indexOfProperty(name: propertyName); |
327 | if (idxProperty != -1) { |
328 | const QMetaProperty property = pmo->property(index: idxProperty); |
329 | const QMetaMethod signal = property.notifySignal(); |
330 | if (signal.name() == signalName) |
331 | QMetaObject::connect(sender: parent, signal_index: signal.methodIndex(), receiver: this, method_index: slotIndex); |
332 | } |
333 | return; |
334 | }; |
335 | const QMetaObject &smo = staticMetaObject; |
336 | static const int valueChangedIndex = smo.indexOfSlot(slot: "valueChanged()" ); |
337 | connectPropertyChangeSignal("value" , "valueChanged" , valueChangedIndex); |
338 | |
339 | static const int cursorPositionChangedIndex = smo.indexOfSlot(slot: "cursorPositionChanged()" ); |
340 | connectPropertyChangeSignal("cursorPosition" , "cursorPositionChanged" , |
341 | cursorPositionChangedIndex); |
342 | } |
343 | |
344 | if (!sigPress.isValid()) { |
345 | sigPress = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::pressAction); |
346 | sigToggle = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::toggleAction); |
347 | sigIncrease = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::increaseAction); |
348 | sigDecrease = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::decreaseAction); |
349 | sigScrollUp = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollUpAction); |
350 | sigScrollDown = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollDownAction); |
351 | sigScrollLeft = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollLeftAction); |
352 | sigScrollRight = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::scrollRightAction); |
353 | sigPreviousPage = QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::previousPageAction); |
354 | sigNextPage= QMetaMethod::fromSignal(signal: &QQuickAccessibleAttached::nextPageAction); |
355 | } |
356 | } |
357 | |
358 | QQuickAccessibleAttached::~QQuickAccessibleAttached() |
359 | { |
360 | } |
361 | |
362 | void QQuickAccessibleAttached::setRole(QAccessible::Role role) |
363 | { |
364 | if (role != m_role) { |
365 | m_role = role; |
366 | Q_EMIT roleChanged(); |
367 | // There is no way to signify role changes at the moment. |
368 | // QAccessible::updateAccessibility(parent(), 0, QAccessible::); |
369 | |
370 | switch (role) { |
371 | case QAccessible::CheckBox: |
372 | case QAccessible::RadioButton: |
373 | if (!m_stateExplicitlySet.focusable) |
374 | m_state.focusable = true; |
375 | if (!m_stateExplicitlySet.checkable) |
376 | m_state.checkable = true; |
377 | break; |
378 | case QAccessible::Button: |
379 | case QAccessible::MenuItem: |
380 | case QAccessible::PageTab: |
381 | case QAccessible::SpinBox: |
382 | case QAccessible::ComboBox: |
383 | case QAccessible::Terminal: |
384 | case QAccessible::ScrollBar: |
385 | if (!m_stateExplicitlySet.focusable) |
386 | m_state.focusable = true; |
387 | break; |
388 | case QAccessible::EditableText: |
389 | if (!m_stateExplicitlySet.editable) |
390 | m_state.editable = true; |
391 | if (!m_stateExplicitlySet.focusable) |
392 | m_state.focusable = true; |
393 | break; |
394 | case QAccessible::StaticText: |
395 | if (!m_stateExplicitlySet.readOnly) |
396 | m_state.readOnly = true; |
397 | if (!m_stateExplicitlySet.focusable) |
398 | m_state.focusable = true; |
399 | break; |
400 | default: |
401 | break; |
402 | } |
403 | } |
404 | } |
405 | |
406 | bool QQuickAccessibleAttached::wasNameExplicitlySet() const |
407 | { |
408 | return m_nameExplicitlySet; |
409 | } |
410 | |
411 | // Allows types to attach an accessible name to an item as a convenience, |
412 | // so long as the user hasn't done so themselves. |
413 | void QQuickAccessibleAttached::setNameImplicitly(const QString &name) |
414 | { |
415 | setName(name); |
416 | m_nameExplicitlySet = false; |
417 | } |
418 | |
419 | QQuickAccessibleAttached *QQuickAccessibleAttached::qmlAttachedProperties(QObject *obj) |
420 | { |
421 | return new QQuickAccessibleAttached(obj); |
422 | } |
423 | |
424 | bool QQuickAccessibleAttached::ignored() const |
425 | { |
426 | return item() ? !item()->d_func()->isAccessible : false; |
427 | } |
428 | |
429 | void QQuickAccessibleAttached::setIgnored(bool ignored) |
430 | { |
431 | if (this->ignored() != ignored && item()) { |
432 | item()->d_func()->isAccessible = !ignored; |
433 | emit ignoredChanged(); |
434 | } |
435 | } |
436 | |
437 | bool QQuickAccessibleAttached::doAction(const QString &actionName) |
438 | { |
439 | QMetaMethod *sig = nullptr; |
440 | if (actionName == QAccessibleActionInterface::pressAction()) |
441 | sig = &sigPress; |
442 | else if (actionName == QAccessibleActionInterface::toggleAction()) |
443 | sig = &sigToggle; |
444 | else if (actionName == QAccessibleActionInterface::increaseAction()) |
445 | sig = &sigIncrease; |
446 | else if (actionName == QAccessibleActionInterface::decreaseAction()) |
447 | sig = &sigDecrease; |
448 | else if (actionName == QAccessibleActionInterface::scrollUpAction()) |
449 | sig = &sigScrollUp; |
450 | else if (actionName == QAccessibleActionInterface::scrollDownAction()) |
451 | sig = &sigScrollDown; |
452 | else if (actionName == QAccessibleActionInterface::scrollLeftAction()) |
453 | sig = &sigScrollLeft; |
454 | else if (actionName == QAccessibleActionInterface::scrollRightAction()) |
455 | sig = &sigScrollRight; |
456 | else if (actionName == QAccessibleActionInterface::previousPageAction()) |
457 | sig = &sigPreviousPage; |
458 | else if (actionName == QAccessibleActionInterface::nextPageAction()) |
459 | sig = &sigNextPage; |
460 | if (sig && isSignalConnected(signal: *sig)) |
461 | return sig->invoke(obj: this); |
462 | return false; |
463 | } |
464 | |
465 | void QQuickAccessibleAttached::availableActions(QStringList *actions) const |
466 | { |
467 | if (isSignalConnected(signal: sigPress)) |
468 | actions->append(t: QAccessibleActionInterface::pressAction()); |
469 | if (isSignalConnected(signal: sigToggle)) |
470 | actions->append(t: QAccessibleActionInterface::toggleAction()); |
471 | if (isSignalConnected(signal: sigIncrease)) |
472 | actions->append(t: QAccessibleActionInterface::increaseAction()); |
473 | if (isSignalConnected(signal: sigDecrease)) |
474 | actions->append(t: QAccessibleActionInterface::decreaseAction()); |
475 | if (isSignalConnected(signal: sigScrollUp)) |
476 | actions->append(t: QAccessibleActionInterface::scrollUpAction()); |
477 | if (isSignalConnected(signal: sigScrollDown)) |
478 | actions->append(t: QAccessibleActionInterface::scrollDownAction()); |
479 | if (isSignalConnected(signal: sigScrollLeft)) |
480 | actions->append(t: QAccessibleActionInterface::scrollLeftAction()); |
481 | if (isSignalConnected(signal: sigScrollRight)) |
482 | actions->append(t: QAccessibleActionInterface::scrollRightAction()); |
483 | if (isSignalConnected(signal: sigPreviousPage)) |
484 | actions->append(t: QAccessibleActionInterface::previousPageAction()); |
485 | if (isSignalConnected(signal: sigNextPage)) |
486 | actions->append(t: QAccessibleActionInterface::nextPageAction()); |
487 | } |
488 | |
489 | QString QQuickAccessibleAttached::stripHtml(const QString &html) |
490 | { |
491 | #ifndef QT_NO_TEXTHTMLPARSER |
492 | QTextDocument doc; |
493 | doc.setHtml(html); |
494 | return doc.toPlainText(); |
495 | #else |
496 | return html; |
497 | #endif |
498 | } |
499 | |
500 | QT_END_NAMESPACE |
501 | |
502 | #include "moc_qquickaccessibleattached_p.cpp" |
503 | |
504 | #endif |
505 | |