Bug 221411 - [Accessibility] Leopard: VoiceOver doesn't follow focus if Browser is in Shell
Summary: [Accessibility] Leopard: VoiceOver doesn't follow focus if Browser is in Shell
Status: RESOLVED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: SWT (show other bugs)
Version: 3.3   Edit
Hardware: Macintosh Mac OS X - Carbon (unsup.)
: P3 normal (vote)
Target Milestone: 3.4 M7   Edit
Assignee: Carolyn MacLeod CLA
QA Contact:
URL:
Whiteboard:
Keywords: accessibility
Depends on:
Blocks:
 
Reported: 2008-03-04 16:58 EST by Carolyn MacLeod CLA
Modified: 2008-04-25 14:51 EDT (History)
5 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Carolyn MacLeod CLA 2008-03-04 16:58:48 EST
3.4 M5 on Mac OS X Leopard Carbon

Adding a Browser (HIWebViewCreate) to a Shell on Leopard seems to mess up accessibility.

Typing Tab moves focus from one Carbon control to the next, and the focus highlight follows the focus, but the VoiceOver cursor does not follow the focus - in fact, VoiceOver can't even tell that the focused control exists. Consequently, VoiceOver does not speak any of the other controls. For example, this breaks accessibility in the SWT ControlExample because of the Browser tab... even when the Browser tab is not visible.

Looking at this with the Accessibility Inspector, it seems that the Browser has essentially "hijacked" the Shell as far as accessibility is concerned, because other controls in the Shell don't show up in the accessible hierarchy.

Looking at it with UI Browser, I can see the other controls in the hierarchy, however there is a [MISMATCH] that seems to indicate a disconnect in the hierarchy below the level of the Shell.

The following PI code (platform calls from within Java) shows the problem, however I do not know how to fix this. I need help from Scott at Apple.

1) Run the code below from inside eclipse with SWT in your workspace.
2) Turn on VoiceOver (command F5) and type the Tab key a bunch of times. Notice that the VoiceOver cursor (black rectangle) follows focus, and VoiceOver speaks the names of the 2 Carbon buttons.
3) Now, in the PI code, set createBrowser = true; and run again. Type tab. This time, VoiceOver doesn't speak the names of the Carbon buttons.
4) Run the Accessibility Inspector (in /Developer/Applications/Utilities/Accessibility Tools) and note that the inspector cannot see either of the 2 buttons.
5) If you have UI Browser, then you need to create a Mac application and run that - ask me, and I will send steps to create an application. You can see the broken hierarchy in UI Browser.

import org.eclipse.swt.internal.*;
import org.eclipse.swt.internal.carbon.*;
import org.eclipse.swt.internal.cocoa.*;

public class CarbonPI {

	boolean createCocoaButtons = false;
	boolean createBrowser = false;
	
	int shellHandle, rootHandle;
	boolean running = true;
	int queue;
	int carbonButton1, carbonButton2;
	int cocoaButton1, cocoaButton2;
	int webViewHandle;
	
public static void main(String[] args) {
	new CarbonPI().doit();
}

void doit() {
	int [] psn = new int [2];
	if (OS.GetCurrentProcess (psn) == OS.noErr) {
		String APP_NAME = "Test Application";
		byte[] buffer = new byte[APP_NAME.length () + 1];
		for (int i = 0; i < buffer.length - 1; i++) {
			buffer[i] = (byte) APP_NAME.charAt (i);					
		}
		OS.CPSSetProcessName (psn, buffer);
		OS.CPSEnableForegroundOperation (psn, 0x03, 0x3C, 0x2C, 0x1103);
		OS.SetFrontProcess (psn);
	}
	queue = OS.GetCurrentEventQueue();
	
	{	/* Shell */
		int windowAttrs = OS.kWindowCompositingAttribute | OS.kWindowCloseBoxAttribute | OS.kWindowFullZoomAttribute | OS.kWindowCollapseBoxAttribute | OS.kWindowResizableAttribute | OS.kWindowStandardHandlerAttribute;
		Rect r = new Rect();
		OS.SetRect(r, (short)100, (short)100, (short)800, (short)800);
		int[] window = new int[1];
		OS.CreateNewWindow(OS.kDocumentWindowClass, windowAttrs, r, window);
		shellHandle = window[0];
		
		Callback windowCallback = new Callback(this, "windowEventHandler", 3);
		int[] eventTypes = new int[] {
				OS.kEventClassWindow, OS.kEventWindowClose,
		};
		int windowEventTarget = OS.GetWindowEventTarget(shellHandle);
		OS.InstallEventHandler(windowEventTarget, windowCallback.getAddress(), eventTypes.length / 2, eventTypes, 0, null);

		int[] root = new int[1];
		OS.HIViewFindByID(OS.HIViewGetRoot(shellHandle), OS.kHIViewWindowContentID(), root);
		rootHandle = root[0];
	}

	{	/* Carbon Button 1 */
		Rect r = new Rect();
		r.left = (short)10;
		r.top = (short)10;
		r.right = (short)200;
		r.bottom = (short)100;
		int outControl[] = new int[1];
		OS.CreatePushButtonControl (shellHandle, r, 0, outControl);
		carbonButton1 = outControl[0];
		OS.HIViewAddSubview(rootHandle, carbonButton1);
		char [] buffer = new char [] {'C','a','r','b','o','n',' ','B','u','t','t','o','n',' ','1'};
		int ptr = OS.CFStringCreateWithCharacters (OS.kCFAllocatorDefault, buffer, buffer.length);
		OS.SetControlTitleWithCFString (carbonButton1, ptr);
		OS.CFRelease (ptr);
	}
	
	{	/* Carbon Button 2 */
		Rect r = new Rect();
		r.left = (short)220;
		r.top = (short)10;
		r.right = (short)410;
		r.bottom = (short)100;
		int outControl[] = new int[1];
		OS.CreatePushButtonControl (shellHandle, r, 0, outControl);
		carbonButton2 = outControl[0];
		OS.HIViewAddSubview(rootHandle, carbonButton2);
		char [] buffer = new char [] {'C','a','r','b','o','n',' ','B','u','t','t','o','n',' ','2'};
		int ptr = OS.CFStringCreateWithCharacters (OS.kCFAllocatorDefault, buffer, buffer.length);
		OS.SetControlTitleWithCFString (carbonButton2, ptr);
		OS.CFRelease (ptr);
	}
	
	if (createCocoaButtons) {	/* Cocoa Button 1 */
		int embedHandle = Cocoa.objc_msgSend(Cocoa.objc_msgSend(Cocoa.objc_getClass("NSButton"), Cocoa.S_alloc), Cocoa.S_initWithFrame, 0);
		int outControl[] = new int[1];
		Cocoa.HICocoaViewCreate(embedHandle, 0, outControl); /* OSX >= 10.5 */
		cocoaButton1 = outControl[0];
		OS.HIViewAddSubview(rootHandle, cocoaButton1);
		OS.HIViewSetVisible(cocoaButton1, true);
		CGRect rect = new CGRect();	
		rect.x = 10;
		rect.y = 120;
		rect.width = 190;
		rect.height = 90;
		OS.HIViewSetFrame(cocoaButton1, rect);
	}

	if (createCocoaButtons) {	/* Cocoa Button 2 */
		int embedHandle = Cocoa.objc_msgSend(Cocoa.objc_msgSend(Cocoa.objc_getClass("NSButton"), Cocoa.S_alloc), Cocoa.S_initWithFrame, 0);
		int outControl[] = new int[1];
		Cocoa.HICocoaViewCreate(embedHandle, 0, outControl); /* OSX >= 10.5 */
		cocoaButton2 = outControl[0];
		OS.HIViewAddSubview(rootHandle, cocoaButton2);
		OS.HIViewSetVisible(cocoaButton2, true);
		CGRect rect = new CGRect();	
		rect.x = 220;
		rect.y = 120;
		rect.width = 190;
		rect.height = 90;
		OS.HIViewSetFrame(cocoaButton2, rect);
	}
	
	if (createBrowser) {	/* Browser */
		int outControl[] = new int[1];
		Cocoa.HIWebViewCreate(outControl);
		webViewHandle = outControl[0];
		OS.HIViewAddSubview(rootHandle, webViewHandle);
		OS.HIViewSetVisible(webViewHandle, true);
		CGRect rect = new CGRect();	
		rect.x = 10; 
		rect.y = 230;
		rect.width = 400;
		rect.height = 300;
		OS.HIViewSetFrame(webViewHandle, rect);
			
		final int webView = Cocoa.HIWebViewGetWebView(webViewHandle);
		String url = "http://www.google.com";
		int length = url.length();
		char[] buffer = new char[length];
		url.getChars(0, length, buffer, 0);
		int sHandle = OS.CFStringCreateWithCharacters(0, buffer, length);
		int inURL= Cocoa.objc_msgSend(Cocoa.C_NSURL, Cocoa.S_URLWithString, sHandle);
		OS.CFRelease(sHandle);
		int request= Cocoa.objc_msgSend(Cocoa.C_NSURLRequest, Cocoa.S_requestWithURL, inURL);
		int mainFrame= Cocoa.objc_msgSend(webView, Cocoa.S_mainFrame);
		Cocoa.objc_msgSend(mainFrame, Cocoa.S_loadRequest, request);
	}
	
	OS.ShowWindow (shellHandle);
	OS.SetKeyboardFocus(shellHandle, carbonButton1, (short)-1);

	int[] eventRef = new int[1];
	int eventTargetRef = OS.GetEventDispatcherTarget();
	while (running) {
		while (running && (OS.ReceiveNextEvent(0, null, OS.kEventDurationForever, true, eventRef)) == OS.noErr) {
			OS.SendEventToEventTarget (eventRef[0], eventTargetRef);
			OS.ReleaseEvent(eventRef[0]);
		}
	}
	OS.DisposeWindow(shellHandle);
}

int windowEventHandler (int eventHandlerCallRef, int eventRef, int userData) {
	int eventClass = OS.GetEventClass(eventRef);
	int eventKind = OS.GetEventKind(eventRef);
	switch (eventClass) {
		case OS.kEventClassWindow:
			switch(eventKind) {
				case OS.kEventWindowClose: 
					running = false;
					break;
			}
			break;
	}
	return OS.CallNextEventHandler(eventHandlerCallRef, eventRef);
}

}
Comment 1 Steve Northover CLA 2008-03-04 17:00:25 EST
This is a real bad one.  Scott, anyone at Apple know about this?
Comment 2 Scott Kovatch CLA 2008-03-04 17:49:24 EST
Please file a bug if you haven't done so already. HIWebViewCreate creates a shadow NSWindow because the underlying WebView eventually needs an NSWindow for event handling, coordinate translation, and so on. I suspect there's no easy way to work around it.
Comment 3 Steve Northover CLA 2008-03-04 18:25:44 EST
It works on Tiger, right Carolyn?
Comment 4 Carolyn MacLeod CLA 2008-03-05 13:44:21 EST
Confirmed that it works correctly on Tiger.
i.e. VoiceOver and other accessible tools are not confused by the presence of a Browser on Tiger.
Comment 5 Carolyn MacLeod CLA 2008-03-05 15:15:29 EST
Opened Apple Bug ID# 5782404
Comment 6 Mike Swingler CLA 2008-03-18 17:34:31 EDT
I'm trying to reproduce this bug with the above sample in it's own class file and org.eclipse.swt.carbon.macosx_3.3.2.v3347a.jar org.eclipse.ui.carbon_3.2.100.I20070605-0010.jar on my classpath, and it only hangs. Would it be possible to get a native C test case for this bug, since that would help move this bug along quicker? Thanks.
Comment 7 Carolyn MacLeod CLA 2008-03-18 23:00:10 EDT
I am at  home at the moment (and my Mac is at work) so I can't test this theory, but did you start java with -XstartOnFirstThread ?

Here's how I start the SWT ControlExample as a Mac application (using the SWT jar and library that are in my workspace, but you can change your classpath and library path to point to your locations).

1) The following command line is in a file called Contents/MacOS/ControlExample:

#!/bin/sh
WORKSPACE=/Users/cmacleod/Documents/workspace
exec java \
	-XstartOnFirstThread \
	-classpath $WORKSPACE/org.eclipse.swt/bin:$WORKSPACE/org.eclipse.swt.examples/bin \
	-Djava.library.path=$WORKSPACE/org.eclipse.swt.carbon.macosx \
	org.eclipse.swt.examples.controlexample.ControlExample


2) And here's the Contents/Info.plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleExecutable</key>
	<string>ControlExample</string>
	<key>CFBundleGetInfoString</key>
	<string>SWT ControlExample</string>
	<key>CFBundleIconFile</key>
	<string>GenericJavaApp.icns</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>ControlExample</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleSignature</key>
	<string>?????</string>
	<key>CFBundleVersion</key>
	<string>1.0</string>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
</dict>
</plist>


3) The GenericJavaApp.icns is just some random icon (any icon will do), and it is in the Contents/Resources directory.

Hopefully this will help you run the PI code if you change "ControlExample" to "CarbonPI" in the batch file and the Info.plist file.

I will try to get the native C test case going tomorrow, but it may have to wait until next week when Steve & Silenio return from EclipseCon.
Comment 8 Carolyn MacLeod CLA 2008-04-07 14:38:35 EDT
We found a work-around, so we are going to mark this fixed at our end.
(i.e. the problem still exists, so I will leave the Mac bug open).

Believe it or not, we just need to create and dispose an NSButton for every Window that contains a WebView (aka SWT Browser). Then the VoiceOver problem just goes away...  <grin>

Creating an NSButton must initialize some global state in Window that a WebView needs in order to work well with VoiceOver.  :)