JUCE Accessibility on `develop`

Hi

OK first: this is fantastic. Thank you. I’ve been porting Surge to JUCE and, by using develop tonight and adding a couple of lines here and there, was able to get proper names on all our sliders. Still loads work to do but this is really wonderful. Thank you for doing it.

A question about juce dev. Is there some ifdef guard I can use to protect code which requires develop branch vs a release? So something like

#if JUCE_VERSION_IS_DEVELOP
// do acc stuff here while I wait for 6.1 to ship but don’t update the other devs yet
#endif

so I can do an opt-in JUCE develop? I see develop at head defines JUCE_VERSION at 0x60008 (and presumably will be 0x610000 when 6.1 ships or something) but is there a way to distinguish dev from 6.0.8 in my C++? Sorry for the noob question but google didn’t help!

I have an issue with multiple notifications of new window being received when I open 1 new window.

Here is how to reproduce on DemoRunner: If I modify WindowsDemo.h this way:

void showAllWindows()
{
    closeAllWindows();

    showDocumentWindow (false);
    //showDocumentWindow (true);
    //showTransparentWindow();
    //showDialogWindow();
}


void showDocumentWindow (bool native)
{
  auto* dw = new DocumentWindow ("Document Window", getRandomBrightColour(), DocumentWindow::allButtons, false);
    windows.add (dw);
    (...)
    dw->setUsingNativeTitleBar (native);

    dw->addToDesktop(dw->getDesktopWindowStyleFlags());
    dw->setVisible (true);
}

So that only one windows is opened , and the addToDesktop arg of DocumentWindow contructor is false, and we call explicitely addToDesktop() before setVisible(true).

Then VoiceOver seems to receive 4 notifications of new window (instead of one). Sometimes it will read “DemoRun Demorunner has new windows” , but most often it will read “DemoRunner has new window DemoRunner has new window DemoRunner has new window DemoRunner has new window”

You’re not the first to wish there were, but unfortunately there’s no agreed upon solution so far. Some possible approaches have been discussed here, but (IIRC) without reaching a solution: JUCE_VERSION master and develop

1 Like

Ok thanks! I can bodge together a flag in my cmake file for use accessibility then and default it off. Appreciate the answer

This change in ListBox
listbox focus
breaks the behaviour at least for single selection: moving with arrow up/down doesn’t select anything with Narrator on.

The TextEditor issues should now be fixed on develop:

It is now also possible to provide read-only text > 1000 characters.

1 Like

With this change on develop, if the overlay is modal (using Component::enterModalState()) it’ll grab the focus and it won’t be possible to move the screen reader focus to blocked components. We’ve updated the Projucer’s log-in form to use this behaviour so you can see how it works there:

1 Like

This should be fixed on develop now. We rely on features only available on 10.10+ for the accessibility support on macOS so have disabled it for lower deployment targets:

1 Like

This has been fixed with the following commit:

1 Like

Thanks for reporting the regression, I’ve reverted that change here:

2 Likes

FWIW, It fails to build with 10.12 SDK

something related to NSAccessibilityRole in getAccessibilityRole and getAccessibilitySubrole

It probably need some 10.13 SDK ifdef check

What SDK/deployment target settings are you using? The NSAccessibilityRole and NSAccessibilityNotificationName errors should be fixed with the usings at the top of the macOS accessibility code:

10.12 SDK with 10.9 deployment target

It doesn’t seem so. A read-only TextEditor with more than 1000 characters is still not read completely by Narrator. This seems to be a restriction -I’ve not seen it documented in this case, but there are many string parameters in Windows that are limited to 1000 characters.

My use case is simple: an about window with three screens of text. The last working implementation is now broken. As I can’t place the whole text in the title, the title is “About ***, page # of 3”. When the page is changed I call setTitle() and invalidateAccessibilityHandler(). This change

makes Narrator get stuck on the previous page and not report the new title or update the cursor, so now I have to explicitly getAccessibilityHandler()->grabFocus() after invalidation. The title, though, is not read at all now, because nameIsAccessibilityValue() returns true for staticText, so Narrator says “text” and that’s it.

I’m making the accessibility handler with

return std::make_unique<juce::AccessibilityHandler> (*this,
    juce::AccessibilityRole::staticText,
    juce::AccessibilityActions(),
    juce::AccessibilityHandler::Interfaces (std::make_unique<DummyValueInterface>(),
                                            std::make_unique<TextInterface> (*this),
                                            nullptr, nullptr));

where DummyValueInterface only exists for

bool isReadOnly() const override { return true; }

to avoid Narrator saying “editing” on read-only text. This is still needed. The only thing missing, if anything, was a way to indicate through AccessibilityTextInterface that this is read-only text. If the title can’t hold the whole text, as it seems to be the case, then it should be the element name as before.

Do you need a custom AccessibilityHandler? If you are just displaying some text then using the provided TextEditor or Label handlers should be sufficient and will handle the case of text > 1000 characters.

Thanks, I see what’s going on now. I’ll push a fix to develop shortly.

Well, a Label can’t be multiline. But TextEditor is not handling the case of > 1000 characters.

struct MyAudioProcessorEditor : juce::AudioProcessorEditor
{
    juce::TextEditor text;

    MyAudioProcessorEditor (MyAudioProcessor& p) : AudioProcessorEditor{ p }
    {
        text.setMultiLine (true);
        text.setReadOnly (true);
        text.setText ("You must know, then, that the above-named gentleman whenever he was at leisure (which was mostly all the year round) gave himself up to reading books of chivalry with such ardour and avidity that he almost entirely neglected the pursuit of his field-sports, and even the management of his property; and to such a pitch did his eagerness and infatuation go that he sold many an acre of tillageland to buy books of chivalry to read, and brought home as many of them as he could get. But of all there were none he liked so well as those of the famous Feliciano de Silva's composition, for their lucidity of style and complicated conceits were as pearls in his sight, particularly when in his reading he came upon courtships and cartels, where he often found passages like the reason of the unreason with which my reason is afflicted so weakens my reason that with reason I murmur at your beauty; or again, the high heavens, that of your divinity divinely fortify you with the stars, render you deserving of the desert your greatness deserves. Over conceits of this sort the poor gentleman lost his wits, and used to lie awake striving to understand them and worm the meaning out of them; what Aristotle himself could not have made out or extracted had he come to life again for that special purpose. He was not at all easy about the wounds which Don Belianis gave and took, because it seemed to him that, great as were the surgeons who had cured him, he must have had his face and body covered all over with seams and scars. He commended, however, the author's way of ending his book with the promise of that interminable adventure, and many a time was he tempted to take up his pen and finish it properly as is there proposed, which no doubt he would have done, and made a successful piece of work of it too, had not greater and more absorbing thoughts prevented him.");
        addAndMakeVisible (text);
        setSize (400, 400);
    }

    void resized() override
    {
        text.setBounds (getLocalBounds());
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyAudioProcessorEditor)
};

This is read up to “deserving” -exactly 1000 characters.

Ok, this should now be fixed on develop in 963fd79. If you look at how the TextEditor handler is implemented it you’ll see it provides an AccessibilityTextValueInterface for its read-only text which will now allow text > 1000 characters to be read out on Windows.

1 Like
std::unique_ptr<juce::AccessibilityHandler> MarkdownViewerComponent::createAccessibilityHandler ()
{
	class MarkdownTextAccessibilityHandler  : public juce::AccessibilityHandler
	{
	public:
		MarkdownTextAccessibilityHandler (MarkdownViewerComponent& textToWrap)
			: AccessibilityHandler (textToWrap, juce::AccessibilityRole::staticText, {}, makeInterfaces (textToWrap))
		{
		}

		static AccessibilityHandler::Interfaces makeInterfaces (MarkdownViewerComponent& viewer)
		{
			return { std::make_unique<MarkdownValueInterface> (viewer) };
		}

	private:
		class MarkdownValueInterface  : public juce::AccessibilityTextValueInterface
		{
		public:
			explicit MarkdownValueInterface (MarkdownViewerComponent& markdownToWrap)
				: markdown (markdownToWrap)
			{
			}

			bool isReadOnly() const override                 { return true; }
			juce::String getCurrentValueAsString() const override  
			{ 
				if ( auto& item = markdown.content.markdownItem )
				{
					return item->getAsPlainText ();
				}
				return {};			}
			void setValueAsString (const juce::String&) override   {}

		private:
			MarkdownViewerComponent& markdown;
		};
	};

	return std::make_unique<MarkdownTextAccessibilityHandler> (*this);
}
//----------------------------------------------------------------------------------

This is what I’ve come up with. But it just reads the title, how do I get Narrator to read all the text?

Fantastic, thanks Ed. It’s working now.

One more little thing. In enterModalState():

if (shouldTakeKeyboardFocus)
    grabKeyboardFocus();

if (auto* handler = getAccessibilityHandler())
    notifyAccessibilityEventInternal (*handler, InternalAccessibilityEvent::focusChanged);

the last two lines seem to mess with automation focus. If the modal is a focus container, it gets the focus even if it doesn’t want keyboard focus. If it’s not a focus container, the focus gets stuck on the previous component (say, a button that triggered the modal). grabKeyboardFocus() already notifies the handler of the default component, which seems like the right thing to do. If I delete those two lines, it behaves as expected.

Narrator doesn’t inmediately read a text, you have to tell it with down arrow (“jump to next text or item”). If you press caps lock + F1 with Narrator active, it will show a list of all commands.