Bug 341685 - Accessibility: JAWS should read description of graphics, and column headers in tables and trees.
Summary: Accessibility: JAWS should read description of graphics, and column headers i...
Status: RESOLVED FIXED
Alias: None
Product: MAT
Classification: Tools
Component: GUI (show other bugs)
Version: unspecified   Edit
Hardware: PC Windows XP
: P3 minor (vote)
Target Milestone: ---   Edit
Assignee: Andrew Johnson CLA
QA Contact:
URL:
Whiteboard:
Keywords: accessibility
Depends on:
Blocks: 300655
  Show dependency tree
 
Reported: 2011-04-01 19:07 EDT by Jonathan Lawrence CLA
Modified: 2014-01-31 08:01 EST (History)
2 users (show)

See Also:


Attachments
Patch enabling JAWS to read graphics and table/tree items better. (27.09 KB, patch)
2011-04-01 19:18 EDT, Jonathan Lawrence CLA
no flags Details | Diff
Revised fix for this bug, includes NLS support for icon labels. (50.46 KB, patch)
2011-04-17 10:10 EDT, Jonathan Lawrence CLA
andrew_johnson: iplog+
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Jonathan Lawrence CLA 2011-04-01 19:07:52 EDT
Build Identifier: 3.6.1

When using MAT with a screen reader (i.e. JAWS), icons are read as "Graphic XXXX".
In tables, the whole row is read with no indication of the meaning of the column contents. In Trees, only the content of the first column is read.

Reproducible: Always

Steps to Reproduce:
1. Start Memory Analyzer on Windows with JAWS
2. Open Histogram view, listen to JAWS read the rows.
3. Open Dominator Tree view, listen to JAWS read the items.
Comment 1 Jonathan Lawrence CLA 2011-04-01 19:18:10 EDT
Created attachment 192401 [details]
Patch enabling JAWS to read graphics and table/tree items better.
Comment 2 Jonathan Lawrence CLA 2011-04-01 19:49:06 EDT
The attached patch contains a partial fix to the reported issues:
1) Modifications to MemoryAnalyzerPlugin.java to maintain descriptive text for Images obtained via MemoryAnalyzerPlugin.java. Currently this text is derived from the file name for the Image, but should be obtained from an NLS-enabled message file.
2) A new class AccessibleCompositeAdapter which allows an AccessibleListener to be added to any Eclipse SWT Table or Tree to return an accessible name to the screen reader with descriptive text for any Images, and column headings describing the content.
3) Addition of the above Accessible decoration to several of the most important tables and views in the MAT UI.

TODO
1) The descriptions for the Images need to be created in an NLS enabled properties file, and loaded according to the language.
2) More bug fixes, as some graphics in the UI are still not read correctly , and some views are not read appropriately.
Comment 3 Pete Robbins CLA 2011-04-04 09:43:45 EDT
I've applied this to my workspace and I get table rows being read out twice, once with the graphic label and once without. I'll investigate.
Comment 4 Jonathan Lawrence CLA 2011-04-17 09:58:29 EDT
(In reply to comment #3)
> I've applied this to my workspace and I get table rows being read out twice,
> once with the graphic label and once without. I'll investigate.

Yes this is how JAWS seems to work with ListView windows.  I was not able to find a way to prevent JAWS from reading out the default content of each row in a table, so what happens now is that you get the customized text, then the original.  If anyone can tell me how to use the SWT Accessibility APIs in order to achieve a better result I will be more than grateful.

It works better with Tree views (Windows TreeViews), in which case JAWS reads out just the customized content for each row. 

These are issues which need to be raised both with the Eclipse SWT developers (to provide documentation for how the Accessibility APIs should be used by application developers, and by the accessibility clients), and with Freedom Scientific.
Comment 5 Jonathan Lawrence CLA 2011-04-17 10:09:19 EDT
New patch, includes mechanism to NLS enable the text which is read out by a screen reader to describe icons in the UI.

In order to NLS enable the text read out for an icon, the text needs to be held in a properties file, so that it can be translated.  This implies a separate property 
for each icon file.  It would be tedious, error prone, and result in excessive code to use a hard-coded key for each icon file.  Instead, the property key for an icon label is generated from it's path relative to its parent /icons directory.
The descriptive text for each icon is also generated automatically by an offline utility (main() method in MemoryAnalyzerPlugin), which scans the MAT icons directories and generates an "iconlabels.properties" file which is then used at runtime to retrieve the readable icon descriptions (labels) and is translatable.

At runtime, when an icon description is needed, the key is generated from the icon file's path, and then the label is retrieved from the relevant NLS enabled properties file.

The new patch is based on the current repository code version as of today's date, and therefore includes and supersedes the previous patch which has not yet been applied to the code base.  The previous version of the patch should be ignored.
Comment 6 Jonathan Lawrence CLA 2011-04-17 10:10:59 EDT
Created attachment 193433 [details]
Revised fix for this bug, includes NLS support for icon labels.

Described in previous comment.  Previous patch is now obsolete.
Comment 7 Andrew Johnson CLA 2011-04-28 12:18:27 EDT
I'm using JAWS 12.
I found this link: http://www.eclipse.org/swt/faq.php#tableheaderswithJAWS
This is a much more flexible way of controlling whether the column headers are read.
Therefore, I don't think we should add the column names ourselves.

There is also a Graphic Labeler option, which should convert a Graphic 252 into a name, but I don't know
how we could preset these.
Comment 8 Andrew Johnson CLA 2011-05-03 04:22:23 EDT
Running Memory Analyzer under the Eclipse debugger gives this exception:

Thread [main] (Suspended (exception NoClassDefFoundError))	
	ClassLoader.defineClassImpl(String, byte[], int, int, Object) line: not available [native method]	
	DefaultClassLoader(ClassLoader).defineClass(String, byte[], int, int, ProtectionDomain) line: 275	
	DefaultClassLoader.defineClass(String, byte[], ClasspathEntry, BundleEntry) line: 188	
	ClasspathManager.defineClass(String, byte[], ClasspathEntry, BundleEntry, ClassLoadingStatsHook[]) line: 580	
	ClasspathManager.findClassImpl(String, ClasspathEntry, ClassLoadingStatsHook[]) line: 550	
	ClasspathManager.findLocalClassImpl(String, ClassLoadingStatsHook[]) line: 481	
	ClasspathManager.findLocalClass_LockClassLoader(String, ClassLoadingStatsHook[]) line: 469	
	ClasspathManager.findLocalClass(String) line: 449	
	DefaultClassLoader.findLocalClass(String) line: 216	
	BundleLoader.findLocalClass(String) line: 393	
	BundleLoader.findClassInternal(String, boolean, ClassLoader) line: 469	
	BundleLoader.findClass(String, boolean) line: 422	
	BundleLoader.findClass(String) line: 410	
	DefaultClassLoader.loadClass(String, boolean) line: 107	
	DefaultClassLoader(ClassLoader).loadClass(String) line: 619	
	Class<T>.forNameImpl(String, boolean, ClassLoader) line: not available [native method]	
	Class<T>.forName(String, boolean, ClassLoader) line: 172	
	ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line: 468	
	ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line: 523	
	ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line: 523	
	ResourceBundle.getBundleImpl(String, Locale, ClassLoader) line: 362	
	ResourceBundle.getBundle(String) line: 104	
	IconLabels.<clinit>() line: 23	
	J9VMInternals.initializeImpl(Class) line: not available [native method]	
	J9VMInternals.initialize(Class) line: 200	
	MemoryAnalyserPlugin.getIconString(String) line: 290	
	MemoryAnalyserPlugin.getPluginImageDescriptor(String) line: 200	
	MemoryAnalyserPlugin.getImageDescriptor(String) line: 184	
	InspectorView.createSyncAction() line: 436	
	InspectorView.createPartControl(Composite) line: 367	
	ViewReference.createPartHelper() line: 375	
	ViewReference.createPart() line: 229	
	ViewReference(WorkbenchPartReference).getPart(boolean) line: 595	
	ViewPane(PartPane).setVisible(boolean) line: 313	
	ViewPane.setVisible(boolean) line: 529	
	PresentablePart.setVisible(boolean) line: 180	
	PresentablePartFolder.select(IPresentablePart) line: 270	
	LeftToRightTabOrder.select(IPresentablePart) line: 65	
	TabbedStackPresentation.selectPart(IPresentablePart) line: 473	
	ViewStack(PartStack).refreshPresentationSelection() line: 1254	
	ViewStack(PartStack).createControl(Composite, StackPresentation) line: 666	
	ViewStack(PartStack).createControl(Composite) line: 574	
	ViewSashContainer(PartSashContainer).createControl(Composite) line: 568	
	PerspectiveHelper.activate(Composite) line: 272	
	Perspective.onActivate() line: 981	
	WorkbenchPage.onActivate() line: 2632	
	WorkbenchWindow$27.run() line: 2986	
	BusyIndicator.showWhile(Display, Runnable) line: 70	
	WorkbenchWindow.setActivePage(IWorkbenchPage) line: 2967	
	WorkbenchWindow$21.runWithException() line: 2284	
	WorkbenchWindow$21(StartupThreading$StartupRunnable).run() line: 31	
	RunnableLock.run() line: 35	
	UISynchronizer(Synchronizer).runAsyncMessages(boolean) line: 134	
	Display.runAsyncMessages(boolean) line: 4041	
	Display.readAndDispatch() line: 3660	
	ApplicationWorkbenchAdvisor(WorkbenchAdvisor).openWindows() line: 803	
	Workbench$31.runWithException() line: 1566	
	Workbench$31(StartupThreading$StartupRunnable).run() line: 31	
	UISynchronizer(Synchronizer).syncExec(Runnable) line: 179	
	UISynchronizer.syncExec(Runnable) line: 150	
	Display.syncExec(Runnable) line: 4584	
	StartupThreading.runWithoutExceptions(StartupThreading$StartupRunnable) line: 94	
	Workbench.init() line: 1561	
	Workbench.runUI() line: 2556	
	Workbench.access$4(Workbench) line: 2427	
	Workbench$7.run() line: 670	
	Realm.runWithDefault(Realm, Runnable) line: 332	
	Workbench.createAndRunWorkbench(Display, WorkbenchAdvisor) line: 663	
	PlatformUI.createAndRunWorkbench(Display, WorkbenchAdvisor) line: 149	
	Application.start(IApplicationContext) line: 52	
	EclipseAppHandle.run(Object) line: 196	
	EclipseAppLauncher.runApplication(Object) line: 110	
	EclipseAppLauncher.start(Object) line: 79	
	EclipseStarter.run(Object) line: 369	
	EclipseStarter.run(String[], Runnable) line: 179	
	NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]	
	NativeMethodAccessorImpl.invoke(Object, Object[]) line: 48	
	DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25	
	Method.invoke(Object, Object...) line: 600	
	Main.invokeFramework(String[], URL[]) line: 619	
	Main.basicRun(String[]) line: 574	
	Main.run(String[]) line: 1407	
	Main.main(String[]) line: 1383	

The line causing it is:
    private static final String BUNDLE_NAME = "org.eclipse.mat.ui.iconlabels"; //$NON-NLS-1$
    private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);

Is this expected? I don't like to see exception thrown as part of normal behaviour as it makes debugging of problems harder.
Comment 9 Andrew Johnson CLA 2011-05-03 11:32:26 EDT
Bug 115947 suggests using display.findWidget(composite, e.childID); for Trees

Also see: give accessible names to a tree and its tree items
http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet291.java?view=co
Comment 10 Andrew Johnson CLA 2011-05-03 12:29:53 EDT
With JAWS 12 I can read a whole tree row using ctrl-shift-right arrow without this patch
Comment 11 Jonathan Lawrence CLA 2011-05-06 11:56:57 EDT
Yes, this is better. Thanks.  One odd thing about using this mechanism is that once the "Customize ListView (Headers)" dialog has been used for a given ListView which has column headers, JAWS 12 appears to stop reading the graphics altogether, and it does not seem to be possible to recover them even by reversing the applied customization.  This doesn't happen if the ListView does not have column headers, eg the Inspector view. Therefore if one wants anything to be read for the graphics in a ListView, which seems desirable, this will need to be done by another mechanism.

Regarding the Graphic Labeler, I could not get this to work in MAT. An error dialog is displayed stating that "The JAWS cursor is NOT positioned on a graphic".  It also does not seem to be possible to preset the contents, confirming the comment mentioned below.
(In reply to comment #7)
> I'm using JAWS 12.
> I found this link: http://www.eclipse.org/swt/faq.php#tableheaderswithJAWS
> This is a much more flexible way of controlling whether the column headers are
> read.
> Therefore, I don't think we should add the column names ourselves.
> 
> There is also a Graphic Labeler option, which should convert a Graphic 252 into
> a name, but I don't know
> how we could preset these.
Comment 12 Jonathan Lawrence CLA 2011-05-06 12:03:24 EDT
The Exception appears to be inherent in the implementation of ResourceBundle.getBundle().  Because of the trial-and-error search mechanism for the most specific (class or property) resource bundle matching the base name & locale, the mechanism appears to rely on throwing & catching Exceptions.
This is highly unfortunate as it appears to be the only place where internal exceptions are used on the mainline path of MAT.  An alternative would be to implement a custom ResourceBundle loader which avoided throwing Exceptions internally, or not to attempt to label the graphics at all.

(In reply to comment #8)
> Running Memory Analyzer under the Eclipse debugger gives this exception:
> 
> Thread [main] (Suspended (exception NoClassDefFoundError))    
>     ClassLoader.defineClassImpl(String, byte[], int, int, Object) line: not
> available [native method]    
>     DefaultClassLoader(ClassLoader).defineClass(String, byte[], int, int,
> ProtectionDomain) line: 275    
>     DefaultClassLoader.defineClass(String, byte[], ClasspathEntry, BundleEntry)
> line: 188    
>     ClasspathManager.defineClass(String, byte[], ClasspathEntry, BundleEntry,
> ClassLoadingStatsHook[]) line: 580    
>     ClasspathManager.findClassImpl(String, ClasspathEntry,
> ClassLoadingStatsHook[]) line: 550    
>     ClasspathManager.findLocalClassImpl(String, ClassLoadingStatsHook[]) line:
> 481    
>     ClasspathManager.findLocalClass_LockClassLoader(String,
> ClassLoadingStatsHook[]) line: 469    
>     ClasspathManager.findLocalClass(String) line: 449    
>     DefaultClassLoader.findLocalClass(String) line: 216    
>     BundleLoader.findLocalClass(String) line: 393    
>     BundleLoader.findClassInternal(String, boolean, ClassLoader) line: 469    
>     BundleLoader.findClass(String, boolean) line: 422    
>     BundleLoader.findClass(String) line: 410    
>     DefaultClassLoader.loadClass(String, boolean) line: 107    
>     DefaultClassLoader(ClassLoader).loadClass(String) line: 619    
>     Class<T>.forNameImpl(String, boolean, ClassLoader) line: not available
> [native method]    
>     Class<T>.forName(String, boolean, ClassLoader) line: 172    
>     ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line:
> 468    
>     ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line:
> 523    
>     ResourceBundle.handleGetBundle(String, String, boolean, ClassLoader) line:
> 523    
>     ResourceBundle.getBundleImpl(String, Locale, ClassLoader) line: 362    
>     ResourceBundle.getBundle(String) line: 104    
>     IconLabels.<clinit>() line: 23    
>     J9VMInternals.initializeImpl(Class) line: not available [native method]    
>     J9VMInternals.initialize(Class) line: 200    
>     MemoryAnalyserPlugin.getIconString(String) line: 290    
>     MemoryAnalyserPlugin.getPluginImageDescriptor(String) line: 200    
>     MemoryAnalyserPlugin.getImageDescriptor(String) line: 184    
>     InspectorView.createSyncAction() line: 436    
>     InspectorView.createPartControl(Composite) line: 367    
>     ViewReference.createPartHelper() line: 375    
>     ViewReference.createPart() line: 229    
>     ViewReference(WorkbenchPartReference).getPart(boolean) line: 595    
>     ViewPane(PartPane).setVisible(boolean) line: 313    
>     ViewPane.setVisible(boolean) line: 529    
>     PresentablePart.setVisible(boolean) line: 180    
>     PresentablePartFolder.select(IPresentablePart) line: 270    
>     LeftToRightTabOrder.select(IPresentablePart) line: 65    
>     TabbedStackPresentation.selectPart(IPresentablePart) line: 473    
>     ViewStack(PartStack).refreshPresentationSelection() line: 1254    
>     ViewStack(PartStack).createControl(Composite, StackPresentation) line: 666  
>     ViewStack(PartStack).createControl(Composite) line: 574    
>     ViewSashContainer(PartSashContainer).createControl(Composite) line: 568    
>     PerspectiveHelper.activate(Composite) line: 272    
>     Perspective.onActivate() line: 981    
>     WorkbenchPage.onActivate() line: 2632    
>     WorkbenchWindow$27.run() line: 2986    
>     BusyIndicator.showWhile(Display, Runnable) line: 70    
>     WorkbenchWindow.setActivePage(IWorkbenchPage) line: 2967    
>     WorkbenchWindow$21.runWithException() line: 2284    
>     WorkbenchWindow$21(StartupThreading$StartupRunnable).run() line: 31    
>     RunnableLock.run() line: 35    
>     UISynchronizer(Synchronizer).runAsyncMessages(boolean) line: 134    
>     Display.runAsyncMessages(boolean) line: 4041    
>     Display.readAndDispatch() line: 3660    
>     ApplicationWorkbenchAdvisor(WorkbenchAdvisor).openWindows() line: 803    
>     Workbench$31.runWithException() line: 1566    
>     Workbench$31(StartupThreading$StartupRunnable).run() line: 31    
>     UISynchronizer(Synchronizer).syncExec(Runnable) line: 179    
>     UISynchronizer.syncExec(Runnable) line: 150    
>     Display.syncExec(Runnable) line: 4584    
>     StartupThreading.runWithoutExceptions(StartupThreading$StartupRunnable)
> line: 94    
>     Workbench.init() line: 1561    
>     Workbench.runUI() line: 2556    
>     Workbench.access$4(Workbench) line: 2427    
>     Workbench$7.run() line: 670    
>     Realm.runWithDefault(Realm, Runnable) line: 332    
>     Workbench.createAndRunWorkbench(Display, WorkbenchAdvisor) line: 663    
>     PlatformUI.createAndRunWorkbench(Display, WorkbenchAdvisor) line: 149    
>     Application.start(IApplicationContext) line: 52    
>     EclipseAppHandle.run(Object) line: 196    
>     EclipseAppLauncher.runApplication(Object) line: 110    
>     EclipseAppLauncher.start(Object) line: 79    
>     EclipseStarter.run(Object) line: 369    
>     EclipseStarter.run(String[], Runnable) line: 179    
>     NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not
> available [native method]    
>     NativeMethodAccessorImpl.invoke(Object, Object[]) line: 48    
>     DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25    
>     Method.invoke(Object, Object...) line: 600    
>     Main.invokeFramework(String[], URL[]) line: 619    
>     Main.basicRun(String[]) line: 574    
>     Main.run(String[]) line: 1407    
>     Main.main(String[]) line: 1383    
> 
> The line causing it is:
>     private static final String BUNDLE_NAME = "org.eclipse.mat.ui.iconlabels";
> //$NON-NLS-1$
>     private static final ResourceBundle RESOURCE_BUNDLE =
> ResourceBundle.getBundle(BUNDLE_NAME);
> 
> Is this expected? I don't like to see exception thrown as part of normal
> behaviour as it makes debugging of problems harder.
Comment 13 Jonathan Lawrence CLA 2011-05-06 12:06:16 EDT
Yes, this could be an alternative.  It's not clear whether it's an improvement on the implementation in the patch which uses getItem() for the same purpose, and seems to be working satisfactorily.
(In reply to comment #9)
> Bug 115947 suggests using display.findWidget(composite, e.childID); for Trees
> 
> Also see: give accessible names to a tree and its tree items
> http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet291.java?view=co
Comment 14 Jonathan Lawrence CLA 2011-05-06 12:08:44 EDT
Thanks for pointing this out.  It does read the whole row, but without column headers and without the graphics.  There doesn't seem to be the equivalent of "Customize ListView" which works for TreeViews.
(In reply to comment #10)
> With JAWS 12 I can read a whole tree row using ctrl-shift-right arrow without
> this patch
Comment 15 Jonathan Lawrence CLA 2011-05-06 12:20:22 EDT
Proposal for changes to the fix, for further comment:
=====================================================

1) Remove the customized reading of ListView rows, except for the graphic label, so that "Customize ListView" can be used for the row content & column headers. Headers will still be read for empty cells, which was overridden in the original patch, however this seems acceptable.

2) Investigate whether there is a viable alternative to using ResourceBundle.getBundle() which does not rely on throwing & catching Exceptions internally, or implement one.  Raise a bug against ResourceBundle to correct this problem.

So with the above changes, the following aspects of the patch would (largely) remain:
1) The current mechanism for generating labels for the MAT graphics, and for reading them out in JAWS, for both ListView & TreeView.
2) The customized TreeView row reading which includes the column headers when a row is read out.
Comment 16 Andrew Johnson CLA 2011-05-09 05:11:45 EDT
(In reply to comment #12)
> The Exception appears to be inherent in the implementation of
> ResourceBundle.getBundle().  Because of the trial-and-error search mechanism for
> the most specific (class or property) resource bundle matching the base name &
> locale, the mechanism appears to rely on throwing & catching Exceptions.
> This is highly unfortunate as it appears to be the only place where internal
> exceptions are used on the mainline path of MAT.  An alternative would be to
> implement a custom ResourceBundle loader which avoided throwing Exceptions
> internally, or not to attempt to label the graphics at all.
> 
> (In reply to comment #8)
> > Running Memory Analyzer under the Eclipse debugger gives this exception:
> >
> > Thread [main] (Suspended (exception NoClassDefFoundError))
> >     ClassLoader.defineClassImpl(String, byte[], int, int, Object) line: not
> > available [native method]

> >     private static final ResourceBundle RESOURCE_BUNDLE =
> > ResourceBundle.getBundle(BUNDLE_NAME);
> >
> > Is this expected? I don't like to see exception thrown as part of normal
> > behaviour as it makes debugging of problems harder.


Strangely enough, the exception doesn't occur when reading the annotations.properties files in org.eclipse.mat.report.
Perhaps ResoruceBundle was attempting to read the file as a class file. The similarity of names of IconLabels.java and iconlabels.properties might be the reason - I tried renaming the properties file to icon_labels.properties and the problem went away. Perhaps you could check too.
Comment 17 Andrew Johnson CLA 2011-05-10 12:51:15 EDT
I've made changes to the patch so that for tables just the first column of the item and the graphic is read out for the name, and the whole row (and column names if required) can be read by JAWS .

For trees, the whole row including columns is given as the name and then is read out by JAWS.

I've also used findWidget for trees.

I've renamed iconlabels.properties to icon_labels.properties to avoid the NoClassDefFoundError.

I'll check in this version - please test.
Comment 18 Andrew Johnson CLA 2011-05-10 12:54:50 EDT
Changes applied.
Comment 19 Krum Tsvetkov CLA 2011-05-13 03:11:52 EDT
I am closing the bug now. In case further improvements are needed let us track them in separate messages.