Source code
Revision control
Copy as Markdown
Other Tools
/* clang-format off */
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* clang-format on */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#import "MOXAccessibleBase.h"
#import "MacSelectorMap.h"
#include "nsObjCExceptions.h"
#include "xpcAccessibleMacInterface.h"
#include "mozilla/Logging.h"
#include "gfxPlatform.h"
using namespace mozilla;
using namespace mozilla::a11y;
#undef LOG
mozilla::LogModule* GetMacAccessibilityLog() {
static mozilla::LazyLogModule sLog("MacAccessibility");
return sLog;
}
#define LOG(type, format, ...) \
do { \
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \
NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \
} \
} while (0)
@interface NSObject (MOXAccessible)
// This NSObject conforms to MOXAccessible.
// This is needed to we know to mutate the value
// (get represented view, check isAccessibilityElement)
// before forwarding it to NSAccessibility.
- (BOOL)isMOXAccessible;
// Same as above, but this checks if the NSObject is an array with
// mozAccessible conforming objects.
- (BOOL)hasMOXAccessibles;
@end
@implementation NSObject (MOXAccessible)
- (BOOL)isMOXAccessible {
return [self conformsToProtocol:@protocol(MOXAccessible)];
}
- (BOOL)hasMOXAccessibles {
return [self isKindOfClass:[NSArray class]] &&
[[(NSArray*)self firstObject] isMOXAccessible];
}
@end
// Private methods
@interface MOXAccessibleBase ()
- (BOOL)isSelectorSupported:(SEL)selector;
@end
@implementation MOXAccessibleBase
#pragma mark - mozAccessible/widget
- (BOOL)hasRepresentedView {
return NO;
}
- (id)representedView {
return nil;
}
- (BOOL)isRoot {
return NO;
}
#pragma mark - mozAccessible/NSAccessibility
- (NSArray*)accessibilityAttributeNames {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return nil;
}
static NSMutableDictionary* attributesForEachClass = nil;
if (!attributesForEachClass) {
attributesForEachClass = [[NSMutableDictionary alloc] init];
}
NSMutableArray* attributes =
attributesForEachClass [[self class]]
?: [[[NSMutableArray alloc] init] autorelease];
NSDictionary* getters = mac::AttributeGetters();
if (![attributes count]) {
// Go through all our attribute getters, if they are supported by this class
// advertise the attribute name.
for (NSString* attribute in getters) {
SEL selector = NSSelectorFromString(getters[attribute]);
if ([self isSelectorSupported:selector]) {
[attributes addObject:attribute];
}
}
// If we have a delegate add all the text marker attributes.
if ([self moxTextMarkerDelegate]) {
[attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
}
// We store a hash table with types as keys, and atttribute lists as values.
// This lets us cache the atttribute list of each subclass so we only
// need to gather its MOXAccessible methods once.
// XXX: Uncomment when accessibilityAttributeNames is removed from all
// subclasses. attributesForEachClass[[self class]] = attributes;
}
return attributes;
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (id)accessibilityAttributeValue:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return nil;
}
id value = nil;
NSDictionary* getters = mac::AttributeGetters();
if (getters[attribute]) {
SEL selector = NSSelectorFromString(getters[attribute]);
if ([self isSelectorSupported:selector]) {
value = [self performSelector:selector];
}
} else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
// If we have a delegate, check if attribute is a text marker
// attribute and call the associated selector on the delegate
// if so.
NSDictionary* textMarkerGetters = mac::TextAttributeGetters();
if (textMarkerGetters[attribute]) {
SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
if ([textMarkerDelegate respondsToSelector:selector]) {
value = [textMarkerDelegate performSelector:selector];
}
}
}
if ([value isMOXAccessible]) {
// If this is a MOXAccessible, get its represented view or filter it if
// it should be ignored.
value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value)
: nil;
}
if ([value hasMOXAccessibles]) {
// If this is an array of mozAccessibles, get each element's represented
// view and remove it from the returned array if it should be ignored.
NSUInteger arrSize = [value count];
NSMutableArray* arr =
[[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease];
for (NSUInteger i = 0; i < arrSize; i++) {
id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
if ([mozAcc isAccessibilityElement]) {
[arr addObject:mozAcc];
}
}
value = arr;
}
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
value);
} else if (![attribute isEqualToString:@"AXParent"] &&
![attribute isEqualToString:@"AXRole"] &&
![attribute isEqualToString:@"AXSubrole"] &&
![attribute isEqualToString:@"AXSize"] &&
![attribute isEqualToString:@"AXPosition"] &&
![attribute isEqualToString:@"AXRole"] &&
![attribute isEqualToString:@"AXChildren"]) {
LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute);
}
}
return value;
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return NO;
}
NSDictionary* setters = mac::AttributeSetters();
if (setters[attribute]) {
SEL selector = NSSelectorFromString(setters[attribute]);
if ([self isSelectorSupported:selector]) {
return YES;
}
} else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
// If we have a delegate, check text setters on delegate
NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
if (textMarkerSetters[attribute]) {
SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
if ([textMarkerDelegate respondsToSelector:selector]) {
return YES;
}
}
}
NS_OBJC_END_TRY_BLOCK_RETURN(NO);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if ([self isExpired]) {
return;
}
LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
value);
NSDictionary* setters = mac::AttributeSetters();
if (setters[attribute]) {
SEL selector = NSSelectorFromString(setters[attribute]);
if ([self isSelectorSupported:selector]) {
[self performSelector:selector withObject:value];
}
} else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
// If we have a delegate, check if attribute is a text marker
// attribute and call the associated selector on the delegate
// if so.
NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
if (textMarkerSetters[attribute]) {
SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
if ([textMarkerDelegate respondsToSelector:selector]) {
[textMarkerDelegate performSelector:selector withObject:value];
}
}
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
- (NSArray*)accessibilityActionNames {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return nil;
}
NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];
NSDictionary* actions = mac::Actions();
for (NSString* action in actions) {
SEL selector = NSSelectorFromString(actions[action]);
if ([self isSelectorSupported:selector]) {
[actionNames addObject:action];
}
}
return actionNames;
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (void)accessibilityPerformAction:(NSString*)action {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
if ([self isExpired]) {
return;
}
LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);
NSDictionary* actions = mac::Actions();
if (actions[action]) {
SEL selector = NSSelectorFromString(actions[action]);
if ([self isSelectorSupported:selector]) {
[self performSelector:selector];
}
}
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
- (NSString*)accessibilityActionDescription:(NSString*)action {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
// by default we return whatever the MacOS API know about.
// if you have custom actions, override.
return NSAccessibilityActionDescription(action);
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (NSArray*)accessibilityParameterizedAttributeNames {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return nil;
}
NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];
NSDictionary* attributes = mac::ParameterizedAttributeGetters();
for (NSString* attribute in attributes) {
SEL selector = NSSelectorFromString(attributes[attribute]);
if ([self isSelectorSupported:selector]) {
[attributeNames addObject:attribute];
}
}
// If we have a delegate add all the text marker attributes.
if ([self moxTextMarkerDelegate]) {
[attributeNames
addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
}
return attributeNames;
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (id)accessibilityAttributeValue:(NSString*)attribute
forParameter:(id)parameter {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return nil;
}
id value = nil;
NSDictionary* getters = mac::ParameterizedAttributeGetters();
if (getters[attribute]) {
SEL selector = NSSelectorFromString(getters[attribute]);
if ([self isSelectorSupported:selector]) {
value = [self performSelector:selector withObject:parameter];
}
} else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
// If we have a delegate, check if attribute is a text marker
// attribute and call the associated selector on the delegate
// if so.
NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters();
if (textMarkerGetters[attribute]) {
SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
if ([textMarkerDelegate respondsToSelector:selector]) {
value = [textMarkerDelegate performSelector:selector
withObject:parameter];
}
}
}
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
attribute, parameter, value);
} else {
LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
}
return value;
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (id)accessibilityHitTest:(NSPoint)point {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
return GetObjectOrRepresentedView([self moxHitTest:point]);
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (id)accessibilityFocusedUIElement {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
return GetObjectOrRepresentedView([self moxFocusedUIElement]);
NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}
- (BOOL)isAccessibilityElement {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if ([self isExpired]) {
return YES;
}
id parent = [self moxParent];
if (![parent isMOXAccessible]) {
return YES;
}
return ![self moxIgnoreWithParent:parent];
NS_OBJC_END_TRY_BLOCK_RETURN(NO);
}
- (BOOL)accessibilityNotifiesWhenDestroyed {
return YES;
}
#pragma mark - MOXAccessible protocol
- (NSNumber*)moxIndexForChildUIElement:(id)child {
return @([[self moxUnignoredChildren] indexOfObject:child]);
}
- (id)moxTopLevelUIElement {
return [self moxWindow];
}
- (id)moxHitTest:(NSPoint)point {
return self;
}
- (id)moxFocusedUIElement {
return self;
}
- (void)moxPostNotification:(NSString*)notification {
[self moxPostNotification:notification withUserInfo:nil];
}
- (void)moxPostNotification:(NSString*)notification
withUserInfo:(NSDictionary*)userInfo {
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo);
} else {
LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
}
// This sends events via nsIObserverService to be consumed by our mochitests.
xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);
if (gfxPlatform::IsHeadless()) {
// Using a headless toolkit for tests and whatnot, posting accessibility
// notification won't work.
return;
}
if (![self isAccessibilityElement]) {
// If this is an ignored object, don't expose it to system.
return;
}
if (userInfo) {
NSAccessibilityPostNotificationWithUserInfo(
GetObjectOrRepresentedView(self), notification, userInfo);
} else {
NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
notification);
}
}
- (BOOL)moxBlockSelector:(SEL)selector {
return NO;
}
- (NSArray*)moxChildren {
return @[];
}
- (NSArray*)moxUnignoredChildren {
NSMutableArray* unignoredChildren =
[[[NSMutableArray alloc] init] autorelease];
NSArray* allChildren = [self moxChildren];
for (MOXAccessibleBase* nativeChild in allChildren) {
if ([nativeChild moxIgnoreWithParent:self]) {
// If this child should be ignored get its unignored children.
// This will in turn recurse to any unignored descendants if the
// child is ignored.
[unignoredChildren
addObjectsFromArray:[nativeChild moxUnignoredChildren]];
} else {
[unignoredChildren addObject:nativeChild];
}
}
return unignoredChildren;
}
- (id<mozAccessible>)moxParent {
return nil;
}
- (id<mozAccessible>)moxUnignoredParent {
id<mozAccessible> nativeParent = [self moxParent];
if (!nativeParent) {
return nil;
}
if (![nativeParent isAccessibilityElement]) {
if ([nativeParent conformsToProtocol:@protocol(MOXAccessible)] &&
[nativeParent respondsToSelector:@selector(moxUnignoredParent)]) {
// Cast away the protocol so we can cast to another protocol.
id bareNativeParent = nativeParent;
id<MOXAccessible> moxNativeParent = bareNativeParent;
return [moxNativeParent moxUnignoredParent];
}
}
return GetObjectOrRepresentedView(nativeParent);
}
- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
return [parent moxIgnoreChild:self];
}
- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
return NO;
}
- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
return nil;
}
- (BOOL)moxIsLiveRegion {
return NO;
}
#pragma mark -
// objc-style description (from NSObject); not to be confused with the
// accessible description above.
- (NSString*)description {
if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) {
return [self moxMozDebugDescription];
}
}
return [NSString stringWithFormat:@"<%@: %p %@>",
NSStringFromClass([self class]), self,
[self moxRole]];
}
- (BOOL)isExpired {
return mIsExpired;
}
- (void)expire {
MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
mIsExpired = YES;
[self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
}
- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
BOOL* stop))findBlock {
for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)];
element = [element moxUnignoredParent]) {
BOOL stop = NO;
if (findBlock(element, &stop)) {
return element;
}
if (stop || ![element respondsToSelector:@selector(moxUnignoredParent)]) {
break;
}
}
return nil;
}
#pragma mark - Private
- (BOOL)isSelectorSupported:(SEL)selector {
return
[self respondsToSelector:selector] && ![self moxBlockSelector:selector];
}
@end