diff --git a/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm b/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm index f6174e29b6..fe7c841e63 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm @@ -53,13 +53,42 @@ constexpr auto juceUIAccessibilityContainerTypeList = #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 { @@ -67,6 +96,71 @@ public: } private: + //============================================================================== + class AccessibilityContainer : public ObjCClass + { + public: + AccessibilityContainer() + : ObjCClass ("JUCEUIAccessibilityElementContainer_") + { + addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); + addMethod (@selector (accessibilityFrame), getAccessibilityFrame, @encode (CGRect), "@:"); + addMethod (@selector (accessibilityElements), getAccessibilityElements, "@@:"); + addMethod (@selector (accessibilityContainerType), getAccessibilityContainerType, "i@:"); + + 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 { @@ -82,6 +176,8 @@ private: private: AccessibilityElement() { + addMethod (@selector (dealloc), dealloc, "v@:"); + addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); addMethod (@selector (accessibilityContainer), getAccessibilityContainer, "@@:"); addMethod (@selector (accessibilityFrame), getAccessibilityFrame, @encode (CGRect), "@:"); @@ -90,14 +186,13 @@ private: addMethod (@selector (accessibilityHint), getAccessibilityHelp, "@@:"); addMethod (@selector (accessibilityValue), getAccessibilityValue, "@@:"); addMethod (@selector (setAccessibilityValue:), setAccessibilityValue, "v@:@"); - addMethod (@selector (accessibilityElements), getAccessibilityChildren, "@@:"); addMethod (@selector (accessibilityElementDidBecomeFocused), onFocusGain, "v@:"); addMethod (@selector (accessibilityElementDidLoseFocus), onFocusLoss, "v@:"); addMethod (@selector (accessibilityElementIsFocused), isFocused, "c@:"); addMethod (@selector (accessibilityViewIsModal), getIsAccessibilityModal, "c@:"); - addMethod (@selector (accessibilityActivate), accessibilityPerformPress, "c@:"); + addMethod (@selector (accessibilityActivate), accessibilityPerformActivate, "c@:"); addMethod (@selector (accessibilityIncrement), accessibilityPerformIncrement, "c@:"); addMethod (@selector (accessibilityDecrement), accessibilityPerformDecrement, "c@:"); @@ -106,8 +201,6 @@ private: addMethod (@selector (accessibilityFrameForLineNumber:), getAccessibilityFrameForLineNumber, @encode (CGRect), "@:i"); addMethod (@selector (accessibilityPageContent), getAccessibilityPageContent, "@@:"); - addMethod (@selector (accessibilityContainerType), getAccessibilityContainerType, "i@:"); - addMethod (@selector (accessibilityDataTableCellElementForRow:column:), getAccessibilityDataTableCellElementForRowColumn, "@@:ii"); addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount, "i@:"); addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount, "i@:"); @@ -116,18 +209,54 @@ private: addProtocol (@protocol (UIAccessibilityReadingContent)); + addIvar ("container"); + registerClass(); } //============================================================================== + static UIAccessibilityElement* getContainer (id self) + { + return getIvar (self, "container"); + } + + //============================================================================== + static void dealloc (id self, SEL) + { + if (UIAccessibilityElement* container = getContainer (self)) + { + [container release]; + object_setInstanceVariable (self, "container", nullptr); + } + + sendSuperclassMessage (self, @selector (dealloc)); + } + static id getAccessibilityContainer (id self, SEL) { if (auto* handler = getHandler (self)) { - if (auto* parent = handler->getParent()) - return (id) parent->getNativeImplementation(); + if (handler->getComponent().isOnDesktop()) + return (id) handler->getComponent().getWindowHandle(); - 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]; + object_setInstanceVariable (container, "handler", handler); + [container retain]; + + object_setInstanceVariable (self, "container", container); + + return (id) getContainer (self); + } + + if (auto* parent = handler->getParent()) + return [(id) parent->getNativeImplementation() accessibilityContainer]; } return nil; @@ -162,10 +291,10 @@ private: 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::editableText: case AccessibilityRole::menuItem: case AccessibilityRole::menuBar: case AccessibilityRole::popupMenu: @@ -190,7 +319,9 @@ private: return UIAccessibilityTraitNone; }(); - if (handler->getCurrentState().isSelected()) + const auto state = handler->getCurrentState(); + + if (state.isSelected() || state.isChecked()) traits |= UIAccessibilityTraitSelected; if (auto* valueInterface = getValueInterface (self)) @@ -228,6 +359,24 @@ private: 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 NSInteger getAccessibilityLineNumberForPoint (id self, SEL, CGPoint point) { if (auto* handler = getHandler (self)) @@ -284,26 +433,6 @@ private: return nil; } - static NSInteger getAccessibilityContainerType (id self, SEL) - { - if (auto* handler = getHandler (self)) - { - if (getTableInterface (self) != nullptr) - return juceUIAccessibilityContainerTypeDataTable; - - const auto role = handler->getRole(); - - if (role == AccessibilityRole::popupMenu - || role == AccessibilityRole::list - || role == AccessibilityRole::tree) - { - return juceUIAccessibilityContainerTypeList; - } - } - - return juceUIAccessibilityContainerTypeNone; - } - static id getAccessibilityDataTableCellElementForRowColumn (id self, SEL, NSUInteger row, NSUInteger column) { if (auto* tableInterface = getTableInterface (self)) @@ -363,12 +492,14 @@ void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, Inte return UIAccessibilityNotifications{}; }(); - const auto shouldMoveToHandler = (eventType == InternalAccessibilityEvent::elementCreated - || eventType == InternalAccessibilityEvent::windowOpened - || (eventType == InternalAccessibilityEvent::focusChanged && handler.hasFocus (false))); - if (notification != UIAccessibilityNotifications{}) - sendAccessibilityEvent (notification, shouldMoveToHandler ? (id) handler.getNativeImplementation() : nil); + { + const bool moveToHandler = (eventType == InternalAccessibilityEvent::focusChanged && handler.hasFocus (false)); + + sendAccessibilityEvent (notification, + moveToHandler ? (id) handler.getNativeImplementation() : nil); + } + } void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const diff --git a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm index 7288e6a5cd..b47ebc0d20 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm @@ -262,6 +262,23 @@ private: return nil; } + static NSArray* getAccessibilityChildren (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + auto children = handler->getChildren(); + + auto* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()]; + + for (auto* childHandler : children) + [accessibleChildren addObject: (id) childHandler->getNativeImplementation()]; + + return accessibleChildren; + } + + return nil; + } + static NSArray* getAccessibilitySelectedChildren (id self, SEL) { return getSelectedChildren ([self accessibilityChildren]); diff --git a/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm index 1141ad46ce..fdae83b8fb 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm @@ -193,8 +193,10 @@ protected: NSString* nsString = juceStringToNS (title); + #if ! JUCE_IOS if (nsString != nil && [[self accessibilityValue] isEqual: nsString]) return @""; + #endif return nsString; } @@ -218,23 +220,6 @@ protected: return NO; } - static NSArray* getAccessibilityChildren (id self, SEL) - { - if (auto* handler = getHandler (self)) - { - auto children = handler->getChildren(); - - NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()]; - - for (auto* childHandler : children) - [accessibleChildren addObject: (id) childHandler->getNativeImplementation()]; - - return accessibleChildren; - } - - return nil; - } - static NSInteger getAccessibilityRowCount (id self, SEL) { if (auto* tableInterface = getTableInterface (self)) diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index fb5ecaaa9a..8788c3f259 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -138,9 +138,8 @@ using namespace juce; - (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text; - (BOOL) isAccessibilityElement; -- (id) accessibilityElementAtIndex: (NSInteger) index; -- (NSInteger) accessibilityElementCount; -- (NSInteger) indexOfAccessibilityElement: (id) element; +- (CGRect) accessibilityFrame; +- (NSArray*) accessibilityElements; @end //============================================================================== @@ -550,25 +549,24 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; return NO; } -- (id) accessibilityElementAtIndex: (NSInteger) index +- (CGRect) accessibilityFrame { if (owner != nullptr) if (auto* handler = owner->getComponent().getAccessibilityHandler()) - return (id) handler->getNativeImplementation(); + return convertToCGRect (handler->getComponent().getScreenBounds()); + + return CGRectZero; +} + +- (NSArray*) accessibilityElements +{ + if (owner != nullptr) + if (auto* handler = owner->getComponent().getAccessibilityHandler()) + return getContainerAccessibilityElements (*handler); return nil; } -- (NSInteger) accessibilityElementCount -{ - return owner != nullptr ? 1 : 0; -} - -- (NSInteger) indexOfAccessibilityElement: (id) element -{ - return 0; -} - @end //============================================================================== @@ -618,9 +616,7 @@ UIViewComponentPeer::UIViewComponentPeer (Component& comp, int windowStyleFlags, #if JUCE_COREGRAPHICS_DRAW_ASYNC if (! getComponentAsyncLayerBackedViewDisabled (component)) - { [[view layer] setDrawsAsynchronously: YES]; - } #endif if (isSharedWindow)