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