gecko/accessible/src/mac/mozAccessible.mm
2009-03-07 23:38:58 +08:00

714 lines
21 KiB
Plaintext

/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2006
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Original Author: HÃ¥kan Waara <hwaara@gmail.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#import "mozAccessible.h"
// to get the mozView formal protocol, that all gecko's ChildViews implement.
#import "mozView.h"
#import "nsRoleMap.h"
#include "nsRect.h"
#include "nsCoord.h"
#include "nsObjCExceptions.h"
#include "nsIAccessible.h"
#include "nsIAccessibleText.h"
#include "nsIAccessibleEditableText.h"
#include "nsRootAccessible.h"
// These constants are only defined in OS X SDK 10.4, so we define them in order
// to be able to use for earlier OS versions.
const NSString *kInstanceDescriptionAttribute = @"AXDescription"; // NSAccessibilityDescriptionAttribute
const NSString *kTopLevelUIElementAttribute = @"AXTopLevelUIElement"; // NSAccessibilityTopLevelUIElementAttribute
const NSString *kTextLinkSubrole = @"AXTextLink"; // NSAccessibilitySecureTextFieldSubrole
const NSString *kURLAttribute = @"AXURL";
// converts a screen-global point in the cocoa coordinate system (with origo in the bottom-left corner
// of the screen), into a top-left screen point, that gecko can use.
static inline void
ConvertCocoaToGeckoPoint(NSPoint &aInPoint, nsPoint &aOutPoint)
{
float mainScreenHeight = [(NSView*)[[NSScreen screens] objectAtIndex:0] frame].size.height;
aOutPoint.MoveTo ((nscoord)aInPoint.x, (nscoord)(mainScreenHeight - aInPoint.y));
}
// all mozAccessibles are either abstract objects (that correspond to XUL widgets, HTML frames, etc) or are
// attached to a certain view; for example a document view. when we hand an object off to an AT, we always want
// to give it the represented view, in the latter case.
static inline id <mozAccessible>
GetObjectOrRepresentedView(id <mozAccessible> anObject)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if ([anObject hasRepresentedView])
return [anObject representedView];
return anObject;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
// returns the passed in object if it is not ignored. if it's ignored, will return
// the first unignored ancestor.
static inline id
GetClosestInterestingAccessible(id anObject)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
// this object is not ignored, so let's return it.
if (![anObject accessibilityIsIgnored])
return GetObjectOrRepresentedView(anObject);
// find the closest ancestor that is not ignored.
id unignoredObject = anObject;
while ((unignoredObject = [unignoredObject accessibilityAttributeValue:NSAccessibilityParentAttribute])) {
if (![unignoredObject accessibilityIsIgnored])
// object is not ignored, so let's stop the search.
break;
}
// if it's a mozAccessible, we need to take care to maybe return the view we
// represent, to the AT.
if ([unignoredObject respondsToSelector:@selector(hasRepresentedView)])
return GetObjectOrRepresentedView(unignoredObject);
return unignoredObject;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
static inline mozAccessible*
GetNativeFromGeckoAccessible(nsIAccessible *anAccessible)
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL;
mozAccessible *native = nil;
anAccessible->GetNativeInterface ((void**)&native);
return native;
NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL;
}
#pragma mark -
@implementation mozAccessible
- (id)initWithAccessible:(nsAccessibleWrap*)geckoAccessible
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if ((self = [super init])) {
mGeckoAccessible = geckoAccessible;
mIsExpired = NO;
geckoAccessible->GetRole(&mRole);
// Check for OS X "role skew"; the role constants in nsIAccessible.idl need to match the ones
// in nsRoleMap.h.
NS_ASSERTION([AXRoles[nsIAccessibleRole::ROLE_LAST_ENTRY] isEqualToString:@"ROLE_LAST_ENTRY"], "Role skew in the role map!");
}
return self;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (void)dealloc
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
[mChildren release];
[super dealloc];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
#pragma mark -
- (BOOL)accessibilityIsIgnored
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
// unknown (either unimplemented, or irrelevant) elements are marked as ignored
// as well as expired elements.
return mIsExpired || [[self role] isEqualToString:NSAccessibilityUnknownRole];
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
}
- (NSArray*)accessibilityAttributeNames
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
// if we're expired, we don't support any attributes.
if (mIsExpired)
return [NSArray array];
static NSArray *generalAttributes = nil;
if (!generalAttributes) {
// standard attributes that are shared and supported by all generic elements.
generalAttributes = [[NSArray alloc] initWithObjects: NSAccessibilityChildrenAttribute,
NSAccessibilityParentAttribute,
NSAccessibilityRoleAttribute,
NSAccessibilityTitleAttribute,
NSAccessibilityValueAttribute,
NSAccessibilitySubroleAttribute,
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
NSAccessibilityRoleDescriptionAttribute,
#endif
NSAccessibilityPositionAttribute,
NSAccessibilityEnabledAttribute,
NSAccessibilitySizeAttribute,
NSAccessibilityWindowAttribute,
NSAccessibilityFocusedAttribute,
NSAccessibilityHelpAttribute,
NSAccessibilityTitleUIElementAttribute,
kTopLevelUIElementAttribute,
kInstanceDescriptionAttribute,
nil];
}
return generalAttributes;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (id)accessibilityAttributeValue:(NSString*)attribute
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if (mIsExpired)
return nil;
if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
return [self children];
if ([attribute isEqualToString:NSAccessibilityParentAttribute])
return [self parent];
#ifdef DEBUG_hakan
NSLog (@"(%@ responding to attr %@)", self, attribute);
#endif
if ([attribute isEqualToString:NSAccessibilityRoleAttribute])
return [self role];
if ([attribute isEqualToString:NSAccessibilityPositionAttribute])
return [self position];
if ([attribute isEqualToString:NSAccessibilitySubroleAttribute])
return [self subrole];
if ([attribute isEqualToString:NSAccessibilityEnabledAttribute])
return [NSNumber numberWithBool:[self isEnabled]];
if ([attribute isEqualToString:NSAccessibilityValueAttribute])
return [self value];
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute])
return NSAccessibilityRoleDescription([self role], nil);
#endif
if ([attribute isEqualToString:kInstanceDescriptionAttribute])
return [self customDescription];
if ([attribute isEqualToString:NSAccessibilityFocusedAttribute])
return [NSNumber numberWithBool:[self isFocused]];
if ([attribute isEqualToString:NSAccessibilitySizeAttribute])
return [self size];
if ([attribute isEqualToString:NSAccessibilityWindowAttribute])
return [self window];
if ([attribute isEqualToString:kTopLevelUIElementAttribute])
return [self window];
if ([attribute isEqualToString:NSAccessibilityTitleAttribute] ||
[attribute isEqualToString:NSAccessibilityTitleUIElementAttribute])
return [self title];
if ([attribute isEqualToString:NSAccessibilityHelpAttribute])
return [self help];
#ifdef DEBUG
NSLog (@"!!! %@ can't respond to attribute %@", self, attribute);
#endif
return nil; // be nice and return empty string instead?
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
if ([attribute isEqualToString:NSAccessibilityFocusedAttribute])
return [self canBeFocused];
return NO;
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_hakan
NSLog (@"[%@] %@='%@'", self, attribute, value);
#endif
// we only support focusing elements so far.
if ([attribute isEqualToString:NSAccessibilityFocusedAttribute] && [value boolValue])
[self focus];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (id)accessibilityHitTest:(NSPoint)point
{
if (mIsExpired)
return nil;
// Convert from cocoa's coordinate system to gecko's. According to the docs
// the point we're given is guaranteed to be bottom-left screen coordinates.
nsPoint geckoPoint;
ConvertCocoaToGeckoPoint (point, geckoPoint);
nsCOMPtr<nsIAccessible> deepestFoundChild;
mGeckoAccessible->GetDeepestChildAtPoint((PRInt32)geckoPoint.x,
(PRInt32)geckoPoint.y,
getter_AddRefs(deepestFoundChild));
// if we found something, return its native accessible.
if (deepestFoundChild) {
mozAccessible *nativeChild = GetNativeFromGeckoAccessible(deepestFoundChild);
if (nativeChild)
return GetClosestInterestingAccessible(nativeChild);
}
// if we didn't find anything, return ourself (or the first unignored ancestor).
return GetClosestInterestingAccessible(self);
}
- (NSArray*)accessibilityActionNames
{
return nil;
}
- (NSString*)accessibilityActionDescription:(NSString*)action
{
return nil;
}
- (void)accessibilityPerformAction:(NSString*)action
{
}
- (id)accessibilityFocusedUIElement
{
if (mIsExpired)
return nil;
nsCOMPtr<nsIAccessible> focusedGeckoChild;
mGeckoAccessible->GetFocusedChild (getter_AddRefs (focusedGeckoChild));
if (focusedGeckoChild) {
mozAccessible *focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild);
if (focusedChild)
return GetClosestInterestingAccessible(focusedChild);
}
// return ourself if we can't get a native focused child.
return GetClosestInterestingAccessible(self);
}
#pragma mark -
- (id <mozAccessible>)parent
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsCOMPtr<nsIAccessible> accessibleParent(mGeckoAccessible->GetUnignoredParent());
if (accessibleParent) {
id nativeParent = GetNativeFromGeckoAccessible(accessibleParent);
if (nativeParent)
return GetClosestInterestingAccessible(nativeParent);
}
// GetUnignoredParent() returns null when there is no unignored accessible all the way up to
// the root accessible. so we'll have to return whatever native accessible is above our root accessible
// (which might be the owning NSWindow in the application, for example).
//
// get the native root accessible, and tell it to return its first parent unignored accessible.
nsRefPtr<nsRootAccessible> root(mGeckoAccessible->GetRootAccessible());
id nativeParent = GetNativeFromGeckoAccessible(static_cast<nsIAccessible*>(root));
NSAssert1 (nativeParent, @"!!! we can't find a parent for %@", self);
return GetClosestInterestingAccessible(nativeParent);
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (BOOL)hasRepresentedView
{
return NO;
}
- (id)representedView
{
return nil;
}
- (BOOL)isRoot
{
return NO;
}
// gets our native children lazily.
// returns nil when there are no children.
- (NSArray*)children
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
if (mChildren)
return mChildren;
mChildren = [[NSMutableArray alloc] init];
// get the array of children.
nsTArray<nsRefPtr<nsAccessibleWrap> > childrenArray;
mGeckoAccessible->GetUnignoredChildren(childrenArray);
// now iterate through the children array, and get each native accessible.
int totalCount = childrenArray.Length();
int index = 0;
for (; index < totalCount; index++) {
nsAccessibleWrap *curAccessible = childrenArray.ElementAt(index);
if (curAccessible) {
mozAccessible *curNative = GetNativeFromGeckoAccessible(curAccessible);
if (curNative)
[mChildren addObject:GetObjectOrRepresentedView(curNative)];
}
}
#ifdef DEBUG_hakan
// make sure we're not returning any ignored accessibles.
NSEnumerator *e = [mChildren objectEnumerator];
mozAccessible *m = nil;
while ((m = [e nextObject])) {
NSAssert1(![m accessibilityIsIgnored], @"we should never return an ignored accessible! (%@)", m);
}
#endif
return mChildren;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (NSValue*)position
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
PRInt32 x, y, width, height;
mGeckoAccessible->GetBounds (&x, &y, &width, &height);
NSPoint p = NSMakePoint (x, y);
// The coords we get from Gecko are top-left screen coordinates.
// Cocoa wants us to return bottom-left screen coordinates.
// This involves two steps:
// 1. Put the rect in the bottom-left coord space
// 2. Subtract the height of the rect's Y-coordinate, to make the
// the rect's origin (0, 0) be in the bottom-left corner.
float mainScreenHeight = [[[NSScreen screens] objectAtIndex:0] frame].size.height;
p.y = mainScreenHeight - p.y - height;
return [NSValue valueWithPoint:p];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (NSValue*)size
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
PRInt32 x, y, width, height;
mGeckoAccessible->GetBounds (&x, &y, &width, &height);
return [NSValue valueWithSize:NSMakeSize (width, height)];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (NSString*)role
{
#ifdef DEBUG_A11Y
NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(mGeckoAccessible),
"Does not support nsIAccessibleText when it should");
#endif
return AXRoles[mRole];
}
- (NSString*)subrole
{
return nil;
}
- (NSString*)title
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsAutoString title;
mGeckoAccessible->GetName (title);
return title.IsEmpty() ? nil : [NSString stringWithCharacters:title.BeginReading() length:title.Length()];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (id)value
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsAutoString value;
mGeckoAccessible->GetValue (value);
return value.IsEmpty() ? nil : [NSString stringWithCharacters:value.BeginReading() length:value.Length()];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (void)valueDidChange
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_hakan
NSLog(@"%@'s value changed!", self);
#endif
// sending out a notification is expensive, so we don't do it other than for really important objects,
// like mozTextAccessible.
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (NSString*)customDescription
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsAutoString desc;
mGeckoAccessible->GetDescription (desc);
return desc.IsEmpty() ? nil : [NSString stringWithCharacters:desc.BeginReading() length:desc.Length()];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (NSString*)help
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsAutoString helpText;
mGeckoAccessible->GetHelp (helpText);
return helpText.IsEmpty() ? nil : [NSString stringWithCharacters:helpText.BeginReading() length:helpText.Length()];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
// objc-style description (from NSObject); not to be confused with the accessible description above.
- (NSString*)description
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
return [NSString stringWithFormat:@"(%p) %@", self, [self role]];
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (BOOL)isFocused
{
PRUint32 state = 0;
mGeckoAccessible->GetState (&state, nsnull);
return (state & nsIAccessibleStates::STATE_FOCUSED) != 0;
}
- (BOOL)canBeFocused
{
PRUint32 state = 0;
mGeckoAccessible->GetState (&state, nsnull);
return (state & nsIAccessibleStates::STATE_FOCUSABLE) != 0;
}
- (BOOL)focus
{
nsresult rv = mGeckoAccessible->TakeFocus();
return NS_SUCCEEDED(rv);
}
- (BOOL)isEnabled
{
PRUint32 state = 0;
mGeckoAccessible->GetState (&state, nsnull);
return (state & nsIAccessibleStates::STATE_UNAVAILABLE) == 0;
}
// The root accessible calls this when the focused node was
// changed to us.
- (void)didReceiveFocus
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
#ifdef DEBUG_hakan
NSLog (@"%@ received focus!", self);
#endif
NSAssert1(![self accessibilityIsIgnored], @"trying to set focus to ignored element! (%@)", self);
NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
NSAccessibilityFocusedUIElementChangedNotification);
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (NSWindow*)window
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
nsAccessibleWrap *accWrap = static_cast<nsAccessibleWrap*>(mGeckoAccessible);
NSWindow *nativeWindow = nil;
accWrap->GetNativeWindow ((void**)&nativeWindow);
NSAssert1(nativeWindow, @"Could not get native window for %@", self);
return nativeWindow;
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}
- (void)invalidateChildren
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
// make room for new children
[mChildren release];
mChildren = nil;
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (void)expire
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
[self invalidateChildren];
mIsExpired = YES;
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (BOOL)isExpired
{
return mIsExpired;
}
#pragma mark -
#pragma mark Debug methods
#pragma mark -
#ifdef DEBUG
// will check that our children actually reference us as their
// parent.
- (void)sanityCheckChildren:(NSArray *)children
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NSAssert(![self accessibilityIsIgnored], @"can't sanity check children of an ignored accessible!");
NSEnumerator *iter = [children objectEnumerator];
mozAccessible *curObj = nil;
NSLog(@"sanity checking %@", self);
while ((curObj = [iter nextObject])) {
id realSelf = GetObjectOrRepresentedView(self);
NSLog(@"checking %@", realSelf);
NSAssert2([curObj parent] == realSelf,
@"!!! %@ not returning %@ as AXParent, even though it is a AXChild of it!", curObj, realSelf);
}
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (void)sanityCheckChildren
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
[self sanityCheckChildren:[self children]];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (void)printHierarchy
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
[self printHierarchyWithLevel:0];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
- (void)printHierarchyWithLevel:(unsigned)level
{
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NSAssert(![self isExpired], @"!!! trying to print hierarchy of expired object!");
// print this node
NSMutableString *indent = [NSMutableString stringWithCapacity:level];
unsigned i=0;
for (;i<level;i++)
[indent appendString:@" "];
NSLog (@"%@(#%i) %@", indent, level, self);
// use |children| method to make sure our children are lazily fetched first.
NSArray *children = [self children];
if (!children)
return;
if (![self accessibilityIsIgnored])
[self sanityCheckChildren];
NSEnumerator *iter = [children objectEnumerator];
mozAccessible *object = nil;
while (iter && (object = [iter nextObject]))
// print every child node's subtree, increasing the indenting
// by two for every level.
[object printHierarchyWithLevel:(level+1)];
NS_OBJC_END_TRY_ABORT_BLOCK;
}
#endif /* DEBUG */
@end