Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "accessibleiteminterface.h"
23 :
24 : #include <QWindow>
25 :
26 : #include "global/translation.h"
27 :
28 : #include "accessibilitycontroller.h"
29 :
30 : #include "log.h"
31 :
32 : //#define MUSE_ACCESSIBILITY_TRACE
33 :
34 : #undef MYLOG
35 : #ifdef MUSE_ACCESSIBILITY_TRACE
36 : #define MYLOG() LOGI()
37 : #else
38 : #define MYLOG() LOGN()
39 : #endif
40 :
41 : using namespace muse;
42 : using namespace muse::accessibility;
43 :
44 10 : AccessibleItemInterface::AccessibleItemInterface(AccessibleObject* object)
45 10 : : muse::Contextable(object->item()->iocContext())
46 : {
47 10 : m_object = object;
48 10 : }
49 :
50 0 : bool AccessibleItemInterface::isValid() const
51 : {
52 0 : return m_object != nullptr;
53 : }
54 :
55 20 : QObject* AccessibleItemInterface::object() const
56 : {
57 20 : return m_object;
58 : }
59 :
60 0 : QWindow* AccessibleItemInterface::window() const
61 : {
62 0 : QWindow* window = m_object->item()->accessibleWindow();
63 0 : if (window) {
64 0 : return window;
65 : }
66 :
67 0 : return interactive()->topWindow();
68 : }
69 :
70 0 : QRect AccessibleItemInterface::rect() const
71 : {
72 0 : return m_object->item()->accessibleRect();
73 : }
74 :
75 0 : QAccessibleInterface* AccessibleItemInterface::parent() const
76 : {
77 0 : QAccessibleInterface* iface = m_object->controller().lock()->parentIface(m_object->item());
78 : MYLOG() << "item: " << m_object->item()->accessibleName() << ", parent: " << (iface ? iface->text(QAccessible::Name) : "null");
79 0 : return iface;
80 : }
81 :
82 0 : int AccessibleItemInterface::childCount() const
83 : {
84 0 : int count = m_object->controller().lock()->childCount(m_object->item());
85 : MYLOG() << "item: " << m_object->item()->accessibleName() << ", childCount: " << count;
86 0 : return count;
87 : }
88 :
89 0 : QAccessibleInterface* AccessibleItemInterface::child(int index) const
90 : {
91 0 : QAccessibleInterface* iface = m_object->controller().lock()->child(m_object->item(), index);
92 : MYLOG() << "item: " << m_object->item()->accessibleName() << ", child: " << index << " " << iface->text(QAccessible::Name);
93 0 : return iface;
94 : }
95 :
96 0 : int AccessibleItemInterface::indexOfChild(const QAccessibleInterface* iface) const
97 : {
98 0 : int idx = m_object->controller().lock()->indexOfChild(m_object->item(), iface);
99 : MYLOG() << "item: " << m_object->item()->accessibleName() << ", indexOfChild: " << iface->text(QAccessible::Name) << " = " << idx;
100 0 : return idx;
101 : }
102 :
103 0 : QAccessibleInterface* AccessibleItemInterface::childAt(int, int) const
104 : {
105 0 : NOT_IMPLEMENTED;
106 0 : return nullptr;
107 : }
108 :
109 0 : QAccessibleInterface* AccessibleItemInterface::focusChild() const
110 : {
111 0 : QAccessibleInterface* child = m_object->controller().lock()->focusedChild(m_object->item());
112 : MYLOG() << "item: " << m_object->item()->accessibleName() << ", focused child: " << (child ? child->text(QAccessible::Name) : "null");
113 0 : return child;
114 : }
115 :
116 0 : QAccessible::State AccessibleItemInterface::state() const
117 : {
118 0 : IAccessible* item = m_object->item();
119 0 : QAccessible::State state;
120 0 : state.invalid = false;
121 0 : state.disabled = !item->accessibleState(IAccessible::State::Enabled);
122 0 : state.invisible = state.disabled;
123 :
124 0 : if (state.disabled) {
125 0 : return state;
126 : }
127 :
128 0 : IAccessible::Role r = item->accessibleRole();
129 0 : switch (r) {
130 0 : case IAccessible::Role::NoRole: break;
131 0 : case IAccessible::Role::Application: {
132 0 : state.active = true;
133 0 : } break;
134 0 : case IAccessible::Role::Dialog: {
135 0 : state.active = item->accessibleState(IAccessible::State::Active);
136 0 : } break;
137 0 : case IAccessible::Role::Panel: {
138 0 : state.active = item->accessibleState(IAccessible::State::Active);
139 0 : } break;
140 0 : case IAccessible::Role::Button: {
141 0 : state.focusable = true;
142 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
143 0 : } break;
144 0 : case IAccessible::Role::RadioButton: {
145 0 : state.focusable = true;
146 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
147 :
148 0 : state.checkable = true;
149 0 : state.checked = item->accessibleState(IAccessible::State::Checked);
150 0 : } break;
151 0 : case IAccessible::Role::EditableText: {
152 0 : state.focusable = true;
153 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
154 0 : } break;
155 0 : case IAccessible::Role::StaticText: {
156 0 : state.focusable = true;
157 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
158 0 : } break;
159 0 : case IAccessible::Role::SilentRole: {
160 0 : state.focusable = true;
161 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
162 0 : } break;
163 0 : case IAccessible::Role::List: {
164 0 : state.active = item->accessibleState(IAccessible::State::Active);
165 0 : } break;
166 0 : case IAccessible::Role::ListItem: {
167 0 : state.focusable = true;
168 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
169 :
170 0 : state.selectable = true;
171 0 : state.selected = item->accessibleState(IAccessible::State::Selected);
172 0 : } break;
173 0 : case IAccessible::Role::Information: {
174 0 : state.focusable = true;
175 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
176 0 : } break;
177 0 : case IAccessible::Role::ElementOnScore: {
178 0 : state.focusable = true;
179 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
180 :
181 : // state.checkable = true;
182 : // state.checked = item->accessibleState(IAccessible::State::Selected);
183 0 : } break;
184 0 : case IAccessible::Role::CheckBox: {
185 0 : state.focusable = true;
186 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
187 :
188 0 : state.checkable = true;
189 0 : state.checked = item->accessibleState(IAccessible::State::Checked);
190 0 : } break;
191 0 : case IAccessible::Role::ComboBox: {
192 0 : state.focusable = true;
193 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
194 0 : } break;
195 0 : case IAccessible::Role::MenuItem: {
196 0 : state.focusable = true;
197 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
198 0 : } break;
199 0 : case IAccessible::Role::SpinBox: {
200 0 : state.focusable = true;
201 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
202 0 : } break;
203 0 : case IAccessible::Role::Range: {
204 0 : state.focusable = true;
205 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
206 0 : } break;
207 0 : case IAccessible::Role::Group: {
208 0 : state.focusable = true;
209 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
210 0 : } break;
211 0 : case IAccessible::Role::Table: {
212 0 : state.focusable = true;
213 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
214 0 : } break;
215 0 : case IAccessible::Role::ColumnHeader: {
216 0 : state.focusable = true;
217 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
218 0 : } break;
219 0 : case IAccessible::Role::RowHeader: {
220 0 : state.focusable = true;
221 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
222 0 : } break;
223 0 : case IAccessible::Role::Column: {
224 0 : state.focusable = true;
225 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
226 0 : } break;
227 0 : case IAccessible::Role::Row: {
228 0 : state.focusable = true;
229 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
230 0 : } break;
231 0 : case IAccessible::Role::Cell: {
232 0 : state.focusable = true;
233 0 : state.focused = item->accessibleState(IAccessible::State::Focused);
234 0 : } break;
235 0 : default: {
236 0 : LOGW() << "not handled role: " << static_cast<int>(r);
237 0 : } break;
238 : }
239 :
240 0 : if (IAccessible* pretendFocus = m_object->controller().lock()->pretendFocus()) {
241 0 : if (item == pretendFocus) {
242 : // Pretend to have focus.
243 0 : state.focusable = true;
244 0 : state.focused = true;
245 : } else {
246 : // Pretend to not have focus.
247 0 : state.focused = false;
248 : }
249 : }
250 :
251 0 : return state;
252 : }
253 :
254 0 : QAccessible::Role AccessibleItemInterface::role() const
255 : {
256 0 : IAccessible::Role r = m_object->item()->accessibleRole();
257 0 : switch (r) {
258 0 : case IAccessible::Role::NoRole: return QAccessible::NoRole;
259 0 : case IAccessible::Role::Application: return QAccessible::Application;
260 0 : case IAccessible::Role::Dialog: return QAccessible::Dialog;
261 0 : case IAccessible::Role::Panel:
262 : #if defined(Q_OS_WIN)
263 : // Narrator: Don't say the panel name twice when changing panels.
264 : return QAccessible::StaticText;
265 : #else
266 0 : return QAccessible::Pane;
267 : #endif
268 0 : case IAccessible::Role::StaticText: return QAccessible::StaticText;
269 0 : case IAccessible::Role::SilentRole: {
270 : // See https://doc.qt.io/qt-5/qaccessible.html#Role-enum
271 : // We want the screen reader to say the name of the current item and
272 : // nothing else (i.e. not the name followed by "button" or "text").
273 : #if defined(Q_OS_MACOS)
274 : // Good on macOS with VoiceOver.
275 : return QAccessible::StaticText;
276 : // VoiceOver gives unwanted additional output if ListItem is used, and it
277 : // doesn't work at all if the role is TreeItem or Cell.
278 : #else
279 : // Good on Windows with Narrator, NVDA, or JAWS; and on Linux with Orca.
280 0 : return QAccessible::ListItem;
281 : // Orca is equally happy with the roles TreeItem or Cell, but these cause
282 : // unwanted additional ouput on Windows. StaticText causes unwanted
283 : // additional output on both Linux and Windows.
284 : #endif
285 : }
286 0 : case IAccessible::Role::EditableText: return QAccessible::EditableText;
287 0 : case IAccessible::Role::Button: return QAccessible::Button;
288 0 : case IAccessible::Role::CheckBox: return QAccessible::CheckBox;
289 0 : case IAccessible::Role::RadioButton: return QAccessible::RadioButton;
290 0 : case IAccessible::Role::ComboBox: return QAccessible::ComboBox;
291 0 : case IAccessible::Role::List: return QAccessible::List;
292 0 : case IAccessible::Role::ListItem: return QAccessible::ListItem;
293 0 : case IAccessible::Role::MenuItem: return QAccessible::MenuItem;
294 0 : case IAccessible::Role::SpinBox: return QAccessible::SpinBox;
295 0 : case IAccessible::Role::Range: return QAccessible::Slider;
296 0 : case IAccessible::Role::Table: return QAccessible::Table;
297 0 : case IAccessible::Role::ColumnHeader: return QAccessible::ColumnHeader;
298 0 : case IAccessible::Role::RowHeader: return QAccessible::RowHeader;
299 0 : case IAccessible::Role::Column: return QAccessible::Column;
300 0 : case IAccessible::Role::Row: return QAccessible::Row;
301 0 : case IAccessible::Role::Cell: return QAccessible::Cell;
302 0 : case IAccessible::Role::Group:
303 : case IAccessible::Role::Information:
304 : case IAccessible::Role::ElementOnScore: {
305 : #ifdef Q_OS_WIN
306 : return QAccessible::StaticText;
307 : #else
308 0 : return QAccessible::UserRole;
309 : #endif
310 : }
311 : }
312 :
313 0 : LOGE() << "not handled role: " << static_cast<int>(r);
314 0 : return QAccessible::NoRole;
315 : }
316 :
317 0 : QString AccessibleItemInterface::text(QAccessible::Text textType) const
318 : {
319 : // NOTE: Handling of text types must match AccessibilityController::propertyChanged().
320 0 : switch (textType) {
321 0 : case QAccessible::Name: {
322 0 : QString ann = announcement();
323 0 : if (!ann.isEmpty()) {
324 0 : return ann;
325 : }
326 0 : QString name = m_object->item()->accessibleName();
327 : #if defined(Q_OS_MACOS)
328 : // VoiceOver doesn't speak descriptions so add it to name instead.
329 : QString desc = description();
330 : if (!desc.isEmpty()) {
331 : name += ", " + desc;
332 : }
333 : #endif
334 0 : if (auto controller = m_object->controller().lock()) {
335 0 : if (m_object->item() == controller->lastFocused() && controller->needToVoicePanelInfo()) {
336 0 : QString panelName = controller->currentPanelAccessibleName();
337 0 : if (!panelName.isEmpty()) {
338 0 : name.prepend(muse::qtrc("accessibility", "%1 panel").arg(panelName) + ", ");
339 : }
340 0 : }
341 0 : }
342 0 : return name;
343 0 : }
344 : #if !defined(Q_OS_MACOS) // Give description separately to name.
345 : #if defined(Q_OS_WINDOWS)
346 : // Narrator doesn't read descriptions but it does read accelerators.
347 : // NVDA reads both so it should be safe to give just an accelerator.
348 : case QAccessible::Accelerator: {
349 : #else
350 : // Orca on Linux does read descriptions.
351 0 : case QAccessible::Description: {
352 : #endif
353 0 : if (!announcement().isEmpty()) {
354 0 : break; // Don't say description after an announcement.
355 : }
356 0 : return description();
357 : }
358 : #endif
359 0 : default: break;
360 : }
361 :
362 0 : return QString();
363 : }
364 :
365 0 : void AccessibleItemInterface::setText(QAccessible::Text, const QString&)
366 : {
367 0 : NOT_IMPLEMENTED;
368 0 : }
369 :
370 0 : QVariant AccessibleItemInterface::currentValue() const
371 : {
372 0 : return m_object->item()->accessibleValue();
373 : }
374 :
375 0 : void AccessibleItemInterface::setCurrentValue(const QVariant&)
376 : {
377 0 : NOT_IMPLEMENTED;
378 0 : }
379 :
380 0 : QVariant AccessibleItemInterface::maximumValue() const
381 : {
382 0 : return m_object->item()->accessibleMaximumValue();
383 : }
384 :
385 0 : QVariant AccessibleItemInterface::minimumValue() const
386 : {
387 0 : return m_object->item()->accessibleMinimumValue();
388 : }
389 :
390 0 : QVariant AccessibleItemInterface::minimumStepSize() const
391 : {
392 0 : return m_object->item()->accessibleValueStepSize();
393 : }
394 :
395 0 : void AccessibleItemInterface::selection(int selectionIndex, int* startOffset, int* endOffset) const
396 : {
397 0 : m_object->item()->accessibleSelection(selectionIndex, startOffset, endOffset);
398 0 : }
399 :
400 0 : int AccessibleItemInterface::selectionCount() const
401 : {
402 0 : return m_object->item()->accessibleSelectionCount();
403 : }
404 :
405 0 : void AccessibleItemInterface::addSelection(int, int)
406 : {
407 0 : NOT_IMPLEMENTED;
408 0 : }
409 :
410 0 : void AccessibleItemInterface::removeSelection(int)
411 : {
412 0 : NOT_IMPLEMENTED;
413 0 : }
414 :
415 0 : void AccessibleItemInterface::setSelection(int, int, int)
416 : {
417 0 : NOT_IMPLEMENTED;
418 0 : }
419 :
420 0 : int AccessibleItemInterface::cursorPosition() const
421 : {
422 0 : return m_object->item()->accessibleCursorPosition();
423 : }
424 :
425 0 : void AccessibleItemInterface::setCursorPosition(int)
426 : {
427 0 : NOT_IMPLEMENTED;
428 0 : }
429 :
430 0 : QString AccessibleItemInterface::text(int startOffset, int endOffset) const
431 : {
432 0 : return m_object->item()->accessibleText(startOffset, endOffset);
433 : }
434 :
435 0 : QString AccessibleItemInterface::textBeforeOffset(int offset, QAccessible::TextBoundaryType boundaryType, int* startOffset,
436 : int* endOffset) const
437 : {
438 0 : return m_object->item()->accessibleTextBeforeOffset(offset, muBoundaryType(boundaryType), startOffset, endOffset);
439 : }
440 :
441 0 : QString AccessibleItemInterface::textAfterOffset(int offset, QAccessible::TextBoundaryType boundaryType, int* startOffset,
442 : int* endOffset) const
443 : {
444 0 : return m_object->item()->accessibleTextAfterOffset(offset, muBoundaryType(boundaryType), startOffset, endOffset);
445 : }
446 :
447 0 : QString AccessibleItemInterface::textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType, int* startOffset,
448 : int* endOffset) const
449 : {
450 0 : return m_object->item()->accessibleTextAtOffset(offset, muBoundaryType(boundaryType), startOffset, endOffset);
451 : }
452 :
453 0 : int AccessibleItemInterface::characterCount() const
454 : {
455 0 : return m_object->item()->accessibleCharacterCount();
456 : }
457 :
458 0 : QRect AccessibleItemInterface::characterRect(int) const
459 : {
460 0 : NOT_IMPLEMENTED;
461 :
462 0 : return QRect();
463 : }
464 :
465 0 : int AccessibleItemInterface::offsetAtPoint(const QPoint&) const
466 : {
467 0 : NOT_IMPLEMENTED;
468 :
469 0 : return -1;
470 : }
471 :
472 0 : void AccessibleItemInterface::scrollToSubstring(int, int)
473 : {
474 0 : NOT_IMPLEMENTED;
475 0 : }
476 :
477 0 : QString AccessibleItemInterface::attributes(int, int* startOffset, int* endOffset) const
478 : {
479 0 : NOT_IMPLEMENTED;
480 :
481 0 : *startOffset = -1;
482 0 : *endOffset = -1;
483 0 : return QString();
484 : }
485 :
486 0 : bool AccessibleItemInterface::isSelected() const
487 : {
488 0 : return m_object->item()->accessibleState(IAccessible::State::Selected);
489 : }
490 :
491 0 : QList<QAccessibleInterface*> AccessibleItemInterface::columnHeaderCells() const
492 : {
493 0 : NOT_IMPLEMENTED;
494 0 : return QList<QAccessibleInterface*>();
495 : }
496 :
497 0 : QList<QAccessibleInterface*> AccessibleItemInterface::rowHeaderCells() const
498 : {
499 0 : NOT_IMPLEMENTED;
500 0 : return QList<QAccessibleInterface*>();
501 : }
502 :
503 0 : int AccessibleItemInterface::columnIndex() const
504 : {
505 0 : NOT_IMPLEMENTED;
506 0 : return 0;
507 : }
508 :
509 0 : int AccessibleItemInterface::rowIndex() const
510 : {
511 0 : return m_object->item()->accessibleRowIndex();
512 : }
513 :
514 0 : int AccessibleItemInterface::columnExtent() const
515 : {
516 0 : NOT_IMPLEMENTED;
517 0 : return 1;
518 : }
519 :
520 0 : int AccessibleItemInterface::rowExtent() const
521 : {
522 0 : NOT_IMPLEMENTED;
523 0 : return 1;
524 : }
525 :
526 0 : QAccessibleInterface* AccessibleItemInterface::table() const
527 : {
528 0 : return parent();
529 : }
530 :
531 0 : void* AccessibleItemInterface::interface_cast(QAccessible::InterfaceType type)
532 : {
533 0 : QAccessible::Role itemRole = role();
534 0 : if (type == QAccessible::InterfaceType::ValueInterface && itemRole == QAccessible::Slider) {
535 0 : return static_cast<QAccessibleValueInterface*>(this);
536 0 : } else if (type == QAccessible::InterfaceType::TextInterface) {
537 0 : return static_cast<QAccessibleTextInterface*>(this);
538 : }
539 :
540 0 : bool isTableType = type == QAccessible::InterfaceType::TableCellInterface;
541 : #ifdef Q_OS_WIN
542 : //! NOTE: Without Action and Text interfaces NVDA doesn't work
543 : isTableType |= type == QAccessible::InterfaceType::ActionInterface;
544 : #endif
545 :
546 0 : if (isTableType && itemRole == QAccessible::ListItem) {
547 : //! NOTE: Without Action and Text interfaces NVDA doesn't work
548 0 : return static_cast<QAccessibleTableCellInterface*>(this);
549 : }
550 :
551 : //! NOTE Not implemented
552 0 : return nullptr;
553 : }
554 :
555 0 : IAccessible::TextBoundaryType AccessibleItemInterface::muBoundaryType(QAccessible::TextBoundaryType qtBoundaryType) const
556 : {
557 0 : switch (qtBoundaryType) {
558 0 : case QAccessible::TextBoundaryType::CharBoundary: return IAccessible::CharBoundary;
559 0 : case QAccessible::TextBoundaryType::WordBoundary: return IAccessible::WordBoundary;
560 0 : case QAccessible::TextBoundaryType::SentenceBoundary: return IAccessible::SentenceBoundary;
561 0 : case QAccessible::TextBoundaryType::ParagraphBoundary: return IAccessible::ParagraphBoundary;
562 0 : case QAccessible::TextBoundaryType::LineBoundary: return IAccessible::LineBoundary;
563 0 : case QAccessible::TextBoundaryType::NoBoundary: return IAccessible::NoBoundary;
564 : }
565 :
566 0 : return IAccessible::NoBoundary;
567 : }
568 :
569 0 : QString AccessibleItemInterface::announcement() const
570 : {
571 0 : if (auto controller = m_object->controller().lock()) {
572 : // Announcements override the current item's accessible name.
573 0 : if (m_object->item() == controller->lastFocused()) {
574 0 : return controller->announcement(); // Can be empty/null.
575 : }
576 0 : }
577 :
578 0 : return QString(); // Don't override the names of any other items.
579 : }
580 :
581 0 : QString AccessibleItemInterface::description() const
582 : {
583 0 : IAccessible* item = m_object->item();
584 0 : QString desc = item->accessibleDescription(); // Can be empty/null.
585 :
586 0 : if (desc.isEmpty() || item->accessibleName().contains(desc, Qt::CaseInsensitive)) {
587 0 : return QString();
588 : }
589 :
590 0 : return desc;
591 0 : }
|