/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - 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 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-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. ============================================================================== */ static void juceFreeAccessibilityPlatformSpecificData (UIAccessibilityElement* element) { if (auto* container = juce::getIvar (element, "container")) { object_setInstanceVariable (element, "container", nullptr); object_setInstanceVariable (container, "handler", nullptr); [container release]; } } namespace juce { #if defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 #define JUCE_IOS_CONTAINER_API_AVAILABLE 1 #endif JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability", "-Wunguarded-availability-new") constexpr auto juceUIAccessibilityContainerTypeNone = #if JUCE_IOS_CONTAINER_API_AVAILABLE UIAccessibilityContainerTypeNone; #else 0; #endif constexpr auto juceUIAccessibilityContainerTypeDataTable = #if JUCE_IOS_CONTAINER_API_AVAILABLE UIAccessibilityContainerTypeDataTable; #else 1; #endif constexpr auto juceUIAccessibilityContainerTypeList = #if JUCE_IOS_CONTAINER_API_AVAILABLE UIAccessibilityContainerTypeList; #else 2; #endif JUCE_END_IGNORE_WARNINGS_GCC_LIKE #define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1 //============================================================================== static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler) { const auto children = handler.getChildren(); NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()]; [accessibleChildren addObject: (id) handler.getNativeImplementation()]; for (auto* childHandler : children) { id accessibleElement = [&childHandler] { id native = (id) childHandler->getNativeImplementation(); if (childHandler->getChildren().size() > 0) return [native accessibilityContainer]; return native; }(); if (accessibleElement != nil) [accessibleChildren addObject: accessibleElement]; } return accessibleChildren; } //============================================================================== class AccessibilityHandler::AccessibilityNativeImpl { public: explicit AccessibilityNativeImpl (AccessibilityHandler& handler) : accessibilityElement (AccessibilityElement::create (handler)) { } UIAccessibilityElement* getAccessibilityElement() const noexcept { return accessibilityElement.get(); } private: //============================================================================== class AccessibilityContainer : public ObjCClass { public: AccessibilityContainer() : ObjCClass ("JUCEUIAccessibilityElementContainer_") { addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement); addMethod (@selector (accessibilityFrame), getAccessibilityFrame); addMethod (@selector (accessibilityElements), getAccessibilityElements); #if JUCE_IOS_CONTAINER_API_AVAILABLE if (@available (iOS 11.0, *)) addMethod (@selector (accessibilityContainerType), getAccessibilityContainerType); #endif addIvar ("handler"); registerClass(); } private: static AccessibilityHandler* getHandler (id self) { return getIvar (self, "handler"); } static BOOL getIsAccessibilityElement (id, SEL) { return NO; } static CGRect getAccessibilityFrame (id self, SEL) { if (auto* handler = getHandler (self)) return convertToCGRect (handler->getComponent().getScreenBounds()); return CGRectZero; } static NSArray* getAccessibilityElements (id self, SEL) { if (auto* handler = getHandler (self)) return getContainerAccessibilityElements (*handler); return nil; } static NSInteger getAccessibilityContainerType (id self, SEL) { if (auto* handler = getHandler (self)) { if (handler->getTableInterface() != nullptr) return juceUIAccessibilityContainerTypeDataTable; const auto role = handler->getRole(); if (role == AccessibilityRole::popupMenu || role == AccessibilityRole::list || role == AccessibilityRole::tree) { return juceUIAccessibilityContainerTypeList; } } return juceUIAccessibilityContainerTypeNone; } }; //============================================================================== class AccessibilityElement : public AccessibleObjCClass { public: enum class Type { defaultElement, textElement }; static Holder create (AccessibilityHandler& handler) { static AccessibilityElement cls { Type::defaultElement }; static AccessibilityElement textCls { Type::textElement }; id instance = (hasEditableText (handler) ? textCls : cls).createInstance(); Holder element ([instance initWithAccessibilityContainer: (id) handler.getComponent().getWindowHandle()]); object_setInstanceVariable (element.get(), "handler", &handler); return element; } AccessibilityElement (Type elementType) { addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement); addMethod (@selector (accessibilityContainer), getAccessibilityContainer); addMethod (@selector (accessibilityFrame), getAccessibilityFrame); addMethod (@selector (accessibilityTraits), getAccessibilityTraits); addMethod (@selector (accessibilityLabel), getAccessibilityTitle); addMethod (@selector (accessibilityHint), getAccessibilityHelp); addMethod (@selector (accessibilityValue), getAccessibilityValue); addMethod (@selector (setAccessibilityValue:), setAccessibilityValue); addMethod (@selector (accessibilityElementDidBecomeFocused), onFocusGain); addMethod (@selector (accessibilityElementDidLoseFocus), onFocusLoss); addMethod (@selector (accessibilityElementIsFocused), isFocused); addMethod (@selector (accessibilityViewIsModal), getIsAccessibilityModal); addMethod (@selector (accessibilityActivate), accessibilityPerformActivate); addMethod (@selector (accessibilityIncrement), accessibilityPerformIncrement); addMethod (@selector (accessibilityDecrement), accessibilityPerformDecrement); addMethod (@selector (accessibilityPerformEscape), accessibilityPerformEscape); #if JUCE_IOS_CONTAINER_API_AVAILABLE if (@available (iOS 11.0, *)) { addMethod (@selector (accessibilityDataTableCellElementForRow:column:), getAccessibilityDataTableCellElementForRowColumn); addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount); addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount); addMethod (@selector (accessibilityRowRange), getAccessibilityRowIndexRange); addMethod (@selector (accessibilityColumnRange), getAccessibilityColumnIndexRange); } #endif if (elementType == Type::textElement) { addMethod (@selector (accessibilityLineNumberForPoint:), getAccessibilityLineNumberForPoint); addMethod (@selector (accessibilityContentForLineNumber:), getAccessibilityContentForLineNumber); addMethod (@selector (accessibilityFrameForLineNumber:), getAccessibilityFrameForLineNumber); addMethod (@selector (accessibilityPageContent), getAccessibilityPageContent); addProtocol (@protocol (UIAccessibilityReadingContent)); } addIvar ("container"); registerClass(); } private: //============================================================================== static UIAccessibilityElement* getContainer (id self) { return getIvar (self, "container"); } //============================================================================== static id getAccessibilityContainer (id self, SEL) { if (auto* handler = getHandler (self)) { if (handler->getComponent().isOnDesktop()) return (id) handler->getComponent().getWindowHandle(); if (handler->getChildren().size() > 0) { if (UIAccessibilityElement* container = getContainer (self)) return container; static AccessibilityContainer cls; id windowHandle = (id) handler->getComponent().getWindowHandle(); UIAccessibilityElement* container = [cls.createInstance() initWithAccessibilityContainer: windowHandle]; [container retain]; object_setInstanceVariable (container, "handler", handler); object_setInstanceVariable (self, "container", container); return container; } if (auto* parent = handler->getParent()) return [(id) parent->getNativeImplementation() accessibilityContainer]; } return nil; } static CGRect getAccessibilityFrame (id self, SEL) { if (auto* handler = getHandler (self)) return convertToCGRect (handler->getComponent().getScreenBounds()); return CGRectZero; } static UIAccessibilityTraits getAccessibilityTraits (id self, SEL) { auto traits = UIAccessibilityTraits{}; if (auto* handler = getHandler (self)) { traits |= [&handler] { switch (handler->getRole()) { case AccessibilityRole::button: case AccessibilityRole::toggleButton: case AccessibilityRole::radioButton: case AccessibilityRole::comboBox: return UIAccessibilityTraitButton; case AccessibilityRole::label: case AccessibilityRole::staticText: return UIAccessibilityTraitStaticText; case AccessibilityRole::image: return UIAccessibilityTraitImage; case AccessibilityRole::tableHeader: return UIAccessibilityTraitHeader; case AccessibilityRole::hyperlink: return UIAccessibilityTraitLink; case AccessibilityRole::editableText: return UIAccessibilityTraitKeyboardKey; case AccessibilityRole::ignored: return UIAccessibilityTraitNotEnabled; case AccessibilityRole::slider: case AccessibilityRole::menuItem: case AccessibilityRole::menuBar: case AccessibilityRole::popupMenu: case AccessibilityRole::table: case AccessibilityRole::column: case AccessibilityRole::row: case AccessibilityRole::cell: case AccessibilityRole::list: case AccessibilityRole::listItem: case AccessibilityRole::tree: case AccessibilityRole::treeItem: case AccessibilityRole::progressBar: case AccessibilityRole::group: case AccessibilityRole::dialogWindow: case AccessibilityRole::window: case AccessibilityRole::scrollBar: case AccessibilityRole::tooltip: case AccessibilityRole::splashScreen: case AccessibilityRole::unspecified: break; } return UIAccessibilityTraitNone; }(); const auto state = handler->getCurrentState(); if (state.isSelected() || state.isChecked()) traits |= UIAccessibilityTraitSelected; if (auto* valueInterface = getValueInterface (self)) if (! valueInterface->isReadOnly() && valueInterface->getRange().isValid()) traits |= UIAccessibilityTraitAdjustable; } return traits | sendSuperclassMessage (self, @selector (accessibilityTraits)); } static NSString* getAccessibilityValue (id self, SEL) { if (auto* handler = getHandler (self)) { if (handler->getCurrentState().isCheckable()) return handler->getCurrentState().isChecked() ? @"1" : @"0"; return (NSString*) getAccessibilityValueFromInterfaces (*handler); } return nil; } static void onFocusGain (id self, SEL) { if (auto* handler = getHandler (self)) { const WeakReference safeComponent (&handler->getComponent()); performActionIfSupported (self, AccessibilityActionType::focus); if (safeComponent != nullptr) handler->grabFocus(); } } static void onFocusLoss (id self, SEL) { if (auto* handler = getHandler (self)) handler->giveAwayFocus(); } static BOOL isFocused (id self, SEL) { if (auto* handler = getHandler (self)) return handler->hasFocus (false); return NO; } static BOOL accessibilityPerformActivate (id self, SEL) { if (auto* handler = getHandler (self)) { // occasionaly VoiceOver sends accessibilityActivate to the wrong element, so we first query // which element it thinks has focus and forward the event on to that element if it differs id focusedElement = UIAccessibilityFocusedElement (UIAccessibilityNotificationVoiceOverIdentifier); if (! [(id) handler->getNativeImplementation() isEqual: focusedElement]) return [focusedElement accessibilityActivate]; if (handler->hasFocus (false)) return accessibilityPerformPress (self, {}); } return NO; } static BOOL accessibilityPerformEscape (id self, SEL) { if (auto* handler = getHandler (self)) { if (auto* modal = Component::getCurrentlyModalComponent()) { if (auto* modalHandler = modal->getAccessibilityHandler()) { if (modalHandler == handler || modalHandler->isParentOf (handler)) { modal->exitModalState (0); return YES; } } } } return NO; } static id getAccessibilityDataTableCellElementForRowColumn (id self, SEL, NSUInteger row, NSUInteger column) { if (auto* tableInterface = getTableInterface (self)) if (auto* cellHandler = tableInterface->getCellHandler ((int) row, (int) column)) return (id) cellHandler->getNativeImplementation(); return nil; } static NSInteger getAccessibilityLineNumberForPoint (id self, SEL, CGPoint point) { if (auto* handler = getHandler (self)) { if (auto* textInterface = handler->getTextInterface()) { auto pointInt = roundToIntPoint (point); if (handler->getComponent().getScreenBounds().contains (pointInt)) { auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() }); for (int i = 0; i < textBounds.getNumRectangles(); ++i) if (textBounds.getRectangle (i).contains (pointInt)) return (NSInteger) i; } } } return NSNotFound; } static NSString* getAccessibilityContentForLineNumber (id self, SEL, NSInteger lineNumber) { if (auto* textInterface = getTextInterface (self)) { auto lines = StringArray::fromLines (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })); if ((int) lineNumber < lines.size()) return juceStringToNS (lines[(int) lineNumber]); } return nil; } static CGRect getAccessibilityFrameForLineNumber (id self, SEL, NSInteger lineNumber) { if (auto* textInterface = getTextInterface (self)) { auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() }); if (lineNumber < textBounds.getNumRectangles()) return convertToCGRect (textBounds.getRectangle ((int) lineNumber)); } return CGRectZero; } static NSString* getAccessibilityPageContent (id self, SEL) { if (auto* textInterface = getTextInterface (self)) return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })); return nil; } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement) }; //============================================================================== AccessibilityElement::Holder accessibilityElement; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl) }; //============================================================================== AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return (AccessibilityNativeHandle*) nativeImpl->getAccessibilityElement(); } static bool areAnyAccessibilityClientsActive() { return UIAccessibilityIsVoiceOverRunning(); } static void sendAccessibilityEvent (UIAccessibilityNotifications notification, id argument) { if (! areAnyAccessibilityClientsActive()) return; jassert (notification != UIAccessibilityNotifications{}); UIAccessibilityPostNotification (notification, argument); } void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, InternalAccessibilityEvent eventType) { auto notification = [eventType] { switch (eventType) { case InternalAccessibilityEvent::elementCreated: case InternalAccessibilityEvent::elementDestroyed: case InternalAccessibilityEvent::elementMovedOrResized: case InternalAccessibilityEvent::focusChanged: return UIAccessibilityLayoutChangedNotification; case InternalAccessibilityEvent::windowOpened: case InternalAccessibilityEvent::windowClosed: return UIAccessibilityScreenChangedNotification; } return UIAccessibilityNotifications{}; }(); if (notification != UIAccessibilityNotifications{}) { const bool moveToHandler = (eventType == InternalAccessibilityEvent::focusChanged && handler.hasFocus (false)); sendAccessibilityEvent (notification, moveToHandler ? (id) handler.getNativeImplementation() : nil); } } void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const { auto notification = [eventType] { switch (eventType) { case AccessibilityEvent::textSelectionChanged: case AccessibilityEvent::rowSelectionChanged: case AccessibilityEvent::textChanged: case AccessibilityEvent::valueChanged: case AccessibilityEvent::titleChanged: break; case AccessibilityEvent::structureChanged: return UIAccessibilityLayoutChangedNotification; } return UIAccessibilityNotifications{}; }(); if (notification != UIAccessibilityNotifications{}) sendAccessibilityEvent (notification, (id) getNativeImplementation()); } void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority) { sendAccessibilityEvent (UIAccessibilityAnnouncementNotification, juceStringToNS (announcementString)); } } // namespace juce