Home | History | Annotate | Download | only in accessibility
      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