Question on Grouping and Accessibility

Hi!

I’ve been implementing accessibility in surge. It’s going well with the 6.1 API. Awesome.

So one thing I’ve heard form our testers is that they want to ‘group’ our controls. That is, all our filter controls are in a filter section etc…

Unfortunately in our UI the controls for a particular function aren’t in a distinct rectilinear area of our UI.

So what I did to get this working was add 8 invisible components to my whole UI and then parent the sub-components to one of those 8. This works inasmuch as I get the accessible hierarchy I want. But it means I keep getting z-order bugs.

I’ve looked at the API and am not sure where I inject a ‘grouping’ parent other than an actual component in the accessibility. What I really want is for my component layout to be ‘all parented by my plugin’ but for my accesibility grouping names to have identities which have an extra logical level.

I’m already writing my own accessibility handlers but it doesn’t seem like I can figure out how to make a fake parent.

So I guess my question is: how are people generating distinct accessibility groups for components which share the same component hierarchy parent. Does this make sense?

This should be possible with Component::setFocusContainerType() though for more complex UI relationships you might need to create a custom ComponentTraverser and override Component::createFocusTraverser(). Check out the docs for those methods for some more detailed explanations of how they work and interact with screen readers.

Also the AccessibilityDemo project in the DemoRunner has some grouping examples that might be a useful reference point.

Thanks! I’m already creating a focusTraverser to sort my items so I will look at both these API spots!

OK I went and re-read those APIs and alas am still stuck.

Here’s another way to think about my question after having read that code and seen how I use it. But here’s my rough problem.

setFocusContainerType lets me set a focus container type and I’m doing that. And that interacts well with my focusTraverser which sorts stuff properly. But AccesibilityHandler::getChildren() is both non-virtual and component based.

So kinda what I want is my component hierarchy to be

root
+- comp1
+- comp2
+- comp3
+- comp4

but my accessibility hierarchy to be

root
+--filter
    +-- comp2
    +-- comp4
+-- osc
    +-- comp1
    +-- comp3

the only way it seems to introduce those layers in accessibility space is to introduce components. I just don’t see how the focus traverser can add a non-component ‘layer’ (getNext/getPrevious/getAll are all juce::Component based and as I mentioned, juce::AccessibilityHandler::getChildren is non virtual so I can’t override it)

But if my screen layout has non-distinct rectangles compntaining comp 1, 2, 3 and 4 then I get overlapping fake parents. Which is what I ended up coding, but again, then I keep stumbling into having to add things like getParent()->toFront() here and there when you do things like drag and drop or popup windows.

So what I really want os something like my top level component accessibility handler to be able to say that its children are a set of accessibility groups which themselves contain the components. Right now I have to do that with components.

And when I do that I get z-order bugs which I’ve been able to fix. but am stumped.

Am I just missing the obvious spot?

Thanks so much as always for getting back to me.

The accessibility hierarchy is coupled to the component hierarchy by design since you generally want to present an accessible UI to screen readers that closely maps the on-screen UI. This was something that came up in speaking to users of accessibility frameworks as they found it difficult when the UIs diverged significantly as it was then hard to communicate with users of the on-screen UI, for example if someone was explaining where a particular plug-in parameter control was located.

I think the way to go here is with what you have described so far, with “container” components that group the controls. Something like this:

class MainComponent  : public juce::Component
{
public:
    //==============================================================================
    MainComponent()
    {
        group1.setTitle ("Group 1");
        group2.setTitle ("Group 2");
        
        group1.setFocusContainerType (FocusContainerType::focusContainer);
        group2.setFocusContainerType (FocusContainerType::focusContainer);
        
        addAndMakeVisible (group1);
        addAndMakeVisible (group2);
        
        auto& random = juce::Random::getSystemRandom();
        constexpr auto width = 600, height = 400;
        
        const auto addSliders = [&] (std::array<juce::Slider, 5>& sliders,
                                     Component& parentGroup)
        {
            constexpr auto sliderWidth = 150, sliderHeight = 35;
            
            for (auto& s : sliders)
            {
                parentGroup.addAndMakeVisible (s);
                
                s.setBounds (random.nextInt (juce::jmax (0, width - sliderWidth)),
                             random.nextInt (juce::jmax (0, height - sliderHeight)),
                             sliderWidth,
                             sliderHeight);
            }
        };
        
        addSliders (sliderGroup1, group1);
        addSliders (sliderGroup2, group2);
    
        setSize (width, height);
    }
    
    //==============================================================================
    void paint (juce::Graphics& g) override
    {
        g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
    }
    
    void resized() override
    {
        group1.setBounds (getLocalBounds());
        group2.setBounds (getLocalBounds());
    }

private:
    //==============================================================================
    Component group1, group2;
    std::array<juce::Slider, 5> sliderGroup1, sliderGroup2;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

In this case “Group 1” and “Group 2” are seen as top-level siblings of the main window and each contains a set of sliders that are positioned randomly on the screen but are navigable as a logical group by the screen reader.

Can you use explicit z-ordering to fix the issues you are having? Or by using Component::setExplicitFocusOrder()?

Ha yeah that’s basically exactly what I’ve done. Basically my previously single component with 120’elements is now 9 components with 14 each.

And then I need to use Zorder apis exactky as you say

Ok you’ve answered my question perfectly! Basically make real component grouping layers which overlap and get the z order right -‘d there’s isn’t a secret other way :slight_smile:

Thank you so much. This is such a great feature and I can’t wait to ship surge with it.

1 Like

I’m still a bit confused as to what methods to use for my app. I have several tabs, each containing lots of controls. Obviously I don’t want the user to have to go through all controls of tab 1 before moving to tab 2. So I need the swipe left/right to move along tabs only.

The selection (click) of a tab would then open that tab and reveal its controls, with the focus being set on the first one. Swiping would move along all controls in the tab.

I can either add a back button to them, or just include the tab itself for returning to “tab mode”.

Could you please suggest what would be the best/easiest way of implementing this?

If anyone interested, I managed to achieve this using simple setAccessible(bool) method of my tabs and panes components alternatively.

Notifying changes with notifyAccessibilityEvent(juce::AccessibilityEvent::structureChanged);

And grabbing focus with component->getAccessibilityHandler()->grabFocus();

3 Likes