// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/accessibility/accessibility_tree_formatter.h" #import #include "base/basictypes.h" #include "base/files/file_path.h" #include "base/json/json_writer.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #include "content/browser/accessibility/browser_accessibility_cocoa.h" #include "content/browser/accessibility/browser_accessibility_mac.h" #include "content/browser/accessibility/browser_accessibility_manager.h" using base::StringPrintf; using base::SysNSStringToUTF8; using base::SysNSStringToUTF16; using std::string; namespace content { namespace { const char* kPositionDictAttr = "position"; const char* kXCoordDictAttr = "x"; const char* kYCoordDictAttr = "y"; const char* kSizeDictAttr = "size"; const char* kWidthDictAttr = "width"; const char* kHeightDictAttr = "height"; const char* kRangeLocDictAttr = "loc"; const char* kRangeLenDictAttr = "len"; scoped_ptr PopulatePosition( const BrowserAccessibility& node) { scoped_ptr position(new base::DictionaryValue); // The NSAccessibility position of an object is in global coordinates and // based on the lower-left corner of the object. To make this easier and less // confusing, convert it to local window coordinates using the top-left // corner when dumping the position. BrowserAccessibility* root = node.manager()->GetRoot(); BrowserAccessibilityCocoa* cocoa_root = root->ToBrowserAccessibilityCocoa(); NSPoint root_position = [[cocoa_root position] pointValue]; NSSize root_size = [[cocoa_root size] sizeValue]; int root_top = -static_cast(root_position.y + root_size.height); int root_left = static_cast(root_position.x); BrowserAccessibilityCocoa* cocoa_node = const_cast(&node)->ToBrowserAccessibilityCocoa(); NSPoint node_position = [[cocoa_node position] pointValue]; NSSize node_size = [[cocoa_node size] sizeValue]; position->SetInteger(kXCoordDictAttr, static_cast(node_position.x - root_left)); position->SetInteger(kYCoordDictAttr, static_cast(-node_position.y - node_size.height - root_top)); return position.Pass(); } scoped_ptr PopulateSize(const BrowserAccessibilityCocoa* cocoa_node) { scoped_ptr size(new base::DictionaryValue); NSSize node_size = [[cocoa_node size] sizeValue]; size->SetInteger(kHeightDictAttr, static_cast(node_size.height)); size->SetInteger(kWidthDictAttr, static_cast(node_size.width)); return size.Pass(); } scoped_ptr PopulateRange(NSRange range) { scoped_ptr rangeDict(new base::DictionaryValue); rangeDict->SetInteger(kRangeLocDictAttr, static_cast(range.location)); rangeDict->SetInteger(kRangeLenDictAttr, static_cast(range.length)); return rangeDict.Pass(); } // Returns true if |value| is an NSValue containing a NSRange. bool IsRangeValue(id value) { if (![value isKindOfClass:[NSValue class]]) return false; return 0 == strcmp([value objCType], @encode(NSRange)); } scoped_ptr PopulateObject(id value); scoped_ptr PopulateArray(NSArray* array) { scoped_ptr list(new base::ListValue); for (NSUInteger i = 0; i < [array count]; i++) list->Append(PopulateObject([array objectAtIndex:i]).release()); return list.Pass(); } scoped_ptr StringForBrowserAccessibility( BrowserAccessibilityCocoa* obj) { NSMutableArray* tokens = [[NSMutableArray alloc] init]; // Always include the role id role = [obj role]; [tokens addObject:role]; // If the role is "group", include the role description as well. id roleDescription = [obj roleDescription]; if ([role isEqualToString:NSAccessibilityGroupRole] && roleDescription != nil && ![roleDescription isEqualToString:@""]) { [tokens addObject:roleDescription]; } // Include the description, title, or value - the first one not empty. id title = [obj title]; id description = [obj description]; id value = [obj value]; if (description && ![description isEqual:@""]) { [tokens addObject:description]; } else if (title && ![title isEqual:@""]) { [tokens addObject:title]; } else if (value && ![value isEqual:@""]) { [tokens addObject:value]; } NSString* result = [tokens componentsJoinedByString:@" "]; return scoped_ptr( new base::StringValue(SysNSStringToUTF16(result))).Pass(); } scoped_ptr PopulateObject(id value) { if ([value isKindOfClass:[NSArray class]]) return scoped_ptr(PopulateArray((NSArray*) value)); if (IsRangeValue(value)) return scoped_ptr(PopulateRange([value rangeValue])); if ([value isKindOfClass:[BrowserAccessibilityCocoa class]]) { std::string str; StringForBrowserAccessibility(value)->GetAsString(&str); return scoped_ptr(StringForBrowserAccessibility( (BrowserAccessibilityCocoa*) value)); } return scoped_ptr( new base::StringValue( SysNSStringToUTF16([NSString stringWithFormat:@"%@", value]))).Pass(); } NSArray* BuildAllAttributesArray() { NSArray* array = [NSArray arrayWithObjects: NSAccessibilityRoleDescriptionAttribute, NSAccessibilityTitleAttribute, NSAccessibilityValueAttribute, NSAccessibilityMinValueAttribute, NSAccessibilityMaxValueAttribute, NSAccessibilityValueDescriptionAttribute, NSAccessibilityDescriptionAttribute, NSAccessibilityHelpAttribute, @"AXInvalid", NSAccessibilityDisclosingAttribute, NSAccessibilityDisclosureLevelAttribute, @"AXAccessKey", @"AXARIAAtomic", @"AXARIABusy", @"AXARIALive", @"AXARIARelevant", @"AXARIASetSize", @"AXARIAPosInSet", NSAccessibilityColumnIndexRangeAttribute, @"AXDropEffects", NSAccessibilityEnabledAttribute, NSAccessibilityExpandedAttribute, NSAccessibilityFocusedAttribute, @"AXGrabbed", NSAccessibilityIndexAttribute, @"AXLoaded", @"AXLoadingProcess", NSAccessibilityNumberOfCharactersAttribute, NSAccessibilitySortDirectionAttribute, NSAccessibilityOrientationAttribute, @"AXPlaceholder", @"AXRequired", NSAccessibilityRowIndexRangeAttribute, NSAccessibilitySelectedChildrenAttribute, NSAccessibilityTitleUIElementAttribute, NSAccessibilityURLAttribute, NSAccessibilityVisibleCharacterRangeAttribute, NSAccessibilityVisibleChildrenAttribute, @"AXVisited", @"AXLinkedUIElements", nil]; return [array retain]; } } // namespace void AccessibilityTreeFormatter::Initialize() { } void AccessibilityTreeFormatter::AddProperties(const BrowserAccessibility& node, base::DictionaryValue* dict) { dict->SetInteger("id", node.GetId()); BrowserAccessibilityCocoa* cocoa_node = const_cast(&node)->ToBrowserAccessibilityCocoa(); NSArray* supportedAttributes = [cocoa_node accessibilityAttributeNames]; string role = SysNSStringToUTF8( [cocoa_node accessibilityAttributeValue:NSAccessibilityRoleAttribute]); dict->SetString(SysNSStringToUTF8(NSAccessibilityRoleAttribute), role); NSString* subrole = [cocoa_node accessibilityAttributeValue:NSAccessibilitySubroleAttribute]; if (subrole != nil) { dict->SetString(SysNSStringToUTF8(NSAccessibilitySubroleAttribute), SysNSStringToUTF8(subrole)); } CR_DEFINE_STATIC_LOCAL(NSArray*, all_attributes, (BuildAllAttributesArray())); for (NSString* requestedAttribute in all_attributes) { if (![supportedAttributes containsObject:requestedAttribute]) continue; id value = [cocoa_node accessibilityAttributeValue:requestedAttribute]; if (value != nil) { dict->Set( SysNSStringToUTF8(requestedAttribute), PopulateObject(value).release()); } } dict->Set(kPositionDictAttr, PopulatePosition(node).release()); dict->Set(kSizeDictAttr, PopulateSize(cocoa_node).release()); } base::string16 AccessibilityTreeFormatter::ToString( const base::DictionaryValue& dict) { base::string16 line; if (show_ids_) { int id_value; dict.GetInteger("id", &id_value); WriteAttribute(true, base::IntToString16(id_value), &line); } NSArray* defaultAttributes = [NSArray arrayWithObjects:NSAccessibilityTitleAttribute, NSAccessibilityValueAttribute, nil]; string s_value; dict.GetString(SysNSStringToUTF8(NSAccessibilityRoleAttribute), &s_value); WriteAttribute(true, base::UTF8ToUTF16(s_value), &line); string subroleAttribute = SysNSStringToUTF8(NSAccessibilitySubroleAttribute); if (dict.GetString(subroleAttribute, &s_value)) { WriteAttribute(false, StringPrintf("%s=%s", subroleAttribute.c_str(), s_value.c_str()), &line); } CR_DEFINE_STATIC_LOCAL(NSArray*, all_attributes, (BuildAllAttributesArray())); for (NSString* requestedAttribute in all_attributes) { string requestedAttributeUTF8 = SysNSStringToUTF8(requestedAttribute); if (dict.GetString(requestedAttributeUTF8, &s_value)) { WriteAttribute([defaultAttributes containsObject:requestedAttribute], StringPrintf("%s='%s'", requestedAttributeUTF8.c_str(), s_value.c_str()), &line); continue; } const base::Value* value; if (dict.Get(requestedAttributeUTF8, &value)) { std::string json_value; base::JSONWriter::Write(*value, &json_value); WriteAttribute( [defaultAttributes containsObject:requestedAttribute], StringPrintf("%s=%s", requestedAttributeUTF8.c_str(), json_value.c_str()), &line); } } const base::DictionaryValue* d_value = NULL; if (dict.GetDictionary(kPositionDictAttr, &d_value)) { WriteAttribute(false, FormatCoordinates(kPositionDictAttr, kXCoordDictAttr, kYCoordDictAttr, *d_value), &line); } if (dict.GetDictionary(kSizeDictAttr, &d_value)) { WriteAttribute(false, FormatCoordinates(kSizeDictAttr, kWidthDictAttr, kHeightDictAttr, *d_value), &line); } return line; } // static const base::FilePath::StringType AccessibilityTreeFormatter::GetActualFileSuffix() { return FILE_PATH_LITERAL("-actual-mac.txt"); } // static const base::FilePath::StringType AccessibilityTreeFormatter::GetExpectedFileSuffix() { return FILE_PATH_LITERAL("-expected-mac.txt"); } // static const string AccessibilityTreeFormatter::GetAllowEmptyString() { return "@MAC-ALLOW-EMPTY:"; } // static const string AccessibilityTreeFormatter::GetAllowString() { return "@MAC-ALLOW:"; } // static const string AccessibilityTreeFormatter::GetDenyString() { return "@MAC-DENY:"; } } // namespace content