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/. */
7
8#import "MOXAccessibleBase.h"
9
10#import "MacSelectorMap.h"
11
12#include "nsObjCExceptions.h"
13#include "xpcAccessibleMacInterface.h"
14#include "mozilla/Logging.h"
15
16using namespace mozilla::a11y;
17
18#undef LOG
19mozilla::LogModule* GetMacAccessibilityLog() {
20  static mozilla::LazyLogModule sLog("MacAccessibility");
21
22  return sLog;
23}
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)
31
32@interface NSObject (MOXAccessible)
33
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;
39
40// Same as above, but this checks if the NSObject is an array with
41// mozAccessible conforming objects.
42- (BOOL)hasMOXAccessibles;
43
44@end
45
46@implementation NSObject (MOXAccessible)
47
48- (BOOL)isMOXAccessible {
49  return [self conformsToProtocol:@protocol(MOXAccessible)];
50}
51
52- (BOOL)hasMOXAccessibles {
53  return [self isKindOfClass:[NSArray class]] &&
54         [[(NSArray*)self firstObject] isMOXAccessible];
55}
56
57@end
58
59// Private methods
60@interface MOXAccessibleBase ()
61
62- (BOOL)isSelectorSupported:(SEL)selector;
63
64@end
65
66@implementation MOXAccessibleBase
67
68#pragma mark - mozAccessible/widget
69
70- (BOOL)hasRepresentedView {
71  return NO;
72}
73
74- (id)representedView {
75  return nil;
76}
77
78- (BOOL)isRoot {
79  return NO;
80}
81
82#pragma mark - mozAccessible/NSAccessibility
83
84- (NSArray*)accessibilityAttributeNames {
85  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
86
87  if ([self isExpired]) {
88    return nil;
89  }
90
91  static NSMutableDictionary* attributesForEachClass = nil;
92
93  if (!attributesForEachClass) {
94    attributesForEachClass = [[NSMutableDictionary alloc] init];
95  }
96
97  NSMutableArray* attributes =
98      attributesForEachClass [[self class]]
99          ?: [[[NSMutableArray alloc] init] autorelease];
100
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    }
111
112    // If we have a delegate add all the text marker attributes.
113    if ([self moxTextMarkerDelegate]) {
114      [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
115    }
116
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  }
123
124  return attributes;
125
126  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
127}
128
129- (id)accessibilityAttributeValue:(NSString*)attribute {
130  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
131  if ([self isExpired]) {
132    return nil;
133  }
134
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  }
154
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  }
161
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    }
174
175    value = arr;
176  }
177
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  }
192
193  return value;
194
195  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
196}
197
198- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
199  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
200
201  if ([self isExpired]) {
202    return NO;
203  }
204
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  }
221
222  NS_OBJC_END_TRY_BLOCK_RETURN(NO);
223}
224
225- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
226  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
227
228  if ([self isExpired]) {
229    return;
230  }
231
232  LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
233      value);
234
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  }
253
254  NS_OBJC_END_TRY_IGNORE_BLOCK;
255}
256
257- (NSArray*)accessibilityActionNames {
258  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
259
260  if ([self isExpired]) {
261    return nil;
262  }
263
264  NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];
265
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  }
273
274  return actionNames;
275
276  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
277}
278
279- (void)accessibilityPerformAction:(NSString*)action {
280  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
281
282  if ([self isExpired]) {
283    return;
284  }
285
286  LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);
287
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  }
295
296  NS_OBJC_END_TRY_IGNORE_BLOCK;
297}
298
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);
305}
306
307- (NSArray*)accessibilityParameterizedAttributeNames {
308  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
309
310  if ([self isExpired]) {
311    return nil;
312  }
313
314  NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];
315
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  }
323
324  // If we have a delegate add all the text marker attributes.
325  if ([self moxTextMarkerDelegate]) {
326    [attributeNames
327        addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
328  }
329
330  return attributeNames;
331
332  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
333}
334
335- (id)accessibilityAttributeValue:(NSString*)attribute
336                     forParameter:(id)parameter {
337  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
338
339  if ([self isExpired]) {
340    return nil;
341  }
342
343  id value = nil;
344
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  }
364
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  }
371
372  return value;
373
374  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
375}
376
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);
381}
382
383- (id)accessibilityFocusedUIElement {
384  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
385  return GetObjectOrRepresentedView([self moxFocusedUIElement]);
386  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
387}
388
389- (BOOL)isAccessibilityElement {
390  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
391
392  if ([self isExpired]) {
393    return YES;
394  }
395
396  id parent = [self moxParent];
397  if (![parent isMOXAccessible]) {
398    return YES;
399  }
400
401  return ![self moxIgnoreWithParent:parent];
402
403  NS_OBJC_END_TRY_BLOCK_RETURN(NO);
404}
405
406- (BOOL)accessibilityNotifiesWhenDestroyed {
407  return YES;
408}
409
410#pragma mark - MOXAccessible protocol
411
412- (NSNumber*)moxIndexForChildUIElement:(id)child {
413  return @([[self moxUnignoredChildren] indexOfObject:child]);
414}
415
416- (id)moxTopLevelUIElement {
417  return [self moxWindow];
418}
419
420- (id)moxHitTest:(NSPoint)point {
421  return self;
422}
423
424- (id)moxFocusedUIElement {
425  return self;
426}
427
428- (void)moxPostNotification:(NSString*)notification {
429  [self moxPostNotification:notification withUserInfo:nil];
430}
431
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  }
439
440  // This sends events via nsIObserverService to be consumed by our mochitests.
441  xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);
442
443  if (gfxPlatform::IsHeadless()) {
444    // Using a headless toolkit for tests and whatnot, posting accessibility
445    // notification won't work.
446    return;
447  }
448
449  if (![self isAccessibilityElement]) {
450    // If this is an ignored object, don't expose it to system.
451    return;
452  }
453
454  if (userInfo) {
455    NSAccessibilityPostNotificationWithUserInfo(
456        GetObjectOrRepresentedView(self), notification, userInfo);
457  } else {
458    NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
459                                    notification);
460  }
461}
462
463- (BOOL)moxBlockSelector:(SEL)selector {
464  return NO;
465}
466
467- (NSArray*)moxChildren {
468  return @[];
469}
470
471- (NSArray*)moxUnignoredChildren {
472  NSMutableArray* unignoredChildren =
473      [[[NSMutableArray alloc] init] autorelease];
474  NSArray* allChildren = [self moxChildren];
475
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  }
487
488  return unignoredChildren;
489}
490
491- (id<mozAccessible>)moxParent {
492  return nil;
493}
494
495- (id<mozAccessible>)moxUnignoredParent {
496  id nativeParent = [self moxParent];
497
498  if (![nativeParent isAccessibilityElement]) {
499    return [nativeParent moxUnignoredParent];
500  }
501
502  return GetObjectOrRepresentedView(nativeParent);
503}
504
505- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
506  return [parent moxIgnoreChild:self];
507}
508
509- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
510  return NO;
511}
512
513- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
514  return nil;
515}
516
517- (BOOL)moxIsLiveRegion {
518  return NO;
519}
520
521#pragma mark -
522
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  }
531
532  return [NSString stringWithFormat:@"<%@: %p %@>",
533                                    NSStringFromClass([self class]), self,
534                                    [self moxRole]];
535}
536
537- (BOOL)isExpired {
538  return mIsExpired;
539}
540
541- (void)expire {
542  MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
543
544  mIsExpired = YES;
545
546  [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
547}
548
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    }
557
558    if (stop) {
559      break;
560    }
561  }
562
563  return nil;
564}
565
566#pragma mark - Private
567
568- (BOOL)isSelectorSupported:(SEL)selector {
569  return
570      [self respondsToSelector:selector] && ![self moxBlockSelector:selector];
571}
572
573@end
574