/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") int AccessibilityNativeHandle::idCounter = 0; //============================================================================== class UIAScrollProvider : public UIAProviderBase, public ComBaseClassHelper { public: using UIAProviderBase::UIAProviderBase; JUCE_COMCALL Scroll (ComTypes::ScrollAmount, ComTypes::ScrollAmount) override { return E_FAIL; } JUCE_COMCALL SetScrollPercent (double, double) override { return E_FAIL; } JUCE_COMCALL get_HorizontalScrollPercent (double*) override { return E_FAIL; } JUCE_COMCALL get_VerticalScrollPercent (double*) override { return E_FAIL; } JUCE_COMCALL get_HorizontalViewSize (double*) override { return E_FAIL; } JUCE_COMCALL get_VerticalViewSize (double*) override { return E_FAIL; } JUCE_COMCALL get_HorizontallyScrollable (BOOL*) override { return E_FAIL; } JUCE_COMCALL get_VerticallyScrollable (BOOL*) override { return E_FAIL; } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAScrollProvider) }; class UIAScrollItemProvider : public UIAProviderBase, public ComBaseClassHelper { public: using UIAProviderBase::UIAProviderBase; JUCE_COMCALL ScrollIntoView() override { if (auto* handler = getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface)) { if (auto* tableInterface = handler->getTableInterface()) { tableInterface->showCell (getHandler()); return S_OK; } } return (HRESULT) UIA_E_NOTSUPPORTED; } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAScrollItemProvider) }; //============================================================================== static String getAutomationId (const AccessibilityHandler& handler) { auto result = handler.getTitle(); auto* parentComponent = handler.getComponent().getParentComponent(); while (parentComponent != nullptr) { if (auto* parentHandler = parentComponent->getAccessibilityHandler()) { auto parentTitle = parentHandler->getTitle(); result << "." << (parentTitle.isNotEmpty() ? parentTitle : ""); } parentComponent = parentComponent->getParentComponent(); } return result; } static auto roleToControlTypeId (AccessibilityRole roleType) { using namespace ComTypes::Constants; switch (roleType) { case AccessibilityRole::popupMenu: case AccessibilityRole::dialogWindow: case AccessibilityRole::splashScreen: case AccessibilityRole::window: return UIA_WindowControlTypeId; case AccessibilityRole::label: case AccessibilityRole::staticText: return UIA_TextControlTypeId; case AccessibilityRole::column: case AccessibilityRole::row: return UIA_ListItemControlTypeId; case AccessibilityRole::button: return UIA_ButtonControlTypeId; case AccessibilityRole::toggleButton: return UIA_CheckBoxControlTypeId; case AccessibilityRole::radioButton: return UIA_RadioButtonControlTypeId; case AccessibilityRole::comboBox: return UIA_ComboBoxControlTypeId; case AccessibilityRole::image: return UIA_ImageControlTypeId; case AccessibilityRole::slider: return UIA_SliderControlTypeId; case AccessibilityRole::editableText: return UIA_EditControlTypeId; case AccessibilityRole::menuItem: return UIA_MenuItemControlTypeId; case AccessibilityRole::menuBar: return UIA_MenuBarControlTypeId; case AccessibilityRole::table: return UIA_TableControlTypeId; case AccessibilityRole::tableHeader: return UIA_HeaderControlTypeId; case AccessibilityRole::cell: return UIA_DataItemControlTypeId; case AccessibilityRole::hyperlink: return UIA_HyperlinkControlTypeId; case AccessibilityRole::list: return UIA_ListControlTypeId; case AccessibilityRole::listItem: return UIA_ListItemControlTypeId; case AccessibilityRole::tree: return UIA_TreeControlTypeId; case AccessibilityRole::treeItem: return UIA_TreeItemControlTypeId; case AccessibilityRole::progressBar: return UIA_ProgressBarControlTypeId; case AccessibilityRole::group: return UIA_GroupControlTypeId; case AccessibilityRole::scrollBar: return UIA_ScrollBarControlTypeId; case AccessibilityRole::tooltip: return UIA_ToolTipControlTypeId; case AccessibilityRole::ignored: case AccessibilityRole::unspecified: break; }; return UIA_CustomControlTypeId; } //============================================================================== AccessibilityNativeHandle::AccessibilityNativeHandle (AccessibilityHandler& handler) : ComBaseClassHelper (0), accessibilityHandler (handler) { } //============================================================================== JUCE_COMRESULT AccessibilityNativeHandle::QueryInterface (REFIID refId, void** result) { *result = nullptr; if (! isElementValid()) return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE; if ((refId == __uuidof (ComTypes::IRawElementProviderFragmentRoot) && ! isFragmentRoot())) return E_NOINTERFACE; return ComBaseClassHelper::QueryInterface (refId, result); } //============================================================================== JUCE_COMRESULT AccessibilityNativeHandle::get_HostRawElementProvider (IRawElementProviderSimple** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { if (isFragmentRoot()) if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating()) return wrapper->hostProviderFromHwnd ((HWND) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::get_ProviderOptions (ProviderOptions* options) { if (options == nullptr) return E_INVALIDARG; *options = (ProviderOptions) (ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading); return S_OK; } JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUnknown** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { *pRetVal = [&]() -> IUnknown* { const auto role = accessibilityHandler.getRole(); const auto fragmentRoot = isFragmentRoot(); const auto isListOrTableCell = [] (auto& handler) { if (auto* tableHandler = getEnclosingHandlerWithInterface (&handler, &AccessibilityHandler::getTableInterface)) { if (auto* tableInterface = tableHandler->getTableInterface()) { const auto row = tableInterface->getRowSpan (handler); const auto column = tableInterface->getColumnSpan (handler); return row.hasValue() && column.hasValue(); } } return false; }; using namespace ComTypes::Constants; switch (pId) { case UIA_WindowPatternId: { if (fragmentRoot) return new UIAWindowProvider (this); break; } case UIA_TransformPatternId: { if (fragmentRoot) return new UIATransformProvider (this); break; } case UIA_TextPatternId: case UIA_TextPattern2Id: { if (accessibilityHandler.getTextInterface() != nullptr) return new UIATextProvider (this); break; } case UIA_ValuePatternId: { if (accessibilityHandler.getValueInterface() != nullptr) return new UIAValueProvider (this); break; } case UIA_RangeValuePatternId: { if (accessibilityHandler.getValueInterface() != nullptr && accessibilityHandler.getValueInterface()->getRange().isValid()) { return new UIARangeValueProvider (this); } break; } case UIA_TogglePatternId: { if (accessibilityHandler.getCurrentState().isCheckable() && (accessibilityHandler.getActions().contains (AccessibilityActionType::toggle) || accessibilityHandler.getActions().contains (AccessibilityActionType::press))) { return new UIAToggleProvider (this); } break; } case UIA_SelectionPatternId: { if (role == AccessibilityRole::list || role == AccessibilityRole::popupMenu || role == AccessibilityRole::tree) { return new UIASelectionProvider (this); } break; } case UIA_SelectionItemPatternId: { auto state = accessibilityHandler.getCurrentState(); if (state.isSelectable() || state.isMultiSelectable() || role == AccessibilityRole::radioButton) { return new UIASelectionItemProvider (this); } break; } case UIA_TablePatternId: case UIA_GridPatternId: { if (accessibilityHandler.getTableInterface() != nullptr && (pId == UIA_GridPatternId || accessibilityHandler.getRole() == AccessibilityRole::table)) return static_cast (new UIAGridProvider (this)); break; } case UIA_TableItemPatternId: case UIA_GridItemPatternId: { if (isListOrTableCell (accessibilityHandler)) return static_cast (new UIAGridItemProvider (this)); break; } case UIA_InvokePatternId: { if (accessibilityHandler.getActions().contains (AccessibilityActionType::press)) return new UIAInvokeProvider (this); break; } case UIA_ExpandCollapsePatternId: { if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu) && accessibilityHandler.getCurrentState().isExpandable()) return new UIAExpandCollapseProvider (this); break; } case UIA_ScrollPatternId: { if (accessibilityHandler.getTableInterface() != nullptr) return new UIAScrollProvider (this); break; } case UIA_ScrollItemPatternId: { if (isListOrTableCell (accessibilityHandler)) return new UIAScrollItemProvider (this); break; } } return nullptr; }(); return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { VariantHelpers::clear (pRetVal); const auto role = accessibilityHandler.getRole(); const auto state = accessibilityHandler.getCurrentState(); const auto ignored = accessibilityHandler.isIgnored(); using namespace ComTypes::Constants; switch (propertyId) { case UIA_AutomationIdPropertyId: VariantHelpers::setString (getAutomationId (accessibilityHandler), pRetVal); break; case UIA_ControlTypePropertyId: VariantHelpers::setInt (roleToControlTypeId (role), pRetVal); break; case UIA_FrameworkIdPropertyId: VariantHelpers::setString ("JUCE", pRetVal); break; case UIA_FullDescriptionPropertyId: VariantHelpers::setString (accessibilityHandler.getDescription(), pRetVal); break; case UIA_HelpTextPropertyId: VariantHelpers::setString (accessibilityHandler.getHelp(), pRetVal); break; case UIA_IsContentElementPropertyId: VariantHelpers::setBool (! ignored && accessibilityHandler.isVisibleWithinParent(), pRetVal); break; case UIA_IsControlElementPropertyId: VariantHelpers::setBool (true, pRetVal); break; case UIA_IsDialogPropertyId: VariantHelpers::setBool (role == AccessibilityRole::dialogWindow, pRetVal); break; case UIA_IsEnabledPropertyId: VariantHelpers::setBool (accessibilityHandler.getComponent().isEnabled(), pRetVal); break; case UIA_IsKeyboardFocusablePropertyId: VariantHelpers::setBool (state.isFocusable(), pRetVal); break; case UIA_HasKeyboardFocusPropertyId: VariantHelpers::setBool (accessibilityHandler.hasFocus (true), pRetVal); break; case UIA_IsOffscreenPropertyId: VariantHelpers::setBool (! accessibilityHandler.isVisibleWithinParent(), pRetVal); break; case UIA_IsPasswordPropertyId: if (auto* textInterface = accessibilityHandler.getTextInterface()) VariantHelpers::setBool (textInterface->isDisplayingProtectedText(), pRetVal); break; case UIA_IsPeripheralPropertyId: VariantHelpers::setBool (role == AccessibilityRole::tooltip || role == AccessibilityRole::popupMenu || role == AccessibilityRole::splashScreen, pRetVal); break; case UIA_NamePropertyId: if (! ignored) VariantHelpers::setString (getElementName(), pRetVal); break; case UIA_ProcessIdPropertyId: VariantHelpers::setInt ((int) GetCurrentProcessId(), pRetVal); break; case UIA_NativeWindowHandlePropertyId: if (isFragmentRoot()) VariantHelpers::setInt ((int) (pointer_sized_int) accessibilityHandler.getComponent().getWindowHandle(), pRetVal); break; } return S_OK; }); } //============================================================================== JUCE_COMRESULT AccessibilityNativeHandle::Navigate (ComTypes::NavigateDirection direction, ComTypes::IRawElementProviderFragment** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { auto* handler = [&]() -> AccessibilityHandler* { if (direction == ComTypes::NavigateDirection_Parent) return accessibilityHandler.getParent(); if (direction == ComTypes::NavigateDirection_FirstChild || direction == ComTypes::NavigateDirection_LastChild) { auto children = accessibilityHandler.getChildren(); return children.empty() ? nullptr : (direction == ComTypes::NavigateDirection_FirstChild ? children.front() : children.back()); } if (direction == ComTypes::NavigateDirection_NextSibling || direction == ComTypes::NavigateDirection_PreviousSibling) { if (auto* parent = accessibilityHandler.getParent()) { const auto siblings = parent->getChildren(); const auto iter = std::find (siblings.cbegin(), siblings.cend(), &accessibilityHandler); if (iter == siblings.end()) return nullptr; if (direction == ComTypes::NavigateDirection_NextSibling && iter != std::prev (siblings.cend())) return *std::next (iter); if (direction == ComTypes::NavigateDirection_PreviousSibling && iter != siblings.cbegin()) return *std::prev (iter); } } return nullptr; }(); if (handler != nullptr) if (auto* provider = handler->getNativeImplementation()) if (provider->isElementValid()) provider->QueryInterface (IID_PPV_ARGS (pRetVal)); return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::GetRuntimeId (SAFEARRAY** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { if (! isFragmentRoot()) { *pRetVal = SafeArrayCreateVector (VT_I4, 0, 2); if (*pRetVal == nullptr) return E_OUTOFMEMORY; for (LONG i = 0; i < 2; ++i) { auto hr = SafeArrayPutElement (*pRetVal, &i, &rtid[(size_t) i]); if (FAILED (hr)) return E_FAIL; } } return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::get_BoundingRectangle (ComTypes::UiaRect* pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { auto bounds = Desktop::getInstance().getDisplays() .logicalToPhysical (accessibilityHandler.getComponent().getScreenBounds()); pRetVal->left = bounds.getX(); pRetVal->top = bounds.getY(); pRetVal->width = bounds.getWidth(); pRetVal->height = bounds.getHeight(); return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) { return withCheckedComArgs (pRetVal, *this, [] { return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::SetFocus() { if (! isElementValid()) return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE; const WeakReference safeComponent (&accessibilityHandler.getComponent()); accessibilityHandler.getActions().invoke (AccessibilityActionType::focus); if (safeComponent != nullptr) accessibilityHandler.grabFocus(); return S_OK; } JUCE_COMRESULT AccessibilityNativeHandle::get_FragmentRoot (ComTypes::IRawElementProviderFragmentRoot** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT { auto* handler = [&]() -> AccessibilityHandler* { if (isFragmentRoot()) return &accessibilityHandler; if (auto* peer = accessibilityHandler.getComponent().getPeer()) return peer->getComponent().getAccessibilityHandler(); return nullptr; }(); if (handler != nullptr) { handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); return S_OK; } return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE; }); } //============================================================================== JUCE_COMRESULT AccessibilityNativeHandle::ElementProviderFromPoint (double x, double y, ComTypes::IRawElementProviderFragment** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { auto* handler = [&] { auto logicalScreenPoint = Desktop::getInstance().getDisplays() .physicalToLogical (Point (roundToInt (x), roundToInt (y))); if (auto* child = accessibilityHandler.getChildAt (logicalScreenPoint)) return child; return &accessibilityHandler; }(); handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); return S_OK; }); } JUCE_COMRESULT AccessibilityNativeHandle::GetFocus (ComTypes::IRawElementProviderFragment** pRetVal) { return withCheckedComArgs (pRetVal, *this, [&] { const auto getFocusHandler = [this]() -> AccessibilityHandler* { if (auto* modal = Component::getCurrentlyModalComponent()) { const auto& component = accessibilityHandler.getComponent(); if (! component.isParentOf (modal) && component.isCurrentlyBlockedByAnotherModalComponent()) { if (auto* modalHandler = modal->getAccessibilityHandler()) { if (auto* focusChild = modalHandler->getChildFocus()) return focusChild; return modalHandler; } } } if (auto* focusChild = accessibilityHandler.getChildFocus()) return focusChild; return nullptr; }; if (auto* focusHandler = getFocusHandler()) focusHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); return S_OK; }); } //============================================================================== String AccessibilityNativeHandle::getElementName() const { if (accessibilityHandler.getRole() == AccessibilityRole::tooltip) return accessibilityHandler.getDescription(); auto name = accessibilityHandler.getTitle(); if (name.isEmpty() && isFragmentRoot()) return getAccessibleApplicationOrPluginName(); return name; } JUCE_END_IGNORE_WARNINGS_GCC_LIKE } // namespace juce