From 921d86e58620a494c48bd0ef06ff8a48369a5ec3 Mon Sep 17 00:00:00 2001 From: reuk Date: Thu, 30 Jun 2022 21:23:32 +0100 Subject: [PATCH] Accessibility: Improve table navigation, row/column index/header reporting --- .../juce_AccessibilityCellInterface.h | 12 -- .../juce_AccessibilityTableInterface.h | 27 ++++ modules/juce_gui_basics/juce_gui_basics.cpp | 12 ++ .../juce_android_Accessibility.cpp | 95 +++++++++++- .../accessibility/juce_ios_Accessibility.mm | 138 ++++++++++++------ .../accessibility/juce_mac_Accessibility.mm | 19 ++- .../juce_mac_AccessibilitySharedCode.mm | 64 ++++---- .../juce_win32_AccessibilityElement.cpp | 90 +++++++++++- .../accessibility/juce_win32_ComInterfaces.h | 58 ++++++++ .../juce_win32_UIAGridItemProvider.h | 107 ++++++++++---- .../juce_win32_UIAGridProvider.h | 72 ++++++++- .../juce_gui_basics/widgets/juce_ListBox.cpp | 74 ++++++---- .../juce_gui_basics/widgets/juce_ListBox.h | 2 +- .../widgets/juce_TableListBox.cpp | 113 +++++++++++--- .../juce_gui_basics/widgets/juce_TreeView.cpp | 92 +++++++++--- .../juce_gui_basics/widgets/juce_TreeView.h | 4 +- 16 files changed, 775 insertions(+), 204 deletions(-) diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h index de901feeff..8c74afef91 100644 --- a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityCellInterface.h @@ -39,18 +39,6 @@ public: /** Destructor. */ virtual ~AccessibilityCellInterface() = default; - /** Returns the column index of the cell in the table. */ - virtual int getColumnIndex() const = 0; - - /** Returns the number of columns occupied by the cell in the table. */ - virtual int getColumnSpan() const = 0; - - /** Returns the row index of the cell in the table. */ - virtual int getRowIndex() const = 0; - - /** Returns the number of rows occupied by the cell in the table. */ - virtual int getRowSpan() const = 0; - /** Returns the indentation level for the cell. */ virtual int getDisclosureLevel() const = 0; diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h index db3cafcb87..a50227ed28 100644 --- a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h @@ -64,6 +64,33 @@ public: as there are columns in the table. */ virtual const AccessibilityHandler* getHeaderHandler() const = 0; + + struct Span { int begin, num; }; + + /** Given the handler of one of the cells in the table, returns the rows covered + by that cell, or null if the cell does not exist in the table. + + This function replaces the getRowIndex and getRowSpan + functions from AccessibilityCellInterface. Most of the time, it's easier for the + table itself to keep track of cell locations, than to delegate to the individual + cells. + */ + virtual Optional getRowSpan (const AccessibilityHandler&) const = 0; + + /** Given the handler of one of the cells in the table, returns the columns covered + by that cell, or null if the cell does not exist in the table. + + This function replaces the getColumnIndex and getColumnSpan + functions from AccessibilityCellInterface. Most of the time, it's easier for the + table itself to keep track of cell locations, than to delegate to the individual + cells. + */ + virtual Optional getColumnSpan (const AccessibilityHandler&) const = 0; + + /** Attempts to scroll the table (if necessary) so that the cell with the given handler + is visible. + */ + virtual void showCell (const AccessibilityHandler&) const = 0; }; } // namespace juce diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 6755bba640..fd31997f53 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -127,6 +127,18 @@ namespace juce ScaledImage image; Point hotspot; }; + + template + static const AccessibilityHandler* getEnclosingHandlerWithInterface (const AccessibilityHandler* handler, MemberFn fn) + { + if (handler == nullptr) + return nullptr; + + if ((handler->*fn)() != nullptr) + return handler; + + return getEnclosingHandlerWithInterface (handler->getParent(), fn); + } } // namespace juce #include "mouse/juce_PointerState.h" diff --git a/modules/juce_gui_basics/native/accessibility/juce_android_Accessibility.cpp b/modules/juce_gui_basics/native/accessibility/juce_android_Accessibility.cpp index 6eeae8d644..20ef6ec36d 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_android_Accessibility.cpp +++ b/modules/juce_gui_basics/native/accessibility/juce_android_Accessibility.cpp @@ -56,6 +56,25 @@ namespace juce DECLARE_JNI_CLASS (AndroidAccessibilityNodeInfo, "android/view/accessibility/AccessibilityNodeInfo") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (setCollectionInfo, "setCollectionInfo", "(Landroid/view/accessibility/AccessibilityNodeInfo$CollectionInfo;)V") \ + METHOD (setCollectionItemInfo, "setCollectionItemInfo", "(Landroid/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo;)V") + + DECLARE_JNI_CLASS (AndroidAccessibilityNodeInfo19, "android/view/accessibility/AccessibilityNodeInfo") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (obtain, "obtain", "(IIZ)Landroid/view/accessibility/AccessibilityNodeInfo$CollectionInfo;") + + DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidAccessibilityNodeInfoCollectionInfo, "android/view/accessibility/AccessibilityNodeInfo$CollectionInfo", 19) +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (obtain, "obtain", "(IIIIZ)Landroid/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo;") + + DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidAccessibilityNodeInfoCollectionItemInfo, "android/view/accessibility/AccessibilityNodeInfo$CollectionItemInfo", 19) +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (obtain, "obtain", "(I)Landroid/view/accessibility/AccessibilityEvent;") \ METHOD (setPackageName, "setPackageName", "(Ljava/lang/CharSequence;)V") \ @@ -124,6 +143,18 @@ static jmethodID nodeInfoSetTextSelection = nullptr; static jmethodID nodeInfoSetLiveRegion = nullptr; static jmethodID accessibilityEventSetContentChangeTypes = nullptr; +template +static AccessibilityHandler* getEnclosingHandlerWithInterface (AccessibilityHandler* handler, MemberFn fn) +{ + if (handler == nullptr) + return nullptr; + + if ((handler->*fn)() != nullptr) + return handler; + + return getEnclosingHandlerWithInterface (handler->getParent(), fn); +} + static void loadSDKDependentMethods() { static bool hasChecked = false; @@ -160,8 +191,6 @@ static constexpr auto getClassName (AccessibilityRole role) case AccessibilityRole::popupMenu: return "android.widget.PopupMenu"; case AccessibilityRole::comboBox: return "android.widget.Spinner"; case AccessibilityRole::tree: return "android.widget.ExpandableListView"; - case AccessibilityRole::list: return "android.widget.ListView"; - case AccessibilityRole::table: return "android.widget.TableLayout"; case AccessibilityRole::progressBar: return "android.widget.ProgressBar"; case AccessibilityRole::scrollBar: @@ -177,6 +206,11 @@ static constexpr auto getClassName (AccessibilityRole role) case AccessibilityRole::splashScreen: case AccessibilityRole::dialogWindow: return "android.widget.PopupWindow"; + // If we don't supply a custom class type, then TalkBack will use the node's CollectionInfo + // to make a sensible decision about how to describe the container + case AccessibilityRole::list: + case AccessibilityRole::table: + case AccessibilityRole::column: case AccessibilityRole::row: case AccessibilityRole::cell: @@ -437,6 +471,63 @@ public: } } } + + if (getAndroidSDKVersion() >= 19) + { + if (auto* tableInterface = accessibilityHandler.getTableInterface()) + { + const auto rows = tableInterface->getNumRows(); + const auto columns = tableInterface->getNumColumns(); + const LocalRef collectionInfo { env->CallStaticObjectMethod (AndroidAccessibilityNodeInfoCollectionInfo, + AndroidAccessibilityNodeInfoCollectionInfo.obtain, + (jint) rows, + (jint) columns, + (jboolean) false) }; + env->CallVoidMethod (info, AndroidAccessibilityNodeInfo19.setCollectionInfo, collectionInfo.get()); + } + + if (auto* enclosingTableHandler = getEnclosingHandlerWithInterface (&accessibilityHandler, &AccessibilityHandler::getTableInterface)) + { + auto* interface = enclosingTableHandler->getTableInterface(); + jassert (interface != nullptr); + const auto rowSpan = interface->getRowSpan (accessibilityHandler); + const auto columnSpan = interface->getColumnSpan (accessibilityHandler); + + enum class IsHeader { no, yes }; + + const auto addCellInfo = [env, &info] (AccessibilityTableInterface::Span rows, AccessibilityTableInterface::Span columns, IsHeader header) + { + const LocalRef collectionItemInfo { env->CallStaticObjectMethod (AndroidAccessibilityNodeInfoCollectionItemInfo, + AndroidAccessibilityNodeInfoCollectionItemInfo.obtain, + (jint) rows.begin, + (jint) rows.num, + (jint) columns.begin, + (jint) columns.num, + (jboolean) (header == IsHeader::yes)) }; + env->CallVoidMethod (info, AndroidAccessibilityNodeInfo19.setCollectionItemInfo, collectionItemInfo.get()); + }; + + if (rowSpan.hasValue() && columnSpan.hasValue()) + { + addCellInfo (*rowSpan, *columnSpan, IsHeader::no); + } + else + { + if (auto* tableHeader = interface->getHeaderHandler()) + { + if (accessibilityHandler.getParent() == tableHeader) + { + const auto children = tableHeader->getChildren(); + const auto column = std::distance (children.cbegin(), std::find (children.cbegin(), children.cend(), &accessibilityHandler)); + + // Talkback will only treat a row as a column header if its row index is zero + // https://github.com/google/talkback/blob/acd0bc7631a3dfbcf183789c7557596a45319e1f/utils/src/main/java/CollectionState.java#L853 + addCellInfo ({ 0, 1 }, { (int) column, 1 }, IsHeader::yes); + } + } + } + } + } } bool performAction (int action, jobject arguments) 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 1c3d34f735..1cd59d2efb 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_ios_Accessibility.mm @@ -54,7 +54,7 @@ static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler { id accessibleElement = [&childHandler] { - id native = (id) childHandler->getNativeImplementation(); + id native = static_cast (childHandler->getNativeImplementation()); if (! childHandler->getChildren().empty()) return [native accessibilityContainer]; @@ -66,7 +66,7 @@ static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler [accessibleChildren addObject: accessibleElement]; } - [accessibleChildren addObject: (id) handler.getNativeImplementation()]; + [accessibleChildren addObject: static_cast (handler.getNativeImplementation())]; return accessibleChildren; } @@ -87,11 +87,11 @@ public: private: //============================================================================== - class AccessibilityContainer : public ObjCClass + class AccessibilityContainer : public AccessibleObjCClass { public: AccessibilityContainer() - : ObjCClass ("JUCEUIAccessibilityElementContainer_") + : AccessibleObjCClass ("JUCEUIAccessibilityContainer_") { addMethod (@selector (isAccessibilityElement), [] (id, SEL) { return false; }); @@ -114,6 +114,43 @@ private: #if JUCE_IOS_CONTAINER_API_AVAILABLE if (@available (iOS 11.0, *)) { + addMethod (@selector (accessibilityDataTableCellElementForRow:column:), [] (id self, SEL, NSUInteger row, NSUInteger column) -> id + { + if (auto* tableHandler = getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface)) + if (auto* tableInterface = tableHandler->getTableInterface()) + if (auto* cellHandler = tableInterface->getCellHandler ((int) row, (int) column)) + if (auto* parent = getAccessibleParent (cellHandler)) + return static_cast (parent->getNativeImplementation()); + + return nil; + }); + + addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount); + addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount); + + addMethod (@selector (accessibilityHeaderElementsForColumn:), [] (id self, SEL, NSUInteger column) -> NSArray* + { + if (auto* tableHandler = getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface)) + { + if (auto* tableInterface = tableHandler->getTableInterface()) + { + if (auto* header = tableInterface->getHeaderHandler()) + { + if (isPositiveAndBelow (column, header->getChildren().size())) + { + auto* result = [NSMutableArray new]; + [result addObject: static_cast (header->getChildren()[(size_t) column]->getNativeImplementation())]; + return result; + } + } + } + } + + return nullptr; + }); + + addProtocol (@protocol (UIAccessibilityContainerDataTable)); + addMethod (@selector (accessibilityContainerType), [] (id self, SEL) -> NSInteger { if (auto* handler = getHandler (self)) @@ -147,12 +184,21 @@ private: } #endif - addIvar ("handler"); - registerClass(); } private: + static const AccessibilityHandler* getAccessibleParent (const AccessibilityHandler* h) + { + if (h == nullptr) + return nullptr; + + if ([static_cast (h->getNativeImplementation()) isAccessibilityElement]) + return h; + + return getAccessibleParent (h->getParent()); + } + static AccessibilityHandler* getHandler (id self) { return getIvar (self, "handler"); @@ -172,7 +218,7 @@ private: id instance = (hasEditableText (handler) ? textCls : cls).createInstance(); - Holder element ([instance initWithAccessibilityContainer: (id) handler.getComponent().getWindowHandle()]); + Holder element ([instance initWithAccessibilityContainer: static_cast (handler.getComponent().getWindowHandle())]); object_setInstanceVariable (element.get(), "handler", &handler); return element; } @@ -183,15 +229,33 @@ private: { auto* handler = getHandler (self); - if (handler == nullptr) - return false; + const auto hasAccessiblePropertiesOrIsTableCell = [] (auto& handlerRef) + { + const auto isTableCell = [&] + { + if (auto* tableHandler = getEnclosingHandlerWithInterface (&handlerRef, &AccessibilityHandler::getTableInterface)) + { + if (auto* tableInterface = tableHandler->getTableInterface()) + { + return tableInterface->getRowSpan (handlerRef).hasValue() + && tableInterface->getColumnSpan (handlerRef).hasValue(); + } + } - return ! handler->isIgnored() - && handler->getRole() != AccessibilityRole::window - && (handler->getTitle().isNotEmpty() - || handler->getDescription().isNotEmpty() - || handler->getHelp().isNotEmpty() - || handler->getValueInterface() != nullptr); + return false; + }; + + return handlerRef.getTitle().isNotEmpty() + || handlerRef.getHelp().isNotEmpty() + || handlerRef.getTextInterface() != nullptr + || handlerRef.getValueInterface() != nullptr + || isTableCell(); + }; + + return handler != nullptr + && ! handler->isIgnored() + && handler->getRole() != AccessibilityRole::window + && hasAccessiblePropertiesOrIsTableCell (*handler); }); addMethod (@selector (accessibilityContainer), [] (id self, SEL) -> id @@ -199,7 +263,7 @@ private: if (auto* handler = getHandler (self)) { if (handler->getComponent().isOnDesktop()) - return (id) handler->getComponent().getWindowHandle(); + return static_cast (handler->getComponent().getWindowHandle()); if (! handler->getChildren().empty()) { @@ -217,7 +281,7 @@ private: } if (auto* parent = handler->getParent()) - return [(id) parent->getNativeImplementation() accessibilityContainer]; + return [static_cast (parent->getNativeImplementation()) accessibilityContainer]; } return nil; @@ -252,9 +316,9 @@ 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::editableText: case AccessibilityRole::slider: case AccessibilityRole::menuItem: case AccessibilityRole::menuBar: @@ -348,7 +412,7 @@ private: // which element it thinks has focus and forward the event on to that element if it differs id focusedElement = UIAccessibilityFocusedElement (UIAccessibilityNotificationVoiceOverIdentifier); - if (focusedElement != nullptr && ! [(id) handler->getNativeImplementation() isEqual: focusedElement]) + if (focusedElement != nullptr && ! [static_cast (handler->getNativeImplementation()) isEqual: focusedElement]) return [focusedElement accessibilityActivate]; if (handler->hasFocus (false)) @@ -381,28 +445,6 @@ private: return NO; }); - #if JUCE_IOS_CONTAINER_API_AVAILABLE - if (@available (iOS 11.0, *)) - { - addMethod (@selector (accessibilityDataTableCellElementForRow:column:), [] (id self, SEL, NSUInteger row, NSUInteger column) -> id - { - if (auto* tableInterface = getEnclosingInterface (getHandler (self), &AccessibilityHandler::getTableInterface)) - if (auto* cellHandler = tableInterface->getCellHandler ((int) row, (int) column)) - return (id) cellHandler->getNativeImplementation(); - - return nil; - }); - - addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount); - addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount); - addProtocol (@protocol (UIAccessibilityContainerDataTable)); - - addMethod (@selector (accessibilityRowRange), getAccessibilityRowIndexRange); - addMethod (@selector (accessibilityColumnRange), getAccessibilityColumnIndexRange); - addProtocol (@protocol (UIAccessibilityContainerDataTableCell)); - } - #endif - if (elementType == Type::textElement) { addMethod (@selector (accessibilityLineNumberForPoint:), [] (id self, SEL, CGPoint point) @@ -464,6 +506,15 @@ private: addProtocol (@protocol (UIAccessibilityReadingContent)); } + #if JUCE_IOS_CONTAINER_API_AVAILABLE + if (@available (iOS 11.0, *)) + { + addMethod (@selector (accessibilityRowRange), getAccessibilityRowIndexRange); + addMethod (@selector (accessibilityColumnRange), getAccessibilityColumnIndexRange); + addProtocol (@protocol (UIAccessibilityContainerDataTableCell)); + } + #endif + addIvar ("container"); registerClass(); @@ -534,9 +585,8 @@ void notifyAccessibilityEventInternal (const AccessibilityHandler& handler, Inte const bool moveToHandler = (eventType == InternalAccessibilityEvent::focusChanged && handler.hasFocus (false)); sendAccessibilityEvent (notification, - moveToHandler ? (id) handler.getNativeImplementation() : nil); + moveToHandler ? static_cast (handler.getNativeImplementation()) : nil); } - } void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const @@ -558,7 +608,7 @@ void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventTyp }(); if (notification != UIAccessibilityNotifications{}) - sendAccessibilityEvent (notification, (id) getNativeImplementation()); + sendAccessibilityEvent (notification, static_cast (getNativeImplementation())); } void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority) 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 6c136dd043..3e08cb384c 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm @@ -210,7 +210,7 @@ private: case AccessibilityRole::editableText: return NSAccessibilityTextAreaRole; case AccessibilityRole::menuItem: return NSAccessibilityMenuItemRole; case AccessibilityRole::menuBar: return NSAccessibilityMenuRole; - case AccessibilityRole::table: return NSAccessibilityListRole; + case AccessibilityRole::table: return NSAccessibilityOutlineRole; case AccessibilityRole::column: return NSAccessibilityColumnRole; case AccessibilityRole::row: return NSAccessibilityRowRole; case AccessibilityRole::cell: return NSAccessibilityCellRole; @@ -504,15 +504,20 @@ private: { if (auto* handler = getHandler (self)) { - if (auto* cellInterface = handler->getCellInterface()) + if (auto* tableHandler = getEnclosingHandlerWithInterface (handler, &AccessibilityHandler::getTableInterface)) { - NSAccessibilityRole handlerRole = [self accessibilityRole]; + if (auto* tableInterface = tableHandler->getTableInterface()) + { + NSAccessibilityRole handlerRole = [self accessibilityRole]; - if ([handlerRole isEqual: NSAccessibilityRowRole]) - return cellInterface->getRowIndex(); + if ([handlerRole isEqual: NSAccessibilityRowRole]) + if (const auto span = tableInterface->getRowSpan (*handler)) + return span->begin; - if ([handlerRole isEqual: NSAccessibilityColumnRole]) - return cellInterface->getColumnIndex(); + if ([handlerRole isEqual: NSAccessibilityColumnRole]) + if (const auto span = tableInterface->getColumnSpan (*handler)) + return span->begin; + } } } 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 770d68f780..f487d894c7 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm @@ -73,18 +73,6 @@ protected: static AccessibilityTableInterface* getTableInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTableInterface); } static AccessibilityCellInterface* getCellInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getCellInterface); } - template - static auto getEnclosingInterface (AccessibilityHandler* handler, MemberFn fn) noexcept -> decltype ((std::declval().*fn)()) - { - if (handler == nullptr) - return nullptr; - - if (auto* interface = (handler->*fn)()) - return interface; - - return getEnclosingInterface (handler->getParent(), fn); - } - static bool hasEditableText (AccessibilityHandler& handler) noexcept { return handler.getRole() == AccessibilityRole::editableText @@ -199,10 +187,8 @@ protected: NSString* nsString = juceStringToNS (title); - #if ! JUCE_IOS if (nsString != nil && [[self accessibilityValue] isEqual: nsString]) return @""; - #endif return nsString; } @@ -228,36 +214,58 @@ protected: static NSInteger getAccessibilityRowCount (id self, SEL) { - if (auto* tableInterface = getTableInterface (self)) - return tableInterface->getNumRows(); + if (auto* tableHandler = getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface)) + if (auto* tableInterface = tableHandler->getTableInterface()) + return tableInterface->getNumRows(); return 0; } static NSInteger getAccessibilityColumnCount (id self, SEL) { - if (auto* tableInterface = getTableInterface (self)) - return tableInterface->getNumColumns(); + if (auto* tableHandler = getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface)) + if (auto* tableInterface = tableHandler->getTableInterface()) + return tableInterface->getNumColumns(); return 0; } + template + static NSRange getCellDimensions (id self, Getter getter) + { + const auto notFound = NSMakeRange (NSNotFound, 0); + + auto* handler = getHandler (self); + + if (handler == nullptr) + return notFound; + + auto* tableHandler = getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface); + + if (tableHandler == nullptr) + return notFound; + + auto* tableInterface = tableHandler->getTableInterface(); + + if (tableInterface == nullptr) + return notFound; + + const auto result = (tableInterface->*getter) (*handler); + + if (! result.hasValue()) + return notFound; + + return NSMakeRange ((NSUInteger) result->begin, (NSUInteger) result->num); + } + static NSRange getAccessibilityRowIndexRange (id self, SEL) { - if (auto* cellInterface = getCellInterface (self)) - return NSMakeRange ((NSUInteger) cellInterface->getRowIndex(), - (NSUInteger) cellInterface->getRowSpan()); - - return NSMakeRange (0, 0); + return getCellDimensions (self, &AccessibilityTableInterface::getRowSpan); } static NSRange getAccessibilityColumnIndexRange (id self, SEL) { - if (auto* cellInterface = getCellInterface (self)) - return NSMakeRange ((NSUInteger) cellInterface->getColumnIndex(), - (NSUInteger) cellInterface->getColumnSpan()); - - return NSMakeRange (0, 0); + return getCellDimensions (self, &AccessibilityTableInterface::getColumnSpan); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibleObjCClass) diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp index de17aae67d..d7712203dc 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_AccessibilityElement.cpp @@ -30,6 +30,50 @@ 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) { @@ -63,7 +107,7 @@ static auto roleToControlTypeId (AccessibilityRole roleType) case AccessibilityRole::staticText: return ComTypes::UIA_TextControlTypeId; case AccessibilityRole::column: - case AccessibilityRole::row: return ComTypes::UIA_HeaderItemControlTypeId; + case AccessibilityRole::row: return ComTypes::UIA_ListItemControlTypeId; case AccessibilityRole::button: return ComTypes::UIA_ButtonControlTypeId; case AccessibilityRole::toggleButton: return ComTypes::UIA_CheckBoxControlTypeId; @@ -146,6 +190,22 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn 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; + }; + switch (pId) { case ComTypes::UIA_WindowPatternId: @@ -213,25 +273,27 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn { auto state = accessibilityHandler.getCurrentState(); - if (state.isSelectable() || state.isMultiSelectable() - || role == AccessibilityRole::radioButton) + if (state.isSelectable() || state.isMultiSelectable() || role == AccessibilityRole::radioButton) { return new UIASelectionItemProvider (this); } break; } + case ComTypes::UIA_TablePatternId: case ComTypes::UIA_GridPatternId: { - if (accessibilityHandler.getTableInterface() != nullptr) - return new UIAGridProvider (this); + if (accessibilityHandler.getTableInterface() != nullptr + && (pId == ComTypes::UIA_GridPatternId || accessibilityHandler.getRole() == AccessibilityRole::table)) + return static_cast (new UIAGridProvider (this)); break; } + case ComTypes::UIA_TableItemPatternId: case ComTypes::UIA_GridItemPatternId: { - if (accessibilityHandler.getCellInterface() != nullptr) - return new UIAGridItemProvider (this); + if (isListOrTableCell (accessibilityHandler)) + return static_cast (new UIAGridItemProvider (this)); break; } @@ -250,6 +312,20 @@ JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUn break; } + case ComTypes::UIA_ScrollPatternId: + { + if (accessibilityHandler.getTableInterface() != nullptr) + return new UIAScrollProvider (this); + + break; + } + case ComTypes::UIA_ScrollItemPatternId: + { + if (isListOrTableCell (accessibilityHandler)) + return new UIAScrollItemProvider (this); + + break; + } } return nullptr; diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_ComInterfaces.h b/modules/juce_gui_basics/native/accessibility/juce_win32_ComInterfaces.h index b03fe8cd8f..cb07817ba7 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_win32_ComInterfaces.h +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_ComInterfaces.h @@ -120,18 +120,38 @@ enum WindowInteractionState WindowInteractionState_NotResponding = 4 }; +enum RowOrColumnMajor +{ + RowOrColumnMajor_RowMajor = 0, + RowOrColumnMajor_ColumnMajor = 1, + RowOrColumnMajor_Indeterminate = 2 +}; + +enum ScrollAmount +{ + ScrollAmount_LargeDecrement = 0, + ScrollAmount_SmallDecrement = 1, + ScrollAmount_NoAmount = 2, + ScrollAmount_LargeIncrement = 3, + ScrollAmount_SmallIncrement = 4 +}; + const long UIA_InvokePatternId = 10000; const long UIA_SelectionPatternId = 10001; const long UIA_ValuePatternId = 10002; const long UIA_RangeValuePatternId = 10003; +const long UIA_ScrollPatternId = 10004; const long UIA_ExpandCollapsePatternId = 10005; const long UIA_GridPatternId = 10006; const long UIA_GridItemPatternId = 10007; const long UIA_WindowPatternId = 10009; const long UIA_SelectionItemPatternId = 10010; +const long UIA_TablePatternId = 10012; +const long UIA_TableItemPatternId = 10013; const long UIA_TextPatternId = 10014; const long UIA_TogglePatternId = 10015; const long UIA_TransformPatternId = 10016; +const long UIA_ScrollItemPatternId = 10017; const long UIA_TextPattern2Id = 10024; const long UIA_StructureChangedEventId = 20002; const long UIA_MenuOpenedEventId = 20003; @@ -220,6 +240,21 @@ public: JUCE_COMCALL get_ColumnCount (__RPC__out int* pRetVal) = 0; }; +JUCE_COMCLASS (ITableItemProvider, "b9734fa6-771f-4d78-9c90-2517999349cd") : public IUnknown +{ +public: + JUCE_COMCALL GetRowHeaderItems (SAFEARRAY** pRetVal) = 0; + JUCE_COMCALL GetColumnHeaderItems (SAFEARRAY** pRetVal) = 0; +}; + +JUCE_COMCLASS (ITableProvider, "9c860395-97b3-490a-b52a-858cc22af166") : public IUnknown +{ +public: + JUCE_COMCALL GetRowHeaders (SAFEARRAY** pRetVal) = 0; + JUCE_COMCALL GetColumnHeaders (SAFEARRAY** pRetVal) = 0; + JUCE_COMCALL get_RowOrColumnMajor (RowOrColumnMajor* pRetVal) = 0; +}; + JUCE_COMCLASS (IInvokeProvider, "54fcb24b-e18e-47a2-b4d3-eccbe77599a2") : public IUnknown { public: @@ -345,6 +380,25 @@ public: JUCE_COMCALL get_IsTopmost (__RPC__out BOOL * pRetVal) = 0; }; +JUCE_COMCLASS (IScrollProvider, "b38b8077-1fc3-42a5-8cae-d40c2215055a") : public IUnknown +{ +public: + JUCE_COMCALL Scroll (ScrollAmount horizontalAmount, ScrollAmount verticalAmount) = 0; + JUCE_COMCALL SetScrollPercent (double horizontalPercent,double verticalPercent) = 0; + JUCE_COMCALL get_HorizontalScrollPercent (double* pRetVal) = 0; + JUCE_COMCALL get_VerticalScrollPercent (double* pRetVal) = 0; + JUCE_COMCALL get_HorizontalViewSize (double* pRetVal) = 0; + JUCE_COMCALL get_VerticalViewSize (double* pRetVal) = 0; + JUCE_COMCALL get_HorizontallyScrollable (BOOL* pRetVal) = 0; + JUCE_COMCALL get_VerticallyScrollable (BOOL* pRetVal) = 0; +}; + +JUCE_COMCLASS (IScrollItemProvider, "2360c714-4bf1-4b26-ba65-9b21316127eb") : public IUnknown +{ +public: + JUCE_COMCALL ScrollIntoView() = 0; +}; + constexpr CLSID CLSID_SpVoice { 0x96749377, 0x3391, 0x11D2, { 0x9E, 0xE3, 0x00, 0xC0, 0x4F, 0x79, 0x73, 0x96 } }; } // namespace ComTypes @@ -368,4 +422,8 @@ __CRT_UUID_DECL (juce::ComTypes::IToggleProvider, 0x56d00bd0, 0x __CRT_UUID_DECL (juce::ComTypes::ITransformProvider, 0x6829ddc4, 0x4f91, 0x4ffa, 0xb8, 0x6f, 0xbd, 0x3e, 0x29, 0x87, 0xcb, 0x4c) __CRT_UUID_DECL (juce::ComTypes::IValueProvider, 0xc7935180, 0x6fb3, 0x4201, 0xb1, 0x74, 0x7d, 0xf7, 0x3a, 0xdb, 0xf6, 0x4a) __CRT_UUID_DECL (juce::ComTypes::IWindowProvider, 0x987df77b, 0xdb06, 0x4d77, 0x8f, 0x8a, 0x86, 0xa9, 0xc3, 0xbb, 0x90, 0xb9) +__CRT_UUID_DECL (juce::ComTypes::ITableItemProvider, 0xb9734fa6, 0x771f, 0x4d78, 0x9c, 0x90, 0x25, 0x17, 0x99, 0x93, 0x49, 0xcd) +__CRT_UUID_DECL (juce::ComTypes::ITableProvider, 0x9c860395, 0x97b3, 0x490a, 0xb5, 0x2a, 0x85, 0x8c, 0xc2, 0x2a, 0xf1, 0x66) +__CRT_UUID_DECL (juce::ComTypes::IScrollProvider, 0xb38b8077, 0x1fc3, 0x42a5, 0x8c, 0xae, 0xd4, 0x0c, 0x22, 0x15, 0x05, 0x5a) +__CRT_UUID_DECL (juce::ComTypes::IScrollItemProvider, 0x2360c714, 0x4bf1, 0x4b26, 0xba, 0x65, 0x9b, 0x21, 0x31, 0x61, 0x27, 0xeb) #endif diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h index 251a3c5a80..494dfeb26c 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridItemProvider.h @@ -28,7 +28,7 @@ namespace juce //============================================================================== class UIAGridItemProvider : public UIAProviderBase, - public ComBaseClassHelper + public ComBaseClassHelper { public: using UIAProviderBase::UIAProviderBase; @@ -36,65 +36,116 @@ public: //============================================================================== JUCE_COMRESULT get_Row (int* pRetVal) override { - return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) - { - *pRetVal = cellInterface.getRowIndex(); - }); + return withTableSpan (pRetVal, + &AccessibilityTableInterface::getRowSpan, + &AccessibilityTableInterface::Span::begin); } JUCE_COMRESULT get_Column (int* pRetVal) override { - return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) - { - *pRetVal = cellInterface.getColumnIndex(); - }); + return withTableSpan (pRetVal, + &AccessibilityTableInterface::getColumnSpan, + &AccessibilityTableInterface::Span::begin); } JUCE_COMRESULT get_RowSpan (int* pRetVal) override { - return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) - { - *pRetVal = cellInterface.getRowSpan(); - }); + return withTableSpan (pRetVal, + &AccessibilityTableInterface::getRowSpan, + &AccessibilityTableInterface::Span::num); } JUCE_COMRESULT get_ColumnSpan (int* pRetVal) override { - return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) - { - *pRetVal = cellInterface.getColumnSpan(); - }); + return withTableSpan (pRetVal, + &AccessibilityTableInterface::getColumnSpan, + &AccessibilityTableInterface::Span::num); } JUCE_COMRESULT get_ContainingGrid (IRawElementProviderSimple** pRetVal) override { - return withCellInterface (pRetVal, [&] (const AccessibilityCellInterface& cellInterface) + return withTableInterface (pRetVal, [&] (const AccessibilityHandler& tableHandler) { JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") - - if (auto* handler = cellInterface.getTableHandler()) - handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); - + tableHandler.getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + return true; }); } + JUCE_COMRESULT GetRowHeaderItems (SAFEARRAY**) override + { + return (HRESULT) UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT GetColumnHeaderItems (SAFEARRAY** pRetVal) override + { + return withTableInterface (pRetVal, [&] (const AccessibilityHandler& tableHandler) + { + if (auto* tableInterface = tableHandler.getTableInterface()) + { + if (const auto column = tableInterface->getColumnSpan (getHandler())) + { + if (auto* header = tableInterface->getHeaderHandler()) + { + const auto children = header->getChildren(); + + if (isPositiveAndBelow (column->begin, children.size())) + { + IRawElementProviderSimple* provider = nullptr; + + if (auto* child = children[(size_t) column->begin]) + { + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") + if (child->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (&provider)) == S_OK && provider != nullptr) + { + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1); + LONG index = 0; + const auto hr = SafeArrayPutElement (*pRetVal, &index, provider); + + return ! FAILED (hr); + } + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + } + } + } + } + } + + return false; + }); + } private: template - JUCE_COMRESULT withCellInterface (Value* pRetVal, Callback&& callback) const + JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const { return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT { - if (auto* cellInterface = getHandler().getCellInterface()) - { - callback (*cellInterface); - return S_OK; - } + if (auto* handler = getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface)) + if (handler->getTableInterface() != nullptr && callback (*handler)) + return S_OK; return (HRESULT) UIA_E_NOTSUPPORTED; }); } + JUCE_COMRESULT withTableSpan (int* pRetVal, + Optional (AccessibilityTableInterface::* getSpan) (const AccessibilityHandler&) const, + int AccessibilityTableInterface::Span::* spanMember) const + { + return withTableInterface (pRetVal, [&] (const AccessibilityHandler& handler) + { + if (const auto span = ((handler.getTableInterface())->*getSpan) (getHandler())) + { + *pRetVal = (*span).*spanMember; + return true; + } + + return false; + }); + } + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridItemProvider) }; diff --git a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h index 6359f74bcc..ef17dd5ecf 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h +++ b/modules/juce_gui_basics/native/accessibility/juce_win32_UIAGridProvider.h @@ -28,7 +28,7 @@ namespace juce //============================================================================== class UIAGridProvider : public UIAProviderBase, - public ComBaseClassHelper + public ComBaseClassHelper { public: using UIAProviderBase::UIAProviderBase; @@ -44,12 +44,21 @@ public: JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") - if (auto* handler = tableInterface.getCellHandler (row, column)) - handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + if (auto* cellHandler = tableInterface.getCellHandler (row, column)) + { + cellHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + return S_OK; + } + + if (auto* rowHandler = tableInterface.getRowHandler (row)) + { + rowHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal)); + return S_OK; + } JUCE_END_IGNORE_WARNINGS_GCC_LIKE - return S_OK; + return E_FAIL; }); } @@ -71,14 +80,65 @@ public: }); } + JUCE_COMRESULT GetRowHeaders (SAFEARRAY**) override + { + return (HRESULT) UIA_E_NOTSUPPORTED; + } + + JUCE_COMRESULT GetColumnHeaders (SAFEARRAY** pRetVal) override + { + return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface) + { + if (auto* header = tableInterface.getHeaderHandler()) + { + const auto children = header->getChildren(); + + *pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, (ULONG) children.size()); + + LONG index = 0; + + for (const auto& child : children) + { + IRawElementProviderSimple* provider = nullptr; + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token") + if (child != nullptr) + child->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (&provider)); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + if (provider == nullptr) + return E_FAIL; + + const auto hr = SafeArrayPutElement (*pRetVal, &index, provider); + + if (FAILED (hr)) + return E_FAIL; + + ++index; + } + + return S_OK; + } + + return (HRESULT) UIA_E_NOTSUPPORTED; + }); + } + + JUCE_COMRESULT get_RowOrColumnMajor (ComTypes::RowOrColumnMajor* pRetVal) override + { + *pRetVal = ComTypes::RowOrColumnMajor_RowMajor; + return S_OK; + } + private: template JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const { return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT { - if (auto* tableInterface = getHandler().getTableInterface()) - return callback (*tableInterface); + if (auto* tableHandler = getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface)) + if (auto* tableInterface = tableHandler->getTableInterface()) + return callback (*tableInterface); return (HRESULT) UIA_E_NOTSUPPORTED; }); diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.cpp b/modules/juce_gui_basics/widgets/juce_ListBox.cpp index 0c105d0e38..f58f2f81ae 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ListBox.cpp @@ -241,16 +241,6 @@ public: public: explicit RowCellInterface (RowAccessibilityHandler& h) : handler (h) {} - int getColumnIndex() const override { return 0; } - int getColumnSpan() const override { return 1; } - - int getRowIndex() const override - { - return handler.rowComponent.row; - } - - int getRowSpan() const override { return 1; } - int getDisclosureLevel() const override { return 0; } const AccessibilityHandler* getTableHandler() const override @@ -289,7 +279,15 @@ public: { setWantsKeyboardFocus (false); - auto content = std::make_unique(); + struct IgnoredComponent : Component + { + std::unique_ptr createAccessibilityHandler() override + { + return createIgnoredAccessibilityHandler (*this); + } + }; + + auto content = std::make_unique(); content->setWantsKeyboardFocus (false); setViewedComponent (content.release()); @@ -301,20 +299,23 @@ public: { const auto startIndex = getIndexOfFirstVisibleRow(); - return (startIndex <= row && row < startIndex + rows.size()) - ? rows[row % jmax (1, rows.size())] : nullptr; + return (startIndex <= row && row < startIndex + (int) rows.size()) + ? rows[(size_t) (row % jmax (1, (int) rows.size()))].get() + : nullptr; } - int getRowNumberOfComponent (Component* const rowComponent) const noexcept + int getRowNumberOfComponent (const Component* const rowComponent) const noexcept { - const int index = getViewedComponent()->getIndexOfChildComponent (rowComponent); - const int num = rows.size(); + const auto iter = std::find_if (rows.begin(), rows.end(), [=] (auto& ptr) { return ptr.get() == rowComponent; }); - for (int i = num; --i >= 0;) - if (((firstIndex + i) % jmax (1, num)) == index) - return firstIndex + i; + if (iter == rows.end()) + return -1; - return -1; + const auto index = (int) std::distance (rows.begin(), iter); + const auto mod = jmax (1, (int) rows.size()); + const auto startIndex = getIndexOfFirstVisibleRow(); + + return index + mod * ((startIndex / mod) + (index < (startIndex % mod) ? 1 : 0)); } void visibleAreaChanged (const Rectangle&) override @@ -357,13 +358,13 @@ public: auto y = getViewPositionY(); auto w = content.getWidth(); - const int numNeeded = 4 + getMaximumVisibleHeight() / rowH; - rows.removeRange (numNeeded, rows.size()); + const auto numNeeded = (size_t) (4 + getMaximumVisibleHeight() / rowH); + rows.resize (jmin (numNeeded, rows.size())); while (numNeeded > rows.size()) { - auto* newRow = rows.add (new RowComponent (owner)); - content.addAndMakeVisible (newRow); + rows.emplace_back (new RowComponent (owner)); + content.addAndMakeVisible (*rows.back()); } firstIndex = y / rowH; @@ -371,7 +372,7 @@ public: lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; const auto startIndex = getIndexOfFirstVisibleRow(); - const auto lastIndex = startIndex + rows.size(); + const auto lastIndex = startIndex + (int) rows.size(); for (auto row = startIndex; row < lastIndex; ++row) { @@ -477,7 +478,7 @@ private: } ListBox& owner; - OwnedArray rows; + std::vector> rows; int firstIndex = 0, firstWholeIndex = 0, lastWholeIndex = 0; bool hasUpdated = false; @@ -839,7 +840,7 @@ Component* ListBox::getComponentForRowNumber (const int row) const noexcept return nullptr; } -int ListBox::getRowNumberOfComponent (Component* const rowComponent) const noexcept +int ListBox::getRowNumberOfComponent (const Component* const rowComponent) const noexcept { return viewport->getRowNumberOfComponent (rowComponent); } @@ -1174,6 +1175,25 @@ std::unique_ptr ListBox::createAccessibilityHandler() return nullptr; } + Optional getRowSpan (const AccessibilityHandler& handler) const override + { + const auto rowNumber = listBox.getRowNumberOfComponent (&handler.getComponent()); + + return rowNumber != -1 ? makeOptional (Span { rowNumber, 1 }) + : nullopt; + } + + Optional getColumnSpan (const AccessibilityHandler&) const override + { + return Span { 0, 1 }; + } + + void showCell (const AccessibilityHandler& h) const override + { + if (const auto row = getRowSpan (h)) + listBox.scrollToEnsureRowIsOnscreen (row->begin); + } + private: ListBox& listBox; diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.h b/modules/juce_gui_basics/widgets/juce_ListBox.h index cb2907e13b..c3a4273c9b 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.h +++ b/modules/juce_gui_basics/widgets/juce_ListBox.h @@ -455,7 +455,7 @@ public: /** Returns the row number that the given component represents. If the component isn't one of the list's rows, this will return -1. */ - int getRowNumberOfComponent (Component* rowComponent) const noexcept; + int getRowNumberOfComponent (const Component* rowComponent) const noexcept; /** Returns the width of a row (which may be less than the width of this component if there's a scrollbar). diff --git a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp index 05fe6920fd..b332e5dc06 100644 --- a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp @@ -46,8 +46,8 @@ public: tableModel->paintRowBackground (g, row, getWidth(), getHeight(), isSelected); auto& headerComp = owner.getHeader(); - auto numColumns = headerComp.getNumColumns (true); - auto clipBounds = g.getClipBounds(); + const auto numColumns = jmin ((int) columnComponents.size(), headerComp.getNumColumns (true)); + const auto clipBounds = g.getClipBounds(); for (int i = 0; i < numColumns; ++i) { @@ -89,8 +89,14 @@ public: if (tableModel != nullptr && row < owner.getNumRows()) { + const ComponentDeleter deleter { columnForComponent }; const auto numColumns = owner.getHeader().getNumColumns (true); - columnComponents.resize ((size_t) numColumns); + + while (numColumns < (int) columnComponents.size()) + columnComponents.pop_back(); + + while ((int) columnComponents.size() < numColumns) + columnComponents.emplace_back (nullptr, deleter); for (int i = 0; i < numColumns; ++i) { @@ -98,11 +104,17 @@ public: auto originalComp = std::move (columnComponents[(size_t) i]); auto oldCustomComp = originalComp != nullptr && ! originalComp->getProperties().contains (tableAccessiblePlaceholderProperty) ? std::move (originalComp) - : nullptr; + : std::unique_ptr { nullptr, deleter }; auto compToRefresh = oldCustomComp != nullptr && columnId == static_cast (oldCustomComp->getProperties()[tableColumnProperty]) ? std::move (oldCustomComp) - : nullptr; - auto newCustomComp = rawToUniquePtr (tableModel->refreshComponentForCell (row, columnId, isSelected, compToRefresh.release())); + : std::unique_ptr { nullptr, deleter }; + + columnForComponent.erase (compToRefresh.get()); + std::unique_ptr newCustomComp { tableModel->refreshComponentForCell (row, + columnId, + isSelected, + compToRefresh.release()), + deleter }; auto columnComp = [&] { @@ -115,12 +127,14 @@ public: return std::move (originalComp); // Create a new placeholder component to use - auto comp = std::make_unique(); + std::unique_ptr comp { new Component, deleter }; comp->setInterceptsMouseClicks (false, false); comp->getProperties().set (tableAccessiblePlaceholderProperty, true); return comp; }(); + columnForComponent.emplace (columnComp.get(), i); + // In order for navigation to work correctly on macOS, the number of child // accessibility elements on each row must match the number of header accessibility // elements. @@ -250,6 +264,12 @@ public: return nullptr; } + int getColumnNumberOfComponent (const Component* comp) const + { + const auto iter = columnForComponent.find (comp); + return iter != columnForComponent.cend() ? iter->second : -1; + } + std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this); @@ -297,6 +317,7 @@ public: return state; } + private: class RowComponentCellInterface : public AccessibilityCellInterface { public: @@ -305,12 +326,6 @@ public: { } - int getColumnIndex() const override { return 0; } - int getColumnSpan() const override { return 1; } - - int getRowIndex() const override { return owner.rowComponent.row; } - int getRowSpan() const override { return 1; } - int getDisclosureLevel() const override { return 0; } const AccessibilityHandler* getTableHandler() const override { return owner.rowComponent.owner.getAccessibilityHandler(); } @@ -324,8 +339,27 @@ public: }; //============================================================================== + class ComponentDeleter + { + public: + explicit ComponentDeleter (std::map& locations) + : columnForComponent (&locations) {} + + void operator() (Component* comp) const + { + columnForComponent->erase (comp); + + if (comp != nullptr) + delete comp; + } + + private: + std::map* columnForComponent; + }; + TableListBox& owner; - std::vector> columnComponents; + std::map columnForComponent; + std::vector> columnComponents; int row = -1; bool isSelected = false, isDragging = false, selectRowOnMouseUp = false; @@ -571,6 +605,22 @@ void TableListBox::updateColumnComponents() const rowComp->resized(); } +template +Optional findRecursively (const AccessibilityHandler& handler, + Component* outermost, + FindIndex&& findIndexOfComponent) +{ + for (auto* comp = &handler.getComponent(); comp != outermost; comp = comp->getParentComponent()) + { + const auto result = findIndexOfComponent (comp); + + if (result != -1) + return AccessibilityTableInterface::Span { result, 1 }; + } + + return nullopt; +} + std::unique_ptr TableListBox::createAccessibilityHandler() { class TableInterface : public AccessibilityTableInterface @@ -591,7 +641,7 @@ std::unique_ptr TableListBox::createAccessibilityHandler() int getNumColumns() const override { - return tableListBox.getHeader().getNumColumns (false); + return tableListBox.getHeader().getNumColumns (true); } const AccessibilityHandler* getRowHandler (int row) const override @@ -606,7 +656,7 @@ std::unique_ptr TableListBox::createAccessibilityHandler() const AccessibilityHandler* getCellHandler (int row, int column) const override { if (isPositiveAndBelow (row, getNumRows()) && isPositiveAndBelow (column, getNumColumns())) - if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, false), row)) + if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, true), row)) return cellComponent->getAccessibilityHandler(); return nullptr; @@ -620,6 +670,35 @@ std::unique_ptr TableListBox::createAccessibilityHandler() return nullptr; } + Optional getRowSpan (const AccessibilityHandler& handler) const override + { + if (tableListBox.isParentOf (&handler.getComponent())) + return findRecursively (handler, &tableListBox, [&] (auto* c) { return tableListBox.getRowNumberOfComponent (c); }); + + return nullopt; + } + + Optional getColumnSpan (const AccessibilityHandler& handler) const override + { + if (const auto rowSpan = getRowSpan (handler)) + if (auto* rowComponent = dynamic_cast (tableListBox.getComponentForRowNumber (rowSpan->begin))) + return findRecursively (handler, &tableListBox, [&] (auto* c) { return rowComponent->getColumnNumberOfComponent (c); }); + + return nullopt; + } + + void showCell (const AccessibilityHandler& handler) const override + { + const auto row = getRowSpan (handler); + const auto col = getColumnSpan (handler); + + if (row.hasValue() && col.hasValue()) + { + tableListBox.scrollToEnsureRowIsOnscreen (row->begin); + tableListBox.scrollToEnsureColumnIsOnscreen (col->begin); + } + } + private: TableListBox& tableListBox; @@ -627,7 +706,7 @@ std::unique_ptr TableListBox::createAccessibilityHandler() }; return std::make_unique (*this, - AccessibilityRole::list, + AccessibilityRole::table, AccessibilityActions{}, AccessibilityHandler::Interfaces { std::make_unique (*this) }); } diff --git a/modules/juce_gui_basics/widgets/juce_TreeView.cpp b/modules/juce_gui_basics/widgets/juce_TreeView.cpp index 991610c1e5..38c2277740 100644 --- a/modules/juce_gui_basics/widgets/juce_TreeView.cpp +++ b/modules/juce_gui_basics/widgets/juce_TreeView.cpp @@ -143,19 +143,6 @@ private: public: explicit ItemCellInterface (ItemComponent& c) : itemComponent (c) {} - int getColumnIndex() const override { return 0; } - int getColumnSpan() const override { return 1; } - - int getRowIndex() const override - { - return itemComponent.getRepresentedItem().getRowNumberInTree(); - } - - int getRowSpan() const override - { - return 1; - } - int getDisclosureLevel() const override { return getItemDepth (&itemComponent.getRepresentedItem()); @@ -312,7 +299,7 @@ public: ItemComponent* getItemComponentAt (Point p) { auto iter = std::find_if (itemComponents.cbegin(), itemComponents.cend(), - [p] (const std::unique_ptr& c) + [p] (const auto& c) { return c->getBounds().contains (p); }); @@ -326,7 +313,7 @@ public: ItemComponent* getComponentForItem (const TreeViewItem* item) const { const auto iter = std::find_if (itemComponents.begin(), itemComponents.end(), - [item] (const std::unique_ptr& c) + [item] (const auto& c) { return &c->getRepresentedItem() == item; }); @@ -340,7 +327,7 @@ public: void itemBeingDeleted (const TreeViewItem* item) { const auto iter = std::find_if (itemComponents.begin(), itemComponents.end(), - [item] (const std::unique_ptr& c) + [item] (const auto& c) { return &c->getRepresentedItem() == item; }); @@ -357,6 +344,12 @@ public: } } + const TreeViewItem* getItemForItemComponent (const Component* comp) const + { + const auto iter = itemForItemComponent.find (comp); + return iter != itemForItemComponent.cend() ? iter->second : nullptr; + } + void updateComponents() { std::set componentsToKeep; @@ -369,7 +362,8 @@ public: } else { - auto newComp = std::make_unique (*treeItem); + std::unique_ptr newComp { new ItemComponent (*treeItem), Deleter { itemForItemComponent } }; + itemForItemComponent.emplace (newComp.get(), treeItem); addAndMakeVisible (*newComp); newComp->addMouseListener (this, treeItem->customComponentUsesTreeViewMouseHandler()); @@ -430,7 +424,7 @@ private: updateItemUnderMouse (e); isDragging = false; - scopedScrollDisabler = nullptr; + scopedScrollDisabler = nullopt; needSelectionOnMouseUp = false; if (! isEnabled()) @@ -523,7 +517,7 @@ private: auto imageOffset = pos.getPosition() - e.getPosition(); dragContainer->startDragging (dragDescription, &owner, { dragImage, additionalScale }, true, &imageOffset, &e.source); - scopedScrollDisabler = std::make_unique (*itemComponent); + scopedScrollDisabler.emplace (*itemComponent); } else { @@ -688,12 +682,32 @@ private: return visibleItems; } + //============================================================================== + class Deleter + { + public: + explicit Deleter (std::map& map) + : itemForItemComponent (&map) {} + + void operator() (ItemComponent* ptr) const + { + itemForItemComponent->erase (ptr); + + if (ptr != nullptr) + delete ptr; + } + + private: + std::map* itemForItemComponent = nullptr; + }; + //============================================================================== TreeView& owner; - std::vector> itemComponents; + std::map itemForItemComponent; + std::vector> itemComponents; ItemComponent* itemUnderMouse = nullptr; - std::unique_ptr scopedScrollDisabler; + Optional scopedScrollDisabler; bool isDragging = false, needSelectionOnMouseUp = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContentComponent) @@ -1081,7 +1095,7 @@ void TreeView::moveSelectedRow (int delta) } } -void TreeView::scrollToKeepItemVisible (TreeViewItem* item) +void TreeView::scrollToKeepItemVisible (const TreeViewItem* item) { if (item != nullptr && item->ownerView == this) { @@ -1494,7 +1508,39 @@ std::unique_ptr TreeView::createAccessibilityHandler() return nullptr; } + Optional getRowSpan (const AccessibilityHandler& handler) const override + { + auto* item = getItemForHandler (handler); + + if (item == nullptr) + return nullopt; + + const auto rowNumber = item->getRowNumberInTree(); + + return rowNumber != -1 ? makeOptional (Span { rowNumber, 1 }) + : nullopt; + } + + Optional getColumnSpan (const AccessibilityHandler&) const override + { + return Span { 0, 1 }; + } + + void showCell (const AccessibilityHandler& cellHandler) const override + { + treeView.scrollToKeepItemVisible (getItemForHandler (cellHandler)); + } + private: + const TreeViewItem* getItemForHandler (const AccessibilityHandler& handler) const + { + for (auto* comp = &handler.getComponent(); comp != &treeView; comp = comp->getParentComponent()) + if (auto* result = treeView.viewport->getContentComp()->getItemForItemComponent (comp)) + return result; + + return nullptr; + } + TreeView& treeView; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableInterface) @@ -1849,7 +1895,7 @@ void TreeViewItem::updatePositions (int newY) } } -TreeViewItem* TreeViewItem::getDeepestOpenParentItem() noexcept +const TreeViewItem* TreeViewItem::getDeepestOpenParentItem() const noexcept { auto* result = this; auto* item = this; diff --git a/modules/juce_gui_basics/widgets/juce_TreeView.h b/modules/juce_gui_basics/widgets/juce_TreeView.h index 51b3f64ece..0a40a90ff1 100644 --- a/modules/juce_gui_basics/widgets/juce_TreeView.h +++ b/modules/juce_gui_basics/widgets/juce_TreeView.h @@ -613,7 +613,7 @@ private: int getIndentX() const noexcept; void setOwnerView (TreeView*) noexcept; TreeViewItem* getTopLevelItem() noexcept; - TreeViewItem* getDeepestOpenParentItem() noexcept; + const TreeViewItem* getDeepestOpenParentItem() const noexcept; int getNumRows() const noexcept; TreeViewItem* getItemOnRow (int) noexcept; void deselectAllRecursively (TreeViewItem*); @@ -798,7 +798,7 @@ public: TreeViewItem* getItemAt (int yPosition) const noexcept; /** Tries to scroll the tree so that this item is on-screen somewhere. */ - void scrollToKeepItemVisible (TreeViewItem* item); + void scrollToKeepItemVisible (const TreeViewItem* item); /** Returns the TreeView's Viewport object. */ Viewport* getViewport() const noexcept;