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 16 namespace { 17 18 // Returns an autoreleased copy of the WebAccessibility's attribute. 19 NSString* 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 31 struct RoleEntry { 32 WebAccessibility::Role value; 33 NSString* string; 34 }; 35 36 static 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. 64 bool 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