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 "accessibilitycontroller.h"
23 :
24 : #include <QGuiApplication>
25 : #include <QAccessible>
26 : #include <QWindow>
27 : #include <QTimer>
28 :
29 : #ifdef Q_OS_LINUX
30 : #include <QKeyEvent>
31 : #include <private/qcoreapplication_p.h>
32 : #endif
33 :
34 : #include "accessibleobject.h"
35 : #include "accessiblestub.h"
36 : #include "accessiblewindowinterface.h"
37 : #include "iqaccessibleinterfaceregister.h"
38 :
39 : #include "global/async/async.h"
40 : #include "log.h"
41 :
42 : // #define MUSE_MODULE_ACCESSIBILITY_TRACE
43 : #ifdef MUSE_MODULE_ACCESSIBILITY_TRACE
44 : #define MYLOG() LOGI()
45 : #else
46 : #define MYLOG() LOGN()
47 : #endif
48 :
49 : using namespace muse;
50 : using namespace muse::modularity;
51 : using namespace muse::accessibility;
52 :
53 0 : static void updateHandlerNoop(QAccessibleEvent*)
54 : {
55 0 : }
56 :
57 2 : AccessibilityController::AccessibilityController(const muse::modularity::ContextPtr& iocCtx)
58 4 : : muse::Contextable(iocCtx)
59 : {
60 2 : m_pretendFocusTimer.setInterval(80); // Value found experimentally.
61 2 : m_pretendFocusTimer.setSingleShot(true);
62 2 : m_pretendFocusTimer.callOnTimeout([this]() {
63 0 : restoreFocus();
64 0 : });
65 4 : }
66 :
67 2 : AccessibilityController::~AccessibilityController()
68 : {
69 2 : }
70 :
71 0 : QAccessibleInterface* AccessibilityController::accessibleInterface(QObject* window)
72 : {
73 0 : QWindow* qwindow = qobject_cast<QWindow*>(window);
74 0 : if (!qwindow) {
75 0 : return nullptr;
76 : }
77 0 : return static_cast<QAccessibleInterface*>(new AccessibleWindowInterface(window));
78 : }
79 :
80 0 : void AccessibilityController::deinit()
81 : {
82 0 : m_pretendFocusTimer.stop();
83 :
84 0 : QWindow* window = mainWindow()->qWindow();
85 0 : if (window) {
86 0 : appRootObject()->unregisterWindow(window);
87 : }
88 :
89 0 : unreg(this);
90 0 : }
91 :
92 2 : void AccessibilityController::setAccessibilityEnabled(bool enabled)
93 : {
94 2 : m_enabled = enabled;
95 2 : }
96 :
97 4 : bool AccessibilityController::isEnabled() const
98 : {
99 4 : if (!appRootObject()->isAccessibilityActive()) {
100 2 : return false;
101 : }
102 :
103 2 : if (!navigationController()) {
104 0 : return false;
105 : }
106 :
107 2 : return navigationController()->activeSection() != nullptr;
108 : }
109 :
110 2 : void AccessibilityController::init()
111 : {
112 2 : reg(this);
113 2 : const Item& self = findItem(this);
114 :
115 : // init() is called when the window is being created, and is not available yet,
116 : // delay the registration
117 2 : async::Async::call(this, [this, windowRoot = self.object]() {
118 0 : QWindow* w = mainWindow()->qWindow();
119 0 : if (w) {
120 0 : appRootObject()->registerWindow(w, windowRoot);
121 :
122 : // Clean-up system-default interface that Qt may create during window construction,
123 : // so all next calls return correct interface
124 0 : QAccessibleInterface* cached = QAccessible::queryAccessibleInterface(w);
125 0 : if (cached && !dynamic_cast<AccessibleWindowInterface*>(cached)) {
126 0 : QAccessible::deleteAccessibleInterface(QAccessible::uniqueId(cached));
127 : }
128 : }
129 0 : m_treeConnected = true;
130 0 : });
131 :
132 0 : auto preDispatch = [this]() {
133 : // About to perform an action. Let's store some values to see if the action changes them.
134 0 : m_preDispatchFocus = m_lastFocused;
135 0 : m_preDispatchName = m_preDispatchFocus ? m_preDispatchFocus->accessibleName() : QString();
136 0 : m_announcement.clear(); // So we can detect if the action sets its own announcement.
137 0 : };
138 :
139 0 : auto postDispatch = [this](const QString& title) {
140 : // Just performed an action. Let's make sure the screen reader says something.
141 :
142 0 : if (!m_lastFocused || !m_announcement.isEmpty()) {
143 0 : return; // No focus item (prevents announcements), or the action set its own announcement.
144 : }
145 :
146 0 : if (m_lastFocused != m_preDispatchFocus || m_lastFocused->accessibleName() != m_preDispatchName) {
147 0 : return; // The screen reader will say something anyway.
148 : }
149 :
150 0 : if (title.isEmpty()) {
151 : // E.g. UI navigation shortcuts: Tab, Space, Enter, Esc, etc.
152 0 : return; // Let the screen reader decide what to say.
153 : }
154 :
155 0 : announce(title); // Say the name of the action we just performed.
156 2 : };
157 :
158 2 : if (auto dispatcher = commandDispatcher()) {
159 0 : dispatcher->preDispatch().onReceive(this, [preDispatch](rcommand::Command, bool*) {
160 0 : preDispatch();
161 0 : });
162 :
163 0 : dispatcher->postDispatch().onReceive(this, [this, postDispatch](rcommand::Command command) {
164 0 : const auto& info = commandsRegister()->commandInfo(command);
165 0 : postDispatch(info.title.qTranslatedWithoutMnemonic());
166 0 : });
167 2 : }
168 :
169 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
170 2 : if (auto dispatcher = actionsDispatcher()) {
171 0 : dispatcher->preDispatch().onReceive(this, [preDispatch](actions::ActionCode) {
172 0 : preDispatch();
173 0 : });
174 :
175 0 : dispatcher->postDispatch().onReceive(this, [this, postDispatch](actions::ActionCode actionCode) {
176 0 : const ui::UiAction action = actionsRegister()->action(actionCode);
177 0 : postDispatch(action.title.qTranslatedWithoutMnemonic());
178 0 : });
179 2 : }
180 : #endif
181 2 : }
182 :
183 10 : void AccessibilityController::reg(IAccessible* item)
184 : {
185 10 : if (!m_enabled) {
186 0 : return;
187 : }
188 :
189 10 : if (!m_inited) {
190 : //! This needed to be done here, because we need to init controller (register factory) after UI is start loaded,
191 : //! thus we register the factory after qt registers its factory so that our factory is called first
192 2 : m_inited = true;
193 2 : init();
194 : }
195 :
196 10 : if (item != this) {
197 8 : if (!item->accessibleParent()) {
198 : MYLOG() << "Skipping item with no parent: " << item->accessibleName();
199 0 : return;
200 : }
201 :
202 8 : if (!m_allItems.contains(item->accessibleParent())) {
203 : MYLOG() << "Skipping item with unregistered parent: " << item->accessibleName();
204 0 : return;
205 : }
206 : }
207 :
208 10 : if (findItem(item).isValid()) {
209 0 : LOGW() << "Already registered";
210 0 : return;
211 : }
212 :
213 : MYLOG() << "item: " << item->accessibleName();
214 :
215 10 : Item it;
216 10 : it.item = item;
217 10 : it.object = new AccessibleObject(item);
218 10 : it.object->setController(weak_from_this());
219 10 : it.iface = QAccessible::queryAccessibleInterface(it.object);
220 :
221 10 : m_allItems.insert(item, it);
222 :
223 10 : if (item->accessibleParent() == this) {
224 4 : m_children.append(item);
225 : }
226 :
227 10 : item->accessiblePropertyChanged().onReceive(this, [this, item](const IAccessible::Property& p, const Val value) {
228 0 : propertyChanged(item, p, value);
229 0 : });
230 :
231 10 : item->accessibleStateChanged().onReceive(this, [this, item](const State& state, bool arg) {
232 4 : stateChanged(item, state, arg);
233 4 : });
234 :
235 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
236 10 : QAccessibleEvent ev(it.object, QAccessible::ObjectCreated);
237 10 : sendEvent(&ev);
238 :
239 : // Register children
240 10 : size_t childCount = item->accessibleChildCount();
241 10 : for (size_t i = 0; i < childCount; ++i) {
242 0 : IAccessible* child = item->accessibleChild(i);
243 0 : if (m_allItems.contains(child)) {
244 0 : continue;
245 : }
246 0 : reg(child);
247 : }
248 10 : }
249 :
250 0 : void AccessibilityController::unreg(IAccessible* aitem)
251 : {
252 : MYLOG() << aitem->accessibleName();
253 :
254 0 : Item item = m_allItems.take(aitem);
255 0 : if (!item.isValid()) {
256 0 : return;
257 : }
258 :
259 0 : if (item.item == m_lastFocused) {
260 0 : m_lastFocused = nullptr;
261 : }
262 :
263 0 : if (item.item == m_pretendFocus) {
264 0 : m_pretendFocus = nullptr;
265 : }
266 :
267 0 : if (m_children.contains(aitem)) {
268 0 : m_children.removeOne(aitem);
269 : }
270 :
271 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
272 0 : QAccessibleEvent ev(item.object, QAccessible::ObjectDestroyed);
273 0 : sendEvent(&ev);
274 :
275 0 : delete item.object;
276 0 : }
277 :
278 0 : bool AccessibilityController::isReg(IAccessible* item) const
279 : {
280 0 : return m_allItems.contains(item);
281 : }
282 :
283 : // Force the screen reader to speak an arbitrary message that isn't covered
284 : // by standard accessibility events. For example, use this function to report
285 : // that an action was performed, a different mode was entered, or a change
286 : // occurred to an item that isn't the current focus item.
287 :
288 : // NOTE: This function shouldn't be (ab)used to report changes in focus, or
289 : // changes to the value, state, or text property of the focus item, unless
290 : // such events are ignored or misreported by a particular screen reader.
291 :
292 : // For standard events, see https://doc.qt.io/qt-6/qaccessible.html#Event-enum
293 : // and classes that inherit from https://doc.qt.io/qt-6/qaccessibleevent.html
294 :
295 0 : void AccessibilityController::announce(const QString& announcement)
296 : {
297 : // Note: No early exit here if the announcement was already set.
298 : // If the user performs the same action multiple times then we
299 : // want to hear the same announcement multiple times.
300 0 : m_announcement = announcement;
301 :
302 0 : if (!m_lastFocused || announcement.isEmpty()) {
303 0 : return;
304 : }
305 :
306 0 : const Item& focused = findItem(m_lastFocused);
307 0 : if (!focused.isValid()) {
308 0 : return;
309 : }
310 :
311 : #if 0
312 : // PROPER SOLUTION, but it requires Qt 6.8+ and has these problems:
313 : // * VoiceOver doesn't interrupt prior speech to say the announcement.
314 : // * When there's no selection, NVDA says "blank" before each announcement.
315 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
316 : QAccessibleAnnouncementEvent event(focused.object, announcement);
317 : event.setPoliteness(QAccessible::AnnouncementPoliteness::Assertive);
318 : sendEvent(&event);
319 : return;
320 : #endif
321 :
322 : // HACKY SOLUTION
323 : // Item returns announcement as its external name in accessibleiteminterface.cpp.
324 : // Note: Its *internal* name is not changed because that would notify subscribers
325 : // to IAccessible::accessiblePropertyChanged(), which has side effects.
326 : static constexpr QAccessible::Event eventType = QAccessible::NameChanged;
327 :
328 0 : if (focused.iface && needsRevoicing(*focused.iface, eventType)) {
329 0 : triggerRevoicing(focused);
330 0 : return;
331 : }
332 :
333 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
334 0 : QAccessibleEvent event(focused.object, eventType);
335 0 : sendEvent(&event);
336 0 : }
337 :
338 0 : QString AccessibilityController::announcement() const
339 : {
340 0 : return m_announcement;
341 : }
342 :
343 0 : const IAccessible* AccessibilityController::accessibleRoot() const
344 : {
345 0 : return this;
346 : }
347 :
348 0 : const IAccessible* AccessibilityController::lastFocused() const
349 : {
350 0 : return m_lastFocused;
351 : }
352 :
353 0 : bool AccessibilityController::needToVoicePanelInfo() const
354 : {
355 0 : return m_needToVoicePanelInfo;
356 : }
357 :
358 0 : QString AccessibilityController::currentPanelAccessibleName() const
359 : {
360 0 : const IAccessible* focusedItemPanel = panel(m_lastFocused);
361 0 : return focusedItemPanel ? focusedItemPanel->accessibleName() : "";
362 : }
363 :
364 0 : void AccessibilityController::setIgnoreQtAccessibilityEvents(bool ignore)
365 : {
366 0 : if (ignore) {
367 0 : QAccessible::installUpdateHandler(updateHandlerNoop);
368 : } else {
369 0 : QAccessible::installUpdateHandler(nullptr);
370 : }
371 0 : }
372 :
373 0 : void AccessibilityController::propertyChanged(IAccessible* item, IAccessible::Property property, const Val& value)
374 : {
375 : // NOTE: Handling of properties must match accessibleiteminterface.cpp.
376 : // See AccessibleItemInterface::text(type) among others.
377 0 : const Item& it = findItem(item);
378 0 : if (!it.isValid()) {
379 0 : return;
380 : }
381 :
382 0 : QAccessible::Event etype = QAccessible::InvalidEvent;
383 :
384 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
385 0 : switch (property) {
386 0 : case IAccessible::Property::Undefined:
387 0 : return;
388 0 : case IAccessible::Property::Parent: etype = QAccessible::ParentChanged;
389 0 : break;
390 0 : case IAccessible::Property::Name:
391 : case IAccessible::Property::Description: {
392 0 : if (item == m_lastFocused) {
393 0 : m_announcement.clear();
394 : }
395 :
396 : #if defined(Q_OS_MAC)
397 : // Names and descriptions are combined in accessibleiteminterface.cpp.
398 : etype = QAccessible::NameChanged;
399 : #elif defined(Q_OS_WIN)
400 : // Descriptions are converted to accelerators in accessibleiteminterface.cpp.
401 : etype = property == IAccessible::Property::Name
402 : ? QAccessible::NameChanged
403 : : QAccessible::AcceleratorChanged;
404 : #else
405 : // No changes in accessibleiteminterface.cpp.
406 0 : etype = property == IAccessible::Property::Name
407 0 : ? QAccessible::NameChanged
408 : : QAccessible::DescriptionChanged;
409 : #endif
410 :
411 0 : if (it.iface && needsRevoicing(*it.iface, etype)) {
412 0 : triggerRevoicing(it);
413 0 : return;
414 : }
415 :
416 0 : m_needToVoicePanelInfo = false;
417 0 : break;
418 : }
419 0 : case IAccessible::Property::Value: {
420 0 : QAccessibleValueChangeEvent ev(it.object, it.item->accessibleValue());
421 0 : sendEvent(&ev);
422 0 : return;
423 0 : }
424 0 : case IAccessible::Property::TextCursor: {
425 0 : QAccessibleTextCursorEvent ev(it.object, it.item->accessibleCursorPosition());
426 0 : sendEvent(&ev);
427 0 : return;
428 0 : }
429 0 : case IAccessible::Property::TextInsert: {
430 0 : IAccessible::TextRange range(value.toQVariant().toMap());
431 0 : QAccessibleTextInsertEvent ev(it.object, range.startPosition,
432 0 : it.item->accessibleText(range.startPosition, range.endPosition));
433 0 : sendEvent(&ev);
434 0 : return;
435 0 : }
436 0 : case IAccessible::Property::TextRemove: {
437 0 : IAccessible::TextRange range(value.toQVariant().toMap());
438 0 : QAccessibleTextRemoveEvent ev(it.object, range.startPosition,
439 0 : it.item->accessibleText(range.startPosition, range.endPosition));
440 0 : sendEvent(&ev);
441 0 : return;
442 0 : }
443 : }
444 :
445 0 : QAccessibleEvent ev(it.object, etype);
446 0 : sendEvent(&ev);
447 : }
448 :
449 4 : void AccessibilityController::stateChanged(IAccessible* aitem, State state, bool arg)
450 : {
451 4 : if (!isEnabled()) {
452 2 : return;
453 : }
454 :
455 : MYLOG() << aitem->accessibleName() << ", state: " << int(state) << ", arg: " << arg;
456 2 : const Item& item = findItem(aitem);
457 2 : IF_ASSERT_FAILED(item.isValid()) {
458 0 : return;
459 : }
460 :
461 2 : if (!item.item->accessibleParent()) {
462 0 : LOGE() << "Null parent for item: " << aitem->accessibleName();
463 0 : return;
464 : }
465 :
466 2 : if (aitem->accessibleState(state) != arg) {
467 : // For Narrator and any in-process screen readers, an item's reported
468 : // state must change BEFORE we send a state changed event.
469 0 : LOGE() << "Inconsistent state for item: " << aitem->accessibleName();
470 0 : return;
471 : }
472 :
473 : // We can't tell external APIs the new value of a state; only that it has changed.
474 2 : QAccessible::State changedState; // True means it changed (to either true or false).
475 2 : switch (state) {
476 0 : case State::Enabled: {
477 0 : changedState.disabled = true;
478 0 : } break;
479 0 : case State::Active: {
480 0 : changedState.active = true;
481 0 : } break;
482 2 : case State::Focused: {
483 2 : changedState.focused = true;
484 2 : if (arg) {
485 : // Affects focused state reported by accessibleiteminterface.cpp.
486 : // Must do this before sending the event.
487 2 : m_pretendFocus = nullptr;
488 2 : m_announcement.clear();
489 : }
490 2 : } break;
491 0 : case State::Selected: {
492 0 : changedState.selected = true;
493 0 : } break;
494 0 : case State::Checked: {
495 0 : changedState.checked = true;
496 0 : } break;
497 0 : default: {
498 0 : LOGE() << "not handled state: " << int(state);
499 0 : return;
500 : }
501 : }
502 :
503 : // Prompt screen readers to fetch the new state from accessibleiteminterface.cpp.
504 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
505 4 : QAccessibleStateChangeEvent ev(item.object, changedState);
506 2 : sendEvent(&ev);
507 :
508 2 : if (state == State::Focused) {
509 2 : if (arg) {
510 2 : cancelPreviousReading();
511 2 : savePanelAccessibleName(m_lastFocused, item.item);
512 :
513 2 : QAccessibleEvent ev2(item.object, QAccessible::Focus);
514 2 : sendEvent(&ev2);
515 2 : m_lastFocused = item.item;
516 2 : }
517 : }
518 : }
519 :
520 14 : void AccessibilityController::sendEvent(QAccessibleEvent* ev)
521 : {
522 14 : if (!m_treeConnected) {
523 14 : return;
524 : }
525 :
526 : #ifdef MUSE_MODULE_ACCESSIBILITY_TRACE
527 : AccessibleObject* obj = qobject_cast<AccessibleObject*>(ev->object());
528 : MYLOG() << "object: " << obj->item()->accessibleName() << ", event: " << int(ev->type());
529 : #endif
530 :
531 0 : QAccessible::updateAccessibility(ev);
532 :
533 0 : m_eventSent.send(ev);
534 : }
535 :
536 2 : void AccessibilityController::cancelPreviousReading()
537 : {
538 2 : if (!appRootObject()->isAccessibilityActive()) {
539 0 : return;
540 : }
541 :
542 : #ifdef Q_OS_LINUX
543 : //! HACK: it needs for canceling reading the name of previous control on accessibility
544 2 : QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Cancel, Qt::KeyboardModifier::NoModifier, 0, 1, 0);
545 2 : QCoreApplicationPrivate::setEventSpontaneous(keyEvent, true);
546 2 : application()->notify(mainWindow()->qWindow(), keyEvent);
547 : #endif
548 : }
549 :
550 2 : void AccessibilityController::savePanelAccessibleName(const IAccessible* oldItem, const IAccessible* newItem)
551 : {
552 2 : if (m_ignorePanelChangingVoice) {
553 0 : m_needToVoicePanelInfo = false;
554 0 : return;
555 : }
556 :
557 2 : const IAccessible* oldItemPanel = panel(oldItem);
558 2 : QString oldItemPanelName = oldItemPanel ? oldItemPanel->accessibleName() : "";
559 :
560 2 : const IAccessible* newItemPanel = panel(newItem);
561 2 : QString newItemPanelName = newItemPanel ? newItemPanel->accessibleName() : "";
562 :
563 2 : m_needToVoicePanelInfo = oldItemPanelName != newItemPanelName;
564 2 : }
565 :
566 0 : bool AccessibilityController::needsRevoicing(const QAccessibleInterface& iface, QAccessible::Event eventType) const
567 : {
568 : QAccessible::Text textType;
569 :
570 0 : switch (eventType) {
571 0 : case QAccessible::NameChanged:
572 0 : textType = QAccessible::Name;
573 0 : break;
574 0 : case QAccessible::DescriptionChanged:
575 0 : textType = QAccessible::Description;
576 0 : break;
577 0 : case QAccessible::AcceleratorChanged:
578 0 : textType = QAccessible::Accelerator;
579 0 : break;
580 0 : default:
581 0 : UNREACHABLE;
582 : return false;
583 : }
584 :
585 : #if defined(Q_OS_WIN)
586 : UNUSED(iface);
587 : IF_ASSERT_FAILED(textType != QAccessible::Description) {
588 : return false; // Narrator doesn't read descriptions.
589 : }
590 : // Narrator & NVDA ignore name & accelerator changes.
591 : return true;
592 : #elif defined(Q_OS_MAC)
593 : IF_ASSERT_FAILED(textType == QAccessible::Name) {
594 : return false; // VoiceOver only reads names.
595 : }
596 : // VoiceOver ignores name changes while editing text.
597 : return iface.role() == QAccessible::EditableText;
598 : #else
599 : // Apparently Orca ignores name changes when new text is multiple characters!
600 : // TODO: Confirm this and check behavior for descriptions and accelerators.
601 0 : return iface.text(textType).length() > 1;
602 : #endif
603 : }
604 :
605 0 : void AccessibilityController::triggerRevoicing(const Item& current)
606 : {
607 0 : if (current.item != m_lastFocused || !appRootObject()->isAccessibilityActive()) {
608 0 : return;
609 : }
610 :
611 0 : IF_ASSERT_FAILED(current.isUsable()) {
612 0 : return;
613 : }
614 :
615 : // Objective: Find a "sibling" item to focus for a short period (not long
616 : // enough for the screen reader to say anything), then restore focus back
617 : // to the current item to make the screen reader speak its updated info.
618 :
619 : // This seems to work best when the sibling isn't a descendant of the
620 : // current item, and the sibling doesn't have any children of its own.
621 :
622 0 : const Item* ancestor = ¤t;
623 : const Item* traversed;
624 :
625 : do {
626 0 : traversed = ancestor;
627 0 : ancestor = &findItem(ancestor->item->accessibleParent());
628 0 : if (!ancestor->isUsable()) {
629 0 : LOGE() << "No usable ancestor for item: " << current.item->accessibleName();
630 0 : return;
631 : }
632 0 : } while (ancestor->item->accessibleChildCount() < 2);
633 :
634 0 : const Item& sibling = findSiblingItem(*ancestor, *traversed);
635 0 : const Item& pretend = sibling.isValid() ? sibling : *ancestor;
636 :
637 : // We'll inform external APIs that this item has focus. Internally,
638 : // we won't really focus it because that would notify subscribers to
639 : // IAccessible::accessibleStateChanged(), which has side effects.
640 :
641 : MYLOG() << "Pretend focus item: " << pretend.item->accessibleName();
642 0 : m_pretendFocus = pretend.item; // See state() in accessibleiteminterface.cpp.
643 0 : m_ignorePanelChangingVoice = true;
644 0 : setExternalFocus(pretend);
645 0 : m_pretendFocusTimer.start(); // Calls restoreFocus() after a short delay.
646 : }
647 :
648 0 : void AccessibilityController::restoreFocus()
649 : {
650 0 : m_ignorePanelChangingVoice = false;
651 :
652 0 : if (!m_pretendFocus) {
653 0 : return;
654 : }
655 :
656 0 : const IAccessible* pretendItem = m_pretendFocus;
657 :
658 0 : m_pretendFocus = nullptr; // Stops pretending in accessibleiteminterface.cpp.
659 : // Must do this before sending focus changed events.
660 :
661 0 : const Item& restore = findItem(m_lastFocused);
662 : //! FIXME we just need to open the project and close the project tab after opening it
663 : //! will triggered this assert
664 : //IF_ASSERT_FAILED(restore.isValid() && restore.item != pretendItem) {
665 0 : if (!(restore.isValid() && restore.item != pretendItem)) {
666 0 : return;
667 : }
668 :
669 : MYLOG() << "Restore focus item: " << restore.item->accessibleName();
670 0 : setExternalFocus(restore);
671 : }
672 :
673 0 : void AccessibilityController::setExternalFocus(const Item& other)
674 : {
675 : // We can't tell external APIs the new value of a state; only that it has changed.
676 0 : QAccessible::State focusChanged;
677 0 : focusChanged.focused = true; // Means focus has changed (to either true or false).
678 :
679 : // Prompt the screen reader to fetch the new state from accessibleiteminterface.cpp.
680 : // VoiceOver: Use QObject not QAccessibleInterface in QAccessible…Event() constructors.
681 0 : QAccessibleStateChangeEvent event1(other.object, focusChanged);
682 0 : sendEvent(&event1);
683 0 : QAccessibleEvent event2(other.object, QAccessible::Focus);
684 0 : sendEvent(&event2);
685 :
686 : // NOTE: We don't need a StateChangeEvent for the item that lost external focus.
687 0 : }
688 :
689 4 : const IAccessible* AccessibilityController::panel(const IAccessible* item) const
690 : {
691 4 : if (!item) {
692 1 : return nullptr;
693 : }
694 :
695 3 : const IAccessible* parent = item->accessibleParent();
696 9 : while (parent) {
697 6 : if (parent->accessibleRole() == IAccessible::Panel) {
698 0 : return parent;
699 : }
700 :
701 6 : parent = parent->accessibleParent();
702 : }
703 :
704 3 : return nullptr;
705 : }
706 :
707 0 : const AccessibilityController::Item& AccessibilityController::findSiblingItem(const Item& parent, const Item& current) const
708 : {
709 0 : TRACEFUNC;
710 : static constexpr Item null;
711 0 : const size_t count = parent.item->accessibleChildCount();
712 :
713 0 : for (size_t i = 0; i < count; ++i) {
714 0 : const Item& sibling = findItem(parent.item->accessibleChild(i));
715 :
716 0 : if (sibling.item == current.item || !sibling.isUsable()) {
717 0 : continue;
718 : }
719 :
720 0 : const IAccessible::Role role = sibling.item->accessibleRole();
721 :
722 0 : if (role != IAccessible::Group
723 0 : && role != IAccessible::Panel
724 0 : && sibling.item->accessibleState(State::Enabled)
725 0 : && sibling.item->accessibleChildCount() == 0) {
726 0 : return sibling;
727 : }
728 :
729 0 : if (sibling.item->accessibleChildCount() > 0) {
730 0 : const Item& subItem = findSiblingItem(sibling, current);
731 0 : if (subItem.isValid()) {
732 0 : return subItem;
733 : }
734 : }
735 : }
736 :
737 0 : return null;
738 0 : }
739 :
740 0 : IAccessible* AccessibilityController::pretendFocus() const
741 : {
742 0 : return m_pretendFocus;
743 : }
744 :
745 0 : async::Channel<QAccessibleEvent*> AccessibilityController::eventSent() const
746 : {
747 0 : return m_eventSent;
748 : }
749 :
750 14 : const AccessibilityController::Item& AccessibilityController::findItem(const IAccessible* aitem) const
751 : {
752 : static constexpr Item null;
753 :
754 14 : if (!aitem) {
755 0 : return null;
756 : }
757 :
758 14 : auto it = m_allItems.find(aitem);
759 :
760 14 : if (it == m_allItems.end()) {
761 10 : return null;
762 : }
763 :
764 4 : return it.value();
765 : }
766 :
767 0 : QAccessibleInterface* AccessibilityController::parentIface(const IAccessible* item) const
768 : {
769 0 : IF_ASSERT_FAILED(item) {
770 0 : return nullptr;
771 : }
772 :
773 0 : const IAccessible* parent = item->accessibleParent();
774 0 : if (!parent) {
775 0 : return nullptr;
776 : }
777 :
778 0 : const Item& it = findItem(parent);
779 0 : if (!it.isValid()) {
780 0 : return nullptr;
781 : }
782 :
783 0 : if (it.item->accessibleRole() == IAccessible::Role::Application) {
784 0 : QWindow* w = item->accessibleWindow();
785 0 : if (!w) {
786 0 : w = mainWindow()->qWindow();
787 : }
788 0 : if (w) {
789 0 : return QAccessible::queryAccessibleInterface(w);
790 : }
791 :
792 0 : return nullptr;
793 : }
794 :
795 0 : return it.iface;
796 : }
797 :
798 0 : int AccessibilityController::childCount(const IAccessible* item) const
799 : {
800 0 : IF_ASSERT_FAILED(item) {
801 0 : return 0;
802 : }
803 :
804 0 : const Item& it = findItem(item);
805 0 : IF_ASSERT_FAILED(it.isValid()) {
806 0 : return 0;
807 : }
808 0 : return static_cast<int>(it.item->accessibleChildCount());
809 : }
810 :
811 0 : QAccessibleInterface* AccessibilityController::child(const IAccessible* item, int i) const
812 : {
813 0 : IF_ASSERT_FAILED(item) {
814 0 : return nullptr;
815 : }
816 :
817 0 : const IAccessible* chld = item->accessibleChild(static_cast<size_t>(i));
818 0 : IF_ASSERT_FAILED(chld) {
819 0 : return nullptr;
820 : }
821 :
822 0 : const Item& chldIt = findItem(chld);
823 0 : IF_ASSERT_FAILED(chldIt.isValid()) {
824 0 : return nullptr;
825 : }
826 :
827 0 : return chldIt.iface;
828 : }
829 :
830 0 : int AccessibilityController::indexOfChild(const IAccessible* item, const QAccessibleInterface* iface) const
831 : {
832 0 : TRACEFUNC;
833 0 : size_t count = item->accessibleChildCount();
834 0 : for (size_t i = 0; i < count; ++i) {
835 0 : const IAccessible* ch = item->accessibleChild(i);
836 0 : const Item& chIt = findItem(ch);
837 0 : if (!chIt.isValid()) {
838 0 : continue;
839 : }
840 :
841 0 : if (chIt.iface == iface) {
842 0 : return static_cast<int>(i);
843 : }
844 : }
845 :
846 0 : return -1;
847 0 : }
848 :
849 0 : QAccessibleInterface* AccessibilityController::focusedChild(const IAccessible* item) const
850 : {
851 0 : TRACEFUNC;
852 0 : size_t count = item->accessibleChildCount();
853 0 : for (size_t i = 0; i < count; ++i) {
854 0 : const IAccessible* ch = item->accessibleChild(i);
855 0 : const Item& chIt = findItem(ch);
856 0 : if (!chIt.isValid() || !chIt.iface || chIt.item->accessibleIgnored()) {
857 0 : continue;
858 : }
859 :
860 0 : if (chIt.iface->state().focused) {
861 0 : return chIt.iface;
862 : }
863 :
864 0 : if (chIt.item->accessibleChildCount() > 0) {
865 0 : QAccessibleInterface* subItem = focusedChild(chIt.item);
866 0 : if (subItem) {
867 0 : return subItem;
868 : }
869 : }
870 : }
871 :
872 0 : return nullptr;
873 0 : }
874 :
875 5 : IAccessible* AccessibilityController::accessibleParent() const
876 : {
877 5 : return nullptr;
878 : }
879 :
880 2 : size_t AccessibilityController::accessibleChildCount() const
881 : {
882 2 : return static_cast<size_t>(m_children.size());
883 : }
884 :
885 0 : IAccessible* AccessibilityController::accessibleChild(size_t i) const
886 : {
887 0 : return m_children.at(static_cast<int>(i));
888 : }
889 :
890 0 : QWindow* AccessibilityController::accessibleWindow() const
891 : {
892 0 : return mainWindow()->qWindow();
893 : }
894 :
895 2 : muse::modularity::ContextPtr AccessibilityController::iocContext() const
896 : {
897 2 : return Contextable::iocContext();
898 : }
899 :
900 3 : IAccessible::Role AccessibilityController::accessibleRole() const
901 : {
902 3 : return IAccessible::Role::Application;
903 : }
904 :
905 0 : QString AccessibilityController::accessibleName() const
906 : {
907 0 : return QString("AccessibilityController");
908 : }
909 :
910 0 : QString AccessibilityController::accessibleDescription() const
911 : {
912 0 : return QString();
913 : }
914 :
915 0 : bool AccessibilityController::accessibleState(State st) const
916 : {
917 0 : switch (st) {
918 0 : case State::Undefined: return false;
919 0 : case State::Enabled: return true;
920 0 : case State::Active: return true;
921 0 : default: {
922 0 : LOGW() << "not handled state: " << static_cast<int>(st);
923 : }
924 : }
925 :
926 0 : return false;
927 : }
928 :
929 0 : QRect AccessibilityController::accessibleRect() const
930 : {
931 0 : return mainWindow()->qWindow()->geometry();
932 : }
933 :
934 0 : bool AccessibilityController::accessibleIgnored() const
935 : {
936 0 : return false;
937 : }
938 :
939 0 : QVariant AccessibilityController::accessibleValue() const
940 : {
941 0 : return QVariant();
942 : }
943 :
944 0 : QVariant AccessibilityController::accessibleMaximumValue() const
945 : {
946 0 : return QVariant();
947 : }
948 :
949 0 : QVariant AccessibilityController::accessibleMinimumValue() const
950 : {
951 0 : return QVariant();
952 : }
953 :
954 0 : QVariant AccessibilityController::accessibleValueStepSize() const
955 : {
956 0 : return QVariant();
957 : }
958 :
959 0 : void AccessibilityController::accessibleSelection(int, int*, int*) const
960 : {
961 0 : }
962 :
963 0 : int AccessibilityController::accessibleSelectionCount() const
964 : {
965 0 : return 0;
966 : }
967 :
968 0 : int AccessibilityController::accessibleCursorPosition() const
969 : {
970 0 : return 0;
971 : }
972 :
973 0 : QString AccessibilityController::accessibleText(int, int) const
974 : {
975 0 : return QString();
976 : }
977 :
978 0 : QString AccessibilityController::accessibleTextBeforeOffset(int, TextBoundaryType, int*, int*) const
979 : {
980 0 : return QString();
981 : }
982 :
983 0 : QString AccessibilityController::accessibleTextAfterOffset(int, TextBoundaryType, int*, int*) const
984 : {
985 0 : return QString();
986 : }
987 :
988 0 : QString AccessibilityController::accessibleTextAtOffset(int, TextBoundaryType, int*, int*) const
989 : {
990 0 : return QString();
991 : }
992 :
993 0 : int AccessibilityController::accessibleCharacterCount() const
994 : {
995 0 : return 0;
996 : }
997 :
998 0 : int AccessibilityController::accessibleRowIndex() const
999 : {
1000 0 : return 0;
1001 : }
1002 :
1003 2 : async::Channel<IAccessible::Property, Val> AccessibilityController::accessiblePropertyChanged() const
1004 : {
1005 3 : static async::Channel<IAccessible::Property, Val> ch;
1006 2 : return ch;
1007 1 : }
1008 :
1009 2 : async::Channel<IAccessible::State, bool> AccessibilityController::accessibleStateChanged() const
1010 : {
1011 3 : static async::Channel<IAccessible::State, bool> ch;
1012 2 : return ch;
1013 1 : }
1014 :
1015 0 : void AccessibilityController::setState(State, bool)
1016 : {
1017 0 : }
|