Search code examples
javaswingjtableaccessibilityvoiceover

Why is my JTable always reported as empty using VoiceOver on OS X?


VoiceOver on OSX 10.10.4 (Yosemite), using JRE 1.7.0_75 and JRE 1.8.0_45, reports the following table as "empty".

package stackoverflow.examples.jtable;

import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.SwingUtilities;

public class TableDemo extends JFrame
{
    private static final long serialVersionUID = 1L;

    public TableDemo()
    {
        super("Accessible JTable?");

        final String[] columnNames = 
            {
                "First Name",
                "Last Name",
                "Sport",
                "# of Years",
                "Vegetarian"
            };
        final Object[][] data = 
            {
                {"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)},
                {"John", "Doe", "Rowing", new Integer(3), new Boolean(true)},
            };

        final JTable jTable = new JTable(data, columnNames);
        jTable.getAccessibleContext().setAccessibleName("data table");
        System.out.println("rows: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleRowCount());
        System.out.println("cols: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleColumnCount());
        System.out.println("java: " + System.getProperty("java.version"));
        jTable.setOpaque(true);
        setContentPane(jTable);
    }

    private static void createAndShowGUI() 
    {
        final TableDemo frame = new TableDemo();
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
}

Apart from VoiceOver saying the table is empty, everything else seems OK:

What am I missing?

Further info:

  • I've tried using JRE 1.6.0_65 which was the Apple supplied JRE and I get the same issue. I tried this in case it was something that changed when moving to the Oracle supplied JRE.

Solution

  • The reason why VoiceOver says "empty" is because the accessibility hierarchy is not being exposed correctly. You can use the Accessibility Inspector tool (one of the Xcode developer tools) to examine the accessibility hierarchy. In this case, with the Accessibility Inspector tool running, hovering the mouse pointer over the JTable shows that there is an AXTable element with 10 AXStaticText children (one for each of the cells). Tables should be exposed as AXTable > AXRow > AXCell > … . Also, according to the Roles reference, an AXTable element should have a Rows attribute among other required attributes, but these have not been exposed to the accessibility hierarchy by the JRE.

    I have tried out your program on Windows 8.1 Pro using Java 1.8.0_51 and I see a similar problem. Similar to the Accessibility Inspector tool, the Windows SDK comes with an Inspect tool that can be used to inspect accessibility data. When running your test case, it appears that the JTable has not been exposed at all. Enabling Windows Narrator, I am unable to navigate to the table or its cells.

    So, it appears that the JRE does not fully support table accessibility.

    In the source of javax.accessibility.AccessibleRole, you can see that the code to define a ROW constant is commented out along with other constants which are documented as "under consideration for potential future use".

    In the source of JTable, you can see that AccessibleJTable, AccessibleTableHeader, AccessibleJTableCell, and AccessibleJTableHeaderCell helper classes are defined, but there isn't an Accessible implementation for the rows of the table.

    In theory you could write a custom AccessibleContext implementation that would operate to expose a more complete accessibility hierarchy for the JTable to the OS. However, I am not sure whether it is possible to completely work around Java's apparent lack of support for table accessibility.

    Whether this is possible might depend on the platform. For example, examining the source code of src/macosx/native/sun/awt/JavaAccessibilityUtilities.m, you can see how Java's accessibility roles are mapped to the NSAccessibility*Role constants. You can see that the ROW_HEADER accessible role is mapped to AXRow. Thus, you could expose AXRow children of the AXTable by using the ROW_HEADER accessible role. Here is some code which succeeds in doing this:

    public static class MyJTable extends JTable {
        public MyJTable(TableModel tm) {
            super(tm);
        }
    
        @Override
        public MyAccessibleJTable getAccessibleContext() {
            if (accessibleContext == null) {
                accessibleContext = new MyAccessibleJTable();
            }
            return (MyAccessibleJTable)accessibleContext;
        }
    
        protected class MyAccessibleJTable extends AccessibleJTable {
    
            @Override
            public int getAccessibleChildrenCount() {
                if (MyJTable.this.getColumnCount() <= 0) {
                    return 0;
                }
                return MyJTable.this.getRowCount();
            }
    
            @Override
            public Accessible getAccessibleChild(int i) {
                if (i < 0 || getAccessibleChildrenCount() <= i) {
                    return null;
                }
                TableColumn firstColumn = getColumnModel().getColumn(0);
                TableCellRenderer renderer = firstColumn.getCellRenderer();
                if (renderer == null) {
                    Class<?> columnClass = getColumnClass(0);
                    renderer = getDefaultRenderer(columnClass);
                }
                Component component = renderer.getTableCellRendererComponent(MyJTable.this, null, false, false, i, 0);
                return new MyAccessibleRow(MyJTable.this, i, component);
            }
        }
    
        protected static class MyAccessibleRow extends AccessibleContext implements Accessible {
            private MyJTable table;
            private int row;
            private Component rendererComponent;
    
            protected MyAccessibleRow(MyJTable table, int row, Component renderComponent) {
                this.table = table;
                this.row = row;
                this.rendererComponent = rendererComponent;
            }
    
            @Override
            public AccessibleRole getAccessibleRole() {
                // ROW_HEADER is used because it maps to NSAccessibilityRowRole
                // on Mac.
                return AccessibleRole.ROW_HEADER;
            }
    
            @Override
            public Locale getLocale() {
                AccessibleContext ac = rendererComponent.getAccessibleContext();
                if (ac != null) {
                    return ac.getLocale();
                } else {
                    return null;
                }
            }
    
            @Override
            public int getAccessibleChildrenCount() {
                return 0; // TODO return the number of columns in this row
            }
            @Override
            public Accessible getAccessibleChild(int i) {
                return null; // TODO return a MyAccessibleJTableCell
            }
            @Override
            public int getAccessibleIndexInParent() {
                return row;
            }
            @Override
            public AccessibleStateSet getAccessibleStateSet() {
                return null; // TODO
            }
            @Override
            public AccessibleContext getAccessibleContext() {
                return this; // TODO
            }
            @Override
            public AccessibleComponent getAccessibleComponent() {
                return table.getAccessibleContext(); // TODO
            }
        }
    }
    

    As you can see from this screenshot:

    Screenshot showing the sample code with Accessibility Inspector running

    .. there are now two AXRow children on the AXTable. However, VoiceOver still announces the table as "empty". I am not sure whether this is because the rows do not have AXCell children, or because the AXTable is missing its required attributes, or some other reason.

    If you were to go the custom AccessibleContext route, it would probably be better to completely avoid trying to expose a table hierarchy. Instead, you could model the table as a list, where each row corresponds to a list item, and each list item contains a group for each of the cells. This is similar to the approach used by Firefox (tested version 39.0). Currently on Mac, Firefox does not use the table role when exposing an HTML table. This should be fixed in Firefox 41, though. See Bug 744790 - [Mac] HTML table semantics are not communicated to VoiceOver at all.

    I am also using Mac OS 10.10.4 and Java 1.8.0_51.

    EDIT: The "empty" table issue has already been reported as OpenJDK Bug JDK-7124284 [macosx] Nothing heard from VoiceOver when navigating in a table. From the comments, there are several known issues with Mac Swing accessibility that are currently deferred to JDK 9.

    Another possible work around is to use the JavaFX TableView class. Trying out the TableView sample from http://docs.oracle.com/javafx/2/ui_controls/table-view.htm I am seeing that VoiceOver is properly announcing the table. JavaFX accessibility was implemented as part of JEP 204 which was implemented in Java 8u40.