From 707767fa4cbc35f1b7191376315bd1e4fe13a3b5 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 28 Jun 2022 12:51:03 +0100 Subject: [PATCH] Accessibility: Multiple table fixes - Fixes an off-by-one error when navigating by rows, caused by treating the table header as a row. The table header now has the header accessibility role. - Fixes a bug where reordering table columns would cause the table to become inaccessible. - Fixes a bug where the screen reader would try to navigate hidden table columns. - Fixes an issue where moving the VoiceOver cursor to a partially hidden cell would cause the focus to move to the table itself, rather than to the cell. --- .../juce_AccessibilityTableInterface.h | 15 +++ .../accessibility/juce_mac_Accessibility.mm | 117 +++++++----------- .../juce_mac_AccessibilitySharedCode.mm | 4 +- .../juce_gui_basics/widgets/juce_ListBox.cpp | 83 +++++-------- .../widgets/juce_TableHeaderComponent.cpp | 79 +++++++----- .../widgets/juce_TableHeaderComponent.h | 10 +- .../widgets/juce_TableListBox.cpp | 100 ++++++++++----- .../juce_gui_basics/widgets/juce_TreeView.cpp | 12 +- 8 files changed, 233 insertions(+), 187 deletions(-) diff --git a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h index 4f7f794497..db3cafcb87 100644 --- a/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h +++ b/modules/juce_gui_basics/accessibility/interfaces/juce_AccessibilityTableInterface.h @@ -49,6 +49,21 @@ public: nullptr if there is no cell at the specified position. */ virtual const AccessibilityHandler* getCellHandler (int row, int column) const = 0; + + /** Returns the AccessibilityHandler for a row in the table, or nullptr if there is + no row at this index. + + The row component should have a child component for each column in the table. + */ + virtual const AccessibilityHandler* getRowHandler (int row) const = 0; + + /** Returns the AccessibilityHandler for the header, or nullptr if there is + no header. + + If you supply a header, it must have exactly the same number of children + as there are columns in the table. + */ + virtual const AccessibilityHandler* getHeaderHandler() const = 0; }; } // 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 bf3c38c95b..47cff2d846 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_Accessibility.mm @@ -116,9 +116,7 @@ private: addMethod (@selector (accessibilitySelectedRows), getAccessibilitySelectedRows); addMethod (@selector (setAccessibilitySelectedRows:), setAccessibilitySelectedRows); addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount); - addMethod (@selector (accessibilityColumns), getAccessibilityColumns); - addMethod (@selector (accessibilitySelectedColumns), getAccessibilitySelectedColumns); - addMethod (@selector (setAccessibilitySelectedColumns:), setAccessibilitySelectedColumns); + addMethod (@selector (accessibilityHeader), getAccessibilityHeader); addMethod (@selector (accessibilityRowIndexRange), getAccessibilityRowIndexRange); addMethod (@selector (accessibilityColumnIndexRange), getAccessibilityColumnIndexRange); @@ -166,26 +164,30 @@ private: return selected; } + static void setSelected (id item, bool selected) + { + auto* handler = getHandler (item); + + if (handler == nullptr) + return; + + const auto currentState = handler->getCurrentState(); + + if (isSelectable (currentState)) + { + if (currentState.isSelected() != selected) + handler->getActions().invoke (AccessibilityActionType::toggle); + } + else if (currentState.isFocusable()) + { + [item setAccessibilityFocused: selected]; + } + } + static void setSelectedChildren (NSArray* children, NSArray* selected) { for (id child in children) - { - if (auto* handler = getHandler (child)) - { - const auto currentState = handler->getCurrentState(); - const BOOL isSelected = [selected containsObject: child]; - - if (isSelectable (currentState)) - { - if (currentState.isSelected() != isSelected) - handler->getActions().invoke (AccessibilityActionType::toggle); - } - else if (currentState.isFocusable()) - { - [child setAccessibilityFocused: isSelected]; - } - } - } + setSelected (child, [selected containsObject: child]); } //============================================================================== @@ -218,15 +220,15 @@ private: if (auto* modalHandler = modal->getAccessibilityHandler()) { if (auto* focusChild = modalHandler->getChildFocus()) - return (id) focusChild->getNativeImplementation(); + return static_cast (focusChild->getNativeImplementation()); - return (id) modalHandler->getNativeImplementation(); + return static_cast (modalHandler->getNativeImplementation()); } } } if (auto* focusChild = handler->getChildFocus()) - return (id) focusChild->getNativeImplementation(); + return static_cast (focusChild->getNativeImplementation()); } return nil; @@ -237,7 +239,7 @@ private: if (auto* handler = getHandler (self)) { if (auto* child = handler->getChildAt (roundToIntPoint (flippedScreenPoint (point)))) - return (id) child->getNativeImplementation(); + return static_cast (child->getNativeImplementation()); return self; } @@ -250,9 +252,9 @@ private: if (auto* handler = getHandler (self)) { if (auto* parentHandler = handler->getParent()) - return NSAccessibilityUnignoredAncestor ((id) parentHandler->getNativeImplementation()); + return NSAccessibilityUnignoredAncestor (static_cast (parentHandler->getNativeImplementation())); - return NSAccessibilityUnignoredAncestor ((id) handler->getComponent().getWindowHandle()); + return NSAccessibilityUnignoredAncestor (static_cast (handler->getComponent().getWindowHandle())); } return nil; @@ -267,7 +269,7 @@ private: auto* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()]; for (auto* childHandler : children) - [accessibleChildren addObject: (id) childHandler->getNativeImplementation()]; + [accessibleChildren addObject: static_cast (childHandler->getNativeImplementation())]; return accessibleChildren; } @@ -581,27 +583,29 @@ private: //============================================================================== static NSArray* getAccessibilityRows (id self, SEL) { - NSMutableArray* rows = [[NSMutableArray new] autorelease]; - if (auto* tableInterface = getTableInterface (self)) { - for (int row = 0; row < tableInterface->getNumRows(); ++row) + auto* rows = [[NSMutableArray new] autorelease]; + + for (int row = 0, numRows = tableInterface->getNumRows(); row < numRows; ++row) { - if (auto* handler = tableInterface->getCellHandler (row, 0)) + if (auto* rowHandler = tableInterface->getRowHandler (row)) { - [rows addObject: (id) handler->getNativeImplementation()]; + [rows addObject: static_cast (rowHandler->getNativeImplementation())]; } else { [rows addObject: [NSAccessibilityElement accessibilityElementWithRole: NSAccessibilityRowRole - frame: NSZeroRect - label: @"Offscreen Row" - parent: self]]; + frame: NSZeroRect + label: @"Offscreen Row" + parent: self]]; } } + + return rows; } - return rows; + return nil; } static NSArray* getAccessibilitySelectedRows (id self, SEL) @@ -614,39 +618,13 @@ private: setSelectedChildren ([self accessibilityRows], selected); } - static NSArray* getAccessibilityColumns (id self, SEL) + static id getAccessibilityHeader (id self, SEL) { - NSMutableArray* columns = [[NSMutableArray new] autorelease]; - if (auto* tableInterface = getTableInterface (self)) - { - for (int column = 0; column < tableInterface->getNumColumns(); ++column) - { - if (auto* handler = tableInterface->getCellHandler (0, column)) - { - [columns addObject: (id) handler->getNativeImplementation()]; - } - else - { - [columns addObject: [NSAccessibilityElement accessibilityElementWithRole: NSAccessibilityColumnRole - frame: NSZeroRect - label: @"Offscreen Column" - parent: self]]; - } - } - } + if (auto* handler = tableInterface->getHeaderHandler()) + return static_cast (handler->getNativeImplementation()); - return columns; - } - - static NSArray* getAccessibilitySelectedColumns (id self, SEL) - { - return getSelectedChildren ([self accessibilityColumns]); - } - - static void setAccessibilitySelectedColumns (id self, SEL, NSArray* selected) - { - setSelectedChildren ([self accessibilityColumns], selected); + return nil; } //============================================================================== @@ -743,8 +721,7 @@ private: @selector (accessibilityRows), @selector (accessibilitySelectedRows), @selector (accessibilityColumnCount), - @selector (accessibilityColumns), - @selector (accessibilitySelectedColumns) }) + @selector (accessibilityHeader) }) { if (selector == tableSelector) return handler->getTableInterface() != nullptr; @@ -881,7 +858,7 @@ static void sendHandlerNotification (const AccessibilityHandler& handler, if (@available (macOS 10.9, *)) { - if (id accessibilityElement = (id) handler.getNativeImplementation()) + if (id accessibilityElement = static_cast (handler.getNativeImplementation())) { sendAccessibilityEvent (accessibilityElement, notification, (notification == NSAccessibilityLayoutChangedNotification @@ -966,7 +943,7 @@ void AccessibilityHandler::postAnnouncement (const String& announcementString, A JUCE_END_IGNORE_WARNINGS_GCC_LIKE }(); - sendAccessibilityEvent ((id) [NSApp mainWindow], + sendAccessibilityEvent (static_cast ([NSApp mainWindow]), NSAccessibilityAnnouncementRequestedNotification, @{ NSAccessibilityAnnouncementKey: juceStringToNS (announcementString), NSAccessibilityPriorityKey: @(nsPriority) }); 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 6eb5b6929a..d949e04ace 100644 --- a/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm +++ b/modules/juce_gui_basics/native/accessibility/juce_mac_AccessibilitySharedCode.mm @@ -46,7 +46,9 @@ public: using Holder = std::unique_ptr; protected: - AccessibleObjCClass() : ObjCClass ("JUCEAccessibilityElement_") + AccessibleObjCClass() : AccessibleObjCClass ("JUCEAccessibilityElement_") {} + + explicit AccessibleObjCClass (const char* name) : ObjCClass (name) { ObjCClass::template addIvar ("handler"); } diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.cpp b/modules/juce_gui_basics/widgets/juce_ListBox.cpp index b2c8f8c1e2..0c105d0e38 100644 --- a/modules/juce_gui_basics/widgets/juce_ListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_ListBox.cpp @@ -246,12 +246,7 @@ public: int getRowIndex() const override { - const auto index = handler.rowComponent.row; - - if (handler.rowComponent.owner.hasAccessibleHeaderComponent()) - return index + 1; - - return index; + return handler.rowComponent.row; } int getRowSpan() const override { return 1; } @@ -300,23 +295,14 @@ public: setViewedComponent (content.release()); } - RowComponent* getComponentForRow (int row) const noexcept - { - if (isPositiveAndBelow (row, rows.size())) - return rows[row]; - - return nullptr; - } - - RowComponent* getComponentForRowWrapped (int row) const noexcept - { - return rows[row % jmax (1, rows.size())]; - } + int getIndexOfFirstVisibleRow() const { return jmax (0, firstIndex - 1); } RowComponent* getComponentForRowIfOnscreen (int row) const noexcept { - return (row >= firstIndex && row < firstIndex + rows.size()) - ? getComponentForRowWrapped (row) : nullptr; + const auto startIndex = getIndexOfFirstVisibleRow(); + + return (startIndex <= row && row < startIndex + rows.size()) + ? rows[row % jmax (1, rows.size())] : nullptr; } int getRowNumberOfComponent (Component* const rowComponent) const noexcept @@ -384,17 +370,20 @@ public: firstWholeIndex = (y + rowH - 1) / rowH; lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; - auto startIndex = jmax (0, firstIndex - 1); + const auto startIndex = getIndexOfFirstVisibleRow(); + const auto lastIndex = startIndex + rows.size(); - for (int i = 0; i < numNeeded; ++i) + for (auto row = startIndex; row < lastIndex; ++row) { - const int row = i + startIndex; - - if (auto* rowComp = getComponentForRowWrapped (row)) + if (auto* rowComp = getComponentForRowIfOnscreen (row)) { rowComp->setBounds (0, row * rowH, w, rowH); rowComp->update (row, owner.isRowSelected (row)); } + else + { + jassertfalse; + } } } @@ -1155,15 +1144,8 @@ std::unique_ptr ListBox::createAccessibilityHandler() { listBox.checkModelPtrIsValid(); - if (listBox.model == nullptr) - return 0; - - const auto numRows = listBox.model->getNumRows(); - - if (listBox.hasAccessibleHeaderComponent()) - return numRows + 1; - - return numRows; + return listBox.model != nullptr ? listBox.model->getNumRows() + : 0; } int getNumColumns() const override @@ -1171,24 +1153,7 @@ std::unique_ptr ListBox::createAccessibilityHandler() return 1; } - const AccessibilityHandler* getCellHandler (int row, int) const override - { - if (auto* headerHandler = getHeaderHandler()) - { - if (row == 0) - return headerHandler; - - --row; - } - - if (auto* rowComponent = listBox.viewport->getComponentForRow (row)) - return rowComponent->getAccessibilityHandler(); - - return nullptr; - } - - private: - const AccessibilityHandler* getHeaderHandler() const + const AccessibilityHandler* getHeaderHandler() const override { if (listBox.hasAccessibleHeaderComponent()) return listBox.headerComponent->getAccessibilityHandler(); @@ -1196,6 +1161,20 @@ std::unique_ptr ListBox::createAccessibilityHandler() return nullptr; } + const AccessibilityHandler* getRowHandler (int row) const override + { + if (auto* rowComponent = listBox.viewport->getComponentForRowIfOnscreen (row)) + return rowComponent->getAccessibilityHandler(); + + return nullptr; + } + + const AccessibilityHandler* getCellHandler (int, int) const override + { + return nullptr; + } + + private: ListBox& listBox; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableInterface) diff --git a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp index 6a787d953f..4dc53dc32c 100644 --- a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp +++ b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp @@ -50,6 +50,7 @@ public: //============================================================================== TableHeaderComponent::TableHeaderComponent() { + setFocusContainerType (FocusContainerType::focusContainer); } TableHeaderComponent::~TableHeaderComponent() @@ -86,7 +87,7 @@ int TableHeaderComponent::getNumColumns (const bool onlyCountVisibleColumns) con String TableHeaderComponent::getColumnName (const int columnId) const { if (auto* ci = getInfoForId (columnId)) - return ci->name; + return ci->getTitle(); return {}; } @@ -95,9 +96,9 @@ void TableHeaderComponent::setColumnName (const int columnId, const String& newN { if (auto* ci = getInfoForId (columnId)) { - if (ci->name != newName) + if (ci->getTitle() != newName) { - ci->name = newName; + ci->setTitle (newName); sendColumnsChanged(); } } @@ -116,7 +117,7 @@ void TableHeaderComponent::addColumn (const String& columnName, jassert (width > 0); auto ci = new ColumnInfo(); - ci->name = columnName; + ci->setTitle (columnName); ci->id = columnId; ci->width = width; ci->lastDeliberateWidth = width; @@ -125,7 +126,11 @@ void TableHeaderComponent::addColumn (const String& columnName, jassert (ci->maximumWidth >= ci->minimumWidth); ci->propertyFlags = propertyFlags; - columns.insert (insertIndex, ci); + auto* added = columns.insert (insertIndex, ci); + addChildComponent (added); + added->setVisible ((propertyFlags & visible) != 0); + + resized(); sendColumnsChanged(); } @@ -197,6 +202,7 @@ void TableHeaderComponent::setColumnWidth (const int columnId, const int newWidt } } + resized(); repaint(); columnsResized = true; triggerAsyncUpdate(); @@ -340,6 +346,7 @@ void TableHeaderComponent::resizeColumnsToFit (int firstColumnIndex, int targetT if (newWidth != ci->width) { ci->width = newWidth; + resized(); repaint(); columnsResized = true; triggerAsyncUpdate(); @@ -354,11 +361,7 @@ void TableHeaderComponent::setColumnVisible (const int columnId, const bool shou { if (shouldBeVisible != ci->isVisible()) { - if (shouldBeVisible) - ci->propertyFlags |= visible; - else - ci->propertyFlags &= ~visible; - + ci->setVisible (shouldBeVisible); sendColumnsChanged(); resized(); } @@ -409,6 +412,7 @@ bool TableHeaderComponent::isSortedForwards() const void TableHeaderComponent::reSortTable() { sortChanged = true; + resized(); repaint(); triggerAsyncUpdate(); } @@ -485,7 +489,7 @@ void TableHeaderComponent::addMenuItems (PopupMenu& menu, const int /*columnIdCl { for (auto* ci : columns) if ((ci->propertyFlags & appearsOnColumnMenu) != 0) - menu.addItem (ci->id, ci->name, + menu.addItem (ci->id, ci->getTitle(), (ci->propertyFlags & (sortedForwards | sortedBackwards)) == 0, isColumnVisible (ci->id)); } @@ -502,28 +506,42 @@ void TableHeaderComponent::paint (Graphics& g) lf.drawTableHeaderBackground (g, *this); - auto clip = g.getClipBounds(); + for (auto* ci : columns) + { + if (ci->isVisible() && ci->getWidth() > 0) + { + Graphics::ScopedSaveState ss (g); + + g.setOrigin (ci->getX(), ci->getY()); + g.reduceClipRegion (0, 0, ci->getWidth(), ci->getHeight()); + + lf.drawTableHeaderColumn (g, *this, ci->getTitle(), ci->id, ci->width, getHeight(), + ci->id == columnIdUnderMouse, + ci->id == columnIdUnderMouse && isMouseButtonDown(), + ci->propertyFlags); + } + } +} + +void TableHeaderComponent::resized() +{ + auto clip = getBounds(); int x = 0; + for (auto* ci : columns) + ci->setBounds (0, 0, 0, 0); + for (auto* ci : columns) { if (ci->isVisible()) { if (x + ci->width > clip.getX() - && (ci->id != columnIdBeingDragged - || dragOverlayComp == nullptr - || ! dragOverlayComp->isVisible())) + && (ci->id != columnIdBeingDragged + || dragOverlayComp == nullptr + || ! dragOverlayComp->isVisible())) { - Graphics::ScopedSaveState ss (g); - - g.setOrigin (x, 0); - g.reduceClipRegion (0, 0, ci->width, getHeight()); - - lf.drawTableHeaderColumn (g, *this, ci->name, ci->id, ci->width, getHeight(), - ci->id == columnIdUnderMouse, - ci->id == columnIdUnderMouse && isMouseButtonDown(), - ci->propertyFlags); + ci->setBounds (x, 0, ci->width, getHeight()); } x += ci->width; @@ -540,6 +558,7 @@ void TableHeaderComponent::mouseExit (const MouseEvent&) { setColumnUnderMou void TableHeaderComponent::mouseDown (const MouseEvent& e) { + resized(); repaint(); columnIdBeingResized = 0; columnIdBeingDragged = 0; @@ -716,6 +735,7 @@ void TableHeaderComponent::endDrag (const int finalIndex) moveColumn (columnIdBeingDragged, finalIndex); columnIdBeingDragged = 0; + resized(); repaint(); for (int i = listeners.size(); --i >= 0;) @@ -735,6 +755,7 @@ void TableHeaderComponent::mouseUp (const MouseEvent& e) c->lastDeliberateWidth = c->width; columnIdBeingResized = 0; + resized(); repaint(); endDrag (getIndexOfColumnId (columnIdBeingDragged, true)); @@ -756,10 +777,6 @@ MouseCursor TableHeaderComponent::getMouseCursor() } //============================================================================== -bool TableHeaderComponent::ColumnInfo::isVisible() const -{ - return (propertyFlags & TableHeaderComponent::visible) != 0; -} TableHeaderComponent::ColumnInfo* TableHeaderComponent::getInfoForId (int id) const { @@ -793,6 +810,7 @@ void TableHeaderComponent::sendColumnsChanged() if (stretchToFit && lastDeliberateWidth > 0) resizeAllColumnsToFit (lastDeliberateWidth); + resized(); repaint(); columnsChanged = true; triggerAsyncUpdate(); @@ -903,4 +921,9 @@ std::unique_ptr TableHeaderComponent::createAccessibilityH return std::make_unique (*this, AccessibilityRole::tableHeader); } +std::unique_ptr TableHeaderComponent::ColumnInfo::createAccessibilityHandler() +{ + return std::make_unique (*this, AccessibilityRole::tableHeader); +} + } // namespace juce diff --git a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h index 8a6034de5b..3d1b017350 100644 --- a/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h +++ b/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h @@ -403,6 +403,8 @@ public: /** @internal */ void paint (Graphics&) override; /** @internal */ + void resized() override; + /** @internal */ void mouseMove (const MouseEvent&) override; /** @internal */ void mouseEnter (const MouseEvent&) override; @@ -421,13 +423,13 @@ public: virtual void showColumnChooserMenu (int columnIdClicked); private: - struct ColumnInfo + struct ColumnInfo : public Component { - String name; + ColumnInfo() { setInterceptsMouseClicks (false, false); } + std::unique_ptr createAccessibilityHandler() override; + int id, propertyFlags, width, minimumWidth, maximumWidth; double lastDeliberateWidth; - - bool isVisible() const; }; OwnedArray columns; diff --git a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp index f513b3c577..05fe6920fd 100644 --- a/modules/juce_gui_basics/widgets/juce_TableListBox.cpp +++ b/modules/juce_gui_basics/widgets/juce_TableListBox.cpp @@ -26,6 +26,9 @@ namespace juce { +static const Identifier tableColumnProperty { "_tableColumnId" }; +static const Identifier tableAccessiblePlaceholderProperty { "_accessiblePlaceholder" }; + class TableListBox::RowComp : public Component, public TooltipClient { @@ -48,7 +51,7 @@ public: for (int i = 0; i < numColumns; ++i) { - if (columnComponents[i] == nullptr) + if (columnComponents[(size_t) i]->getProperties().contains (tableAccessiblePlaceholderProperty)) { auto columnRect = headerComp.getColumnPosition (i).withHeight (getHeight()); @@ -86,33 +89,48 @@ public: if (tableModel != nullptr && row < owner.getNumRows()) { - const Identifier columnProperty ("_tableColumnId"); - auto numColumns = owner.getHeader().getNumColumns (true); + const auto numColumns = owner.getHeader().getNumColumns (true); + columnComponents.resize ((size_t) numColumns); for (int i = 0; i < numColumns; ++i) { auto columnId = owner.getHeader().getColumnIdOfIndex (i, true); - auto* comp = columnComponents[i]; + auto originalComp = std::move (columnComponents[(size_t) i]); + auto oldCustomComp = originalComp != nullptr && ! originalComp->getProperties().contains (tableAccessiblePlaceholderProperty) + ? std::move (originalComp) + : nullptr; + auto compToRefresh = oldCustomComp != nullptr && columnId == static_cast (oldCustomComp->getProperties()[tableColumnProperty]) + ? std::move (oldCustomComp) + : nullptr; + auto newCustomComp = rawToUniquePtr (tableModel->refreshComponentForCell (row, columnId, isSelected, compToRefresh.release())); - if (comp != nullptr && columnId != static_cast (comp->getProperties() [columnProperty])) + auto columnComp = [&] { - columnComponents.set (i, nullptr); - comp = nullptr; - } + // We got a result from refreshComponentForCell, so use that + if (newCustomComp != nullptr) + return std::move (newCustomComp); - comp = tableModel->refreshComponentForCell (row, columnId, isSelected, comp); - columnComponents.set (i, comp, false); + // There was already a placeholder component for this column + if (originalComp != nullptr) + return std::move (originalComp); - if (comp != nullptr) - { - comp->getProperties().set (columnProperty, columnId); + // Create a new placeholder component to use + auto comp = std::make_unique(); + comp->setInterceptsMouseClicks (false, false); + comp->getProperties().set (tableAccessiblePlaceholderProperty, true); + return comp; + }(); - addAndMakeVisible (comp); - resizeCustomComp (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. + columnComp->setFocusContainerType (FocusContainerType::focusContainer); + columnComp->getProperties().set (tableColumnProperty, columnId); + addAndMakeVisible (*columnComp); + + columnComponents[(size_t) i] = std::move (columnComp); + resizeCustomComp (i); } - - columnComponents.removeRange (numColumns, columnComponents.size()); } else { @@ -122,15 +140,19 @@ public: void resized() override { - for (int i = columnComponents.size(); --i >= 0;) + for (auto i = (int) columnComponents.size(); --i >= 0;) resizeCustomComp (i); } void resizeCustomComp (int index) { - if (auto* c = columnComponents.getUnchecked (index)) - c->setBounds (owner.getHeader().getColumnPosition (index) - .withY (0).withHeight (getHeight())); + if (auto& c = columnComponents[(size_t) index]) + { + c->setBounds (owner.getHeader() + .getColumnPosition (index) + .withY (0) + .withHeight (getHeight())); + } } void mouseDown (const MouseEvent& e) override @@ -220,7 +242,12 @@ public: Component* findChildComponentForColumn (int columnId) const { - return columnComponents [owner.getHeader().getIndexOfColumnId (columnId, true)]; + const auto index = (size_t) owner.getHeader().getIndexOfColumnId (columnId, true); + + if (isPositiveAndBelow (index, columnComponents.size())) + return columnComponents[index].get(); + + return nullptr; } std::unique_ptr createAccessibilityHandler() override @@ -298,7 +325,7 @@ public: //============================================================================== TableListBox& owner; - OwnedArray columnComponents; + std::vector> columnComponents; int row = -1; bool isSelected = false, isDragging = false, selectRowOnMouseUp = false; @@ -567,17 +594,28 @@ std::unique_ptr TableListBox::createAccessibilityHandler() return tableListBox.getHeader().getNumColumns (false); } - const AccessibilityHandler* getCellHandler (int row, int column) const override + const AccessibilityHandler* getRowHandler (int row) const override { if (isPositiveAndBelow (row, getNumRows())) - { - if (isPositiveAndBelow (column, getNumColumns())) - if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, false), row)) - return cellComponent->getAccessibilityHandler(); - if (auto* rowComp = tableListBox.getComponentForRowNumber (row)) return rowComp->getAccessibilityHandler(); - } + + return nullptr; + } + + 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)) + return cellComponent->getAccessibilityHandler(); + + return nullptr; + } + + const AccessibilityHandler* getHeaderHandler() const override + { + if (tableListBox.hasAccessibleHeaderComponent()) + return tableListBox.headerComponent->getAccessibilityHandler(); return nullptr; } diff --git a/modules/juce_gui_basics/widgets/juce_TreeView.cpp b/modules/juce_gui_basics/widgets/juce_TreeView.cpp index c040a67de3..2940845c13 100644 --- a/modules/juce_gui_basics/widgets/juce_TreeView.cpp +++ b/modules/juce_gui_basics/widgets/juce_TreeView.cpp @@ -1448,7 +1448,12 @@ std::unique_ptr TreeView::createAccessibilityHandler() int getNumRows() const override { return treeView.getNumRowsInTree(); } int getNumColumns() const override { return 1; } - const AccessibilityHandler* getCellHandler (int row, int) const override + const AccessibilityHandler* getHeaderHandler() const override + { + return nullptr; + } + + const AccessibilityHandler* getRowHandler (int row) const override { if (auto* itemComp = treeView.getItemComponent (treeView.getItemOnRow (row))) return itemComp->getAccessibilityHandler(); @@ -1456,6 +1461,11 @@ std::unique_ptr TreeView::createAccessibilityHandler() return nullptr; } + const AccessibilityHandler* getCellHandler (int, int) const override + { + return nullptr; + } + private: TreeView& treeView;