browser_accessibility_cocoa.mm revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 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_->GetChildCount()] );
97    for (uint32 index = 0;
98         index < browserAccessibility_->GetChildCount();
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_->GetParent()->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_->GetParent()) {
205      return NSAccessibilityUnignoredAncestor(
206          browserAccessibility_->GetParent()->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      return [NSNumber numberWithInt:GetState(
238          browserAccessibility_, WebAccessibility::STATE_CHECKED) ? 1 : 0];
239    } else {
240      return base::SysUTF16ToNSString(browserAccessibility_->value());
241    }
242  }
243  if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) {
244    return [self roleDescription];
245  }
246  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
247    NSNumber* ret = [NSNumber numberWithBool:
248        GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSED)];
249    return ret;
250  }
251  if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) {
252    return [NSNumber numberWithBool:
253        !GetState(browserAccessibility_, WebAccessibility::STATE_UNAVAILABLE)];
254  }
255  if ([attribute isEqualToString:@"AXVisited"]) {
256    return [NSNumber numberWithBool:
257        GetState(browserAccessibility_, WebAccessibility::STATE_TRAVERSED)];
258  }
259
260  // AXWebArea attributes.
261  if ([attribute isEqualToString:@"AXLoaded"])
262    return [NSNumber numberWithBool:YES];
263  if ([attribute isEqualToString:@"AXURL"]) {
264    WebAccessibility::Attribute urlAttribute =
265        [[self role] isEqualToString:@"AXWebArea"] ?
266            WebAccessibility::ATTR_DOC_URL :
267            WebAccessibility::ATTR_URL;
268    return NSStringForWebAccessibilityAttribute(
269        browserAccessibility_->attributes(),
270        urlAttribute);
271  }
272
273  // Text related attributes.
274  if ([attribute isEqualToString:
275      NSAccessibilityNumberOfCharactersAttribute]) {
276    return [NSNumber numberWithInt:browserAccessibility_->value().length()];
277  }
278  if ([attribute isEqualToString:
279      NSAccessibilityVisibleCharacterRangeAttribute]) {
280    return [NSValue valueWithRange:
281        NSMakeRange(0, browserAccessibility_->value().length())];
282  }
283
284  int selStart, selEnd;
285  if (browserAccessibility_->
286          GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_START, &selStart) &&
287      browserAccessibility_->
288          GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_END, &selEnd)) {
289    if (selStart > selEnd)
290      std::swap(selStart, selEnd);
291    int selLength = selEnd - selStart;
292    if ([attribute isEqualToString:
293        NSAccessibilityInsertionPointLineNumberAttribute]) {
294      return [NSNumber numberWithInt:0];
295    }
296    if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
297      return base::SysUTF16ToNSString(browserAccessibility_->value().substr(
298          selStart, selLength));
299    }
300    if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
301      return [NSValue valueWithRange:NSMakeRange(selStart, selLength)];
302    }
303  }
304  return nil;
305}
306
307// Returns an array of action names that this object will respond to.
308- (NSArray*)accessibilityActionNames {
309  NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease];
310
311  // General actions.
312  [ret addObject:NSAccessibilityShowMenuAction];
313
314  // TODO(dtseng): this should only get set when there's a default action.
315  if ([self role] != NSAccessibilityStaticTextRole &&
316      [self role] != NSAccessibilityTextAreaRole &&
317      [self role] != NSAccessibilityTextFieldRole) {
318    [ret addObject:NSAccessibilityPressAction];
319  }
320
321  return ret;
322}
323
324// Returns a sub-array of values for the given attribute value, starting at
325// index, with up to maxCount items.  If the given index is out of bounds,
326// or there are no values for the given attribute, it will return nil.
327// This method is used for querying subsets of values, without having to
328// return a large set of data, such as elements with a large number of
329// children.
330- (NSArray*)accessibilityArrayAttributeValues:(NSString*)attribute
331                                        index:(NSUInteger)index
332                                     maxCount:(NSUInteger)maxCount {
333  NSArray* fullArray = [self accessibilityAttributeValue:attribute];
334  if (!fullArray)
335    return nil;
336  NSUInteger arrayCount = [fullArray count];
337  if (index >= arrayCount)
338    return nil;
339  NSRange subRange;
340  if ((index + maxCount) > arrayCount) {
341    subRange = NSMakeRange(index, arrayCount - index);
342  } else {
343    subRange = NSMakeRange(index, maxCount);
344  }
345  return [fullArray subarrayWithRange:subRange];
346}
347
348// Returns the count of the specified accessibility array attribute.
349- (NSUInteger)accessibilityArrayAttributeCount:(NSString*)attribute {
350  NSArray* fullArray = [self accessibilityAttributeValue:attribute];
351  return [fullArray count];
352}
353
354// Returns the list of accessibility attributes that this object supports.
355- (NSArray*)accessibilityAttributeNames {
356  NSMutableArray* ret = [[NSMutableArray alloc] init];
357
358  // General attributes.
359  [ret addObjectsFromArray:[NSArray arrayWithObjects:
360      NSAccessibilityChildrenAttribute,
361      NSAccessibilityDescriptionAttribute,
362      NSAccessibilityEnabledAttribute,
363      NSAccessibilityFocusedAttribute,
364      NSAccessibilityHelpAttribute,
365      NSAccessibilityParentAttribute,
366      NSAccessibilityPositionAttribute,
367      NSAccessibilityRoleAttribute,
368      NSAccessibilityRoleDescriptionAttribute,
369      NSAccessibilitySizeAttribute,
370      NSAccessibilityTitleAttribute,
371      NSAccessibilityTopLevelUIElementAttribute,
372      NSAccessibilityValueAttribute,
373      NSAccessibilityWindowAttribute,
374      @"AXURL",
375      @"AXVisited",
376      nil]];
377
378  // Specific role attributes.
379  if ([self role] == @"AXWebArea") {
380    [ret addObjectsFromArray:[NSArray arrayWithObjects:
381        @"AXLoaded",
382        nil]];
383  }
384
385  if ([self role] == NSAccessibilityTextFieldRole) {
386    [ret addObjectsFromArray:[NSArray arrayWithObjects:
387        NSAccessibilityInsertionPointLineNumberAttribute,
388        NSAccessibilityNumberOfCharactersAttribute,
389        NSAccessibilitySelectedTextAttribute,
390        NSAccessibilitySelectedTextRangeAttribute,
391        NSAccessibilityVisibleCharacterRangeAttribute,
392        nil]];
393  }
394  return ret;
395}
396
397// Returns the index of the child in this objects array of children.
398- (NSUInteger)accessibilityIndexOfChild:(id)child {
399  NSUInteger index = 0;
400  for (BrowserAccessibilityCocoa* childToCheck in [self children]) {
401    if ([child isEqual:childToCheck])
402      return index;
403    ++index;
404  }
405  return NSNotFound;
406}
407
408// Returns whether or not the specified attribute can be set by the
409// accessibility API via |accessibilitySetValue:forAttribute:|.
410- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
411  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute])
412    return GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSABLE);
413  if ([attribute isEqualToString:NSAccessibilityValueAttribute])
414    return !GetState(browserAccessibility_, WebAccessibility::STATE_READONLY);
415  return NO;
416}
417
418// Returns whether or not this object should be ignored in the accessibilty
419// tree.
420- (BOOL)accessibilityIsIgnored {
421  return [self isIgnored];
422}
423
424// Performs the given accessibilty action on the webkit accessibility object
425// that backs this object.
426- (void)accessibilityPerformAction:(NSString*)action {
427  // TODO(feldstein): Support more actions.
428  if ([action isEqualToString:NSAccessibilityPressAction]) {
429    [delegate_ doDefaultAction:browserAccessibility_->renderer_id()];
430  } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) {
431    // TODO(dtseng): implement.
432  }
433}
434
435// Returns the description of the given action.
436- (NSString*)accessibilityActionDescription:(NSString*)action {
437  return NSAccessibilityActionDescription(action);
438}
439
440// Sets an override value for a specific accessibility attribute.
441// This class does not support this.
442- (BOOL)accessibilitySetOverrideValue:(id)value
443                         forAttribute:(NSString*)attribute {
444  return NO;
445}
446
447// Sets the value for an accessibility attribute via the accessibility API.
448- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
449  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
450    NSNumber* focusedNumber = value;
451    BOOL focused = [focusedNumber intValue];
452    [delegate_ setAccessibilityFocus:focused
453                     accessibilityId:browserAccessibility_->renderer_id()];
454  }
455}
456
457// Returns the deepest accessibility child that should not be ignored.
458// It is assumed that the hit test has been narrowed down to this object
459// or one of its children, so this will never return nil.
460- (id)accessibilityHitTest:(NSPoint)point {
461  id hit = self;
462  for (id child in [self children]) {
463    NSPoint origin = [child origin];
464    NSSize size = [child size];
465    NSRect rect;
466    rect.origin = origin;
467    rect.size = size;
468    if (NSPointInRect(point, rect)) {
469      hit = child;
470      id childResult = [child accessibilityHitTest:point];
471      if (![childResult accessibilityIsIgnored]) {
472        hit = childResult;
473        break;
474      }
475    }
476  }
477  return NSAccessibilityUnignoredAncestor(hit);
478}
479
480- (BOOL)isEqual:(id)object {
481  if (![object isKindOfClass:[BrowserAccessibilityCocoa class]])
482    return NO;
483  return ([self hash] == [object hash]);
484}
485
486- (NSUInteger)hash {
487  // Potentially called during dealloc.
488  if (!browserAccessibility_)
489    return [super hash];
490  return browserAccessibility_->renderer_id();
491}
492
493@end
494
495