diff --git a/framework/accessibility/CMakeLists.txt b/framework/accessibility/CMakeLists.txt index 9a2c18ffb8..82cc2e3b7c 100644 --- a/framework/accessibility/CMakeLists.txt +++ b/framework/accessibility/CMakeLists.txt @@ -24,9 +24,8 @@ target_sources(muse_accessibility PRIVATE accessibilitymodule.cpp accessibilitymodule.h iaccessible.h + iaccessibleapprootobject.h iaccessibilitycontroller.h - iaccessibilityconfiguration.h - iaccessibilitycontextconfiguration.h iqaccessibleinterfaceregister.h api/accessibilityapi.cpp @@ -34,6 +33,10 @@ target_sources(muse_accessibility PRIVATE internal/accessibilitycontroller.cpp internal/accessibilitycontroller.h + internal/accessibleapprootobject.cpp + internal/accessibleapprootobject.h + internal/accessibleapprootinterface.cpp + internal/accessibleapprootinterface.h internal/accessibleobject.cpp internal/accessibleobject.h internal/accessiblestub.cpp @@ -42,10 +45,6 @@ target_sources(muse_accessibility PRIVATE internal/accessibleiteminterface.h internal/accessiblewindowinterface.cpp internal/accessiblewindowinterface.h - internal/accessibilityconfiguration.cpp - internal/accessibilityconfiguration.h - internal/accessibilitycontextconfiguration.cpp - internal/accessibilitycontextconfiguration.h internal/qaccessibleinterfaceregister.cpp internal/qaccessibleinterfaceregister.h ) diff --git a/framework/accessibility/accessibilitymodule.cpp b/framework/accessibility/accessibilitymodule.cpp index b21eed3671..9089f812f5 100644 --- a/framework/accessibility/accessibilitymodule.cpp +++ b/framework/accessibility/accessibilitymodule.cpp @@ -21,12 +21,16 @@ */ #include "accessibilitymodule.h" +#include + #include "modularity/ioc.h" +#include "iaccessibleapprootobject.h" #include "internal/accessibilitycontroller.h" -#include "internal/accessibilityconfiguration.h" -#include "internal/accessibilitycontextconfiguration.h" +#include "internal/accessibleapprootobject.h" +#include "internal/accessiblestub.h" #include "internal/qaccessibleinterfaceregister.h" +#include "iqaccessibleinterfaceregister.h" #include "global/api/iapiregister.h" #include "api/accessibilityapi.h" @@ -43,20 +47,17 @@ std::string AccessibilityModule::moduleName() const void AccessibilityModule::registerExports() { - m_configuration = std::make_shared(); - - globalIoc()->registerExport(mname, m_configuration); globalIoc()->registerExport(mname, new QAccessibleInterfaceRegister()); + globalIoc()->registerExport(mname, new AccessibleAppRootObject()); } void AccessibilityModule::resolveImports() { auto accr = globalIoc()->resolve(mname); if (accr) { -#ifndef Q_OS_LINUX // https://github.com/musescore/MuseScore/pull/32258#issuecomment-3972545361 accr->registerInterfaceGetter("QQuickWindow", AccessibilityController::accessibleInterface); -#endif accr->registerInterfaceGetter("muse::accessibility::AccessibleObject", AccessibleObject::accessibleInterface); + accr->registerInterfaceGetter("muse::accessibility::AccessibleAppRootObject", AccessibleAppRootObject::accessibleInterface); } } @@ -70,9 +71,27 @@ void AccessibilityModule::registerApi() } } +static QAccessibleInterface* accessibleFactory(const QString& classname, QObject* object) +{ + auto accr = globalIoc()->resolve("accessibility"); + if (accr) { + auto interfaceGetter = accr->interfaceGetter(classname); + if (interfaceGetter) { + return interfaceGetter(object); + } + } + + return AccessibleStub::accessibleInterface(object); +} + void AccessibilityModule::onInit(const IApplication::RunMode&) { - m_configuration->init(); + QAccessible::installFactory(accessibleFactory); + + auto appRoot = globalIoc()->resolve(mname); + if (appRoot) { + appRoot->init(); + } } IContextSetup* AccessibilityModule::newContext(const muse::modularity::ContextPtr& ctx) const @@ -87,8 +106,6 @@ void AccessibilityContext::registerExports() // It probably needs to be split into two separate classes. m_controller = std::make_shared(iocContext()); ioc()->registerExport(mname, m_controller); - - ioc()->registerExport(mname, new AccessibilityContextConfiguration(iocContext())); } void AccessibilityContext::onPreInit(const IApplication::RunMode&) diff --git a/framework/accessibility/accessibilitymodule.h b/framework/accessibility/accessibilitymodule.h index 2da25eb400..7fdf743b1b 100644 --- a/framework/accessibility/accessibilitymodule.h +++ b/framework/accessibility/accessibilitymodule.h @@ -28,7 +28,6 @@ #include "modularity/imodulesetup.h" namespace muse::accessibility { -class AccessibilityConfiguration; class AccessibilityController; class AccessibilityModule : public modularity::IModuleSetup { @@ -42,9 +41,6 @@ class AccessibilityModule : public modularity::IModuleSetup void onInit(const IApplication::RunMode& mode) override; modularity::IContextSetup* newContext(const muse::modularity::ContextPtr& ctx) const override; - -private: - std::shared_ptr m_configuration; }; class AccessibilityContext : public modularity::IContextSetup diff --git a/framework/accessibility/iaccessibilityconfiguration.h b/framework/accessibility/iaccessibilityconfiguration.h deleted file mode 100644 index cdd5942c11..0000000000 --- a/framework/accessibility/iaccessibilityconfiguration.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include "modularity/imoduleinterface.h" - -namespace muse::accessibility { -class IAccessibilityConfiguration : MODULE_GLOBAL_INTERFACE -{ - INTERFACE_ID(IAccessibilityConfiguration) -public: - virtual ~IAccessibilityConfiguration() = default; - - virtual bool isAccessibleActive() const = 0; -}; -} diff --git a/framework/accessibility/iaccessibilitycontextconfiguration.h b/framework/accessibility/iaccessibilitycontextconfiguration.h deleted file mode 100644 index bdbfb0467a..0000000000 --- a/framework/accessibility/iaccessibilitycontextconfiguration.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "modularity/imoduleinterface.h" - -namespace muse::accessibility { -class IAccessibilityContextConfiguration : MODULE_CONTEXT_INTERFACE -{ - INTERFACE_ID(IAccessibilityContextConfiguration) -public: - virtual ~IAccessibilityContextConfiguration() = default; - - virtual bool isAccessibleActive() const = 0; - virtual bool isAccessibleEnabled() const = 0; -}; -} diff --git a/framework/accessibility/iaccessibilitycontroller.h b/framework/accessibility/iaccessibilitycontroller.h index d8c21aa9e6..9fff404685 100644 --- a/framework/accessibility/iaccessibilitycontroller.h +++ b/framework/accessibility/iaccessibilitycontroller.h @@ -47,6 +47,8 @@ class IAccessibilityController : MODULE_CONTEXT_INTERFACE virtual bool needToVoicePanelInfo() const = 0; virtual QString currentPanelAccessibleName() const = 0; + virtual bool isEnabled() const = 0; + virtual void setIgnoreQtAccessibilityEvents(bool ignore) = 0; }; } diff --git a/framework/accessibility/iaccessibleapprootobject.h b/framework/accessibility/iaccessibleapprootobject.h new file mode 100644 index 0000000000..a77bcad205 --- /dev/null +++ b/framework/accessibility/iaccessibleapprootobject.h @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include "modularity/imoduleinterface.h" + +class QAccessibleInterface; +class QObject; +class QWindow; + +namespace muse::accessibility { +class AccessibleObject; + +class IAccessibleAppRootObject : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(IAccessibleAppRootObject) +public: + virtual ~IAccessibleAppRootObject() = default; + + virtual void init() = 0; + + virtual QObject* asQObject() = 0; + + virtual void registerWindow(QWindow* window, AccessibleObject* windowRoot) = 0; + virtual void unregisterWindow(QWindow* window) = 0; + + virtual int windowCount() const = 0; + virtual QWindow* windowAt(int index) const = 0; + virtual AccessibleObject* windowRoot(QWindow* window) const = 0; + virtual QAccessibleInterface* windowIface(int index) const = 0; + + virtual bool isAccessibilityActive() const = 0; +}; +} diff --git a/framework/accessibility/internal/accessibilityconfiguration.cpp b/framework/accessibility/internal/accessibilityconfiguration.cpp deleted file mode 100644 index fedc5db659..0000000000 --- a/framework/accessibility/internal/accessibilityconfiguration.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include "accessibilityconfiguration.h" - -#include - -using namespace muse::accessibility; - -class AccessibilityActivationObserver : public QAccessible::ActivationObserver -{ -public: - AccessibilityActivationObserver() - { - m_isAccessibilityActive = QAccessible::isActive(); - } - - bool isAccessibilityActive() const - { - return m_isAccessibilityActive; - } - - void accessibilityActiveChanged(bool active) override - { - m_isAccessibilityActive = active; - } - -private: - bool m_isAccessibilityActive = false; -}; - -AccessibilityActivationObserver* s_accessibilityActivationObserver = nullptr; - -AccessibilityConfiguration::~AccessibilityConfiguration() -{ - QAccessible::installActivationObserver(nullptr); - delete s_accessibilityActivationObserver; -} - -void AccessibilityConfiguration::init() -{ - s_accessibilityActivationObserver = new AccessibilityActivationObserver(); - - QAccessible::installActivationObserver(s_accessibilityActivationObserver); - - m_inited = true; -} - -bool AccessibilityConfiguration::isAccessibleActive() const -{ - if (!m_inited) { - return false; - } - - return s_accessibilityActivationObserver->isAccessibilityActive(); -} diff --git a/framework/accessibility/internal/accessibilityconfiguration.h b/framework/accessibility/internal/accessibilityconfiguration.h deleted file mode 100644 index 39815a0a1d..0000000000 --- a/framework/accessibility/internal/accessibilityconfiguration.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include "../iaccessibilityconfiguration.h" - -namespace muse::accessibility { -class AccessibilityConfiguration : public IAccessibilityConfiguration -{ -public: - AccessibilityConfiguration() = default; - - ~AccessibilityConfiguration() override; - - void init(); - - bool isAccessibleActive() const override; - -private: - bool m_inited = false; -}; -} diff --git a/framework/accessibility/internal/accessibilitycontextconfiguration.cpp b/framework/accessibility/internal/accessibilitycontextconfiguration.cpp deleted file mode 100644 index 17b3730099..0000000000 --- a/framework/accessibility/internal/accessibilitycontextconfiguration.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "accessibilitycontextconfiguration.h" - -using namespace muse::accessibility; - -bool AccessibilityContextConfiguration::isAccessibleActive() const -{ - return configuration()->isAccessibleActive(); -} - -bool AccessibilityContextConfiguration::isAccessibleEnabled() const -{ - if (!configuration()->isAccessibleActive()) { - return false; - } - - if (!navigationController()) { - return false; - } - - //! NOTE Accessibility available if navigation is used - return navigationController()->activeSection() != nullptr; -} diff --git a/framework/accessibility/internal/accessibilitycontextconfiguration.h b/framework/accessibility/internal/accessibilitycontextconfiguration.h deleted file mode 100644 index c508ce0ec2..0000000000 --- a/framework/accessibility/internal/accessibilitycontextconfiguration.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include "../iaccessibilitycontextconfiguration.h" - -#include "modularity/ioc.h" -#include "ui/inavigationcontroller.h" -#include "../iaccessibilityconfiguration.h" - -namespace muse::accessibility { -class AccessibilityContextConfiguration : public IAccessibilityContextConfiguration, public muse::Contextable -{ - GlobalInject configuration; - ContextInject navigationController = { this }; - -public: - AccessibilityContextConfiguration(const muse::modularity::ContextPtr& iocCtx) - : Contextable(iocCtx) {} - - bool isAccessibleActive() const override; - bool isAccessibleEnabled() const override; -}; -} diff --git a/framework/accessibility/internal/accessibilitycontroller.cpp b/framework/accessibility/internal/accessibilitycontroller.cpp index a5f8264f3f..bfa1c32834 100644 --- a/framework/accessibility/internal/accessibilitycontroller.cpp +++ b/framework/accessibility/internal/accessibilitycontroller.cpp @@ -36,6 +36,7 @@ #include "accessiblewindowinterface.h" #include "iqaccessibleinterfaceregister.h" +#include "global/async/async.h" #include "log.h" // #define MUSE_MODULE_ACCESSIBILITY_TRACE @@ -49,9 +50,6 @@ using namespace muse; using namespace muse::modularity; using namespace muse::accessibility; -AccessibleObject* s_rootObject = nullptr; -std::shared_ptr s_accessibleInterfaceRegister = nullptr; - static void updateHandlerNoop(QAccessibleEvent*) { } @@ -72,12 +70,22 @@ AccessibilityController::~AccessibilityController() QAccessibleInterface* AccessibilityController::accessibleInterface(QObject* window) { - return static_cast(new AccessibleWindowInterface(window, s_rootObject)); + QWindow* qwindow = qobject_cast(window); + if (!qwindow) { + return nullptr; + } + return static_cast(new AccessibleWindowInterface(window)); } void AccessibilityController::deinit() { m_pretendFocusTimer.stop(); + + QWindow* window = mainWindow()->qWindow(); + if (window) { + appRootObject()->unregisterWindow(window); + } + unreg(this); } @@ -86,30 +94,40 @@ void AccessibilityController::setAccessibilityEnabled(bool enabled) m_enabled = enabled; } -static QAccessibleInterface* muAccessibleFactory(const QString& classname, QObject* object) +bool AccessibilityController::isEnabled() const { - if (!s_accessibleInterfaceRegister) { - s_accessibleInterfaceRegister = globalIoc()->resolve("accessibility"); + if (!appRootObject()->isAccessibilityActive()) { + return false; } - auto interfaceGetter = s_accessibleInterfaceRegister->interfaceGetter(classname); - if (interfaceGetter) { - return interfaceGetter(object); + if (!navigationController()) { + return false; } - return AccessibleStub::accessibleInterface(object); + return navigationController()->activeSection() != nullptr; } void AccessibilityController::init() { - QAccessible::installFactory(muAccessibleFactory); - reg(this); const Item& self = findItem(this); - s_rootObject = self.object; - QAccessible::installRootObjectHandler(nullptr); - QAccessible::setRootObject(s_rootObject); + // init() is called when the window is being created, and is not available yet, + // delay the registration + async::Async::call(this, [this, windowRoot = self.object]() { + QWindow* w = mainWindow()->qWindow(); + if (w) { + appRootObject()->registerWindow(w, windowRoot); + + // Clean-up system-default interface that Qt may create during window construction, + // so all next calls return correct interface + QAccessibleInterface* cached = QAccessible::queryAccessibleInterface(w); + if (cached && !dynamic_cast(cached)) { + QAccessible::deleteAccessibleInterface(QAccessible::uniqueId(cached)); + } + } + m_treeConnected = true; + }); auto dispatcher = actionsDispatcher(); if (!dispatcher) { @@ -417,7 +435,7 @@ void AccessibilityController::propertyChanged(IAccessible* item, IAccessible::Pr void AccessibilityController::stateChanged(IAccessible* aitem, State state, bool arg) { - if (!configuration()->isAccessibleEnabled()) { + if (!isEnabled()) { return; } @@ -488,6 +506,10 @@ void AccessibilityController::stateChanged(IAccessible* aitem, State state, bool void AccessibilityController::sendEvent(QAccessibleEvent* ev) { + if (!m_treeConnected) { + return; + } + #ifdef MUSE_MODULE_ACCESSIBILITY_TRACE AccessibleObject* obj = qobject_cast(ev->object()); MYLOG() << "object: " << obj->item()->accessibleName() << ", event: " << int(ev->type()); @@ -500,7 +522,7 @@ void AccessibilityController::sendEvent(QAccessibleEvent* ev) void AccessibilityController::cancelPreviousReading() { - if (!configuration()->isAccessibleActive()) { + if (!appRootObject()->isAccessibilityActive()) { return; } @@ -569,7 +591,7 @@ bool AccessibilityController::needsRevoicing(const QAccessibleInterface& iface, void AccessibilityController::triggerRevoicing(const Item& current) { - if (current.item != m_lastFocused || !configuration()->isAccessibleActive()) { + if (current.item != m_lastFocused || !appRootObject()->isAccessibilityActive()) { return; } @@ -746,11 +768,15 @@ QAccessibleInterface* AccessibilityController::parentIface(const IAccessible* it } if (it.item->accessibleRole() == IAccessible::Role::Application) { - if (!qApp->isQuitLockEnabled()) { - return QAccessible::queryAccessibleInterface(interactive()->topWindow()); - } else { - return QAccessible::queryAccessibleInterface(qApp->focusWindow()); + QWindow* w = item->accessibleWindow(); + if (!w) { + w = mainWindow()->qWindow(); + } + if (w) { + return QAccessible::queryAccessibleInterface(w); } + + return nullptr; } return it.iface; @@ -795,7 +821,7 @@ int AccessibilityController::indexOfChild(const IAccessible* item, const QAccess for (size_t i = 0; i < count; ++i) { const IAccessible* ch = item->accessibleChild(i); const Item& chIt = findItem(ch); - IF_ASSERT_FAILED(chIt.isValid()) { + if (!chIt.isValid()) { continue; } diff --git a/framework/accessibility/internal/accessibilitycontroller.h b/framework/accessibility/internal/accessibilitycontroller.h index fc7c3587d2..56c1873e77 100644 --- a/framework/accessibility/internal/accessibilitycontroller.h +++ b/framework/accessibility/internal/accessibilitycontroller.h @@ -33,12 +33,13 @@ #include "global/iapplication.h" #include "modularity/ioc.h" -#include "../iaccessibilitycontextconfiguration.h" +#include "../iaccessibleapprootobject.h" #include "../iaccessibilitycontroller.h" #include "accessibleobject.h" #include "actions/iactionsdispatcher.h" #include "interactive/iinteractive.h" #include "ui/imainwindow.h" +#include "ui/inavigationcontroller.h" #include "ui/iuiactionsregister.h" class QAccessibleInterface; @@ -53,12 +54,12 @@ class AccessibilityController : public IAccessibilityController, public IAccessi public std::enable_shared_from_this { public: - GlobalInject application; - ContextInject configuration = { this }; + GlobalInject appRootObject; ContextInject actionsDispatcher = { this }; ContextInject interactive = { this }; ContextInject mainWindow = { this }; + ContextInject navigationController = { this }; ContextInject actionsRegister = { this }; public: @@ -84,6 +85,8 @@ class AccessibilityController : public IAccessibilityController, public IAccessi bool needToVoicePanelInfo() const override; QString currentPanelAccessibleName() const override; + bool isEnabled() const override; + void setIgnoreQtAccessibilityEvents(bool ignore) override; // ----- @@ -188,6 +191,7 @@ class AccessibilityController : public IAccessibilityController, public IAccessi bool m_inited = false; bool m_enabled = false; + bool m_treeConnected = false; bool m_ignorePanelChangingVoice = false; bool m_needToVoicePanelInfo = false; diff --git a/framework/accessibility/internal/accessibleapprootinterface.cpp b/framework/accessibility/internal/accessibleapprootinterface.cpp new file mode 100644 index 0000000000..2b2973a2f3 --- /dev/null +++ b/framework/accessibility/internal/accessibleapprootinterface.cpp @@ -0,0 +1,147 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "accessibleapprootinterface.h" + +#include +#include + +#include "accessibleapprootobject.h" + +using namespace muse::accessibility; + +AccessibleAppRootInterface::AccessibleAppRootInterface(AccessibleAppRootObject* root) + : m_root(root) +{ +} + +bool AccessibleAppRootInterface::isValid() const +{ + return m_root != nullptr; +} + +QObject* AccessibleAppRootInterface::object() const +{ + return m_root; +} + +QWindow* AccessibleAppRootInterface::window() const +{ + return nullptr; +} + +QRect AccessibleAppRootInterface::rect() const +{ + return {}; +} + +QAccessibleInterface* AccessibleAppRootInterface::parent() const +{ + return nullptr; +} + +int AccessibleAppRootInterface::childCount() const +{ + return m_root ? m_root->windowCount() : 0; +} + +QAccessibleInterface* AccessibleAppRootInterface::child(int index) const +{ + if (!m_root) { + return nullptr; + } + + return m_root->windowIface(index); +} + +int AccessibleAppRootInterface::indexOfChild(const QAccessibleInterface* iface) const +{ + if (!m_root || !iface) { + return -1; + } + + QWindow* childWindow = iface->window(); + for (int i = 0; i < m_root->windowCount(); ++i) { + if (m_root->windowAt(i) == childWindow) { + return i; + } + } + + return -1; +} + +QAccessibleInterface* AccessibleAppRootInterface::focusChild() const +{ + if (!m_root) { + return nullptr; + } + + QWindow* w = qApp->focusWindow(); + while (w) { + for (int i = 0; i < m_root->windowCount(); ++i) { + if (m_root->windowAt(i) == w) { + QAccessibleInterface* windowIface = child(i); + if (windowIface) { + QAccessibleInterface* focusedChild = windowIface->focusChild(); + return focusedChild ? focusedChild : windowIface; + } + return nullptr; + } + } + w = w->transientParent(); + } + + return nullptr; +} + +QAccessibleInterface* AccessibleAppRootInterface::childAt(int, int) const +{ + return nullptr; +} + +QAccessible::State AccessibleAppRootInterface::state() const +{ + QAccessible::State st; + st.active = true; + return st; +} + +QAccessible::Role AccessibleAppRootInterface::role() const +{ + return QAccessible::Application; +} + +QString AccessibleAppRootInterface::text(QAccessible::Text t) const +{ + if (t == QAccessible::Name) { + return QGuiApplication::applicationName(); + } + return {}; +} + +void AccessibleAppRootInterface::setText(QAccessible::Text, const QString&) +{ +} + +void* AccessibleAppRootInterface::interface_cast(QAccessible::InterfaceType) +{ + return nullptr; +} diff --git a/framework/accessibility/internal/accessibleapprootinterface.h b/framework/accessibility/internal/accessibleapprootinterface.h new file mode 100644 index 0000000000..88ab1c90b5 --- /dev/null +++ b/framework/accessibility/internal/accessibleapprootinterface.h @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +namespace muse::accessibility { +class AccessibleAppRootObject; + +class AccessibleAppRootInterface : public QAccessibleInterface +{ +public: + AccessibleAppRootInterface(AccessibleAppRootObject* root); + + bool isValid() const override; + QObject* object() const override; + QWindow* window() const override; + QRect rect() const override; + + QAccessibleInterface* focusChild() const override; + QAccessibleInterface* childAt(int x, int y) const override; + + QAccessibleInterface* parent() const override; + QAccessibleInterface* child(int index) const override; + int childCount() const override; + int indexOfChild(const QAccessibleInterface* iface) const override; + + QAccessible::State state() const override; + QAccessible::Role role() const override; + QString text(QAccessible::Text) const override; + void setText(QAccessible::Text, const QString& text) override; + +protected: + void* interface_cast(QAccessible::InterfaceType t) override; + +private: + AccessibleAppRootObject* m_root = nullptr; +}; +} diff --git a/framework/accessibility/internal/accessibleapprootobject.cpp b/framework/accessibility/internal/accessibleapprootobject.cpp new file mode 100644 index 0000000000..5b733c47c4 --- /dev/null +++ b/framework/accessibility/internal/accessibleapprootobject.cpp @@ -0,0 +1,153 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "accessibleapprootobject.h" + +#include +#include + +#include "accessibleapprootinterface.h" + +#include "log.h" + +using namespace muse::accessibility; + +class AccessibilityActivationObserver : public QAccessible::ActivationObserver +{ +public: + AccessibilityActivationObserver() + : m_active(QAccessible::isActive()) {} + + bool isActive() const { return m_active; } + + void accessibilityActiveChanged(bool active) override { m_active = active; } + +private: + bool m_active = false; +}; + +static AccessibilityActivationObserver* s_activationObserver = nullptr; + +AccessibleAppRootObject::AccessibleAppRootObject() + : QObject(nullptr) +{ + s_activationObserver = new AccessibilityActivationObserver(); + QAccessible::installActivationObserver(s_activationObserver); +} + +void AccessibleAppRootObject::init() +{ + if (m_inited) { + return; + } + m_inited = true; + + QAccessible::installRootObjectHandler(nullptr); + QAccessible::setRootObject(this); +} + +AccessibleAppRootObject::~AccessibleAppRootObject() +{ + QAccessible::installActivationObserver(nullptr); + delete s_activationObserver; + s_activationObserver = nullptr; +} + +QObject* AccessibleAppRootObject::asQObject() +{ + return this; +} + +QAccessibleInterface* AccessibleAppRootObject::accessibleInterface(QObject* object) +{ + AccessibleAppRootObject* root = qobject_cast(object); + if (!root) { + return nullptr; + } + return new AccessibleAppRootInterface(root); +} + +void AccessibleAppRootObject::registerWindow(QWindow* window, AccessibleObject* windowRoot) +{ + if (!window || !windowRoot) { + return; + } + + for (const WindowEntry& entry : m_windows) { + if (entry.window == window) { + LOGW() << "Window already registered"; + return; + } + } + + m_windows.append({ window, windowRoot }); +} + +void AccessibleAppRootObject::unregisterWindow(QWindow* window) +{ + for (int i = 0; i < m_windows.size(); ++i) { + if (m_windows[i].window == window) { + QAccessibleInterface* iface = QAccessible::queryAccessibleInterface(window); + if (iface) { + QAccessible::deleteAccessibleInterface(QAccessible::uniqueId(iface)); + } + m_windows.removeAt(i); + return; + } + } +} + +int AccessibleAppRootObject::windowCount() const +{ + return m_windows.size(); +} + +QWindow* AccessibleAppRootObject::windowAt(int index) const +{ + if (index < 0 || index >= m_windows.size()) { + return nullptr; + } + return m_windows[index].window; +} + +AccessibleObject* AccessibleAppRootObject::windowRoot(QWindow* window) const +{ + for (const WindowEntry& entry : m_windows) { + if (entry.window == window) { + return entry.windowRoot; + } + } + return nullptr; +} + +QAccessibleInterface* AccessibleAppRootObject::windowIface(int index) const +{ + QWindow* w = windowAt(index); + if (!w) { + return nullptr; + } + return QAccessible::queryAccessibleInterface(w); +} + +bool AccessibleAppRootObject::isAccessibilityActive() const +{ + return s_activationObserver && s_activationObserver->isActive(); +} diff --git a/framework/accessibility/internal/accessibleapprootobject.h b/framework/accessibility/internal/accessibleapprootobject.h new file mode 100644 index 0000000000..871aae7483 --- /dev/null +++ b/framework/accessibility/internal/accessibleapprootobject.h @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include + +#include "../iaccessibleapprootobject.h" +#include "accessibleobject.h" + +class QAccessibleInterface; + +namespace muse::accessibility { +class AccessibleAppRootObject : public QObject, public IAccessibleAppRootObject +{ + Q_OBJECT +public: + AccessibleAppRootObject(); + ~AccessibleAppRootObject() override; + + static QAccessibleInterface* accessibleInterface(QObject* object); + + void init() override; + + QObject* asQObject() override; + + void registerWindow(QWindow* window, AccessibleObject* windowRoot) override; + void unregisterWindow(QWindow* window) override; + + int windowCount() const override; + QWindow* windowAt(int index) const override; + AccessibleObject* windowRoot(QWindow* window) const override; + QAccessibleInterface* windowIface(int index) const override; + + bool isAccessibilityActive() const override; + +private: + + struct WindowEntry { + QWindow* window = nullptr; + AccessibleObject* windowRoot = nullptr; + }; + + QList m_windows; + bool m_inited = false; +}; +} diff --git a/framework/accessibility/internal/accessiblewindowinterface.cpp b/framework/accessibility/internal/accessiblewindowinterface.cpp index 4acc304353..faf425e45c 100644 --- a/framework/accessibility/internal/accessiblewindowinterface.cpp +++ b/framework/accessibility/internal/accessiblewindowinterface.cpp @@ -26,6 +26,9 @@ #include "accessibilitycontroller.h" +#include "modularity/ioc.h" +#include "../iaccessibleapprootobject.h" + #include "translation.h" #include "log.h" @@ -40,12 +43,38 @@ using namespace muse::accessibility; -AccessibleWindowInterface::AccessibleWindowInterface(QObject* window, AccessibleObject* children) - : m_children(children) +AccessibleWindowInterface::AccessibleWindowInterface(QObject* window) { m_window = qobject_cast(window); } +AccessibleObject* AccessibleWindowInterface::resolveWindowRoot() const +{ + if (!m_window) { + return nullptr; + } + + auto appRoot = muse::modularity::globalIoc()->resolve("accessibility"); + if (!appRoot) { + return nullptr; + } + +#ifdef Q_OS_LINUX + // On Linux do not walk transientParent to prevent Orca frow re-interperting + // the whole window tree, that results in VO delay. + return appRoot->windowRoot(m_window); +#else + QWindow* w = m_window; + while (w) { + if (AccessibleObject* root = appRoot->windowRoot(w)) { + return root; + } + w = w->transientParent(); + } + return nullptr; +#endif +} + bool AccessibleWindowInterface::isValid() const { return m_window != nullptr; @@ -71,27 +100,63 @@ QRect AccessibleWindowInterface::rect() const QAccessibleInterface* AccessibleWindowInterface::parent() const { - return nullptr; + auto appRoot = muse::modularity::globalIoc()->resolve("accessibility"); + if (!appRoot) { + return nullptr; + } + return QAccessible::queryAccessibleInterface(appRoot->asQObject()); } int AccessibleWindowInterface::childCount() const { - int count = m_children->controller().lock()->childCount(m_children->item()); - MYLOG() << "item: " << m_children->item()->accessibleName() << ", childCount: " << count; + AccessibleObject* root = resolveWindowRoot(); + if (!root) { + return 0; + } + + auto controller = root->controller().lock(); + if (!controller) { + return 0; + } + + int count = controller->childCount(root->item()); + MYLOG() << "item: " << root->item()->accessibleName() << ", childCount: " << count; return count; } QAccessibleInterface* AccessibleWindowInterface::child(int index) const { - QAccessibleInterface* iface = m_children->controller().lock()->child(m_children->item(), index); - MYLOG() << "item: " << m_children->item()->accessibleName() << ", child: " << index << " " << iface->text(QAccessible::Name); + AccessibleObject* root = resolveWindowRoot(); + if (!root) { + return nullptr; + } + + auto controller = root->controller().lock(); + if (!controller) { + return nullptr; + } + + QAccessibleInterface* iface = controller->child(root->item(), index); + MYLOG() << "item: " << root->item()->accessibleName() << ", child: " << index << " " << + (iface ? iface->text(QAccessible::Name) : "null"); return iface; } int AccessibleWindowInterface::indexOfChild(const QAccessibleInterface* iface) const { - int idx = m_children->controller().lock()->indexOfChild(m_children->item(), iface); - MYLOG() << "item: " << m_children->item()->accessibleName() << ", indexOfChild: " << iface->text(QAccessible::Name) << " = " << idx; + AccessibleObject* root = resolveWindowRoot(); + if (!root) { + return -1; + } + + auto controller = root->controller().lock(); + if (!controller) { + return -1; + } + + int idx = controller->indexOfChild(root->item(), iface); + MYLOG() << "item: " << root->item()->accessibleName() << ", indexOfChild: " << + (iface ? iface->text(QAccessible::Name) : "null") << " = " << idx; return idx; } @@ -103,8 +168,18 @@ QAccessibleInterface* AccessibleWindowInterface::childAt(int, int) const QAccessibleInterface* AccessibleWindowInterface::focusChild() const { - QAccessibleInterface* child = m_children->controller().lock()->focusedChild(m_children->item()); - MYLOG() << "item: " << m_children->item()->accessibleName() << ", focused child: " << (child ? child->text(QAccessible::Name) : "null"); + AccessibleObject* root = resolveWindowRoot(); + if (!root) { + return nullptr; + } + + auto controller = root->controller().lock(); + if (!controller) { + return nullptr; + } + + QAccessibleInterface* child = controller->focusedChild(root->item()); + MYLOG() << "item: " << root->item()->accessibleName() << ", focused child: " << (child ? child->text(QAccessible::Name) : "null"); return child; } diff --git a/framework/accessibility/internal/accessiblewindowinterface.h b/framework/accessibility/internal/accessiblewindowinterface.h index a3736f3f83..505433b51f 100644 --- a/framework/accessibility/internal/accessiblewindowinterface.h +++ b/framework/accessibility/internal/accessiblewindowinterface.h @@ -29,7 +29,7 @@ namespace muse::accessibility { class AccessibleWindowInterface : public QAccessibleInterface { public: - AccessibleWindowInterface(QObject* window, AccessibleObject* children); + explicit AccessibleWindowInterface(QObject* window); bool isValid() const override; QObject* object() const override; @@ -53,7 +53,8 @@ class AccessibleWindowInterface : public QAccessibleInterface void* interface_cast(QAccessible::InterfaceType t) override; private: + AccessibleObject* resolveWindowRoot() const; + QWindow* m_window = nullptr; - AccessibleObject* m_children = nullptr; }; } diff --git a/framework/accessibility/tests/CMakeLists.txt b/framework/accessibility/tests/CMakeLists.txt index bb36689c5d..5c1a637421 100644 --- a/framework/accessibility/tests/CMakeLists.txt +++ b/framework/accessibility/tests/CMakeLists.txt @@ -23,8 +23,7 @@ set(MODULE_TEST muse_accessibility_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/environment.cpp ${CMAKE_CURRENT_LIST_DIR}/accessibilitycontroller_tests.cpp - ${CMAKE_CURRENT_LIST_DIR}/mocks/accessibilityconfigurationmock.h - ${CMAKE_CURRENT_LIST_DIR}/mocks/accessibilitycontextconfigurationmock.h + ${CMAKE_CURRENT_LIST_DIR}/mocks/accessibleapprootobjectmock.h ) set(MODULE_TEST_LINK diff --git a/framework/accessibility/tests/accessibilitycontroller_tests.cpp b/framework/accessibility/tests/accessibilitycontroller_tests.cpp index 7f1915d18b..db7e6fd425 100644 --- a/framework/accessibility/tests/accessibilitycontroller_tests.cpp +++ b/framework/accessibility/tests/accessibilitycontroller_tests.cpp @@ -39,7 +39,8 @@ #include "modularity/ioc.h" #include "ui/tests/mocks/mainwindowmock.h" #include "global/tests/mocks/applicationmock.h" -#include "mocks/accessibilitycontextconfigurationmock.h" +#include "mocks/accessibleapprootobjectmock.h" +#include "ui/tests/mocks/navigationmocks.h" class QEvent; @@ -70,8 +71,11 @@ class Accessibility_ControllerTests : public ::testing::Test m_application = std::make_shared >(); m_controller->application.set(m_application); - m_configuration = std::make_shared >(); - m_controller->configuration.set(m_configuration); + m_appRootObject = std::make_shared >(); + m_controller->appRootObject.set(m_appRootObject); + + m_navigationController = std::make_shared >(); + m_controller->navigationController.set(m_navigationController); } class AccessibleItem : public IAccessible @@ -173,17 +177,17 @@ class Accessibility_ControllerTests : public ::testing::Test std::shared_ptr m_controller; std::shared_ptr m_mainWindow; - std::shared_ptr m_configuration; + std::shared_ptr m_appRootObject; std::shared_ptr m_application; + std::shared_ptr m_navigationController; }; TEST_F(Accessibility_ControllerTests, SendEventOnFocusChanged) { - //! [GIVEN] Accessibility is enabled - ON_CALL(*m_configuration, isAccessibleEnabled()).WillByDefault(Return(true)); - //! [GIVEN] Accessibility is active - ON_CALL(*m_configuration, isAccessibleActive()).WillByDefault(Return(true)); + ON_CALL(*m_appRootObject, isAccessibilityActive()).WillByDefault(Return(true)); + //! [GIVEN] Navigation has an active section (so isEnabled() returns true) + ON_CALL(*m_navigationController, activeSection()).WillByDefault(Return(reinterpret_cast(1))); //! [GIVEN] Two items AccessibleItem* item1 = makeItemWithRegisteredParent(); @@ -211,16 +215,14 @@ TEST_F(Accessibility_ControllerTests, SendEventOnFocusChanged) //! NOTE: need if tested class was created as a shared pointer testing::Mock::AllowLeak(m_mainWindow.get()); testing::Mock::AllowLeak(m_application.get()); - testing::Mock::AllowLeak(m_configuration.get()); + testing::Mock::AllowLeak(m_appRootObject.get()); + testing::Mock::AllowLeak(m_navigationController.get()); } TEST_F(Accessibility_ControllerTests, NotSendEventOnFocusChangedIfAccessibilityIsNotActive) { - //! [GIVEN] Accessibility is enabled - ON_CALL(*m_configuration, isAccessibleEnabled()).WillByDefault(Return(true)); - //! [GIVEN] Accessibility is not active - ON_CALL(*m_configuration, isAccessibleActive()).WillByDefault(Return(false)); + ON_CALL(*m_appRootObject, isAccessibilityActive()).WillByDefault(Return(false)); //! [GIVEN] Two items AccessibleItem* item1 = makeItemWithRegisteredParent(); @@ -244,5 +246,6 @@ TEST_F(Accessibility_ControllerTests, NotSendEventOnFocusChangedIfAccessibilityI //! NOTE: need if tested class was created as a shared pointer testing::Mock::AllowLeak(m_mainWindow.get()); testing::Mock::AllowLeak(m_application.get()); - testing::Mock::AllowLeak(m_configuration.get()); + testing::Mock::AllowLeak(m_appRootObject.get()); + testing::Mock::AllowLeak(m_navigationController.get()); } diff --git a/framework/accessibility/tests/mocks/accessibilityconfigurationmock.h b/framework/accessibility/tests/mocks/accessibilityconfigurationmock.h deleted file mode 100644 index 9973466985..0000000000 --- a/framework/accessibility/tests/mocks/accessibilityconfigurationmock.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2021 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include - -#include "framework/accessibility/iaccessibilityconfiguration.h" - -namespace muse::accessibility { -class AccessibilityConfigurationMock : public IAccessibilityConfiguration -{ -public: - - MOCK_METHOD(bool, isAccessibleActive, (), (const, override)); -}; -} diff --git a/framework/accessibility/tests/mocks/accessibilitycontextconfigurationmock.h b/framework/accessibility/tests/mocks/accessibilitycontextconfigurationmock.h deleted file mode 100644 index 72febcc760..0000000000 --- a/framework/accessibility/tests/mocks/accessibilitycontextconfigurationmock.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - #pragma once - - #include - - #include "framework/accessibility/iaccessibilitycontextconfiguration.h" - -namespace muse::accessibility { -class AccessibilityContextConfigurationMock : public IAccessibilityContextConfiguration -{ -public: - - MOCK_METHOD(bool, isAccessibleActive, (), (const, override)); - MOCK_METHOD(bool, isAccessibleEnabled, (), (const, override)); -}; -} diff --git a/framework/accessibility/tests/mocks/accessibleapprootobjectmock.h b/framework/accessibility/tests/mocks/accessibleapprootobjectmock.h new file mode 100644 index 0000000000..7b3ab4dc22 --- /dev/null +++ b/framework/accessibility/tests/mocks/accessibleapprootobjectmock.h @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +#include "framework/accessibility/iaccessibleapprootobject.h" + +namespace muse::accessibility { +class AccessibleAppRootObjectMock : public IAccessibleAppRootObject +{ +public: + MOCK_METHOD(void, init, (), (override)); + MOCK_METHOD(QObject*, asQObject, (), (override)); + MOCK_METHOD(void, registerWindow, (QWindow*, AccessibleObject*), (override)); + MOCK_METHOD(void, unregisterWindow, (QWindow*), (override)); + MOCK_METHOD(int, windowCount, (), (const, override)); + MOCK_METHOD(QWindow*, windowAt, (int), (const, override)); + MOCK_METHOD(AccessibleObject*, windowRoot, (QWindow*), (const, override)); + MOCK_METHOD(QAccessibleInterface*, windowIface, (int), (const, override)); + MOCK_METHOD(bool, isAccessibilityActive, (), (const, override)); +}; +} diff --git a/framework/stubs/accessibility/CMakeLists.txt b/framework/stubs/accessibility/CMakeLists.txt index dbe5b1d37d..e7d92e85cb 100644 --- a/framework/stubs/accessibility/CMakeLists.txt +++ b/framework/stubs/accessibility/CMakeLists.txt @@ -23,7 +23,7 @@ muse_create_module(muse_accessibility ALIAS muse::accessibility STUB) target_sources(muse_accessibility PRIVATE accessibilitystubmodule.cpp accessibilitystubmodule.h - accessibilityconfigurationstub.h accessibilitycontrollerstub.cpp accessibilitycontrollerstub.h + accessibleapprootobjectstub.h ) diff --git a/framework/stubs/accessibility/accessibilityconfigurationstub.h b/framework/stubs/accessibility/accessibilityconfigurationstub.h deleted file mode 100644 index e7df4a184e..0000000000 --- a/framework/stubs/accessibility/accessibilityconfigurationstub.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2023 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#ifndef MU_ACCESSIBILITY_ACCESSIBILITYCONFIGURATIONSTUB_H -#define MU_ACCESSIBILITY_ACCESSIBILITYCONFIGURATIONSTUB_H - -#include "accessibility/iaccessibilityconfiguration.h" - -namespace muse::accessibility { -class AccessibilityConfigurationStub : public IAccessibilityConfiguration -{ -public: - AccessibilityConfigurationStub() = default; - - bool isAccessibleActive() const override { return false; } -}; -} - -#endif // MU_ACCESSIBILITY_ACCESSIBILITYCONFIGURATIONSTUB_H diff --git a/framework/stubs/accessibility/accessibilitycontextconfigurationstub.h b/framework/stubs/accessibility/accessibilitycontextconfigurationstub.h deleted file mode 100644 index 6be34f644b..0000000000 --- a/framework/stubs/accessibility/accessibilitycontextconfigurationstub.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2026 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "accessibility/iaccessibilitycontextconfiguration.h" - -namespace muse::accessibility { -class AccessibilityContextConfigurationStub : public IAccessibilityContextConfiguration -{ -public: - AccessibilityContextConfigurationStub() = default; - - bool isAccessibleActive() const override { return false; } - bool isAccessibleEnabled() const override { return false; } -}; -} diff --git a/framework/stubs/accessibility/accessibilitycontrollerstub.cpp b/framework/stubs/accessibility/accessibilitycontrollerstub.cpp index af701218c1..53652b4299 100644 --- a/framework/stubs/accessibility/accessibilitycontrollerstub.cpp +++ b/framework/stubs/accessibility/accessibilitycontrollerstub.cpp @@ -65,6 +65,11 @@ QString AccessibilityControllerStub::currentPanelAccessibleName() const return QString(); } +bool AccessibilityControllerStub::isEnabled() const +{ + return false; +} + void AccessibilityControllerStub::setIgnoreQtAccessibilityEvents(bool) { } diff --git a/framework/stubs/accessibility/accessibilitycontrollerstub.h b/framework/stubs/accessibility/accessibilitycontrollerstub.h index ded5f053a6..91ed98b9cd 100644 --- a/framework/stubs/accessibility/accessibilitycontrollerstub.h +++ b/framework/stubs/accessibility/accessibilitycontrollerstub.h @@ -42,6 +42,8 @@ class AccessibilityControllerStub : public IAccessibilityController bool needToVoicePanelInfo() const override; QString currentPanelAccessibleName() const override; + bool isEnabled() const override; + void setIgnoreQtAccessibilityEvents(bool ignore) override; }; } diff --git a/framework/stubs/accessibility/accessibilitystubmodule.cpp b/framework/stubs/accessibility/accessibilitystubmodule.cpp index 7de1a89228..00f6e2ba7a 100644 --- a/framework/stubs/accessibility/accessibilitystubmodule.cpp +++ b/framework/stubs/accessibility/accessibilitystubmodule.cpp @@ -23,9 +23,8 @@ #include "modularity/ioc.h" -#include "accessibilityconfigurationstub.h" -#include "accessibilitycontextconfigurationstub.h" #include "accessibilitycontrollerstub.h" +#include "accessibleapprootobjectstub.h" using namespace muse::accessibility; using namespace muse::modularity; @@ -39,7 +38,7 @@ std::string AccessibilityModule::moduleName() const void AccessibilityModule::registerExports() { - globalIoc()->registerExport(mname, new AccessibilityConfigurationStub()); + globalIoc()->registerExport(moduleName(), new AccessibleAppRootObjectStub()); } IContextSetup* AccessibilityModule::newContext(const muse::modularity::ContextPtr& ctx) const @@ -49,6 +48,5 @@ IContextSetup* AccessibilityModule::newContext(const muse::modularity::ContextPt void AccessibilityContext::registerExports() { - ioc()->registerExport(mname, new AccessibilityContextConfigurationStub()); ioc()->registerExport(mname, new AccessibilityControllerStub()); } diff --git a/framework/stubs/accessibility/accessibleapprootobjectstub.h b/framework/stubs/accessibility/accessibleapprootobjectstub.h new file mode 100644 index 0000000000..6051c60d2c --- /dev/null +++ b/framework/stubs/accessibility/accessibleapprootobjectstub.h @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include "accessibility/iaccessibleapprootobject.h" + +namespace muse::accessibility { +class AccessibleAppRootObjectStub : public IAccessibleAppRootObject +{ +public: + void init() override {} + QObject* asQObject() override { return nullptr; } + void registerWindow(QWindow*, AccessibleObject*) override {} + void unregisterWindow(QWindow*) override {} + int windowCount() const override { return 0; } + QWindow* windowAt(int) const override { return nullptr; } + AccessibleObject* windowRoot(QWindow*) const override { return nullptr; } + QAccessibleInterface* windowIface(int) const override { return nullptr; } + bool isAccessibilityActive() const override { return false; } +}; +} diff --git a/framework/ui/tests/mocks/navigationmocks.h b/framework/ui/tests/mocks/navigationmocks.h index 1b75b69b9a..0eaf99c83a 100644 --- a/framework/ui/tests/mocks/navigationmocks.h +++ b/framework/ui/tests/mocks/navigationmocks.h @@ -25,6 +25,7 @@ #include #include "framework/ui/inavigation.h" +#include "framework/ui/inavigationcontroller.h" namespace muse::ui { class NavigationSectionMock : public INavigationSection @@ -108,6 +109,38 @@ class NavigationControlMock : public INavigationControl MOCK_METHOD(void, trigger, (), (override)); MOCK_METHOD(void, requestActive, (bool), (override)); }; +class NavigationControllerMock : public INavigationController +{ +public: + MOCK_METHOD(void, reg, (INavigationSection*), (override)); + MOCK_METHOD(void, unreg, (INavigationSection*), (override)); + + MOCK_METHOD(bool, requestActivateByName, (const std::string&, const std::string&, const std::string&), (override)); + MOCK_METHOD(bool, requestActivateByIndex, (const std::string&, const std::string&, const INavigation::Index&), (override)); + + MOCK_METHOD(void, resetNavigation, (), (override)); + + MOCK_METHOD(INavigationSection*, activeSection, (), (const, override)); + MOCK_METHOD(INavigationPanel*, activePanel, (), (const, override)); + MOCK_METHOD(INavigationControl*, activeControl, (), (const, override)); + + MOCK_METHOD(const std::set&, sections, (), (const, override)); + MOCK_METHOD(const INavigationSection*, findSection, (const std::string&), (const, override)); + MOCK_METHOD(const INavigationPanel*, findPanel, (const std::string&, const std::string&), (const, override)); + MOCK_METHOD(const INavigationControl*, findControl, (const std::string&, const std::string&, const std::string&), (const, override)); + + MOCK_METHOD(void, setDefaultNavigationControl, (INavigationControl*), (override)); + + MOCK_METHOD(async::Notification, navigationChanged, (), (const, override)); + + MOCK_METHOD(bool, isHighlight, (), (const, override)); + MOCK_METHOD(void, setIsHighlight, (bool), (override)); + MOCK_METHOD(async::Notification, highlightChanged, (), (const, override)); + + MOCK_METHOD(void, setIsResetOnMousePress, (bool), (override)); + + MOCK_METHOD(void, dump, (), (const, override)); +}; } #endif // MUSE_UI_NAVIGATIONSECTIONMOCK_H