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..bd8091511caaf06cc205107d205e8a09f8d8dab7 100644 |
--- a/content/browser/accessibility/browser_accessibility_cocoa.mm |
+++ b/content/browser/accessibility/browser_accessibility_cocoa.mm |
@@ -13,6 +13,7 @@ |
#include "base/strings/sys_string_conversions.h" |
#include "base/strings/utf_string_conversions.h" |
#include "content/app/strings/grit/content_strings.h" |
+#include "content/browser/accessibility/accessibility_tree_search.h" |
#include "content/browser/accessibility/browser_accessibility_manager.h" |
#include "content/browser/accessibility/browser_accessibility_manager_mac.h" |
#include "content/public/common/content_client.h" |
@@ -24,6 +25,8 @@ |
extern "C" void NSAccessibilityUnregisterUniqueIdForUIElement(id element); |
using ui::AXNodeData; |
+using content::AccessibilityMatchPredicate; |
+using content::AccessibilityTreeSearch; |
using content::BrowserAccessibility; |
using content::BrowserAccessibilityDelegate; |
using content::BrowserAccessibilityManager; |
@@ -50,6 +53,251 @@ 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 AccessibilityTreeSearch. |
+AccessibilityMatchPredicate PredicateForSearchKey(NSString* searchKey) { |
+ if ([searchKey isEqualToString:@"AXAnyTypeSearchKey"]) { |
David Tseng
2015/05/08 21:58:25
Is this string defined in NS*? Is this API availab
dmazzoni
2015/05/13 04:22:21
It's not even in 10.10. As far as I can tell the s
|
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return true; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXBlockquoteSameLevelSearchKey"] || |
+ [searchKey isEqualToString:@"AXBlockquoteSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_BLOCKQUOTE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXBoldFontSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
David Tseng
2015/05/08 21:58:25
Can you move the TODO into its own line above?
dmazzoni
2015/05/13 04:22:21
Done throughout.
|
+ } else if ([searchKey isEqualToString:@"AXButtonSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return (current->GetRole() == ui::AX_ROLE_BUTTON || |
David Tseng
2015/05/08 21:58:25
Not sure if they exist on Mac, but menuButton and
dmazzoni
2015/05/13 04:22:21
Done.
|
+ current->GetRole() == ui::AX_ROLE_POP_UP_BUTTON || |
+ 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) { |
+ return current->HasState(ui::AX_STATE_FOCUSABLE); |
David Tseng
2015/05/08 21:58:25
If a button doesn't take focus (like if it were di
dmazzoni
2015/05/13 04:22:20
No, good point. I implemented IsControl but decide
|
+ }; |
+ } else if ([searchKey isEqualToString:@"AXDifferentTypeSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() != start->GetRole(); |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXFontChangeSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
David Tseng
2015/05/08 21:58:25
Ditto
|
+ } else if ([searchKey isEqualToString:@"AXFontColorChangeSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
David Tseng
2015/05/08 21:58:25
Ditto
|
+ } 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); |
David Tseng
2015/05/08 21:58:25
Maybe worth writing a helper returning a predicate
dmazzoni
2015/05/13 04:22:21
It's only 6 heading levels and they're 2 lines eac
|
+ }; |
+ } 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 && |
+ current->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL) == |
+ start->GetIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL)); |
David Tseng
2015/05/08 21:58:25
Need to check start is a heading too?
dmazzoni
2015/05/13 04:22:20
Done.
|
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHeadingSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_HEADING; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXHighlightedSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } else if ([searchKey isEqualToString:@"AXItalicFontSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } 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"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } else if ([searchKey isEqualToString:@"AXOutlineSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_TREE; |
+ }; |
+ } else if ([searchKey isEqualToString:@"AXPlainTextSearchKey"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } 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"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } else if ([searchKey isEqualToString:@"AXTableSameLevelSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return current->GetRole() == ui::AX_ROLE_TABLE; |
David Tseng
2015/05/08 21:58:25
What about grid? I think this means you need to ch
dmazzoni
2015/05/13 04:22:21
Done.
Added TODO for nested, I wasn't planning on
|
+ }; |
+ } else if ([searchKey isEqualToString:@"AXTableSearchKey"]) { |
+ return [](BrowserAccessibility* start, BrowserAccessibility* current) { |
+ return 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"]) { |
+ return nullptr; // TODO(dmazzoni): implement this. |
+ } 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 an AccessibilityTreeSearch object given the parameters |
+// passed to AXUIElementCountForSearchPredicate or |
+// AXUIElementsForSearchPredicate. Return true on success. |
+bool InitializeAccessibilityTreeSearch( |
+ AccessibilityTreeSearch* 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::AccessibilityTreeSearch::Direction direction = |
+ content::AccessibilityTreeSearch::FORWARDS; |
+ NSString* directionParameter = [dictionary objectForKey:@"AXDirection"]; |
+ if ([directionParameter isKindOfClass:[NSString class]]) { |
+ if ([directionParameter isEqualToString:@"AXDirectionNext"]) |
+ direction = content::AccessibilityTreeSearch::FORWARDS; |
+ else if ([directionParameter isEqualToString:@"AXDirectionPrevious"]) |
+ direction = content::AccessibilityTreeSearch::BACKWARDS; |
+ } |
+ |
+ int resultsLimit = 0; |
+ 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->SetResultLimit(resultsLimit); |
+ search->SetImmediateDescendantsOnly(immediateDescendantsOnly); |
+ search->SetVisibleOnly(visibleOnly); |
+ search->SetSearchText(searchText); |
David Tseng
2015/05/08 21:58:25
Looks like you do have all args available here; co
dmazzoni
2015/05/13 04:22:20
Two reasons not to:
* I think this is more readabl
|
+ |
+ 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 +805,10 @@ NSDictionary* attributeToMethodNameMap = nil; |
nil; |
} |
+- (content::BrowserAccessibility*)browserAccessibility { |
+ return browserAccessibility_; |
+} |
+ |
- (NSPoint)pointInScreen:(NSPoint)origin |
size:(NSSize)size { |
if (!browserAccessibility_) |
@@ -1213,6 +1465,26 @@ NSDictionary* attributeToMethodNameMap = nil; |
pointInScreen.x, pointInScreen.y, rect.width(), rect.height()); |
return [NSValue valueWithRect:nsrect]; |
} |
+ if ([attribute isEqualToString:@"AXUIElementCountForSearchPredicate"]) { |
+ AccessibilityTreeSearch search(browserAccessibility_->manager()); |
+ if (InitializeAccessibilityTreeSearch(&search, parameter)) |
+ return [NSNumber numberWithInt:search.CountMatches()]; |
+ return nil; |
+ } |
+ |
+ if ([attribute isEqualToString:@"AXUIElementsForSearchPredicate"]) { |
+ AccessibilityTreeSearch 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 +1505,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 +1528,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. |