LCOV - code coverage report
Current view: top level - muse/framework/accessibility/internal - accessibilitycontroller.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 25.3 % 487 123
Test Date: 2026-11-05 04:07:02

            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 = &current;
     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 : }
        

Generated by: LCOV version 2.0-1