blob: 7bbb831aea4d4ff94fce38acdaf7eec5178688ed [file] [log] [blame]
// Copyright 2020 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_tools_utils_mac.h"
#include "base/callback.h"
#include "base/strings/pattern.h"
#include "base/strings/sys_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility_cocoa.h"
// error: 'accessibilityAttributeNames' is deprecated: first deprecated in
// macOS 10.10 - Use the NSAccessibility protocol methods instead (see
// NSAccessibilityProtocols.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
namespace content {
namespace a11y {
using base::SysNSStringToUTF8;
const char kChromeTitle[] = "Google Chrome";
const char kChromiumTitle[] = "Chromium";
const char kFirefoxTitle[] = "Firefox";
const char kSafariTitle[] = "Safari";
bool IsBrowserAccessibilityCocoa(const id node) {
return [node isKindOfClass:[BrowserAccessibilityCocoa class]];
}
bool IsAXUIElement(const id node) {
return CFGetTypeID(node) == AXUIElementGetTypeID();
}
NSArray* ChildrenOf(const id node) {
if (IsBrowserAccessibilityCocoa(node))
return [node children];
if (IsAXUIElement(node)) {
CFTypeRef children_ref;
if ((AXUIElementCopyAttributeValue(static_cast<AXUIElementRef>(node),
kAXChildrenAttribute, &children_ref)) ==
kAXErrorSuccess)
return static_cast<NSArray*>(children_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
NSSize SizeOf(const id node) {
if (IsBrowserAccessibilityCocoa(node)) {
return [[static_cast<BrowserAccessibilityCocoa*>(node) size] sizeValue];
}
if (!IsAXUIElement(node)) {
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return NSMakeSize(0, 0);
}
id value = AttributeValueOf(node, NSAccessibilitySizeAttribute);
if (value && CFGetTypeID(value) == AXValueGetTypeID()) {
AXValueType type = AXValueGetType(static_cast<AXValueRef>(value));
if (type == kAXValueCGSizeType) {
NSSize size;
if (AXValueGetValue(static_cast<AXValueRef>(value), type, &size)) {
return size;
}
}
}
return NSMakeSize(0, 0);
}
NSPoint PositionOf(const id node) {
if (IsBrowserAccessibilityCocoa(node)) {
return
[[static_cast<BrowserAccessibilityCocoa*>(node) position] pointValue];
}
if (!IsAXUIElement(node)) {
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return NSMakePoint(0, 0);
}
id value = AttributeValueOf(node, NSAccessibilityPositionAttribute);
if (value && CFGetTypeID(value) == AXValueGetTypeID()) {
AXValueType type = AXValueGetType(static_cast<AXValueRef>(value));
if (type == kAXValueCGPointType) {
NSPoint point;
if (AXValueGetValue(static_cast<AXValueRef>(value), type, &point)) {
return point;
}
}
}
return NSMakePoint(0, 0);
}
NSArray* AttributeNamesOf(const id node) {
if (IsBrowserAccessibilityCocoa(node))
return [node accessibilityAttributeNames];
if (IsAXUIElement(node)) {
CFArrayRef attributes_ref;
if (AXUIElementCopyAttributeNames(static_cast<AXUIElementRef>(node),
&attributes_ref) == kAXErrorSuccess)
return static_cast<NSArray*>(attributes_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
NSArray* ParameterizedAttributeNamesOf(const id node) {
if (IsBrowserAccessibilityCocoa(node))
return [node accessibilityParameterizedAttributeNames];
if (IsAXUIElement(node)) {
CFArrayRef attributes_ref;
if (AXUIElementCopyParameterizedAttributeNames(
static_cast<AXUIElementRef>(node), &attributes_ref) ==
kAXErrorSuccess)
return static_cast<NSArray*>(attributes_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
id AttributeValueOf(const id node, NSString* attribute) {
if (IsBrowserAccessibilityCocoa(node))
return [node accessibilityAttributeValue:attribute];
if (IsAXUIElement(node)) {
CFTypeRef value_ref;
if ((AXUIElementCopyAttributeValue(static_cast<AXUIElementRef>(node),
static_cast<CFStringRef>(attribute),
&value_ref)) == kAXErrorSuccess)
return static_cast<id>(value_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
id ParameterizedAttributeValueOf(const id node,
NSString* attribute,
id parameter) {
if (IsBrowserAccessibilityCocoa(node))
return [node accessibilityAttributeValue:attribute forParameter:parameter];
if (IsAXUIElement(node)) {
CFTypeRef value_ref;
if ((AXUIElementCopyParameterizedAttributeValue(
static_cast<AXUIElementRef>(node),
static_cast<CFStringRef>(attribute),
static_cast<CFTypeRef>(parameter), &value_ref)) == kAXErrorSuccess)
return static_cast<id>(value_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
bool IsAttributeSettable(const id node, NSString* attribute) {
if (IsBrowserAccessibilityCocoa(node))
return [node accessibilityIsAttributeSettable:attribute];
if (IsAXUIElement(node)) {
Boolean settable;
if (AXUIElementIsAttributeSettable(static_cast<AXUIElementRef>(node),
static_cast<CFStringRef>(attribute),
&settable) == kAXErrorSuccess)
return static_cast<bool>(settable);
return false;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return false;
}
void SetAttributeValueOf(const id node, NSString* attribute, id value) {
if (IsBrowserAccessibilityCocoa(node)) {
[node accessibilitySetValue:value forAttribute:attribute];
return;
}
if (IsAXUIElement(node)) {
AXUIElementSetAttributeValue(static_cast<AXUIElementRef>(node),
static_cast<CFStringRef>(attribute),
static_cast<CFTypeRef>(value));
return;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
}
std::string GetDOMId(const id node) {
const id domid_value =
AttributeValueOf(node, base::SysUTF8ToNSString("AXDOMIdentifier"));
return base::SysNSStringToUTF8(static_cast<NSString*>(domid_value));
}
AXUIElementRef FindAXUIElement(const AXUIElementRef node,
const FindCriteria& criteria) {
if (criteria.Run(node))
return node;
NSArray* children = ChildrenOf(static_cast<id>(node));
for (id child in children) {
AXUIElementRef found =
FindAXUIElement(static_cast<AXUIElementRef>(child), criteria);
if (found != nil)
return found;
}
return nil;
}
std::pair<AXUIElementRef, int> FindAXUIElement(const AXTreeSelector& selector) {
NSArray* windows = static_cast<NSArray*>(CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID));
std::string title;
if (selector.types & AXTreeSelector::Chrome) {
title = kChromeTitle;
} else if (selector.types & AXTreeSelector::Chromium) {
title = kChromiumTitle;
} else if (selector.types & AXTreeSelector::Firefox) {
title = kFirefoxTitle;
} else if (selector.types & AXTreeSelector::Safari) {
title = kSafariTitle;
} else {
LOG(ERROR) << selector.AppName()
<< " application is not supported on the system";
return {nil, 0};
}
for (NSDictionary* window_info in windows) {
int pid =
[static_cast<NSNumber*>([window_info objectForKey:@"kCGWindowOwnerPID"])
intValue];
std::string window_name = SysNSStringToUTF8(static_cast<NSString*>(
[window_info objectForKey:@"kCGWindowOwnerName"]));
AXUIElementRef node = nil;
// Application pre-defined selectors match or application title exact match.
bool appTitleMatch = window_name == selector.pattern;
if (window_name == title || appTitleMatch)
node = AXUIElementCreateApplication(pid);
// Window title match. Application contain an AXWindow accessible object as
// a first child, which accessible name contain a window title. For example:
// 'Inbox (2) - asurkov@igalia.com - Gmail'.
if (!selector.pattern.empty() && !appTitleMatch) {
if (!node)
node = AXUIElementCreateApplication(pid);
AXUIElementRef window = FindAXWindowChild(node, selector.pattern);
if (window)
node = window;
}
// ActiveTab selector.
if (node && selector.types & AXTreeSelector::ActiveTab) {
node = FindAXUIElement(
node, base::BindRepeating([](const AXUIElementRef node) {
// Only active tab in exposed in browsers, thus find first
// AXWebArea role.
NSString* role = AttributeValueOf(static_cast<id>(node),
NSAccessibilityRoleAttribute);
return SysNSStringToUTF8(role) == "AXWebArea";
}));
}
// Found a match.
if (node)
return {node, pid};
}
return {nil, 0};
}
AXUIElementRef FindAXWindowChild(AXUIElementRef parent,
const std::string& pattern) {
NSArray* children = ChildrenOf(static_cast<id>(parent));
if ([children count] == 0)
return nil;
id window = [children objectAtIndex:0];
NSString* role = AttributeValueOf(window, NSAccessibilityRoleAttribute);
if (SysNSStringToUTF8(role) != "AXWindow")
return nil;
NSString* window_title =
AttributeValueOf(window, NSAccessibilityTitleAttribute);
if (base::MatchPattern(SysNSStringToUTF8(window_title), pattern))
return static_cast<AXUIElementRef>(window);
return nil;
}
} // namespace a11y
} // namespace content
#pragma clang diagnostic pop