Index: content/browser/accessibility/browser_accessibility_cocoa.mm |
diff --git a/content/browser/accessibility/browser_accessibility_cocoa.mm b/content/browser/accessibility/browser_accessibility_cocoa.mm |
index d333ae1d748999b0776c392e4dbca52ce15bf8bc..5f7bcacbae1f02ca1b5f66c6d723242393ae4c31 100644 |
--- a/content/browser/accessibility/browser_accessibility_cocoa.mm |
+++ b/content/browser/accessibility/browser_accessibility_cocoa.mm |
@@ -15,6 +15,7 @@ |
#include "content/app/strings/grit/content_strings.h" |
#include "content/browser/accessibility/browser_accessibility_manager.h" |
#include "content/browser/accessibility/browser_accessibility_manager_mac.h" |
+#include "content/browser/accessibility/one_shot_accessibility_tree_search.h" |
#include "content/public/common/content_client.h" |
#import "ui/accessibility/platform/ax_platform_node_mac.h" |
@@ -24,15 +25,20 @@ |
extern "C" void NSAccessibilityUnregisterUniqueIdForUIElement(id element); |
using ui::AXNodeData; |
+using content::AccessibilityMatchPredicate; |
using content::BrowserAccessibility; |
using content::BrowserAccessibilityDelegate; |
using content::BrowserAccessibilityManager; |
using content::BrowserAccessibilityManagerMac; |
using content::ContentClient; |
+using content::OneShotAccessibilityTreeSearch; |
typedef ui::AXStringAttribute StringAttribute; |
namespace { |
+// VoiceOver uses -1 to mean "no limit" for AXResultsLimit. |
+const int kAXResultsLimitNoLimit = -1; |
+ |
// Returns an autoreleased copy of the AXNodeData's attribute. |
NSString* NSStringForStringAttribute( |
BrowserAccessibility* browserAccessibility, |
@@ -50,6 +56,279 @@ bool GetState(BrowserAccessibility* accessibility, ui::AXState state) { |
// A mapping from an accessibility attribute to its method name. |
NSDictionary* attributeToMethodNameMap = nil; |
+// Given a search key provided to AXUIElementCountForSearchPredicate or |
+// AXUIElementsForSearchPredicate, return a predicate that can be added |
+// to OneShotAccessibilityTreeSearch. |
+AccessibilityMatchPredicate PredicateForSearchKey(NSString* searchKey) { |
+ if ([searchKey isEqualToString:@"AXAnyTypeSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return true; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXBlockquoteSameLevelSearchKey"] || |
+ [searchKey isEqualToString:@"AXBlockquoteSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ // TODO(dmazzoni): implement the "same level" part. |
+ return current->GetRole() == ui::AX_ROLE_BLOCKQUOTE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXBoldFontSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXButtonSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_BUTTON || |
+ current->GetRole() == ui::AX_ROLE_MENU_BUTTON || |
+ current->GetRole() == ui::AX_ROLE_POP_UP_BUTTON || |
+ current->GetRole() == ui::AX_ROLE_SWITCH || |
+ current->GetRole() == ui::AX_ROLE_TOGGLE_BUTTON); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXCheckBoxSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_CHECK_BOX || |
+ current->GetRole() == ui::AX_ROLE_MENU_ITEM_CHECK_BOX); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXControlSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ if (current->IsControl()) |
+ return true; |
+ if (current->HasState(ui::AX_STATE_FOCUSABLE) && |
+ current->GetRole() != ui::AX_ROLE_IMAGE_MAP_LINK && |
+ current->GetRole() != ui::AX_ROLE_LINK) { |
+ return true; |
+ } |
+ return false; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXDifferentTypeSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() != start->GetRole(); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXFontChangeSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXFontColorChangeSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXFrameSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ if (current->IsWebAreaForPresentationalIframe()) |
+ return false; |
+ return (current->GetRole() == ui::AX_ROLE_WEB_AREA || |
+ current->GetRole() == ui::AX_ROLE_ROOT_WEB_AREA); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXGraphicSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_IMAGE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel1SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 1); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel2SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 2); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel3SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 3); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel4SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 4); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel5SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 5); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingLevel6SearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == 6); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingSameLevelSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_HEADING && |
+ start->GetRole() == ui::AX_ROLE_HEADING && |
+ (current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == |
+ start->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL))); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_HEADING; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHighlightedSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXItalicFontSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXLandmarkSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_APPLICATION || |
+ current->GetRole() == ui::AX_ROLE_BANNER || |
+ current->GetRole() == ui::AX_ROLE_COMPLEMENTARY || |
+ current->GetRole() == ui::AX_ROLE_CONTENT_INFO || |
+ current->GetRole() == ui::AX_ROLE_FORM || |
+ current->GetRole() == ui::AX_ROLE_MAIN || |
+ current->GetRole() == ui::AX_ROLE_NAVIGATION || |
+ current->GetRole() == ui::AX_ROLE_SEARCH); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXLinkSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_LINK; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXListSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_LIST; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXLiveRegionSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->HasStringAttribute(ui::AX_ATTR_LIVE_STATUS); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXMisspelledWordSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXOutlineSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_TREE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXPlainTextSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXRadioGroupSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_RADIO_GROUP; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXSameTypeSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == start->GetRole(); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXStaticTextSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_STATIC_TEXT; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXStyleChangeSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXTableSameLevelSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ // TODO(dmazzoni): implement the "same level" part. |
+ return current->GetRole() == ui::AX_ROLE_GRID || |
+ current->GetRole() == ui::AX_ROLE_TABLE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXTableSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_GRID || |
+ current->GetRole() == ui::AX_ROLE_TABLE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXTextFieldSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_TEXT_FIELD; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXUnderlineSearchKey"]) { |
+ // TODO(dmazzoni): implement this. |
+ return nullptr; |
+ } else if ([searchKey isEqualToString:@"AXUnvisitedLinkSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_LINK && |
+ !current->HasState(ui::AX_STATE_VISITED)); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXVisitedLinkSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_LINK && |
+ current->HasState(ui::AX_STATE_VISITED)); |
+ }; |
+ } |
+ |
+ return nullptr; |
+} |
+ |
+// Initialize a OneShotAccessibilityTreeSearch object given the parameters |
+// passed to AXUIElementCountForSearchPredicate or |
+// AXUIElementsForSearchPredicate. Return true on success. |
+bool InitializeAccessibilityTreeSearch( |
+ OneShotAccessibilityTreeSearch* search, |
+ id parameter) { |
+ if (![parameter isKindOfClass:[NSDictionary class]]) |
+ return false; |
+ NSDictionary* dictionary = parameter; |
+ |
+ id startElementParameter = [dictionary objectForKey:@"AXStartElement"]; |
+ BrowserAccessibility* startNode = nullptr; |
+ if ([startElementParameter isKindOfClass:[BrowserAccessibilityCocoa class]]) { |
+ BrowserAccessibilityCocoa* startNodeCocoa = |
+ (BrowserAccessibilityCocoa*)startElementParameter; |
+ startNode = [startNodeCocoa browserAccessibility]; |
+ } |
+ |
+ bool immediateDescendantsOnly = false; |
+ NSNumber *immediateDescendantsOnlyParameter = |
+ [dictionary objectForKey:@"AXImmediateDescendantsOnly"]; |
+ if ([immediateDescendantsOnlyParameter isKindOfClass:[NSNumber class]]) |
+ immediateDescendantsOnly = [immediateDescendantsOnlyParameter boolValue]; |
+ |
+ bool visibleOnly = false; |
+ NSNumber *visibleOnlyParameter = [dictionary objectForKey:@"AXVisibleOnly"]; |
+ if ([visibleOnlyParameter isKindOfClass:[NSNumber class]]) |
+ visibleOnly = [visibleOnlyParameter boolValue]; |
+ |
+ content::OneShotAccessibilityTreeSearch::Direction direction = |
+ content::OneShotAccessibilityTreeSearch::FORWARDS; |
+ NSString* directionParameter = [dictionary objectForKey:@"AXDirection"]; |
+ if ([directionParameter isKindOfClass:[NSString class]]) { |
+ if ([directionParameter isEqualToString:@"AXDirectionNext"]) |
+ direction = content::OneShotAccessibilityTreeSearch::FORWARDS; |
+ else if ([directionParameter isEqualToString:@"AXDirectionPrevious"]) |
+ direction = content::OneShotAccessibilityTreeSearch::BACKWARDS; |
+ } |
+ |
+ int resultsLimit = kAXResultsLimitNoLimit; |
+ NSNumber* resultsLimitParameter = [dictionary objectForKey:@"AXResultsLimit"]; |
+ if ([resultsLimitParameter isKindOfClass:[NSNumber class]]) |
+ resultsLimit = [resultsLimitParameter intValue]; |
+ |
+ std::string searchText; |
+ NSString* searchTextParameter = [dictionary objectForKey:@"AXSearchText"]; |
+ if ([searchTextParameter isKindOfClass:[NSString class]]) |
+ searchText = base::SysNSStringToUTF8(searchTextParameter); |
+ |
+ search->SetStartNode(startNode); |
+ search->SetDirection(direction); |
+ search->SetImmediateDescendantsOnly(immediateDescendantsOnly); |
+ search->SetVisibleOnly(visibleOnly); |
+ search->SetSearchText(searchText); |
+ |
+ // Mac uses resultsLimit == -1 for unlimited, that that's |
+ // the default for OneShotAccessibilityTreeSearch already. |
+ // Only set the results limit if it's nonnegative. |
+ if (resultsLimit >= 0) |
+ search->SetResultLimit(resultsLimit); |
+ |
+ id searchKey = [dictionary objectForKey:@"AXSearchKey"]; |
+ if ([searchKey isKindOfClass:[NSString class]]) { |
+ AccessibilityMatchPredicate predicate = |
+ PredicateForSearchKey((NSString*)searchKey); |
+ if (predicate) |
+ search->AddPredicate(predicate); |
+ } else if ([searchKey isKindOfClass:[NSArray class]]) { |
+ size_t searchKeyCount = static_cast<size_t>([searchKey count]); |
+ for (size_t i = 0; i < searchKeyCount; ++i) { |
+ id key = [searchKey objectAtIndex:i]; |
+ if ([key isKindOfClass:[NSString class]]) { |
+ AccessibilityMatchPredicate predicate = |
+ PredicateForSearchKey((NSString*)key); |
+ if (predicate) |
+ search->AddPredicate(predicate); |
+ } |
+ } |
+ } |
+ |
+ return true; |
+} |
+ |
} // namespace |
@implementation BrowserAccessibilityCocoa |
@@ -557,6 +836,10 @@ NSDictionary* attributeToMethodNameMap = nil; |
nil; |
} |
+- (content::BrowserAccessibility*)browserAccessibility { |
+ return browserAccessibility_; |
+} |
+ |
- (NSPoint)pointInScreen:(NSPoint)origin |
size:(NSSize)size { |
if (!browserAccessibility_) |
@@ -1213,6 +1496,26 @@ NSDictionary* attributeToMethodNameMap = nil; |
pointInScreen.x, pointInScreen.y, rect.width(), rect.height()); |
return [NSValue valueWithRect:nsrect]; |
} |
+ if ([attribute isEqualToString:@"AXUIElementCountForSearchPredicate"]) { |
+ OneShotAccessibilityTreeSearch search(browserAccessibility_->manager()); |
+ if (InitializeAccessibilityTreeSearch(&search, parameter)) |
+ return [NSNumber numberWithInt:search.CountMatches()]; |
+ return nil; |
+ } |
+ |
+ if ([attribute isEqualToString:@"AXUIElementsForSearchPredicate"]) { |
+ OneShotAccessibilityTreeSearch search(browserAccessibility_->manager()); |
+ if (InitializeAccessibilityTreeSearch(&search, parameter)) { |
+ size_t count = search.CountMatches(); |
+ NSMutableArray* result = [NSMutableArray arrayWithCapacity:count]; |
+ for (size_t i = 0; i < count; ++i) { |
+ BrowserAccessibility* match = search.GetMatchAtIndex(i); |
+ [result addObject:match->ToBrowserAccessibilityCocoa()]; |
+ } |
+ return result; |
+ } |
+ return nil; |
+ } |
// TODO(dtseng): support the following attributes. |
if ([attribute isEqualTo: |
@@ -1233,14 +1536,20 @@ NSDictionary* attributeToMethodNameMap = nil; |
if (!browserAccessibility_) |
return nil; |
+ // General attributes. |
+ NSMutableArray* ret = [NSMutableArray arrayWithObjects: |
+ @"AXUIElementCountForSearchPredicate", |
+ @"AXUIElementsForSearchPredicate", |
+ nil]; |
+ |
if ([[self role] isEqualToString:NSAccessibilityTableRole] || |
[[self role] isEqualToString:NSAccessibilityGridRole]) { |
- return [NSArray arrayWithObjects: |
+ [ret addObjectsFromArray:[NSArray arrayWithObjects: |
NSAccessibilityCellForColumnAndRowParameterizedAttribute, |
- nil]; |
+ nil]]; |
} |
if ([[self role] isEqualToString:NSAccessibilityTextFieldRole]) { |
- return [NSArray arrayWithObjects: |
+ [ret addObjectsFromArray:[NSArray arrayWithObjects: |
NSAccessibilityLineForIndexParameterizedAttribute, |
NSAccessibilityRangeForLineParameterizedAttribute, |
NSAccessibilityStringForRangeParameterizedAttribute, |
@@ -1250,14 +1559,14 @@ NSDictionary* attributeToMethodNameMap = nil; |
NSAccessibilityRTFForRangeParameterizedAttribute, |
NSAccessibilityAttributedStringForRangeParameterizedAttribute, |
NSAccessibilityStyleRangeForIndexParameterizedAttribute, |
- nil]; |
+ nil]]; |
} |
if ([self internalRole] == ui::AX_ROLE_STATIC_TEXT) { |
- return [NSArray arrayWithObjects: |
+ [ret addObjectsFromArray:[NSArray arrayWithObjects: |
NSAccessibilityBoundsForRangeParameterizedAttribute, |
- nil]; |
+ nil]]; |
} |
- return nil; |
+ return ret; |
} |
// Returns an array of action names that this object will respond to. |