MOXAccessibleBase.mm (17204B)
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 <Accessibility/Accessibility.h> 9 10 #import "MOXAccessibleBase.h" 11 12 #import "MacSelectorMap.h" 13 14 #include "nsObjCExceptions.h" 15 #include "xpcAccessibleMacInterface.h" 16 #include "mozilla/Logging.h" 17 #include "gfxPlatform.h" 18 19 using namespace mozilla; 20 using namespace mozilla::a11y; 21 22 #undef LOG 23 mozilla::LogModule* GetMacAccessibilityLog() { 24 static mozilla::LazyLogModule sLog("MacAccessibility"); 25 26 return sLog; 27 } 28 #define LOG(type, format, ...) \ 29 do { \ 30 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \ 31 NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ 32 MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \ 33 } \ 34 } while (0) 35 36 @interface NSObject (MOXAccessible) 37 38 // This NSObject conforms to MOXAccessible. 39 // This is needed to we know to mutate the value 40 // (get represented view, check isAccessibilityElement) 41 // before forwarding it to NSAccessibility. 42 - (BOOL)isMOXAccessible; 43 44 // Same as above, but this checks if the NSObject is an array with 45 // mozAccessible conforming objects. 46 - (BOOL)hasMOXAccessibles; 47 48 @end 49 50 @implementation NSObject (MOXAccessible) 51 52 - (BOOL)isMOXAccessible { 53 return [self conformsToProtocol:@protocol(MOXAccessible)]; 54 } 55 56 - (BOOL)hasMOXAccessibles { 57 return [self isKindOfClass:[NSArray class]] && 58 [[(NSArray*)self firstObject] isMOXAccessible]; 59 } 60 61 @end 62 63 // Private methods 64 @interface MOXAccessibleBase () 65 66 - (BOOL)isSelectorSupported:(SEL)selector; 67 68 @end 69 70 @implementation MOXAccessibleBase 71 72 #pragma mark - mozAccessible/widget 73 74 - (BOOL)hasRepresentedView { 75 return NO; 76 } 77 78 - (id)representedView { 79 return nil; 80 } 81 82 - (BOOL)isRoot { 83 return NO; 84 } 85 86 #pragma mark - mozAccessible/NSAccessibility 87 88 - (NSArray*)accessibilityAttributeNames { 89 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 90 91 if ([self isExpired]) { 92 return nil; 93 } 94 95 static NSMutableDictionary* attributesForEachClass = nil; 96 97 if (!attributesForEachClass) { 98 attributesForEachClass = [[NSMutableDictionary alloc] init]; 99 } 100 101 NSMutableArray* attributes = 102 attributesForEachClass [[self class]] 103 ?: [[[NSMutableArray alloc] init] autorelease]; 104 105 NSDictionary* getters = mac::AttributeGetters(); 106 if (![attributes count]) { 107 // Go through all our attribute getters, if they are supported by this class 108 // advertise the attribute name. 109 for (NSString* attribute in getters) { 110 SEL selector = NSSelectorFromString(getters[attribute]); 111 if ([self isSelectorSupported:selector]) { 112 [attributes addObject:attribute]; 113 } 114 } 115 116 // If we have a delegate add all the text marker attributes. 117 if ([self moxTextMarkerDelegate]) { 118 [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]]; 119 } 120 121 // We store a hash table with types as keys, and atttribute lists as values. 122 // This lets us cache the atttribute list of each subclass so we only 123 // need to gather its MOXAccessible methods once. 124 // XXX: Uncomment when accessibilityAttributeNames is removed from all 125 // subclasses. attributesForEachClass[[self class]] = attributes; 126 } 127 128 return attributes; 129 130 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 131 } 132 133 - (id)accessibilityAttributeValue:(NSString*)attribute { 134 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 135 if ([self isExpired]) { 136 return nil; 137 } 138 139 id value = nil; 140 NSDictionary* getters = mac::AttributeGetters(); 141 if (getters[attribute]) { 142 SEL selector = NSSelectorFromString(getters[attribute]); 143 if ([self isSelectorSupported:selector]) { 144 value = [self performSelector:selector]; 145 } 146 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { 147 // If we have a delegate, check if attribute is a text marker 148 // attribute and call the associated selector on the delegate 149 // if so. 150 NSDictionary* textMarkerGetters = mac::TextAttributeGetters(); 151 if (textMarkerGetters[attribute]) { 152 SEL selector = NSSelectorFromString(textMarkerGetters[attribute]); 153 if ([textMarkerDelegate respondsToSelector:selector]) { 154 value = [textMarkerDelegate performSelector:selector]; 155 } 156 } 157 } 158 159 if ([value isMOXAccessible]) { 160 // If this is a MOXAccessible, get its represented view or filter it if 161 // it should be ignored. 162 value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value) 163 : nil; 164 } 165 166 if ([value hasMOXAccessibles]) { 167 // If this is an array of mozAccessibles, get each element's represented 168 // view and remove it from the returned array if it should be ignored. 169 NSUInteger arrSize = [value count]; 170 NSMutableArray* arr = 171 [[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease]; 172 for (NSUInteger i = 0; i < arrSize; i++) { 173 id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]); 174 if ([mozAcc isAccessibilityElement]) { 175 [arr addObject:mozAcc]; 176 } 177 } 178 179 value = arr; 180 } 181 182 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) { 183 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { 184 LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute, 185 value); 186 } else if (![attribute isEqualToString:@"AXParent"] && 187 ![attribute isEqualToString:@"AXRole"] && 188 ![attribute isEqualToString:@"AXSubrole"] && 189 ![attribute isEqualToString:@"AXSize"] && 190 ![attribute isEqualToString:@"AXPosition"] && 191 ![attribute isEqualToString:@"AXRole"] && 192 ![attribute isEqualToString:@"AXChildren"]) { 193 LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute); 194 } 195 } 196 197 return value; 198 199 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 200 } 201 202 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { 203 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 204 205 if ([self isExpired]) { 206 return NO; 207 } 208 209 NSDictionary* setters = mac::AttributeSetters(); 210 if (setters[attribute]) { 211 SEL selector = NSSelectorFromString(setters[attribute]); 212 if ([self isSelectorSupported:selector]) { 213 return YES; 214 } 215 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { 216 // If we have a delegate, check text setters on delegate 217 NSDictionary* textMarkerSetters = mac::TextAttributeSetters(); 218 if (textMarkerSetters[attribute]) { 219 SEL selector = NSSelectorFromString(textMarkerSetters[attribute]); 220 if ([textMarkerDelegate respondsToSelector:selector]) { 221 return YES; 222 } 223 } 224 } 225 226 NS_OBJC_END_TRY_BLOCK_RETURN(NO); 227 } 228 229 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { 230 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 231 232 if ([self isExpired]) { 233 return; 234 } 235 236 LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute, 237 value); 238 239 NSDictionary* setters = mac::AttributeSetters(); 240 if (setters[attribute]) { 241 SEL selector = NSSelectorFromString(setters[attribute]); 242 if ([self isSelectorSupported:selector]) { 243 [self performSelector:selector withObject:value]; 244 } 245 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { 246 // If we have a delegate, check if attribute is a text marker 247 // attribute and call the associated selector on the delegate 248 // if so. 249 NSDictionary* textMarkerSetters = mac::TextAttributeSetters(); 250 if (textMarkerSetters[attribute]) { 251 SEL selector = NSSelectorFromString(textMarkerSetters[attribute]); 252 if ([textMarkerDelegate respondsToSelector:selector]) { 253 [textMarkerDelegate performSelector:selector withObject:value]; 254 } 255 } 256 } 257 258 NS_OBJC_END_TRY_IGNORE_BLOCK; 259 } 260 261 - (NSArray*)accessibilityActionNames { 262 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 263 264 if ([self isExpired]) { 265 return nil; 266 } 267 268 NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease]; 269 270 NSDictionary* actions = mac::Actions(); 271 for (NSString* action in actions) { 272 SEL selector = NSSelectorFromString(actions[action]); 273 if ([self isSelectorSupported:selector]) { 274 [actionNames addObject:action]; 275 } 276 } 277 278 return actionNames; 279 280 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 281 } 282 283 - (void)accessibilityPerformAction:(NSString*)action { 284 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 285 286 if ([self isExpired]) { 287 return; 288 } 289 290 LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action); 291 292 NSDictionary* actions = mac::Actions(); 293 if (actions[action]) { 294 SEL selector = NSSelectorFromString(actions[action]); 295 if ([self isSelectorSupported:selector]) { 296 [self performSelector:selector]; 297 } 298 } 299 300 NS_OBJC_END_TRY_IGNORE_BLOCK; 301 } 302 303 - (NSString*)accessibilityActionDescription:(NSString*)action { 304 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 305 // by default we return whatever the MacOS API know about. 306 // if you have custom actions, override. 307 return NSAccessibilityActionDescription(action); 308 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 309 } 310 311 - (NSArray*)accessibilityParameterizedAttributeNames { 312 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 313 314 if ([self isExpired]) { 315 return nil; 316 } 317 318 NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease]; 319 320 NSDictionary* attributes = mac::ParameterizedAttributeGetters(); 321 for (NSString* attribute in attributes) { 322 SEL selector = NSSelectorFromString(attributes[attribute]); 323 if ([self isSelectorSupported:selector]) { 324 [attributeNames addObject:attribute]; 325 } 326 } 327 328 // If we have a delegate add all the text marker attributes. 329 if ([self moxTextMarkerDelegate]) { 330 [attributeNames 331 addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]]; 332 } 333 334 return attributeNames; 335 336 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 337 } 338 339 - (id)accessibilityAttributeValue:(NSString*)attribute 340 forParameter:(id)parameter { 341 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 342 343 if ([self isExpired]) { 344 return nil; 345 } 346 347 id value = nil; 348 349 NSDictionary* getters = mac::ParameterizedAttributeGetters(); 350 if (getters[attribute]) { 351 SEL selector = NSSelectorFromString(getters[attribute]); 352 if ([self isSelectorSupported:selector]) { 353 value = [self performSelector:selector withObject:parameter]; 354 } 355 } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) { 356 // If we have a delegate, check if attribute is a text marker 357 // attribute and call the associated selector on the delegate 358 // if so. 359 NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters(); 360 if (textMarkerGetters[attribute]) { 361 SEL selector = NSSelectorFromString(textMarkerGetters[attribute]); 362 if ([textMarkerDelegate respondsToSelector:selector]) { 363 value = [textMarkerDelegate performSelector:selector 364 withObject:parameter]; 365 } 366 } 367 } 368 369 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { 370 LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self, 371 attribute, parameter, value); 372 } else { 373 LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute); 374 } 375 376 return value; 377 378 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 379 } 380 381 - (id)accessibilityHitTest:(NSPoint)point { 382 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 383 return GetObjectOrRepresentedView([self moxHitTest:point]); 384 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 385 } 386 387 - (id)accessibilityFocusedUIElement { 388 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 389 return GetObjectOrRepresentedView([self moxFocusedUIElement]); 390 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 391 } 392 393 - (id)accessibilityCustomActions { 394 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 395 return [self moxCustomActions]; 396 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 397 } 398 399 - (BOOL)isAccessibilityElement { 400 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 401 402 if ([self isExpired]) { 403 return YES; 404 } 405 406 id parent = [self moxParent]; 407 if (![parent isMOXAccessible]) { 408 return YES; 409 } 410 411 return ![self moxIgnoreWithParent:parent]; 412 413 NS_OBJC_END_TRY_BLOCK_RETURN(NO); 414 } 415 416 - (BOOL)accessibilityNotifiesWhenDestroyed { 417 return YES; 418 } 419 420 #pragma mark - AXCustomContentProvider protocol 421 422 - (NSArray*)accessibilityCustomContent { 423 return [self moxCustomContent]; 424 } 425 426 #pragma mark - MOXAccessible protocol 427 428 - (NSNumber*)moxIndexForChildUIElement:(id)child { 429 return @([[self moxUnignoredChildren] indexOfObject:child]); 430 } 431 432 - (id)moxTopLevelUIElement { 433 return [self moxWindow]; 434 } 435 436 - (id)moxHitTest:(NSPoint)point { 437 return self; 438 } 439 440 - (id)moxFocusedUIElement { 441 return self; 442 } 443 444 - (void)moxPostNotification:(NSString*)notification { 445 [self moxPostNotification:notification withUserInfo:nil]; 446 } 447 448 - (void)moxPostNotification:(NSString*)notification 449 withUserInfo:(NSDictionary*)userInfo { 450 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) { 451 LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo); 452 } else { 453 LOG(LogLevel::Debug, @"%@ notify %@", self, notification); 454 } 455 456 // This sends events via nsIObserverService to be consumed by our mochitests. 457 xpcAccessibleMacEvent::FireEvent(self, notification, userInfo); 458 459 if (gfxPlatform::IsHeadless()) { 460 // Using a headless toolkit for tests and whatnot, posting accessibility 461 // notification won't work. 462 return; 463 } 464 465 if (![self isAccessibilityElement]) { 466 // If this is an ignored object, don't expose it to system. 467 return; 468 } 469 470 if (userInfo) { 471 NSAccessibilityPostNotificationWithUserInfo( 472 GetObjectOrRepresentedView(self), notification, userInfo); 473 } else { 474 NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), 475 notification); 476 } 477 } 478 479 - (BOOL)moxBlockSelector:(SEL)selector { 480 return NO; 481 } 482 483 - (NSArray*)moxChildren { 484 return @[]; 485 } 486 487 - (NSArray*)moxUnignoredChildren { 488 NSMutableArray* unignoredChildren = 489 [[[NSMutableArray alloc] init] autorelease]; 490 NSArray* allChildren = [self moxChildren]; 491 492 for (MOXAccessibleBase* nativeChild in allChildren) { 493 if ([nativeChild moxIgnoreWithParent:self]) { 494 // If this child should be ignored get its unignored children. 495 // This will in turn recurse to any unignored descendants if the 496 // child is ignored. 497 [unignoredChildren 498 addObjectsFromArray:[nativeChild moxUnignoredChildren]]; 499 } else { 500 [unignoredChildren addObject:nativeChild]; 501 } 502 } 503 504 return unignoredChildren; 505 } 506 507 - (id<mozAccessible>)moxParent { 508 return nil; 509 } 510 511 - (id<mozAccessible>)moxUnignoredParent { 512 id<mozAccessible> nativeParent = [self moxParent]; 513 if (!nativeParent) { 514 return nil; 515 } 516 517 if (![nativeParent isAccessibilityElement]) { 518 if ([nativeParent conformsToProtocol:@protocol(MOXAccessible)] && 519 [nativeParent respondsToSelector:@selector(moxUnignoredParent)]) { 520 // Cast away the protocol so we can cast to another protocol. 521 id bareNativeParent = nativeParent; 522 id<MOXAccessible> moxNativeParent = bareNativeParent; 523 return [moxNativeParent moxUnignoredParent]; 524 } 525 } 526 527 return GetObjectOrRepresentedView(nativeParent); 528 } 529 530 - (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent { 531 return [parent moxIgnoreChild:self]; 532 } 533 534 - (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child { 535 return NO; 536 } 537 538 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { 539 return nil; 540 } 541 542 - (BOOL)moxIsLiveRegion { 543 return NO; 544 } 545 546 - (BOOL)moxIsTextField { 547 return NO; 548 } 549 550 #pragma mark - 551 552 // objc-style description (from NSObject); not to be confused with the 553 // accessible description above. 554 - (NSString*)description { 555 if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) { 556 if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) { 557 return [self moxMozDebugDescription]; 558 } 559 } 560 561 return [NSString stringWithFormat:@"<%@: %p %@>", 562 NSStringFromClass([self class]), self, 563 [self moxRole]]; 564 } 565 566 - (BOOL)isExpired { 567 return mIsExpired; 568 } 569 570 - (void)expire { 571 MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!"); 572 573 mIsExpired = YES; 574 575 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; 576 } 577 578 - (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc, 579 BOOL* stop))findBlock { 580 for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)]; 581 element = [element moxUnignoredParent]) { 582 BOOL stop = NO; 583 if (findBlock(element, &stop)) { 584 return element; 585 } 586 587 if (stop || ![element respondsToSelector:@selector(moxUnignoredParent)]) { 588 break; 589 } 590 } 591 592 return nil; 593 } 594 595 - (NSArray*)moxCustomContent { 596 return nil; 597 } 598 599 - (NSArray*)moxCustomActions { 600 return nil; 601 } 602 603 #pragma mark - Private 604 605 - (BOOL)isSelectorSupported:(SEL)selector { 606 return 607 [self respondsToSelector:selector] && ![self moxBlockSelector:selector]; 608 } 609 610 @end