diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 7d61b26146..2d21b96c70 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -276,7 +276,10 @@ bool getComponentAsyncLayerBackedViewDisabled (juce::Component& comp) } // namespace juce #if JUCE_MAC || JUCE_IOS + #include "native/accessibility/juce_mac_AccessibilitySharedCode.mm" + #if JUCE_IOS + #include "native/accessibility/juce_ios_Accessibility.mm" #include "native/juce_ios_UIViewComponentPeer.mm" #include "native/juce_ios_Windowing.mm" #include "native/juce_ios_FileChooser.mm" diff --git a/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm b/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm new file mode 100644 index 0000000000..f0aa105d1c --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm @@ -0,0 +1,417 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +namespace juce +{ + +#if (defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_11_0) + #define JUCE_IOS_CONTAINER_API_AVAILABLE 1 +#endif + +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 + +#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1 + +//============================================================================== +class AccessibilityHandler::AccessibilityNativeImpl +{ +public: + explicit AccessibilityNativeImpl (AccessibilityHandler& handler) + : accessibilityElement (AccessibilityElement::create (handler)) + {} + + UIAccessibilityElement* getAccessibilityElement() const noexcept + { + return accessibilityElement.get(); + } + +private: + //============================================================================== + class AccessibilityElement : public AccessibleObjCClass + { + public: + static Holder create (AccessibilityHandler& handler) + { + static AccessibilityElement cls; + Holder element ([cls.createInstance() initWithAccessibilityContainer: (id) handler.getComponent().getWindowHandle()]); + object_setInstanceVariable (element.get(), "handler", &handler); + return element; + } + + private: + AccessibilityElement() + { + addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); + addMethod (@selector (accessibilityContainer), getAccessibilityContainer, "@@:"); + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") + addMethod (@selector (accessibilityHitTest:), accessibilityHitTest, "@@:", @encode (CGPoint)); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + addMethod (@selector (accessibilityFrame), getAccessibilityFrame, @encode (CGRect), "@:"); + addMethod (@selector (accessibilityTraits), getAccessibilityTraits, "i@:"); + addMethod (@selector (accessibilityLabel), getAccessibilityTitle, "@@:"); + 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 (accessibilityIncrement), accessibilityPerformIncrement, "c@:"); + addMethod (@selector (accessibilityDecrement), accessibilityPerformDecrement, "c@:"); + + addMethod (@selector (accessibilityLineNumberForPoint:), getAccessibilityLineNumberForPoint, "i@:", @encode (CGPoint)); + addMethod (@selector (accessibilityContentForLineNumber:), getAccessibilityContentForLineNumber, "@@:i"); + 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@:"); + addMethod (@selector (accessibilityRowRange), getAccessibilityRowIndexRange, @encode (NSRange), "@:"); + addMethod (@selector (accessibilityColumnRange), getAccessibilityColumnIndexRange, @encode (NSRange), "@:"); + + addProtocol (@protocol (UIAccessibilityReadingContent)); + + registerClass(); + } + + //============================================================================== + static id getAccessibilityContainer (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* parent = handler->getParent()) + return (id) parent->getNativeImplementation(); + + return (id) handler->getComponent().getWindowHandle(); + } + + return nil; + } + + static id accessibilityHitTest (id self, SEL, CGPoint point) + { + if (auto* handler = getHandler (self)) + { + if (auto* child = handler->getChildAt (roundToIntPoint (point))) + return (id) child->getNativeImplementation(); + + return self; + } + + 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::ignored: return UIAccessibilityTraitNotEnabled; + + case AccessibilityRole::slider: + case AccessibilityRole::editableText: + 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; + }(); + + if (handler->getCurrentState().isSelected()) + traits |= UIAccessibilityTraitSelected; + + if (auto* valueInterface = getValueInterface (self)) + if (! valueInterface->isReadOnly() && valueInterface->getRange().isValid()) + traits |= UIAccessibilityTraitAdjustable; + } + + return traits | sendSuperclassMessage (self, @selector (accessibilityTraits)); + } + + 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 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; + } + + 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)) + if (auto* cellHandler = tableInterface->getCellHandler ((int) row, (int) column)) + return (id) cellHandler->getNativeImplementation(); + + 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(); +} + +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{}; + }(); + + 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); +} + +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 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 58d498684f..7288e6a5cd 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm @@ -56,21 +56,9 @@ public: private: //============================================================================== - class AccessibilityElement : public ObjCClass> + class AccessibilityElement : public AccessibleObjCClass> { - private: - struct Deleter - { - void operator() (NSAccessibilityElement* element) const - { - object_setInstanceVariable (element, "handler", nullptr); - [element release]; - } - }; - public: - using Holder = std::unique_ptr, Deleter>; - static Holder create (AccessibilityHandler& handler) { #if JUCE_OBJC_HAS_AVAILABLE_FEATURE @@ -87,10 +75,8 @@ private: } private: - AccessibilityElement() : ObjCClass> ("JUCEAccessibilityElement_") + AccessibilityElement() { - addIvar ("handler"); - addMethod (@selector (accessibilityNotifiesWhenDestroyed), getAccessibilityNotifiesWhenDestroyed, "c@:"); addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement, "c@:"); addMethod (@selector (isAccessibilityEnabled), getIsAccessibilityEnabled, "c@:"); @@ -160,30 +146,7 @@ private: registerClass(); } - private: - static AccessibilityHandler* getHandler (id self) { return getIvar (self, "handler"); } - - template - static auto getInterface (id self, MemberFn fn) noexcept -> decltype ((std::declval().*fn)()) - { - if (auto* handler = getHandler (self)) - return (handler->*fn)(); - - return nullptr; - } - - static AccessibilityTextInterface* getTextInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTextInterface); } - static AccessibilityValueInterface* getValueInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getValueInterface); } - static AccessibilityTableInterface* getTableInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTableInterface); } - static AccessibilityCellInterface* getCellInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getCellInterface); } - - static bool hasEditableText (AccessibilityHandler& handler) noexcept - { - return handler.getRole() == AccessibilityRole::editableText - && handler.getTextInterface() != nullptr - && ! handler.getTextInterface()->isReadOnly(); - } - + //============================================================================== static bool isSelectable (AccessibleState state) noexcept { return state.isSelectable() || state.isMultiSelectable(); @@ -229,27 +192,9 @@ private: } } - static BOOL performActionIfSupported (id self, AccessibilityActionType actionType) - { - if (auto* handler = getHandler (self)) - if (handler->getActions().invoke (actionType)) - return YES; - - return NO; - } - //============================================================================== static BOOL getAccessibilityNotifiesWhenDestroyed (id, SEL) { return YES; } - static BOOL getIsAccessibilityElement (id self, SEL) - { - if (auto* handler = getHandler (self)) - return ! handler->isIgnored() - && handler->getRole() != AccessibilityRole::window; - - return NO; - } - static BOOL getIsAccessibilityEnabled (id self, SEL) { if (auto* handler = getHandler (self)) @@ -317,23 +262,6 @@ private: return nil; } - 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 NSArray* getAccessibilitySelectedChildren (id self, SEL) { return getSelectedChildren ([self accessibilityChildren]); @@ -379,14 +307,6 @@ private: } } - static BOOL getIsAccessibilityModal (id self, SEL) - { - if (auto* handler = getHandler (self)) - return handler->getComponent().isCurrentlyModal(); - - return NO; - } - static NSRect getAccessibilityFrame (id self, SEL) { if (auto* handler = getHandler (self)) @@ -481,26 +401,6 @@ private: return NSAccessibilityUnknownRole; } - static NSString* getAccessibilityTitle (id self, SEL) - { - if (auto* handler = getHandler (self)) - { - auto title = handler->getTitle(); - - if (title.isEmpty() && handler->getComponent().isOnDesktop()) - title = getAccessibleApplicationOrPluginName(); - - NSString* nsString = juceStringToNS (title); - - if (nsString != nil && [[self accessibilityValue] isEqual: nsString]) - return @""; - - return nsString; - } - - return nil; - } - static NSString* getAccessibilityLabel (id self, SEL) { if (auto* handler = getHandler (self)) @@ -509,47 +409,6 @@ private: return nil; } - static NSString* getAccessibilityHelp (id self, SEL) - { - if (auto* handler = getHandler (self)) - return juceStringToNS (handler->getHelp()); - - return nil; - } - - static id getAccessibilityValue (id self, SEL) - { - if (auto* handler = getHandler (self)) - { - if (auto* textInterface = handler->getTextInterface()) - return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })); - - if (handler->getCurrentState().isCheckable()) - return handler->getCurrentState().isChecked() ? @(1) : @(0); - - if (auto* valueInterface = handler->getValueInterface()) - return juceStringToNS (valueInterface->getCurrentValueAsString()); - } - - return nil; - } - - static void setAccessibilityValue (id self, SEL, NSString* value) - { - if (auto* handler = getHandler (self)) - { - if (hasEditableText (*handler)) - { - handler->getTextInterface()->setText (nsStringToJuce (value)); - return; - } - - if (auto* valueInterface = handler->getValueInterface()) - if (! valueInterface->isReadOnly()) - valueInterface->setValueAsString (nsStringToJuce (value)); - } - } - //============================================================================== static NSInteger getAccessibilityInsertionPointLineNumber (id self, SEL) { @@ -694,14 +553,6 @@ private: } //============================================================================== - static NSInteger getAccessibilityRowCount (id self, SEL) - { - if (auto* tableInterface = getTableInterface (self)) - return tableInterface->getNumRows(); - - return 0; - } - static NSArray* getAccessibilityRows (id self, SEL) { NSMutableArray* rows = [[NSMutableArray new] autorelease]; @@ -737,14 +588,6 @@ private: setSelectedChildren ([self accessibilityRows], selected); } - static NSInteger getAccessibilityColumnCount (id self, SEL) - { - if (auto* tableInterface = getTableInterface (self)) - return tableInterface->getNumColumns(); - - return 0; - } - static NSArray* getAccessibilityColumns (id self, SEL) { NSMutableArray* columns = [[NSMutableArray new] autorelease]; @@ -781,24 +624,6 @@ private: } //============================================================================== - static NSRange getAccessibilityRowIndexRange (id self, SEL) - { - if (auto* cellInterface = getCellInterface (self)) - return NSMakeRange ((NSUInteger) cellInterface->getRowIndex(), - (NSUInteger) cellInterface->getRowSpan()); - - return NSMakeRange (0, 0); - } - - static NSRange getAccessibilityColumnIndexRange (id self, SEL) - { - if (auto* cellInterface = getCellInterface (self)) - return NSMakeRange ((NSUInteger) cellInterface->getColumnIndex(), - (NSUInteger) cellInterface->getColumnSpan()); - - return NSMakeRange (0, 0); - } - static NSInteger getAccessibilityIndex (id self, SEL) { if (auto* handler = getHandler (self)) @@ -836,53 +661,9 @@ private: } //============================================================================== - static BOOL accessibilityPerformPress (id self, SEL) { return performActionIfSupported (self, AccessibilityActionType::press); } static BOOL accessibilityPerformShowMenu (id self, SEL) { return performActionIfSupported (self, AccessibilityActionType::showMenu); } - static BOOL accessibilityPerformRaise (id self, SEL) { [self setAccessibilityFocused: YES]; return YES; } - static BOOL accessibilityPerformIncrement (id self, SEL) - { - if (auto* valueInterface = getValueInterface (self)) - { - if (! valueInterface->isReadOnly()) - { - auto range = valueInterface->getRange(); - - if (range.isValid()) - { - valueInterface->setValue (jlimit (range.getMinimumValue(), - range.getMaximumValue(), - valueInterface->getCurrentValue() + range.getInterval())); - return YES; - } - } - } - - return NO; - } - - static BOOL accessibilityPerformDecrement (id self, SEL) - { - if (auto* valueInterface = getValueInterface (self)) - { - if (! valueInterface->isReadOnly()) - { - auto range = valueInterface->getRange(); - - if (range.isValid()) - { - valueInterface->setValue (jlimit (range.getMinimumValue(), - range.getMaximumValue(), - valueInterface->getCurrentValue() - range.getInterval())); - return YES; - } - } - } - - return NO; - } - static BOOL accessibilityPerformDelete (id self, SEL) { if (auto* handler = getHandler (self)) diff --git a/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm new file mode 100644 index 0000000000..1141ad46ce --- /dev/null +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm @@ -0,0 +1,275 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +template +class AccessibleObjCClass : public ObjCClass +{ +private: + struct Deleter + { + void operator() (Base* element) const + { + object_setInstanceVariable (element, "handler", nullptr); + [element release]; + } + }; + +public: + using Holder = std::unique_ptr; + +protected: + AccessibleObjCClass() : ObjCClass ("JUCEAccessibilityElement_") + { + ObjCClass::template addIvar ("handler"); + } + + //============================================================================== + static AccessibilityHandler* getHandler (id self) + { + return getIvar (self, "handler"); + } + + template + static auto getInterface (id self, MemberFn fn) noexcept -> decltype ((std::declval().*fn)()) + { + if (auto* handler = getHandler (self)) + return (handler->*fn)(); + + return nullptr; + } + + static AccessibilityTextInterface* getTextInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTextInterface); } + static AccessibilityValueInterface* getValueInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getValueInterface); } + static AccessibilityTableInterface* getTableInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTableInterface); } + static AccessibilityCellInterface* getCellInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getCellInterface); } + + static bool hasEditableText (AccessibilityHandler& handler) noexcept + { + return handler.getRole() == AccessibilityRole::editableText + && handler.getTextInterface() != nullptr + && ! handler.getTextInterface()->isReadOnly(); + } + + //============================================================================== + static BOOL getIsAccessibilityElement (id self, SEL) + { + if (auto* handler = getHandler (self)) + return ! handler->isIgnored() + && handler->getRole() != AccessibilityRole::window; + + return NO; + } + + static id getAccessibilityValue (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + if (auto* textInterface = handler->getTextInterface()) + return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() })); + + if (handler->getCurrentState().isCheckable()) + { + return handler->getCurrentState().isChecked() + #if JUCE_IOS + ? @"1" : @"0"; + #else + ? @(1) : @(0); + #endif + } + + if (auto* valueInterface = handler->getValueInterface()) + return juceStringToNS (valueInterface->getCurrentValueAsString()); + } + + return nil; + } + + static void setAccessibilityValue (id self, SEL, NSString* value) + { + if (auto* handler = getHandler (self)) + { + if (hasEditableText (*handler)) + { + handler->getTextInterface()->setText (nsStringToJuce (value)); + return; + } + + if (auto* valueInterface = handler->getValueInterface()) + if (! valueInterface->isReadOnly()) + valueInterface->setValueAsString (nsStringToJuce (value)); + } + } + + static BOOL performActionIfSupported (id self, AccessibilityActionType actionType) + { + if (auto* handler = getHandler (self)) + if (handler->getActions().invoke (actionType)) + return YES; + + return NO; + } + + static BOOL accessibilityPerformPress (id self, SEL) + { + return performActionIfSupported (self, AccessibilityActionType::press); + } + + static BOOL accessibilityPerformIncrement (id self, SEL) + { + if (auto* valueInterface = getValueInterface (self)) + { + if (! valueInterface->isReadOnly()) + { + auto range = valueInterface->getRange(); + + if (range.isValid()) + { + valueInterface->setValue (jlimit (range.getMinimumValue(), + range.getMaximumValue(), + valueInterface->getCurrentValue() + range.getInterval())); + return YES; + } + } + } + + return NO; + } + + static BOOL accessibilityPerformDecrement (id self, SEL) + { + if (auto* valueInterface = getValueInterface (self)) + { + if (! valueInterface->isReadOnly()) + { + auto range = valueInterface->getRange(); + + if (range.isValid()) + { + valueInterface->setValue (jlimit (range.getMinimumValue(), + range.getMaximumValue(), + valueInterface->getCurrentValue() - range.getInterval())); + return YES; + } + } + } + + return NO; + } + + static NSString* getAccessibilityTitle (id self, SEL) + { + if (auto* handler = getHandler (self)) + { + auto title = handler->getTitle(); + + if (title.isEmpty() && handler->getComponent().isOnDesktop()) + title = getAccessibleApplicationOrPluginName(); + + NSString* nsString = juceStringToNS (title); + + if (nsString != nil && [[self accessibilityValue] isEqual: nsString]) + return @""; + + return nsString; + } + + return nil; + } + + static NSString* getAccessibilityHelp (id self, SEL) + { + if (auto* handler = getHandler (self)) + return juceStringToNS (handler->getHelp()); + + return nil; + } + + static BOOL getIsAccessibilityModal (id self, SEL) + { + if (auto* handler = getHandler (self)) + return handler->getComponent().isCurrentlyModal(); + + 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)) + return tableInterface->getNumRows(); + + return 0; + } + + static NSInteger getAccessibilityColumnCount (id self, SEL) + { + if (auto* tableInterface = getTableInterface (self)) + return tableInterface->getNumColumns(); + + return 0; + } + + static NSRange getAccessibilityRowIndexRange (id self, SEL) + { + if (auto* cellInterface = getCellInterface (self)) + return NSMakeRange ((NSUInteger) cellInterface->getRowIndex(), + (NSUInteger) cellInterface->getRowSpan()); + + return NSMakeRange (0, 0); + } + + static NSRange getAccessibilityColumnIndexRange (id self, SEL) + { + if (auto* cellInterface = getCellInterface (self)) + return NSMakeRange ((NSUInteger) cellInterface->getColumnIndex(), + (NSUInteger) cellInterface->getColumnSpan()); + + return NSMakeRange (0, 0); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibleObjCClass) +}; + +} diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index adc0479fc9..fb5ecaaa9a 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -136,6 +136,11 @@ using namespace juce; - (BOOL) canBecomeFirstResponder; - (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text; + +- (BOOL) isAccessibilityElement; +- (id) accessibilityElementAtIndex: (NSInteger) index; +- (NSInteger) accessibilityElementCount; +- (NSInteger) indexOfAccessibilityElement: (id) element; @end //============================================================================== @@ -540,6 +545,30 @@ MultiTouchMapper UIViewComponentPeer::currentTouches; nsStringToJuce (text)); } +- (BOOL) isAccessibilityElement +{ + return NO; +} + +- (id) accessibilityElementAtIndex: (NSInteger) index +{ + if (owner != nullptr) + if (auto* handler = owner->getComponent().getAccessibilityHandler()) + return (id) handler->getNativeImplementation(); + + return nil; +} + +- (NSInteger) accessibilityElementCount +{ + return owner != nullptr ? 1 : 0; +} + +- (NSInteger) indexOfAccessibilityElement: (id) element +{ + return 0; +} + @end //==============================================================================