Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Eclipse Projects » Standard Widget Toolkit (SWT) » Accessible custom control
Accessible custom control [message #1062254] Thu, 06 June 2013 14:12
David Steinberg is currently offline David Steinberg
Messages: 489
Registered: July 2009
Senior Member
Hi,

I'm having some trouble trying to do make a custom control accessible. I've seen examples involving a manually drawn control based on Canvas, but what I'm doing is a little bit different from that and, I had hoped, would be a little bit easier. I've just placed some labels in a composite, and I'm simulating focus over them by manually handling the relevant keyboard/mouse events, maintaining the focus state myself, and painting a focus indicator. Now I just want to enable accessibility by changing the role and state for the labels and sending the right events as the focus and selection change.

I've created a simple example to illustrate what I'm trying to do and the difficulties I'm having. It's based on an example that Carolyn MacLeod posted on platform-swt-dev back in 2004 called AccessibleShapes. Mine is called AccessibleLabels:

import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.AccessibleControlAdapter;
import org.eclipse.swt.accessibility.AccessibleControlEvent;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;

public class AccessibleLabels {
    static Display display;
    static Shell parentShell;
    static Shell shell;
    static Composite composite;
    static Label blueLabel;
    static Label redLabel;
    static Control focus;
    static boolean dialog = false; // true for dialog, false for plain shell
    
    public static void main(String[] args) {
        display = new Display();
        if (dialog) {
            parentShell = new Shell(display);
            shell = new Shell(parentShell);
        } else {
            shell = new Shell(display);
        }
        shell.setText(dialog ? "Dialog" : "Shell");
        shell.setLayout(new FillLayout());
        
        composite = new Composite(shell, SWT.NONE);
        
        GridLayout gridLayout = new GridLayout();
        gridLayout.marginWidth = 20;
        gridLayout.marginHeight = 20;
        gridLayout.verticalSpacing = 20;
        gridLayout.horizontalSpacing = 20;
        gridLayout.numColumns = 2;
        gridLayout.makeColumnsEqualWidth = true;
        composite.setLayout(gridLayout);
        
        blueLabel = new Label(composite, SWT.CENTER);
        blueLabel.setText("Blue");
        blueLabel.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
        blueLabel.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE));
        GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
        gridData.widthHint = 100;
        gridData.heightHint = 100;
        blueLabel.setLayoutData(gridData);
        
        redLabel = new Label(composite, SWT.CENTER);
        redLabel.setText("Red");
        redLabel.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
        redLabel.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
        gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
        gridData.widthHint = 100;
        gridData.heightHint = 100;
        redLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        
        PaintListener paintListener = new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
                if (e.widget == focus) {
                    Rectangle bounds = ((Control)e.widget).getBounds();
                    e.gc.drawFocus(0, 0, bounds.width, bounds.height);
                }
            }
        };
        blueLabel.addPaintListener(paintListener);
        redLabel.addPaintListener(paintListener);
        
        MouseListener mouseListener = new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                setFocus((Control)e.widget);
            }
        };
        blueLabel.addMouseListener(mouseListener);
        redLabel.addMouseListener(mouseListener);
        
        composite.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                updateFocus();
            }
        });
        
        composite.addTraverseListener(new TraverseListener() {
            @Override
            public void keyTraversed(TraverseEvent e) {
                switch (e.detail) {
                case SWT.TRAVERSE_TAB_NEXT:
                case SWT.TRAVERSE_TAB_PREVIOUS:
                    setFocus(focus == blueLabel ? redLabel : blueLabel);
                    e.doit = true;
                    break;
                }
            }
        });
        
        composite.getAccessible().addAccessibleControlListener(new AccessibleControlAdapter() {
            @Override
            public void getFocus(AccessibleControlEvent e) {
                if (composite.isFocusControl()) {
                    if (focus == null) {
                        e.childID = ACC.CHILDID_SELF;
                    } else {
                        e.accessible = focus.getAccessible();
                    }
                } else {
                    e.childID = ACC.CHILDID_NONE;
                }
            }
            
            @Override
            public void getSelection(AccessibleControlEvent e) {
                getFocus(e);
            }
        });
        
        blueLabel.getAccessible().addAccessibleControlListener(new AccessibleControlAdapter() {
            @Override
            public void getRole(AccessibleControlEvent e) {
                if (e.childID == ACC.CHILDID_SELF) {
                    e.detail = ACC.ROLE_RADIOBUTTON;
                }
            }
            
            @Override
            public void getState(AccessibleControlEvent e) {
                if (e.childID == ACC.CHILDID_SELF) {
                    e.detail = ACC.STATE_FOCUSABLE | ACC.STATE_SELECTABLE;
                    if (composite.isFocusControl() && blueLabel == focus) {
                        e.detail |= ACC.STATE_FOCUSED | ACC.STATE_SELECTED | ACC.STATE_CHECKED;
                    }
                }
            }
        });
        
        redLabel.getAccessible().addAccessibleControlListener(new AccessibleControlAdapter() {
            @Override
            public void getRole(AccessibleControlEvent e) {
                if (e.childID == ACC.CHILDID_SELF) {
                    e.detail = ACC.ROLE_RADIOBUTTON;
                }
            }
            
            @Override
            public void getState(AccessibleControlEvent e) {
                if (e.childID == ACC.CHILDID_SELF) {
                    e.detail = ACC.STATE_FOCUSABLE | ACC.STATE_SELECTABLE;
                    if (composite.isFocusControl() && redLabel == focus) {
                        e.detail |= ACC.STATE_FOCUSED | ACC.STATE_SELECTED | ACC.STATE_CHECKED;
                    }
                }
            }
        });
        
        shell.pack();
        shell.open();
        if (dialog) {
            parentShell.pack();
            parentShell.open();
            shell.setFocus();
            shell = parentShell;
        }
        composite.setFocus();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
    }
    
    static void setFocus(Control newFocus) {
        focus = newFocus;
        redraw();
        updateFocus();
    }
    
    static void updateFocus() {
        if (focus == null) {
            composite.getAccessible().setFocus(ACC.CHILDID_SELF);
        } else {
//            composite.getAccessible().setFocus(???);
        }
    }
    
    static void redraw() {
        composite.redraw();
        blueLabel.redraw();
        redLabel.redraw();
    }
}


So you can see, I'm trying to rely on the Composite to know what it's children, the Labels, are and where they are and such.

Is this a reasonable thing to even try to do? I've been totally unable to find any examples of this approach, but it seems fairly natural to me.

There's just one obvious hole: I can't figure out how I can send the right focus change event. Notice the commented out line in updateFocus(). I'm not assigning child IDs myself, and I can't find a way to access whatever ID is being used internally (if there even is one). It seems to me there should be such a way, or there should be an alternative form of setFocus() that takes, perhaps, the Accessible of the focused child. Am I missing something here?

That idea comes from something analogous above. Notice how in the AccessibleControlListener for the composite, I'm setting the AccessibleControlEvent.accessible to the Accessible of the child Control that I consider focused. Is this correct? The Javadoc for AccessibleControlListener suggests it is, but again, I haven't found an example of that anywhere.

That brings me to my last problem: This doesn't work at all. If I have Insepct listening, it crashes immediately with an NPE:

Exception in thread "main" java.lang.NullPointerException
	at org.eclipse.swt.accessibility.Accessible.Skip(Accessible.java:2776)
	at org.eclipse.swt.accessibility.Accessible$12.method4(Accessible.java:504)
	at org.eclipse.swt.internal.ole.win32.COMObject.callback4(COMObject.java:101)
	at org.eclipse.swt.internal.win32.OS.PeekMessageW(Native Method)
	at org.eclipse.swt.internal.win32.OS.PeekMessage(OS.java:3129)
	at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3753)
	at snippets.AccessibleLabels.main(AccessibleLabels.java:185)


It's not clear to me just how the NPE relates to the code I've written, so naturally I just tried commenting stuff out. It turns out that if I don't add the AccessibleControlListener to the Composite, the problem goes away. But here's something interesting: If I still add the listener, but comment out my overrides of getFocus() and getSelection(), I still get the NPE. So, somehow just having a listener there, even if it does nothing, still causes the problem.

The Javadoc for AccessibleControlAdapter says that "classes that wish to deal with AccessibleControlEvents can extend this class and override only the methods that they are interested in," but it seems there's something I need to be overriding that I'm not. Or, perhaps, once again, my whole approach is flawed to begin with?

Any suggestions or advice would be much appreciated. I've been trying this on 32-bit SWT for Windows, from the 4.3RC3 build.

Thanks in advance,
Dave
Previous Topic:MenuItem Tooltip bug
Next Topic:new Image(ImageData) can never be "fast" on Kepler/Linux64/Gtk ?
Goto Forum:
  


Current Time: Fri Aug 29 08:17:07 EDT 2014

Powered by FUDForum. Page generated in 0.02287 seconds