blob: 43a38b2f32d0368d4977c9f59ab8a61fe27cb6b1 [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 "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h"
#include "base/callback.h"
#include "base/containers/fixed_flat_set.h"
#include "base/strings/pattern.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/platform/ax_private_attributes_mac.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 ui {
using base::SysNSStringToUTF8;
const char kChromeTitle[] = "Google Chrome";
const char kChromiumTitle[] = "Chromium";
const char kFirefoxTitle[] = "Firefox";
const char kSafariTitle[] = "Safari";
struct NSStringComparator {
bool operator()(NSString* lhs, NSString* rhs) const {
return [lhs compare:rhs] == NSOrderedAscending;
}
};
bool IsValidAXAttribute(const std::string& attribute) {
// static local to avoid a global static constructor.
static auto kValidAttributes = base::MakeFixedFlatSet<NSString*>(
{NSAccessibilityAccessKeyAttribute,
NSAccessibilityARIAAtomicAttribute,
NSAccessibilityARIABusyAttribute,
NSAccessibilityARIAColumnCountAttribute,
NSAccessibilityARIAColumnIndexAttribute,
NSAccessibilityARIACurrentAttribute,
NSAccessibilityARIALiveAttribute,
NSAccessibilityARIAPosInSetAttribute,
NSAccessibilityARIARelevantAttribute,
NSAccessibilityARIARowCountAttribute,
NSAccessibilityARIARowIndexAttribute,
NSAccessibilityARIASetSizeAttribute,
NSAccessibilityAutocompleteValueAttribute,
NSAccessibilityBlockQuoteLevelAttribute,
NSAccessibilityColumnHeaderUIElementsAttribute,
NSAccessibilityDetailsElementsAttribute,
NSAccessibilityDOMClassList,
NSAccessibilityDropEffectsAttribute,
NSAccessibilityHasPopupAttribute,
NSAccessibilityInvalidAttribute,
NSAccessibilityMathFractionNumeratorAttribute,
NSAccessibilityMathFractionDenominatorAttribute,
NSAccessibilityMathRootRadicandAttribute,
NSAccessibilityMathRootIndexAttribute,
NSAccessibilityMathBaseAttribute,
NSAccessibilityMathSubscriptAttribute,
NSAccessibilityMathSuperscriptAttribute,
NSAccessibilityMathUnderAttribute,
NSAccessibilityMathOverAttribute,
NSAccessibilityMathPostscriptsAttribute,
NSAccessibilityMathPrescriptsAttribute,
NSAccessibilityPopupValueAttribute,
NSAccessibilityRequiredAttributeChrome,
NSAccessibilityRoleDescriptionAttribute,
NSAccessibilityURLAttribute},
NSStringComparator());
return kValidAttributes.contains(base::SysUTF8ToNSString(attribute));
}
bool IsNSAccessibilityElement(const id node) {
return [node isKindOfClass:[NSAccessibilityElement class]];
}
bool IsAXUIElement(const id node) {
return CFGetTypeID(node) == AXUIElementGetTypeID();
}
NSArray* AXChildrenOf(const id node) {
if (IsNSAccessibilityElement(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 AXSizeOf(const id node) {
if (IsNSAccessibilityElement(node)) {
return [node accessibilityFrame].size;
}
if (!IsAXUIElement(node)) {
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return NSMakeSize(0, 0);
}
id value = AXAttributeValueOf(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 AXPositionOf(const id node) {
if (IsNSAccessibilityElement(node)) {
return [node accessibilityFrame].origin;
}
if (!IsAXUIElement(node)) {
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return NSMakePoint(0, 0);
}
id value = AXAttributeValueOf(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* AXAttributeNamesOf(const id node) {
if (IsNSAccessibilityElement(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* AXParameterizedAttributeNamesOf(const id node) {
if (IsNSAccessibilityElement(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 AXAttributeValueOf(const id node, NSString* attribute) {
if (IsNSAccessibilityElement(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 AXParameterizedAttributeValueOf(const id node,
NSString* attribute,
id parameter) {
if (IsNSAccessibilityElement(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;
}
absl::optional<id> PerformAXSelector(const id node,
const std::string& selector_string) {
if (![node conformsToProtocol:@protocol(NSAccessibility)])
return absl::nullopt;
SEL selector = NSSelectorFromString(base::SysUTF8ToNSString(selector_string));
if ([node respondsToSelector:selector])
return [node performSelector:selector];
return absl::nullopt;
}
void SetAXAttributeValueOf(const id node, NSString* attribute, id value) {
if (IsNSAccessibilityElement(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.";
}
NSArray* AXActionNamesOf(const id node) {
if (IsNSAccessibilityElement(node))
return [node accessibilityActionNames];
if (IsAXUIElement(node)) {
CFArrayRef attributes_ref;
if ((AXUIElementCopyActionNames(static_cast<AXUIElementRef>(node),
&attributes_ref)) == kAXErrorSuccess)
return static_cast<NSArray*>(attributes_ref);
return nil;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
return nil;
}
void PerformAXAction(const id node, NSString* action) {
if (IsNSAccessibilityElement(node)) {
[node accessibilityPerformAction:action];
return;
}
if (IsAXUIElement(node)) {
AXUIElementPerformAction(static_cast<AXUIElementRef>(node),
static_cast<CFStringRef>(action));
return;
}
NOTREACHED()
<< "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
}
std::string GetDOMId(const id node) {
const id domid_value =
AXAttributeValueOf(node, base::SysUTF8ToNSString("AXDOMIdentifier"));
return base::SysNSStringToUTF8(static_cast<NSString*>(domid_value));
}
AXUIElementRef FindAXUIElement(const AXUIElementRef node,
const AXFindCriteria& criteria) {
if (criteria.Run(node))
return node;
NSArray* children = AXChildrenOf(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) {
if (selector.widget) {
return {AXUIElementCreateApplication(selector.widget), selector.widget};
}
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
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 = AXAttributeValueOf(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 = AXChildrenOf(static_cast<id>(parent));
if ([children count] == 0)
return nil;
id window = [children objectAtIndex:0];
NSString* role = AXAttributeValueOf(window, NSAccessibilityRoleAttribute);
if (SysNSStringToUTF8(role) != "AXWindow")
return nil;
NSString* window_title =
AXAttributeValueOf(window, NSAccessibilityTitleAttribute);
if (base::MatchPattern(SysNSStringToUTF8(window_title), pattern))
return static_cast<AXUIElementRef>(window);
return nil;
}
} // namespace ui
#pragma clang diagnostic pop