1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include <execinfo.h> 6 7#import "chrome/browser/accessibility/browser_accessibility_cocoa.h" 8 9#include "base/string16.h" 10#include "base/sys_string_conversions.h" 11#include "chrome/browser/renderer_host/render_widget_host_view_mac.h" 12#include "grit/webkit_strings.h" 13#include "third_party/WebKit/Source/WebKit/chromium/public/WebRect.h" 14#include "ui/base/l10n/l10n_util_mac.h" 15 16namespace { 17 18// Returns an autoreleased copy of the WebAccessibility's attribute. 19NSString* NSStringForWebAccessibilityAttribute( 20 const std::map<int32, string16>& attributes, 21 WebAccessibility::Attribute attribute) { 22 std::map<int32, string16>::const_iterator iter = 23 attributes.find(attribute); 24 NSString* returnValue = @""; 25 if (iter != attributes.end()) { 26 returnValue = base::SysUTF16ToNSString(iter->second); 27 } 28 return returnValue; 29} 30 31struct RoleEntry { 32 WebAccessibility::Role value; 33 NSString* string; 34}; 35 36static const RoleEntry roles[] = { 37 { WebAccessibility::ROLE_NONE, NSAccessibilityUnknownRole }, 38 { WebAccessibility::ROLE_BUTTON, NSAccessibilityButtonRole }, 39 { WebAccessibility::ROLE_CHECKBOX, NSAccessibilityCheckBoxRole }, 40 { WebAccessibility::ROLE_COLUMN, NSAccessibilityColumnRole }, 41 { WebAccessibility::ROLE_GRID, NSAccessibilityGridRole }, 42 { WebAccessibility::ROLE_GROUP, NSAccessibilityGroupRole }, 43 { WebAccessibility::ROLE_HEADING, @"AXHeading" }, 44 { WebAccessibility::ROLE_IGNORED, NSAccessibilityUnknownRole }, 45 { WebAccessibility::ROLE_IMAGE, NSAccessibilityImageRole }, 46 { WebAccessibility::ROLE_LINK, NSAccessibilityLinkRole }, 47 { WebAccessibility::ROLE_LIST, NSAccessibilityListRole }, 48 { WebAccessibility::ROLE_RADIO_BUTTON, NSAccessibilityRadioButtonRole }, 49 { WebAccessibility::ROLE_RADIO_GROUP, NSAccessibilityRadioGroupRole }, 50 { WebAccessibility::ROLE_ROW, NSAccessibilityRowRole }, 51 { WebAccessibility::ROLE_SCROLLAREA, NSAccessibilityScrollAreaRole }, 52 { WebAccessibility::ROLE_SCROLLBAR, NSAccessibilityScrollBarRole }, 53 { WebAccessibility::ROLE_STATIC_TEXT, NSAccessibilityStaticTextRole }, 54 { WebAccessibility::ROLE_TABLE, NSAccessibilityTableRole }, 55 { WebAccessibility::ROLE_TAB_GROUP, NSAccessibilityTabGroupRole }, 56 { WebAccessibility::ROLE_TEXT_FIELD, NSAccessibilityTextFieldRole }, 57 { WebAccessibility::ROLE_TEXTAREA, NSAccessibilityTextAreaRole }, 58 { WebAccessibility::ROLE_WEB_AREA, @"AXWebArea" }, 59 { WebAccessibility::ROLE_WEBCORE_LINK, NSAccessibilityLinkRole }, 60}; 61 62// GetState checks the bitmask used in webaccessibility.h to check 63// if the given state was set on the accessibility object. 64bool GetState(BrowserAccessibility* accessibility, int state) { 65 return ((accessibility->state() >> state) & 1); 66} 67 68} // namespace 69 70@implementation BrowserAccessibilityCocoa 71 72- (id)initWithObject:(BrowserAccessibility*)accessibility 73 delegate:(id<BrowserAccessibilityDelegateCocoa>)delegate { 74 if ((self = [super init])) { 75 browserAccessibility_ = accessibility; 76 delegate_ = delegate; 77 } 78 return self; 79} 80 81// Deletes our associated BrowserAccessibilityMac. 82- (void)dealloc { 83 if (browserAccessibility_) { 84 delete browserAccessibility_; 85 browserAccessibility_ = NULL; 86 } 87 88 [super dealloc]; 89} 90 91// Returns an array of BrowserAccessibilityCocoa objects, representing the 92// accessibility children of this object. 93- (NSArray*)children { 94 if (!children_.get()) { 95 children_.reset([[NSMutableArray alloc] 96 initWithCapacity:browserAccessibility_->child_count()] ); 97 for (uint32 index = 0; 98 index < browserAccessibility_->child_count(); 99 ++index) { 100 BrowserAccessibilityCocoa* child = 101 browserAccessibility_->GetChild(index)->toBrowserAccessibilityCocoa(); 102 if ([child isIgnored]) 103 [children_ addObjectsFromArray:[child children]]; 104 else 105 [children_ addObject:child]; 106 } 107 } 108 return children_; 109} 110 111- (void)childrenChanged { 112 if (![self isIgnored]) { 113 children_.reset(); 114 } else { 115 [browserAccessibility_->parent()->toBrowserAccessibilityCocoa() 116 childrenChanged]; 117 } 118} 119 120// Returns whether or not this node should be ignored in the 121// accessibility tree. 122- (BOOL)isIgnored { 123 return [self role] == NSAccessibilityUnknownRole; 124} 125 126// The origin of this accessibility object in the page's document. 127// This is relative to webkit's top-left origin, not Cocoa's 128// bottom-left origin. 129- (NSPoint)origin { 130 return NSMakePoint(browserAccessibility_->location().x(), 131 browserAccessibility_->location().y()); 132} 133 134// Returns a string indicating the role of this object. 135- (NSString*)role { 136 WebAccessibility::Role value = 137 static_cast<WebAccessibility::Role>( browserAccessibility_->role()); 138 139 // Roles that we only determine at runtime. 140 if (value == WebAccessibility::ROLE_TEXT_FIELD && 141 GetState(browserAccessibility_, WebAccessibility::STATE_PROTECTED)) { 142 return @"AXSecureTextField"; 143 } 144 145 NSString* role = NSAccessibilityUnknownRole; 146 const size_t numRoles = sizeof(roles) / sizeof(roles[0]); 147 for (size_t i = 0; i < numRoles; ++i) { 148 if (roles[i].value == value) { 149 role = roles[i].string; 150 break; 151 } 152 } 153 154 return role; 155} 156 157// Returns a string indicating the role description of this object. 158- (NSString*)roleDescription { 159 // The following descriptions are specific to webkit. 160 if ([[self role] isEqualToString:@"AXWebArea"]) 161 return l10n_util::GetNSString(IDS_AX_ROLE_WEB_AREA); 162 163 if ([[self role] isEqualToString:@"NSAccessibilityLinkRole"]) 164 return l10n_util::GetNSString(IDS_AX_ROLE_LINK); 165 166 if ([[self role] isEqualToString:@"AXHeading"]) 167 return l10n_util::GetNSString(IDS_AX_ROLE_HEADING); 168 169 return NSAccessibilityRoleDescription([self role], nil); 170} 171 172// Returns the size of this object. 173- (NSSize)size { 174 return NSMakeSize(browserAccessibility_->location().width(), 175 browserAccessibility_->location().height()); 176} 177 178// Returns the accessibility value for the given attribute. If the value isn't 179// supported this will return nil. 180- (id)accessibilityAttributeValue:(NSString*)attribute { 181 if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { 182 return [self role]; 183 } 184 if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) { 185 return NSStringForWebAccessibilityAttribute( 186 browserAccessibility_->attributes(), 187 WebAccessibility::ATTR_DESCRIPTION); 188 } 189 if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) { 190 return [NSValue valueWithPoint:[delegate_ accessibilityPointInScreen:self]]; 191 } 192 if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) { 193 return [NSValue valueWithSize:[self size]]; 194 } 195 if ([attribute isEqualToString:NSAccessibilityTopLevelUIElementAttribute] || 196 [attribute isEqualToString:NSAccessibilityWindowAttribute]) { 197 return [delegate_ window]; 198 } 199 if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) { 200 return [self children]; 201 } 202 if ([attribute isEqualToString:NSAccessibilityParentAttribute]) { 203 // A nil parent means we're the root. 204 if (browserAccessibility_->parent()) { 205 return NSAccessibilityUnignoredAncestor( 206 browserAccessibility_->parent()->toBrowserAccessibilityCocoa()); 207 } else { 208 // Hook back up to RenderWidgetHostViewCocoa. 209 return browserAccessibility_->manager()->GetParentView(); 210 } 211 } 212 if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) { 213 return base::SysUTF16ToNSString(browserAccessibility_->name()); 214 } 215 if ([attribute isEqualToString:NSAccessibilityHelpAttribute]) { 216 return NSStringForWebAccessibilityAttribute( 217 browserAccessibility_->attributes(), 218 WebAccessibility::ATTR_HELP); 219 } 220 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { 221 // WebCore uses an attachmentView to get the below behavior. 222 // We do not have any native views backing this object, so need 223 // to approximate Cocoa ax behavior best as we can. 224 if ([self role] == @"AXHeading") { 225 NSString* headingLevel = 226 NSStringForWebAccessibilityAttribute( 227 browserAccessibility_->attributes(), 228 WebAccessibility::ATTR_HTML_TAG); 229 if ([headingLevel length] >= 2) { 230 return [NSNumber numberWithInt: 231 [[headingLevel substringFromIndex:1] intValue]]; 232 } 233 } else if ([self role] == NSAccessibilityButtonRole) { 234 // AXValue does not make sense for pure buttons. 235 return @""; 236 } else if ([self role] == NSAccessibilityCheckBoxRole || 237 [self role] == NSAccessibilityRadioButtonRole) { 238 return [NSNumber numberWithInt:GetState( 239 browserAccessibility_, WebAccessibility::STATE_CHECKED) ? 1 : 0]; 240 } else { 241 return base::SysUTF16ToNSString(browserAccessibility_->value()); 242 } 243 } 244 if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) { 245 return [self roleDescription]; 246 } 247 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { 248 NSNumber* ret = [NSNumber numberWithBool: 249 GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSED)]; 250 return ret; 251 } 252 if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) { 253 return [NSNumber numberWithBool: 254 !GetState(browserAccessibility_, WebAccessibility::STATE_UNAVAILABLE)]; 255 } 256 if ([attribute isEqualToString:@"AXVisited"]) { 257 return [NSNumber numberWithBool: 258 GetState(browserAccessibility_, WebAccessibility::STATE_TRAVERSED)]; 259 } 260 261 // AXWebArea attributes. 262 if ([attribute isEqualToString:@"AXLoaded"]) 263 return [NSNumber numberWithBool:YES]; 264 if ([attribute isEqualToString:@"AXURL"]) { 265 WebAccessibility::Attribute urlAttribute = 266 [[self role] isEqualToString:@"AXWebArea"] ? 267 WebAccessibility::ATTR_DOC_URL : 268 WebAccessibility::ATTR_URL; 269 return NSStringForWebAccessibilityAttribute( 270 browserAccessibility_->attributes(), 271 urlAttribute); 272 } 273 274 // Text related attributes. 275 if ([attribute isEqualToString: 276 NSAccessibilityNumberOfCharactersAttribute]) { 277 return [NSNumber numberWithInt:browserAccessibility_->value().length()]; 278 } 279 if ([attribute isEqualToString: 280 NSAccessibilityVisibleCharacterRangeAttribute]) { 281 return [NSValue valueWithRange: 282 NSMakeRange(0, browserAccessibility_->value().length())]; 283 } 284 285 int selStart, selEnd; 286 if (browserAccessibility_-> 287 GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_START, &selStart) && 288 browserAccessibility_-> 289 GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_END, &selEnd)) { 290 if (selStart > selEnd) 291 std::swap(selStart, selEnd); 292 int selLength = selEnd - selStart; 293 if ([attribute isEqualToString: 294 NSAccessibilityInsertionPointLineNumberAttribute]) { 295 return [NSNumber numberWithInt:0]; 296 } 297 if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) { 298 return base::SysUTF16ToNSString(browserAccessibility_->value().substr( 299 selStart, selLength)); 300 } 301 if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) { 302 return [NSValue valueWithRange:NSMakeRange(selStart, selLength)]; 303 } 304 } 305 return nil; 306} 307 308// Returns an array of action names that this object will respond to. 309- (NSArray*)accessibilityActionNames { 310 NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; 311 312 // General actions. 313 [ret addObject:NSAccessibilityShowMenuAction]; 314 315 // TODO(dtseng): this should only get set when there's a default action. 316 if ([self role] != NSAccessibilityStaticTextRole && 317 [self role] != NSAccessibilityTextAreaRole && 318 [self role] != NSAccessibilityTextFieldRole) { 319 [ret addObject:NSAccessibilityPressAction]; 320 } 321 322 return ret; 323} 324 325// Returns a sub-array of values for the given attribute value, starting at 326// index, with up to maxCount items. If the given index is out of bounds, 327// or there are no values for the given attribute, it will return nil. 328// This method is used for querying subsets of values, without having to 329// return a large set of data, such as elements with a large number of 330// children. 331- (NSArray*)accessibilityArrayAttributeValues:(NSString*)attribute 332 index:(NSUInteger)index 333 maxCount:(NSUInteger)maxCount { 334 NSArray* fullArray = [self accessibilityAttributeValue:attribute]; 335 if (!fullArray) 336 return nil; 337 NSUInteger arrayCount = [fullArray count]; 338 if (index >= arrayCount) 339 return nil; 340 NSRange subRange; 341 if ((index + maxCount) > arrayCount) { 342 subRange = NSMakeRange(index, arrayCount - index); 343 } else { 344 subRange = NSMakeRange(index, maxCount); 345 } 346 return [fullArray subarrayWithRange:subRange]; 347} 348 349// Returns the count of the specified accessibility array attribute. 350- (NSUInteger)accessibilityArrayAttributeCount:(NSString*)attribute { 351 NSArray* fullArray = [self accessibilityAttributeValue:attribute]; 352 return [fullArray count]; 353} 354 355// Returns the list of accessibility attributes that this object supports. 356- (NSArray*)accessibilityAttributeNames { 357 NSMutableArray* ret = [[NSMutableArray alloc] init]; 358 359 // General attributes. 360 [ret addObjectsFromArray:[NSArray arrayWithObjects: 361 NSAccessibilityChildrenAttribute, 362 NSAccessibilityDescriptionAttribute, 363 NSAccessibilityEnabledAttribute, 364 NSAccessibilityFocusedAttribute, 365 NSAccessibilityHelpAttribute, 366 NSAccessibilityParentAttribute, 367 NSAccessibilityPositionAttribute, 368 NSAccessibilityRoleAttribute, 369 NSAccessibilityRoleDescriptionAttribute, 370 NSAccessibilitySizeAttribute, 371 NSAccessibilityTitleAttribute, 372 NSAccessibilityTopLevelUIElementAttribute, 373 NSAccessibilityValueAttribute, 374 NSAccessibilityWindowAttribute, 375 @"AXURL", 376 @"AXVisited", 377 nil]]; 378 379 // Specific role attributes. 380 if ([self role] == @"AXWebArea") { 381 [ret addObjectsFromArray:[NSArray arrayWithObjects: 382 @"AXLoaded", 383 nil]]; 384 } 385 386 if ([self role] == NSAccessibilityTextFieldRole) { 387 [ret addObjectsFromArray:[NSArray arrayWithObjects: 388 NSAccessibilityInsertionPointLineNumberAttribute, 389 NSAccessibilityNumberOfCharactersAttribute, 390 NSAccessibilitySelectedTextAttribute, 391 NSAccessibilitySelectedTextRangeAttribute, 392 NSAccessibilityVisibleCharacterRangeAttribute, 393 nil]]; 394 } 395 return ret; 396} 397 398// Returns the index of the child in this objects array of children. 399- (NSUInteger)accessibilityGetIndexOf:(id)child { 400 NSUInteger index = 0; 401 for (BrowserAccessibilityCocoa* childToCheck in [self children]) { 402 if ([child isEqual:childToCheck]) 403 return index; 404 ++index; 405 } 406 return NSNotFound; 407} 408 409// Returns whether or not the specified attribute can be set by the 410// accessibility API via |accessibilitySetValue:forAttribute:|. 411- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { 412 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) 413 return GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSABLE); 414 if ([attribute isEqualToString:NSAccessibilityValueAttribute]) 415 return !GetState(browserAccessibility_, WebAccessibility::STATE_READONLY); 416 return NO; 417} 418 419// Returns whether or not this object should be ignored in the accessibilty 420// tree. 421- (BOOL)accessibilityIsIgnored { 422 return [self isIgnored]; 423} 424 425// Performs the given accessibilty action on the webkit accessibility object 426// that backs this object. 427- (void)accessibilityPerformAction:(NSString*)action { 428 // TODO(feldstein): Support more actions. 429 if ([action isEqualToString:NSAccessibilityPressAction]) { 430 [delegate_ doDefaultAction:browserAccessibility_->renderer_id()]; 431 } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) { 432 // TODO(dtseng): implement. 433 } 434} 435 436// Returns the description of the given action. 437- (NSString*)accessibilityActionDescription:(NSString*)action { 438 return NSAccessibilityActionDescription(action); 439} 440 441// Sets an override value for a specific accessibility attribute. 442// This class does not support this. 443- (BOOL)accessibilitySetOverrideValue:(id)value 444 forAttribute:(NSString*)attribute { 445 return NO; 446} 447 448// Sets the value for an accessibility attribute via the accessibility API. 449- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { 450 if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { 451 NSNumber* focusedNumber = value; 452 BOOL focused = [focusedNumber intValue]; 453 [delegate_ setAccessibilityFocus:focused 454 accessibilityId:browserAccessibility_->renderer_id()]; 455 } 456} 457 458// Returns the deepest accessibility child that should not be ignored. 459// It is assumed that the hit test has been narrowed down to this object 460// or one of its children, so this will never return nil. 461- (id)accessibilityHitTest:(NSPoint)point { 462 id hit = self; 463 for (id child in [self children]) { 464 NSPoint origin = [child origin]; 465 NSSize size = [child size]; 466 NSRect rect; 467 rect.origin = origin; 468 rect.size = size; 469 if (NSPointInRect(point, rect)) { 470 hit = child; 471 id childResult = [child accessibilityHitTest:point]; 472 if (![childResult accessibilityIsIgnored]) { 473 hit = childResult; 474 break; 475 } 476 } 477 } 478 return NSAccessibilityUnignoredAncestor(hit); 479} 480 481- (BOOL)isEqual:(id)object { 482 if (![object isKindOfClass:[BrowserAccessibilityCocoa class]]) 483 return NO; 484 return ([self hash] == [object hash]); 485} 486 487- (NSUInteger)hash { 488 // Potentially called during dealloc. 489 if (!browserAccessibility_) 490 return [super hash]; 491 return browserAccessibility_->renderer_id(); 492} 493 494@end 495 496