Accessibility Mac: Fix handling of top level widget

This simplifies how we handle QNSView for accessibility purposes.
Instead of trying to half-merge the top level widget
(window->accessibleRoot) into the view, just have the view
always return it as child.
This makes the accessibility implementation for QNSView simpler
and makes applications that show a top level widget such as a button
possible. (We would return accessibility ignored for the button before).

As a side effect finding the active focus and hit-testing should be more reliable as
well.
Task-number: QTBUG-37794

Change-Id: Ib52037f88da8887a0bdc77204b0f3daddfe7709d
Reviewed-by: Morten Johan Sørvig <morten.sorvig@digia.com>
Reviewed-by: Jan Arve Sæther <jan-arve.saether@digia.com>
This commit is contained in:
Frederik Gladhorn 2014-03-26 10:14:45 +01:00 committed by The Qt Project
parent 7aec099ca3
commit f16d690a2f
5 changed files with 121 additions and 56 deletions

View File

@ -417,7 +417,23 @@
}
- (id)accessibilityFocusedUIElement {
return NSAccessibilityUnignoredAncestor(self);
QAccessibleInterface *iface = QAccessible::accessibleInterface(axid);
if (!iface || !iface->isValid()) {
qWarning() << "FocusedUIElement for INVALID";
return nil;
}
QAccessibleInterface *childInterface = iface->focusChild();
if (childInterface) {
QAccessible::Id childAxid = QAccessible::uniqueId(childInterface);
// FIXME: parent could be wrong
QCocoaAccessibleElement *accessibleElement = [QCocoaAccessibleElement createElementWithId:childAxid parent:self];
[accessibleElement autorelease];
return accessibleElement;
}
// no focus found
return nil;
}
@end

View File

@ -54,6 +54,15 @@
@implementation QNSView (QNSViewAccessibility)
- (id)childAccessibleElement {
if (!m_window->accessibleRoot())
return nil;
QAccessible::Id childId = QAccessible::uniqueId(m_window->accessibleRoot());
QCocoaAccessibleElement *child = [QCocoaAccessibleElement createElementWithId: childId parent: self];
return [child autorelease];
}
// The QNSView is a container that the user does not interact directly with:
// Remove it from the user-visible accessibility tree.
- (BOOL)accessibilityIsIgnored {
@ -61,58 +70,22 @@
}
- (id)accessibilityAttributeValue:(NSString *)attribute {
// activate accessibility updates
QCocoaIntegration::instance()->accessibility()->setActive(true);
if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) {
if (m_window->accessibleRoot())
return QCocoaAccessible::macRole(m_window->accessibleRoot());
return NSAccessibilityUnknownRole;
} else if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) {
return NSAccessibilityRoleDescriptionForUIElement(self);
} else if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) {
if (!m_window->accessibleRoot())
return [super accessibilityAttributeValue:attribute];
return QCocoaAccessible::unignoredChildren(self, m_window->accessibleRoot());
if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) {
return NSAccessibilityUnignoredChildrenForOnlyChild([self childAccessibleElement]);
} else {
return [super accessibilityAttributeValue:attribute];
}
}
- (id)accessibilityHitTest:(NSPoint)point {
if (!m_window->accessibleRoot())
return [super accessibilityHitTest:point];
QAccessibleInterface *childInterface = m_window->accessibleRoot()->childAt(point.x, qt_mac_flipYCoordinate(point.y));
// No child found, meaning we hit the NSView
if (!childInterface) {
return [super accessibilityHitTest:point];
}
// Hit a child, forward to child accessible interface.
QAccessible::Id childAxid = QAccessible::uniqueId(childInterface);
// FIXME: parent could be wrong
QCocoaAccessibleElement *accessibleElement = [QCocoaAccessibleElement createElementWithId:childAxid parent:self ];
[accessibleElement autorelease];
return [accessibleElement accessibilityHitTest:point];
return [[self childAccessibleElement] accessibilityHitTest: point];
}
- (id)accessibilityFocusedUIElement {
if (!m_window->accessibleRoot())
return [super accessibilityFocusedUIElement];
QAccessibleInterface *childInterface = m_window->accessibleRoot()->focusChild();
if (childInterface) {
QAccessible::Id childAxid = QAccessible::uniqueId(childInterface);
// FIXME: parent could be wrong
QCocoaAccessibleElement *accessibleElement = [QCocoaAccessibleElement createElementWithId:childAxid parent:self];
[accessibleElement autorelease];
return accessibleElement;
}
// should not happen
return nil;
return [[self childAccessibleElement] accessibilityFocusedUIElement];
}
@end

View File

@ -79,6 +79,7 @@ private slots:
void init();
void cleanup();
void singleWidgetTest();
void lineEditTest();
void hierarchyTest();
private:
@ -101,6 +102,16 @@ void tst_QAccessibilityMac::cleanup()
delete m_window;
}
void tst_QAccessibilityMac::singleWidgetTest()
{
if (!macNativeAccessibilityEnabled())
return;
delete m_window;
m_window = 0;
QVERIFY(singleWidget());
}
void tst_QAccessibilityMac::lineEditTest()
{
@ -122,14 +133,19 @@ void tst_QAccessibilityMac::hierarchyTest()
QWidget *w = new QWidget(m_window);
m_window->addWidget(w);
QPushButton *b = new QPushButton(w);
w->setLayout(new QVBoxLayout());
QPushButton *b = new QPushButton(w);
w->layout()->addWidget(b);
b->setText("I am a button");
QPushButton *b2 = new QPushButton(w);
w->layout()->addWidget(b2);
b2->setText("Button 2");
QVERIFY(QTest::qWaitForWindowExposed(m_window));
QCoreApplication::processEvents();
QVERIFY(testHierarchy());
QVERIFY(testHierarchy(w));
}
QTEST_MAIN(tst_QAccessibilityMac)

View File

@ -48,4 +48,5 @@
bool macNativeAccessibilityEnabled();
bool trusted();
bool testLineEdit();
bool testHierarchy();
bool testHierarchy(QWidget *w);
bool singleWidget();

View File

@ -43,8 +43,10 @@
#define QT_NO_KEYWORDS
#include "tst_qaccessibilitymac_helpers.h"
#include <QApplication>
#include <QDebug>
#include <QtWidgets/qapplication.h>
#include <QtWidgets/qlineedit.h>
#include <QtWidgets/qpushbutton.h>
#include <QtTest>
#include <unistd.h>
#import <Cocoa/Cocoa.h>
@ -148,9 +150,41 @@ bool trusted()
return p;
}
+ (TestAXObject *) getApplicationAXObject
{
pid_t pid = getpid();
AXUIElementRef appRef = AXUIElementCreateApplication(pid);
TestAXObject *appObject = [[TestAXObject alloc] initWithAXUIElementRef: appRef];
return appObject;
}
@end
bool singleWidget()
{
QLineEdit le;
le.setText("button");
le.show();
EXPECT(QTest::qWaitForWindowExposed(&le));
QCoreApplication::processEvents();
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
EXPECT(appObject);
NSArray *windows = [appObject windowList];
EXPECT([windows count] == 1);
AXUIElementRef windowRef = (AXUIElementRef) [windows objectAtIndex: 0];
EXPECT(windowRef != nil);
TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];
AXUIElementRef lineEdit = [window findDirectChildByRole: kAXTextFieldRole];
EXPECT(lineEdit != nil);
return true;
}
bool testLineEdit()
{
// not sure if this is needed. on my machine the calls succeed.
@ -159,10 +193,8 @@ bool testLineEdit()
// AXError e = AXMakeProcessTrusted((CFStringRef) path);
// NSLog(@"error: %i", e);
pid_t pid = getpid();
AXUIElementRef app = AXUIElementCreateApplication(pid);
EXPECT(app != nil);
TestAXObject *appObject = [[TestAXObject alloc] initWithAXUIElementRef: app];
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
EXPECT(appObject);
NSArray *windowList = [appObject windowList];
// one window
@ -184,12 +216,10 @@ bool testLineEdit()
return true;
}
bool testHierarchy()
bool testHierarchy(QWidget *w)
{
pid_t pid = getpid();
AXUIElementRef app = AXUIElementCreateApplication(pid);
EXPECT(app != nil);
TestAXObject *appObject = [[TestAXObject alloc] initWithAXUIElementRef: app];
TestAXObject *appObject = [TestAXObject getApplicationAXObject];
EXPECT(appObject);
NSArray *windowList = [appObject windowList];
// one window
@ -207,7 +237,36 @@ bool testHierarchy()
TestAXObject *parentObject = [[TestAXObject alloc] initWithAXUIElementRef: [buttonObject parent]];
// check that the parent is a window
EXPECT([[parentObject role] isEqualToString: (NSString *)kAXWindowRole]);
EXPECT([[parentObject role] isEqualToString: NSAccessibilityWindowRole]);
// test the focus
// child 0 is the layout, then button1 and 2
QPushButton *button1 = qobject_cast<QPushButton*>(w->children().at(1));
EXPECT(button1);
QPushButton *button2 = qobject_cast<QPushButton*>(w->children().at(2));
EXPECT(button2);
button2->setFocus();
AXUIElementRef systemWideElement = AXUIElementCreateSystemWide();
AXUIElementRef focussedElement = NULL;
AXError error = AXUIElementCopyAttributeValue(systemWideElement,
(CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
EXPECT(!error);
EXPECT(focussedElement);
TestAXObject *focusButton2 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];
EXPECT([[focusButton2 role] isEqualToString: NSAccessibilityButtonRole]);
EXPECT([[focusButton2 description] isEqualToString: @"Button 2"]);
button1->setFocus();
error = AXUIElementCopyAttributeValue(systemWideElement,
(CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
EXPECT(!error);
EXPECT(focussedElement);
TestAXObject *focusButton1 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];
EXPECT([[focusButton1 role] isEqualToString: NSAccessibilityButtonRole]);
EXPECT([[focusButton1 description] isEqualToString: @"I am a button"]);
return true;
}