2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #import "MOXAccessibleBase.h"
10 #import "MacSelectorMap.h"
12 #include "nsObjCExceptions.h"
13 #include "xpcAccessibleMacInterface.h"
14 #include "mozilla/Logging.h"
16 using namespace mozilla::a11y;
19 mozilla::LogModule* GetMacAccessibilityLog() {
20 static mozilla::LazyLogModule sLog("MacAccessibility");
24 #define LOG(type, format, ...) \
26 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \
27 NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
28 MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \
32 @interface NSObject (MOXAccessible)
34 // This NSObject conforms to MOXAccessible.
35 // This is needed to we know to mutate the value
36 // (get represented view, check isAccessibilityElement)
37 // before forwarding it to NSAccessibility.
38 - (BOOL)isMOXAccessible;
40 // Same as above, but this checks if the NSObject is an array with
41 // mozAccessible conforming objects.
42 - (BOOL)hasMOXAccessibles;
46 @implementation NSObject (MOXAccessible)
48 - (BOOL)isMOXAccessible {
49 return [self conformsToProtocol:@protocol(MOXAccessible)];
52 - (BOOL)hasMOXAccessibles {
53 return [self isKindOfClass:[NSArray class]] &&
54 [[(NSArray*)self firstObject] isMOXAccessible];
60 @interface MOXAccessibleBase ()
62 - (BOOL)isSelectorSupported:(SEL)selector;
66 @implementation MOXAccessibleBase
68 #pragma mark - mozAccessible/widget
70 - (BOOL)hasRepresentedView {
74 - (id)representedView {
82 #pragma mark - mozAccessible/NSAccessibility
84 - (NSArray*)accessibilityAttributeNames {
85 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
87 if ([self isExpired]) {
91 static NSMutableDictionary* attributesForEachClass = nil;
93 if (!attributesForEachClass) {
94 attributesForEachClass = [[NSMutableDictionary alloc] init];
97 NSMutableArray* attributes =
98 attributesForEachClass [[self class]]
99 ?: [[[NSMutableArray alloc] init] autorelease];
101 NSDictionary* getters = mac::AttributeGetters();
102 if (![attributes count]) {
103 // Go through all our attribute getters, if they are supported by this class
104 // advertise the attribute name.
105 for (NSString* attribute in getters) {
106 SEL selector = NSSelectorFromString(getters[attribute]);
107 if ([self isSelectorSupported:selector]) {
108 [attributes addObject:attribute];
112 // If we have a delegate add all the text marker attributes.
113 if ([self moxTextMarkerDelegate]) {
114 [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
117 // We store a hash table with types as keys, and atttribute lists as values.
118 // This lets us cache the atttribute list of each subclass so we only
119 // need to gather its MOXAccessible methods once.
120 // XXX: Uncomment when accessibilityAttributeNames is removed from all
121 // subclasses. attributesForEachClass[[self class]] = attributes;
126 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
129 - (id)accessibilityAttributeValue:(NSString*)attribute {
130 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
131 if ([self isExpired]) {
136 NSDictionary* getters = mac::AttributeGetters();
137 if (getters[attribute]) {
138 SEL selector = NSSelectorFromString(getters[attribute]);
139 if ([self isSelectorSupported:selector]) {
140 value = [self performSelector:selector];
142 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
143 // If we have a delegate, check if attribute is a text marker
144 // attribute and call the associated selector on the delegate
146 NSDictionary* textMarkerGetters = mac::TextAttributeGetters();
147 if (textMarkerGetters[attribute]) {
148 SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
149 if ([textMarkerDelegate respondsToSelector:selector]) {
150 value = [textMarkerDelegate performSelector:selector];
155 if ([value isMOXAccessible]) {
156 // If this is a MOXAccessible, get its represented view or filter it if
157 // it should be ignored.
158 value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value)
162 if ([value hasMOXAccessibles]) {
163 // If this is an array of mozAccessibles, get each element's represented
164 // view and remove it from the returned array if it should be ignored.
165 NSUInteger arrSize = [value count];
166 NSMutableArray* arr =
167 [[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease];
168 for (NSUInteger i = 0; i < arrSize; i++) {
169 id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
170 if ([mozAcc isAccessibilityElement]) {
171 [arr addObject:mozAcc];
178 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
179 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
180 LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
182 } else if (![attribute isEqualToString:@"AXParent"] &&
183 ![attribute isEqualToString:@"AXRole"] &&
184 ![attribute isEqualToString:@"AXSubrole"] &&
185 ![attribute isEqualToString:@"AXSize"] &&
186 ![attribute isEqualToString:@"AXPosition"] &&
187 ![attribute isEqualToString:@"AXRole"] &&
188 ![attribute isEqualToString:@"AXChildren"]) {
189 LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute);
195 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
198 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
199 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
201 if ([self isExpired]) {
205 NSDictionary* setters = mac::AttributeSetters();
206 if (setters[attribute]) {
207 SEL selector = NSSelectorFromString(setters[attribute]);
208 if ([self isSelectorSupported:selector]) {
211 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
212 // If we have a delegate, check text setters on delegate
213 NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
214 if (textMarkerSetters[attribute]) {
215 SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
216 if ([textMarkerDelegate respondsToSelector:selector]) {
222 NS_OBJC_END_TRY_BLOCK_RETURN(NO);
225 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
226 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
228 if ([self isExpired]) {
232 LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
235 NSDictionary* setters = mac::AttributeSetters();
236 if (setters[attribute]) {
237 SEL selector = NSSelectorFromString(setters[attribute]);
238 if ([self isSelectorSupported:selector]) {
239 [self performSelector:selector withObject:value];
241 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
242 // If we have a delegate, check if attribute is a text marker
243 // attribute and call the associated selector on the delegate
245 NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
246 if (textMarkerSetters[attribute]) {
247 SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
248 if ([textMarkerDelegate respondsToSelector:selector]) {
249 [textMarkerDelegate performSelector:selector withObject:value];
254 NS_OBJC_END_TRY_IGNORE_BLOCK;
257 - (NSArray*)accessibilityActionNames {
258 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
260 if ([self isExpired]) {
264 NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];
266 NSDictionary* actions = mac::Actions();
267 for (NSString* action in actions) {
268 SEL selector = NSSelectorFromString(actions[action]);
269 if ([self isSelectorSupported:selector]) {
270 [actionNames addObject:action];
276 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
279 - (void)accessibilityPerformAction:(NSString*)action {
280 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
282 if ([self isExpired]) {
286 LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);
288 NSDictionary* actions = mac::Actions();
289 if (actions[action]) {
290 SEL selector = NSSelectorFromString(actions[action]);
291 if ([self isSelectorSupported:selector]) {
292 [self performSelector:selector];
296 NS_OBJC_END_TRY_IGNORE_BLOCK;
299 - (NSString*)accessibilityActionDescription:(NSString*)action {
300 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
301 // by default we return whatever the MacOS API know about.
302 // if you have custom actions, override.
303 return NSAccessibilityActionDescription(action);
304 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
307 - (NSArray*)accessibilityParameterizedAttributeNames {
308 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
310 if ([self isExpired]) {
314 NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];
316 NSDictionary* attributes = mac::ParameterizedAttributeGetters();
317 for (NSString* attribute in attributes) {
318 SEL selector = NSSelectorFromString(attributes[attribute]);
319 if ([self isSelectorSupported:selector]) {
320 [attributeNames addObject:attribute];
324 // If we have a delegate add all the text marker attributes.
325 if ([self moxTextMarkerDelegate]) {
327 addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
330 return attributeNames;
332 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
335 - (id)accessibilityAttributeValue:(NSString*)attribute
336 forParameter:(id)parameter {
337 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
339 if ([self isExpired]) {
345 NSDictionary* getters = mac::ParameterizedAttributeGetters();
346 if (getters[attribute]) {
347 SEL selector = NSSelectorFromString(getters[attribute]);
348 if ([self isSelectorSupported:selector]) {
349 value = [self performSelector:selector withObject:parameter];
351 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
352 // If we have a delegate, check if attribute is a text marker
353 // attribute and call the associated selector on the delegate
355 NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters();
356 if (textMarkerGetters[attribute]) {
357 SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
358 if ([textMarkerDelegate respondsToSelector:selector]) {
359 value = [textMarkerDelegate performSelector:selector
360 withObject:parameter];
365 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
366 LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
367 attribute, parameter, value);
369 LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
374 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
377 - (id)accessibilityHitTest:(NSPoint)point {
378 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
379 return GetObjectOrRepresentedView([self moxHitTest:point]);
380 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
383 - (id)accessibilityFocusedUIElement {
384 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
385 return GetObjectOrRepresentedView([self moxFocusedUIElement]);
386 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
389 - (BOOL)isAccessibilityElement {
390 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
392 if ([self isExpired]) {
396 id parent = [self moxParent];
397 if (![parent isMOXAccessible]) {
401 return ![self moxIgnoreWithParent:parent];
403 NS_OBJC_END_TRY_BLOCK_RETURN(NO);
406 - (BOOL)accessibilityNotifiesWhenDestroyed {
410 #pragma mark - MOXAccessible protocol
412 - (NSNumber*)moxIndexForChildUIElement:(id)child {
413 return @([[self moxUnignoredChildren] indexOfObject:child]);
416 - (id)moxTopLevelUIElement {
417 return [self moxWindow];
420 - (id)moxHitTest:(NSPoint)point {
424 - (id)moxFocusedUIElement {
428 - (void)moxPostNotification:(NSString*)notification {
429 [self moxPostNotification:notification withUserInfo:nil];
432 - (void)moxPostNotification:(NSString*)notification
433 withUserInfo:(NSDictionary*)userInfo {
434 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
435 LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo);
437 LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
440 // This sends events via nsIObserverService to be consumed by our mochitests.
441 xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);
443 if (gfxPlatform::IsHeadless()) {
444 // Using a headless toolkit for tests and whatnot, posting accessibility
445 // notification won't work.
449 if (![self isAccessibilityElement]) {
450 // If this is an ignored object, don't expose it to system.
455 NSAccessibilityPostNotificationWithUserInfo(
456 GetObjectOrRepresentedView(self), notification, userInfo);
458 NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
463 - (BOOL)moxBlockSelector:(SEL)selector {
467 - (NSArray*)moxChildren {
471 - (NSArray*)moxUnignoredChildren {
472 NSMutableArray* unignoredChildren =
473 [[[NSMutableArray alloc] init] autorelease];
474 NSArray* allChildren = [self moxChildren];
476 for (MOXAccessibleBase* nativeChild in allChildren) {
477 if ([nativeChild moxIgnoreWithParent:self]) {
478 // If this child should be ignored get its unignored children.
479 // This will in turn recurse to any unignored descendants if the
482 addObjectsFromArray:[nativeChild moxUnignoredChildren]];
484 [unignoredChildren addObject:nativeChild];
488 return unignoredChildren;
491 - (id<mozAccessible>)moxParent {
495 - (id<mozAccessible>)moxUnignoredParent {
496 id nativeParent = [self moxParent];
498 if (![nativeParent isAccessibilityElement]) {
499 return [nativeParent moxUnignoredParent];
502 return GetObjectOrRepresentedView(nativeParent);
505 - (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
506 return [parent moxIgnoreChild:self];
509 - (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
513 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
517 - (BOOL)moxIsLiveRegion {
523 // objc-style description (from NSObject); not to be confused with the
524 // accessible description above.
525 - (NSString*)description {
526 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
527 if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) {
528 return [self moxMozDebugDescription];
532 return [NSString stringWithFormat:@"<%@: %p %@>",
533 NSStringFromClass([self class]), self,
542 MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
546 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
549 - (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
550 BOOL* stop))findBlock {
551 for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)];
552 element = [element moxUnignoredParent]) {
554 if (findBlock(element, &stop)) {
566 #pragma mark - Private
568 - (BOOL)isSelectorSupported:(SEL)selector {
570 [self respondsToSelector:selector] && ![self moxBlockSelector:selector];