| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <execinfo.h> |
| |
| #import "chrome/browser/accessibility/browser_accessibility_cocoa.h" |
| |
| #include "app/l10n_util_mac.h" |
| #include "base/string16.h" |
| #include "base/sys_string_conversions.h" |
| #include "chrome/browser/renderer_host/render_widget_host_view_mac.h" |
| #include "grit/webkit_strings.h" |
| #include "third_party/WebKit/WebKit/chromium/public/WebRect.h" |
| |
| namespace { |
| |
| // Returns an autoreleased copy of the WebAccessibility's attribute. |
| NSString* NSStringForWebAccessibilityAttribute( |
| const std::map<int32, string16>& attributes, |
| WebAccessibility::Attribute attribute) { |
| std::map<int32, string16>::const_iterator iter = |
| attributes.find(attribute); |
| NSString* returnValue = @""; |
| if (iter != attributes.end()) { |
| returnValue = base::SysUTF16ToNSString(iter->second); |
| } |
| return returnValue; |
| } |
| |
| struct RoleEntry { |
| WebAccessibility::Role value; |
| NSString* string; |
| }; |
| |
| static const RoleEntry roles[] = { |
| { WebAccessibility::ROLE_NONE, NSAccessibilityUnknownRole }, |
| { WebAccessibility::ROLE_BUTTON, NSAccessibilityButtonRole }, |
| { WebAccessibility::ROLE_CHECKBOX, NSAccessibilityCheckBoxRole }, |
| { WebAccessibility::ROLE_COLUMN, NSAccessibilityColumnRole }, |
| { WebAccessibility::ROLE_GRID, NSAccessibilityGridRole }, |
| { WebAccessibility::ROLE_GROUP, NSAccessibilityGroupRole }, |
| { WebAccessibility::ROLE_HEADING, @"AXHeading" }, |
| { WebAccessibility::ROLE_IGNORED, NSAccessibilityUnknownRole }, |
| { WebAccessibility::ROLE_IMAGE, NSAccessibilityImageRole }, |
| { WebAccessibility::ROLE_LINK, NSAccessibilityLinkRole }, |
| { WebAccessibility::ROLE_LIST, NSAccessibilityListRole }, |
| { WebAccessibility::ROLE_RADIO_BUTTON, NSAccessibilityRadioButtonRole }, |
| { WebAccessibility::ROLE_RADIO_GROUP, NSAccessibilityRadioGroupRole }, |
| { WebAccessibility::ROLE_ROW, NSAccessibilityRowRole }, |
| { WebAccessibility::ROLE_SCROLLAREA, NSAccessibilityScrollAreaRole }, |
| { WebAccessibility::ROLE_SCROLLBAR, NSAccessibilityScrollBarRole }, |
| { WebAccessibility::ROLE_STATIC_TEXT, NSAccessibilityStaticTextRole }, |
| { WebAccessibility::ROLE_TABLE, NSAccessibilityTableRole }, |
| { WebAccessibility::ROLE_TAB_GROUP, NSAccessibilityTabGroupRole }, |
| { WebAccessibility::ROLE_TEXT_FIELD, NSAccessibilityTextFieldRole }, |
| { WebAccessibility::ROLE_TEXTAREA, NSAccessibilityTextAreaRole }, |
| { WebAccessibility::ROLE_WEB_AREA, @"AXWebArea" }, |
| { WebAccessibility::ROLE_WEBCORE_LINK, NSAccessibilityLinkRole }, |
| }; |
| |
| // GetState checks the bitmask used in webaccessibility.h to check |
| // if the given state was set on the accessibility object. |
| bool GetState(BrowserAccessibility* accessibility, int state) { |
| return ((accessibility->state() >> state) & 1); |
| } |
| |
| } // namespace |
| |
| @implementation BrowserAccessibilityCocoa |
| |
| - (id)initWithObject:(BrowserAccessibility*)accessibility |
| delegate:(id<BrowserAccessibilityDelegateCocoa>)delegate { |
| if ((self = [super init])) { |
| browserAccessibility_ = accessibility; |
| delegate_ = delegate; |
| } |
| return self; |
| } |
| |
| // Deletes our associated BrowserAccessibilityMac. |
| - (void)dealloc { |
| if (browserAccessibility_) { |
| delete browserAccessibility_; |
| browserAccessibility_ = NULL; |
| } |
| |
| [super dealloc]; |
| } |
| |
| // Returns an array of BrowserAccessibilityCocoa objects, representing the |
| // accessibility children of this object. |
| - (NSArray*)children { |
| if (!children_.get()) { |
| children_.reset([[NSMutableArray alloc] |
| initWithCapacity:browserAccessibility_->GetChildCount()] ); |
| for (uint32 index = 0; |
| index < browserAccessibility_->GetChildCount(); |
| ++index) { |
| BrowserAccessibilityCocoa* child = |
| browserAccessibility_->GetChild(index)->toBrowserAccessibilityCocoa(); |
| if ([child isIgnored]) |
| [children_ addObjectsFromArray:[child children]]; |
| else |
| [children_ addObject:child]; |
| } |
| } |
| return children_; |
| } |
| |
| - (void)childrenChanged { |
| if (![self isIgnored]) { |
| children_.reset(); |
| } else { |
| [browserAccessibility_->GetParent()->toBrowserAccessibilityCocoa() |
| childrenChanged]; |
| } |
| } |
| |
| // Returns whether or not this node should be ignored in the |
| // accessibility tree. |
| - (BOOL)isIgnored { |
| return [self role] == NSAccessibilityUnknownRole; |
| } |
| |
| // The origin of this accessibility object in the page's document. |
| // This is relative to webkit's top-left origin, not Cocoa's |
| // bottom-left origin. |
| - (NSPoint)origin { |
| return NSMakePoint(browserAccessibility_->location().x, |
| browserAccessibility_->location().y); |
| } |
| |
| // Returns a string indicating the role of this object. |
| - (NSString*)role { |
| NSString* role = NSAccessibilityUnknownRole; |
| WebAccessibility::Role value = |
| static_cast<WebAccessibility::Role>( browserAccessibility_->role()); |
| const size_t numRoles = sizeof(roles) / sizeof(roles[0]); |
| for (size_t i = 0; i < numRoles; ++i) { |
| if (roles[i].value == value) { |
| role = roles[i].string; |
| break; |
| } |
| } |
| return role; |
| } |
| |
| // Returns a string indicating the role description of this object. |
| - (NSString*)roleDescription { |
| // The following descriptions are specific to webkit. |
| if ([[self role] isEqualToString:@"AXWebArea"]) |
| return l10n_util::GetNSString(IDS_AX_ROLE_WEB_AREA); |
| |
| if ([[self role] isEqualToString:@"NSAccessibilityLinkRole"]) |
| return l10n_util::GetNSString(IDS_AX_ROLE_LINK); |
| |
| if ([[self role] isEqualToString:@"AXHeading"]) |
| return l10n_util::GetNSString(IDS_AX_ROLE_HEADING); |
| |
| return NSAccessibilityRoleDescription([self role], nil); |
| } |
| |
| // Returns the size of this object. |
| - (NSSize)size { |
| return NSMakeSize(browserAccessibility_->location().width, |
| browserAccessibility_->location().height); |
| } |
| |
| // Returns the accessibility value for the given attribute. If the value isn't |
| // supported this will return nil. |
| - (id)accessibilityAttributeValue:(NSString*)attribute { |
| if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { |
| return [self role]; |
| } |
| if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) { |
| return NSStringForWebAccessibilityAttribute( |
| browserAccessibility_->attributes(), |
| WebAccessibility::ATTR_DESCRIPTION); |
| } |
| if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) { |
| return [NSValue valueWithPoint:[delegate_ accessibilityPointInScreen:self]]; |
| } |
| if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) { |
| return [NSValue valueWithSize:[self size]]; |
| } |
| if ([attribute isEqualToString:NSAccessibilityTopLevelUIElementAttribute] || |
| [attribute isEqualToString:NSAccessibilityWindowAttribute]) { |
| return [delegate_ window]; |
| } |
| if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) { |
| return [self children]; |
| } |
| if ([attribute isEqualToString:NSAccessibilityParentAttribute]) { |
| // A nil parent means we're the root. |
| if (browserAccessibility_->GetParent()) { |
| return NSAccessibilityUnignoredAncestor( |
| browserAccessibility_->GetParent()->toBrowserAccessibilityCocoa()); |
| } else { |
| // Hook back up to RenderWidgetHostViewCocoa. |
| return browserAccessibility_->manager()->GetParentView(); |
| } |
| } |
| if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) { |
| return base::SysUTF16ToNSString(browserAccessibility_->name()); |
| } |
| if ([attribute isEqualToString:NSAccessibilityHelpAttribute]) { |
| return NSStringForWebAccessibilityAttribute( |
| browserAccessibility_->attributes(), |
| WebAccessibility::ATTR_HELP); |
| } |
| if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { |
| if ([self role] == @"AXHeading") { |
| NSString* headingLevel = |
| NSStringForWebAccessibilityAttribute( |
| browserAccessibility_->attributes(), |
| WebAccessibility::ATTR_HTML_TAG); |
| if ([headingLevel length] >= 2) { |
| return [NSNumber numberWithInt: |
| [[headingLevel substringFromIndex:1] intValue]]; |
| } |
| } else if ([self role] == NSAccessibilityCheckBoxRole) { |
| return [NSNumber numberWithInt:GetState( |
| browserAccessibility_, WebAccessibility::STATE_CHECKED) ? 1 : 0]; |
| } else { |
| return base::SysUTF16ToNSString(browserAccessibility_->value()); |
| } |
| } |
| if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) { |
| return [self roleDescription]; |
| } |
| if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { |
| NSNumber* ret = [NSNumber numberWithBool: |
| GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSED)]; |
| return ret; |
| } |
| if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) { |
| return [NSNumber numberWithBool: |
| !GetState(browserAccessibility_, WebAccessibility::STATE_UNAVAILABLE)]; |
| } |
| |
| // AXWebArea attributes. |
| if ([attribute isEqualToString:@"AXLoaded"]) |
| return [NSNumber numberWithBool:YES]; |
| if ([attribute isEqualToString:@"AXURL"]) { |
| return NSStringForWebAccessibilityAttribute( |
| browserAccessibility_->attributes(), |
| WebAccessibility::ATTR_DOC_URL); |
| } |
| |
| // TODO(dtseng): provide complete implementations for the following. |
| if ([attribute isEqualToString:@"AXVisited"]) |
| return [NSNumber numberWithBool:NO]; |
| |
| // Text related attributes. |
| if ([attribute isEqualToString: |
| NSAccessibilityNumberOfCharactersAttribute]) { |
| return [NSNumber numberWithInt:browserAccessibility_->value().length()]; |
| } |
| if ([attribute isEqualToString: |
| NSAccessibilityVisibleCharacterRangeAttribute]) { |
| return [NSValue valueWithRange: |
| NSMakeRange(0, browserAccessibility_->value().length())]; |
| } |
| |
| int selStart, selEnd; |
| if (browserAccessibility_-> |
| GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_START, &selStart) && |
| browserAccessibility_-> |
| GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_END, &selEnd)) { |
| if (selStart > selEnd) |
| std::swap(selStart, selEnd); |
| int selLength = selEnd - selStart; |
| if ([attribute isEqualToString: |
| NSAccessibilityInsertionPointLineNumberAttribute]) { |
| return [NSNumber numberWithInt:0]; |
| } |
| if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) { |
| return base::SysUTF16ToNSString(browserAccessibility_->value().substr( |
| selStart, selLength)); |
| } |
| if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { |
| return [NSValue valueWithRange:NSMakeRange(selStart, selLength)]; |
| } |
| } |
| return nil; |
| } |
| |
| // Returns an array of action names that this object will respond to. |
| - (NSArray*)accessibilityActionNames { |
| NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; |
| |
| // General actions. |
| [ret addObject:NSAccessibilityShowMenuAction]; |
| |
| // TODO(dtseng): this should only get set when there's a default action. |
| if ([self role] != NSAccessibilityStaticTextRole) |
| [ret addObject:NSAccessibilityPressAction]; |
| |
| return ret; |
| } |
| |
| // Returns a sub-array of values for the given attribute value, starting at |
| // index, with up to maxCount items. If the given index is out of bounds, |
| // or there are no values for the given attribute, it will return nil. |
| // This method is used for querying subsets of values, without having to |
| // return a large set of data, such as elements with a large number of |
| // children. |
| - (NSArray*)accessibilityArrayAttributeValues:(NSString*)attribute |
| index:(NSUInteger)index |
| maxCount:(NSUInteger)maxCount { |
| NSArray* fullArray = [self accessibilityAttributeValue:attribute]; |
| if (!fullArray) |
| return nil; |
| NSUInteger arrayCount = [fullArray count]; |
| if (index >= arrayCount) |
| return nil; |
| NSRange subRange; |
| if ((index + maxCount) > arrayCount) { |
| subRange = NSMakeRange(index, arrayCount - index); |
| } else { |
| subRange = NSMakeRange(index, maxCount); |
| } |
| return [fullArray subarrayWithRange:subRange]; |
| } |
| |
| // Returns the count of the specified accessibility array attribute. |
| - (NSUInteger)accessibilityArrayAttributeCount:(NSString*)attribute { |
| NSArray* fullArray = [self accessibilityAttributeValue:attribute]; |
| return [fullArray count]; |
| } |
| |
| // Returns the list of accessibility attributes that this object supports. |
| - (NSArray*)accessibilityAttributeNames { |
| NSMutableArray* ret = [[NSMutableArray alloc] init]; |
| |
| // General attributes. |
| [ret addObjectsFromArray:[NSArray arrayWithObjects: |
| NSAccessibilityChildrenAttribute, |
| NSAccessibilityDescriptionAttribute, |
| NSAccessibilityEnabledAttribute, |
| NSAccessibilityFocusedAttribute, |
| NSAccessibilityHelpAttribute, |
| NSAccessibilityParentAttribute, |
| NSAccessibilityPositionAttribute, |
| NSAccessibilityRoleAttribute, |
| NSAccessibilityRoleDescriptionAttribute, |
| NSAccessibilitySizeAttribute, |
| NSAccessibilityTitleAttribute, |
| NSAccessibilityTopLevelUIElementAttribute, |
| NSAccessibilityValueAttribute, |
| NSAccessibilityWindowAttribute, |
| nil]]; |
| |
| // Specific role attributes. |
| if ([self role] == @"AXWebArea") { |
| [ret addObjectsFromArray:[NSArray arrayWithObjects: |
| @"AXLoaded", |
| @"AXURL", |
| @"AXVisited", |
| nil]]; |
| } |
| |
| if ([self role] == NSAccessibilityTextFieldRole) { |
| [ret addObjectsFromArray:[NSArray arrayWithObjects: |
| NSAccessibilityInsertionPointLineNumberAttribute, |
| NSAccessibilityNumberOfCharactersAttribute, |
| NSAccessibilitySelectedTextAttribute, |
| NSAccessibilitySelectedTextRangeAttribute, |
| NSAccessibilityVisibleCharacterRangeAttribute, |
| nil]]; |
| } |
| return ret; |
| } |
| |
| // Returns the index of the child in this objects array of children. |
| - (NSUInteger)accessibilityIndexOfChild:(id)child { |
| NSUInteger index = 0; |
| for (BrowserAccessibilityCocoa* childToCheck in [self children]) { |
| if ([child isEqual:childToCheck]) |
| return index; |
| ++index; |
| } |
| return NSNotFound; |
| } |
| |
| // Returns whether or not the specified attribute can be set by the |
| // accessibility API via |accessibilitySetValue:forAttribute:|. |
| - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { |
| if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) |
| return GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSABLE); |
| if ([attribute isEqualToString:NSAccessibilityValueAttribute]) |
| return !GetState(browserAccessibility_, WebAccessibility::STATE_READONLY); |
| return NO; |
| } |
| |
| // Returns whether or not this object should be ignored in the accessibilty |
| // tree. |
| - (BOOL)accessibilityIsIgnored { |
| return [self isIgnored]; |
| } |
| |
| // Performs the given accessibilty action on the webkit accessibility object |
| // that backs this object. |
| - (void)accessibilityPerformAction:(NSString*)action { |
| // TODO(feldstein): Support more actions. |
| [delegate_ doDefaultAction:browserAccessibility_->renderer_id()]; |
| } |
| |
| // Returns the description of the given action. |
| - (NSString*)accessibilityActionDescription:(NSString*)action { |
| return NSAccessibilityActionDescription(action); |
| } |
| |
| // Sets an override value for a specific accessibility attribute. |
| // This class does not support this. |
| - (BOOL)accessibilitySetOverrideValue:(id)value |
| forAttribute:(NSString*)attribute { |
| return NO; |
| } |
| |
| // Sets the value for an accessibility attribute via the accessibility API. |
| - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { |
| if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { |
| NSNumber* focusedNumber = value; |
| BOOL focused = [focusedNumber intValue]; |
| [delegate_ setAccessibilityFocus:focused |
| accessibilityId:browserAccessibility_->renderer_id()]; |
| } |
| } |
| |
| // Returns the deepest accessibility child that should not be ignored. |
| // It is assumed that the hit test has been narrowed down to this object |
| // or one of its children, so this will never return nil. |
| - (id)accessibilityHitTest:(NSPoint)point { |
| id hit = self; |
| for (id child in [self children]) { |
| NSPoint origin = [child origin]; |
| NSSize size = [child size]; |
| NSRect rect; |
| rect.origin = origin; |
| rect.size = size; |
| if (NSPointInRect(point, rect)) { |
| hit = child; |
| id childResult = [child accessibilityHitTest:point]; |
| if (![childResult accessibilityIsIgnored]) { |
| hit = childResult; |
| break; |
| } |
| } |
| } |
| return NSAccessibilityUnignoredAncestor(hit); |
| } |
| |
| - (BOOL)isEqual:(id)object { |
| if (![object isKindOfClass:[BrowserAccessibilityCocoa class]]) |
| return NO; |
| return ([self hash] == [object hash]); |
| } |
| |
| - (NSUInteger)hash { |
| // Potentially called during dealloc. |
| if (!browserAccessibility_) |
| return [super hash]; |
| return browserAccessibility_->renderer_id(); |
| } |
| |
| @end |
| |