Rasmus Faber
Rasmus Faber

Reputation: 49677

JEditorPane, JScrollPane and accessibility

I have a simple Swing application with a JEditorPane wrapped in a JScrollPane.

Unfortunately screen reader software like JAWS or NVDA does not behave correctly.

When focus enters the JEditorPane it only reads the accessible name followed by "text" and then stops, when the expected behavior is to continue reading the contents of the JEditorPane.

If I do not wrap the JEditorPane in the JScrollPane it works as expected.

I have tried inspecting the accessible tree using Monkey, but I cannot see any relevant difference between a JEditorPane wrapped in a JScrollPane and one that is not wrapped.

Any ideas?

Here is a brief sample that demonstrates the problem. If focus enters the first JEditorPane, JAWS reads "first editorpane - edit". If focus enters the second JEditorPane, JAWS reads "second editorpane - edit - bar".

public final class SmallExample {
  public static void main(String... aArgs){
    JFrame frame = new JFrame("Test Frame"); 

    JPanel panel = new JPanel();

    JEditorPane editorPane1 = new JEditorPane();
    editorPane1.setText("Foo");
    editorPane1.getAccessibleContext().setAccessibleName("first editorpane");
    editorPane1.getAccessibleContext().setAccessibleDescription("");

    JScrollPane scrollPane = new JScrollPane( editorPane1, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
    panel.add(scrollPane);

    JEditorPane editorPane2 = new JEditorPane();
    panel.add(editorPane2);
    editorPane2.setText("Bar");
    editorPane2.getAccessibleContext().setAccessibleName("second editorpane");
    editorPane2.getAccessibleContext().setAccessibleDescription("");    

    frame.getContentPane().add(panel);      

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
}

Upvotes: 2

Views: 1082

Answers (1)

Rasmus Faber
Rasmus Faber

Reputation: 49677

I found a workaround myself:

If I

  1. modify the accessible tree to skip the JScrollPane and JViewPort
  2. avoid sending accessible property-changed events from the JEditorPane

then it works.

I would still very much appreciate any insight into why this works (I found the workaround by replacing the AccessibleContext for editorPane2 with the one for editorPane1 and gradually switching the methods back until I found the ones that I needed to override).

Here is a working example (it is not so brief anymore):

public final class Example {

  public static void main(String... aArgs){
    JFrame frame = new JFrame("Test Frame"); 

    final JPanel panel = new JPanel(){
        public AccessibleContext getAccessibleContext() {
            if(accessibleContext==null){
                accessibleContext = new AccessibleContextWrapper(super.getAccessibleContext()){
                    public Accessible getAccessibleChild(int i) {
                        Accessible accessibleChild = super.getAccessibleChild(i);
                        while(accessibleChild!=null && (accessibleChild instanceof JScrollPane || accessibleChild instanceof JViewport)){
                            accessibleChild = accessibleChild.getAccessibleContext().getAccessibleChild(0);
                        }
                        return accessibleChild;
                    }
                };
            }
            return accessibleContext;
        }
    };

    final JEditorPane editorPane = new JEditorPane(){
        public AccessibleContext getSuperAccessibleContext() {
            return super.getAccessibleContext();
        }
        @Override
        public AccessibleContext getAccessibleContext() {
            return new AccessibleContextWrapper(super.getAccessibleContext()){
                public Accessible getAccessibleParent() {
                    Accessible parent = super.getAccessibleParent();
                    while(parent!=null && (parent instanceof JScrollPane || parent instanceof JViewport)){
                        parent = parent.getAccessibleContext().getAccessibleParent();
                    }
                    return parent;
                }

                public int getAccessibleIndexInParent() {
                    int res = super.getAccessibleIndexInParent();                           
                    Accessible parent = super.getAccessibleParent();
                    while(parent!=null && (parent instanceof JScrollPane || parent instanceof JViewport)){
                        res = parent.getAccessibleContext().getAccessibleIndexInParent();
                        parent = parent.getAccessibleContext().getAccessibleParent();
                    }
                    return res;
                }

                public void addPropertyChangeListener(
                        PropertyChangeListener listener) {                  
                }

                public void removePropertyChangeListener(
                        PropertyChangeListener listener) {                  
                }           
            };
        }       
    };
    editorPane.setText("Foo");
    editorPane.getAccessibleContext().setAccessibleName("first editorpane");
    editorPane.getAccessibleContext().setAccessibleDescription("");
    editorPane.getAccessibleContext();
    JScrollPane scrollPane = new JScrollPane( editorPane, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );  

    panel.add(scrollPane);

    frame.getContentPane().add(panel);      

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }

  public static class AccessibleContextWrapper extends AccessibleContext {
    private final AccessibleContext inner;

    public AccessibleContextWrapper(AccessibleContext inner) {
        this.inner = inner;
    }

    public String getAccessibleName() {
        return inner.getAccessibleName();
    }

    public void setAccessibleName(String s) {
        inner.setAccessibleName(s);
    }

    public String getAccessibleDescription() {
        return inner.getAccessibleDescription();
    }

    public void setAccessibleDescription(String s) {
        inner.setAccessibleDescription(s);
    }

    public AccessibleRole getAccessibleRole() {
        return inner.getAccessibleRole();
    }

    public AccessibleStateSet getAccessibleStateSet() {
        return inner.getAccessibleStateSet();
    }

    public Accessible getAccessibleParent() {
        return inner.getAccessibleParent();
    }

    public void setAccessibleParent(Accessible a) {
        inner.setAccessibleParent(a);
    }

    public int getAccessibleIndexInParent() {
        return inner.getAccessibleIndexInParent();
    }

    public int getAccessibleChildrenCount() {
        return inner.getAccessibleChildrenCount();
    }

    public Accessible getAccessibleChild(int i) {
        return inner.getAccessibleChild(i);
    }

    public Locale getLocale() throws IllegalComponentStateException {
        return inner.getLocale();
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        inner.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        inner.removePropertyChangeListener(listener);
    }

    public AccessibleAction getAccessibleAction() {
        return inner.getAccessibleAction();
    }

    public AccessibleComponent getAccessibleComponent() {
        return inner.getAccessibleComponent();
    }

    public AccessibleSelection getAccessibleSelection() {
        return inner.getAccessibleSelection();
    }

    public AccessibleText getAccessibleText() {
        return inner.getAccessibleText();
    }

    public AccessibleEditableText getAccessibleEditableText() {
        return inner.getAccessibleEditableText();
    }

    public AccessibleValue getAccessibleValue() {
        return inner.getAccessibleValue();
    }

    public AccessibleIcon[] getAccessibleIcon() {
        return inner.getAccessibleIcon();
    }

    public AccessibleRelationSet getAccessibleRelationSet() {
        return inner.getAccessibleRelationSet();
    }

    public AccessibleTable getAccessibleTable() {
        return inner.getAccessibleTable();
    }

    public void firePropertyChange(String propertyName, Object oldValue,
            Object newValue) {
        inner.firePropertyChange(propertyName, oldValue, newValue);
    }
  }

}

Upvotes: 2

Related Questions