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