Bug 1734067 [wpt PR 31108] - Update wpt metadata, a=testonly
[gecko.git] / accessible / mac / MOXAccessibleBase.mm
blob098a248a03623546430cd85edbe5f9e5e2bbe1a5
1 /* clang-format off */
2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
3 /* clang-format on */
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;
18 #undef LOG
19 mozilla::LogModule* GetMacAccessibilityLog() {
20   static mozilla::LazyLogModule sLog("MacAccessibility");
22   return sLog;
24 #define LOG(type, format, ...)                                             \
25   do {                                                                     \
26     if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) {                    \
27       NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
28       MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String]));   \
29     }                                                                      \
30   } while (0)
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;
44 @end
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];
57 @end
59 // Private methods
60 @interface MOXAccessibleBase ()
62 - (BOOL)isSelectorSupported:(SEL)selector;
64 @end
66 @implementation MOXAccessibleBase
68 #pragma mark - mozAccessible/widget
70 - (BOOL)hasRepresentedView {
71   return NO;
74 - (id)representedView {
75   return nil;
78 - (BOOL)isRoot {
79   return NO;
82 #pragma mark - mozAccessible/NSAccessibility
84 - (NSArray*)accessibilityAttributeNames {
85   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
87   if ([self isExpired]) {
88     return nil;
89   }
91   static NSMutableDictionary* attributesForEachClass = nil;
93   if (!attributesForEachClass) {
94     attributesForEachClass = [[NSMutableDictionary alloc] init];
95   }
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];
109       }
110     }
112     // If we have a delegate add all the text marker attributes.
113     if ([self moxTextMarkerDelegate]) {
114       [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
115     }
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;
122   }
124   return 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]) {
132     return nil;
133   }
135   id value = nil;
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];
141     }
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
145     // if so.
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];
151       }
152     }
153   }
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)
159                                            : nil;
160   }
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];
172       }
173     }
175     value = arr;
176   }
178   if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
179     if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
180       LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
181           value);
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);
190     }
191   }
193   return value;
195   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
198 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
199   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
201   if ([self isExpired]) {
202     return NO;
203   }
205   NSDictionary* setters = mac::AttributeSetters();
206   if (setters[attribute]) {
207     SEL selector = NSSelectorFromString(setters[attribute]);
208     if ([self isSelectorSupported:selector]) {
209       return YES;
210     }
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]) {
217         return YES;
218       }
219     }
220   }
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]) {
229     return;
230   }
232   LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
233       value);
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];
240     }
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
244     // if so.
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];
250       }
251     }
252   }
254   NS_OBJC_END_TRY_IGNORE_BLOCK;
257 - (NSArray*)accessibilityActionNames {
258   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
260   if ([self isExpired]) {
261     return nil;
262   }
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];
271     }
272   }
274   return actionNames;
276   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
279 - (void)accessibilityPerformAction:(NSString*)action {
280   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
282   if ([self isExpired]) {
283     return;
284   }
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];
293     }
294   }
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]) {
311     return nil;
312   }
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];
321     }
322   }
324   // If we have a delegate add all the text marker attributes.
325   if ([self moxTextMarkerDelegate]) {
326     [attributeNames
327         addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
328   }
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]) {
340     return nil;
341   }
343   id value = nil;
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];
350     }
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
354     // if so.
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];
361       }
362     }
363   }
365   if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
366     LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
367         attribute, parameter, value);
368   } else {
369     LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
370   }
372   return value;
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]) {
393     return YES;
394   }
396   id parent = [self moxParent];
397   if (![parent isMOXAccessible]) {
398     return YES;
399   }
401   return ![self moxIgnoreWithParent:parent];
403   NS_OBJC_END_TRY_BLOCK_RETURN(NO);
406 - (BOOL)accessibilityNotifiesWhenDestroyed {
407   return YES;
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 {
421   return self;
424 - (id)moxFocusedUIElement {
425   return self;
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);
436   } else {
437     LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
438   }
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.
446     return;
447   }
449   if (![self isAccessibilityElement]) {
450     // If this is an ignored object, don't expose it to system.
451     return;
452   }
454   if (userInfo) {
455     NSAccessibilityPostNotificationWithUserInfo(
456         GetObjectOrRepresentedView(self), notification, userInfo);
457   } else {
458     NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
459                                     notification);
460   }
463 - (BOOL)moxBlockSelector:(SEL)selector {
464   return NO;
467 - (NSArray*)moxChildren {
468   return @[];
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
480       // child is ignored.
481       [unignoredChildren
482           addObjectsFromArray:[nativeChild moxUnignoredChildren]];
483     } else {
484       [unignoredChildren addObject:nativeChild];
485     }
486   }
488   return unignoredChildren;
491 - (id<mozAccessible>)moxParent {
492   return nil;
495 - (id<mozAccessible>)moxUnignoredParent {
496   id nativeParent = [self moxParent];
498   if (![nativeParent isAccessibilityElement]) {
499     return [nativeParent moxUnignoredParent];
500   }
502   return GetObjectOrRepresentedView(nativeParent);
505 - (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
506   return [parent moxIgnoreChild:self];
509 - (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
510   return NO;
513 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
514   return nil;
517 - (BOOL)moxIsLiveRegion {
518   return NO;
521 #pragma mark -
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];
529     }
530   }
532   return [NSString stringWithFormat:@"<%@: %p %@>",
533                                     NSStringFromClass([self class]), self,
534                                     [self moxRole]];
537 - (BOOL)isExpired {
538   return mIsExpired;
541 - (void)expire {
542   MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
544   mIsExpired = YES;
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]) {
553     BOOL stop = NO;
554     if (findBlock(element, &stop)) {
555       return element;
556     }
558     if (stop) {
559       break;
560     }
561   }
563   return nil;
566 #pragma mark - Private
568 - (BOOL)isSelectorSupported:(SEL)selector {
569   return
570       [self respondsToSelector:selector] && ![self moxBlockSelector:selector];
573 @end