mozAccessible.mm (36082B)
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 "mozAccessible.h" 11 #include "MOXAccessibleBase.h" 12 13 #import "MacUtils.h" 14 #import "mozView.h" 15 #import "MOXSearchInfo.h" 16 #import "MOXTextMarkerDelegate.h" 17 #import "MOXWebAreaAccessible.h" 18 #import "mozRootAccessible.h" 19 #import "mozTextAccessible.h" 20 21 #include "LocalAccessible-inl.h" 22 #include "nsAccUtils.h" 23 #include "DocAccessibleParent.h" 24 #include "Relation.h" 25 #include "mozilla/a11y/Role.h" 26 #include "RootAccessible.h" 27 #include "mozilla/a11y/PDocAccessible.h" 28 #include "mozilla/dom/BrowserParent.h" 29 #include "OuterDocAccessible.h" 30 #include "nsChildView.h" 31 #include "TextLeafRange.h" 32 #include "xpcAccessibleMacInterface.h" 33 34 #include "nsRect.h" 35 #include "nsCocoaUtils.h" 36 #include "nsCoord.h" 37 #include "nsObjCExceptions.h" 38 #include "nsWhitespaceTokenizer.h" 39 #include <prdtoa.h> 40 41 using namespace mozilla; 42 using namespace mozilla::a11y; 43 44 #pragma mark - 45 46 @interface mozAccessible () 47 - (void)maybePostA11yUtilNotification; 48 @end 49 50 @implementation mozAccessible 51 52 - (id)initWithAccessible:(Accessible*)aAcc { 53 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 54 MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null"); 55 if ((self = [super init])) { 56 mGeckoAccessible = aAcc; 57 mRole = aAcc->Role(); 58 } 59 60 return self; 61 62 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 63 } 64 65 - (void)dealloc { 66 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 67 68 [super dealloc]; 69 70 NS_OBJC_END_TRY_IGNORE_BLOCK; 71 } 72 73 #pragma mark - mozAccessible widget 74 75 - (BOOL)hasRepresentedView { 76 return NO; 77 } 78 79 - (id)representedView { 80 return nil; 81 } 82 83 - (BOOL)isRoot { 84 return NO; 85 } 86 87 #pragma mark - 88 89 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { 90 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { 91 if (acc->IsContent() && acc->GetContent()->IsXULElement()) { 92 if (acc->VisibilityState() & states::INVISIBLE) { 93 return YES; 94 } 95 } 96 } 97 98 return [parent moxIgnoreChild:self]; 99 } 100 101 - (BOOL)moxIgnoreChild:(mozAccessible*)child { 102 return nsAccUtils::MustPrune(mGeckoAccessible); 103 } 104 105 - (id)childAt:(uint32_t)i { 106 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 107 108 Accessible* child = mGeckoAccessible->ChildAt(i); 109 return child ? GetNativeFromGeckoAccessible(child) : nil; 110 111 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 112 } 113 114 - (uint64_t)state { 115 return mGeckoAccessible->State(); 116 } 117 118 - (uint64_t)stateWithMask:(uint64_t)mask { 119 return [self state] & mask; 120 } 121 122 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { 123 if (state == states::BUSY) { 124 [self moxPostNotification:@"AXElementBusyChanged"]; 125 } 126 127 if (state == states::EXPANDED) { 128 [self moxPostNotification:@"AXExpandedChanged"]; 129 } 130 } 131 132 - (mozilla::a11y::Accessible*)geckoAccessible { 133 return mGeckoAccessible; 134 } 135 136 #pragma mark - MOXAccessible protocol 137 138 - (BOOL)moxBlockSelector:(SEL)selector { 139 if (selector == @selector(moxPerformPress)) { 140 uint8_t actionCount = mGeckoAccessible->ActionCount(); 141 142 // If we have no action, we don't support press, so return YES. 143 return actionCount == 0; 144 } 145 146 if (selector == @selector(moxSetFocused:)) { 147 return [self stateWithMask:states::FOCUSABLE] == 0; 148 } 149 150 if (selector == @selector(moxARIALive) || 151 selector == @selector(moxARIAAtomic) || 152 selector == @selector(moxARIARelevant)) { 153 return ![self moxIsLiveRegion]; 154 } 155 156 if (selector == @selector(moxARIAPosInSet) || selector == @selector 157 (moxARIASetSize)) { 158 GroupPos groupPos = mGeckoAccessible->GroupPosition(); 159 return groupPos.setSize == 0; 160 } 161 162 if (selector == @selector(moxExpanded)) { 163 return [self stateWithMask:states::EXPANDABLE] == 0; 164 } 165 166 if ([self blockTextFieldMethod:selector]) { 167 return YES; 168 } 169 170 return [super moxBlockSelector:selector]; 171 } 172 173 - (id)moxFocusedUIElement { 174 MOZ_ASSERT(mGeckoAccessible); 175 // This only gets queried on the web area or the root group 176 // so just use the doc's focused child instead of trying to get 177 // the focused child of mGeckoAccessible. 178 Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible); 179 mozAccessible* focusedChild = 180 GetNativeFromGeckoAccessible(doc->FocusedChild()); 181 182 if ([focusedChild isAccessibilityElement]) { 183 return focusedChild; 184 } 185 186 // return ourself if we can't get a native focused child. 187 return self; 188 } 189 190 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { 191 MOZ_ASSERT(mGeckoAccessible); 192 193 return [MOXTextMarkerDelegate 194 getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)]; 195 } 196 197 - (BOOL)moxIsLiveRegion { 198 return mIsLiveRegion; 199 } 200 201 - (id)moxHitTest:(NSPoint)point { 202 MOZ_ASSERT(mGeckoAccessible); 203 204 // Convert the given screen-global point in the cocoa coordinate system (with 205 // origin in the bottom-left corner of the screen) into point in the Gecko 206 // coordinate system (with origin in a top-left screen point). 207 NSScreen* scalingView = utils::GetNSScreenForAcc(self); 208 // Regardless of screen selected above, VO is only happy if we use the 209 // main screen height for Y coordinate conversion. This is consistent with 210 // moxFrame and GeckoTextMarkerRange::Bounds(). 211 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; 212 NSPoint tmpPoint = 213 NSMakePoint(point.x, [mainView frame].size.height - point.y); 214 LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels( 215 tmpPoint, nsCocoaUtils::GetBackingScaleFactor(scalingView)); 216 217 Accessible* child = mGeckoAccessible->ChildAtPoint( 218 geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild); 219 220 if (child) { 221 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); 222 return [nativeChild isAccessibilityElement] 223 ? nativeChild 224 : [nativeChild moxUnignoredParent]; 225 } 226 227 // if we didn't find anything, return ourself or child view. 228 return self; 229 } 230 231 - (id<mozAccessible>)moxParent { 232 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 233 if ([self isExpired]) { 234 return nil; 235 } 236 237 Accessible* parent = mGeckoAccessible->Parent(); 238 239 if (!parent) { 240 return nil; 241 } 242 243 id nativeParent = GetNativeFromGeckoAccessible(parent); 244 if ([nativeParent isKindOfClass:[MOXWebAreaAccessible class]]) { 245 // Before returning a WebArea as parent, check to see if 246 // there is a generated root group that is an intermediate container. 247 if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) { 248 nativeParent = rootGroup; 249 } 250 } 251 252 if (!nativeParent && mGeckoAccessible->IsLocal()) { 253 // Return native of root accessible if we have no direct parent. 254 // XXX: need to return a sensible fallback in proxy case as well 255 nativeParent = GetNativeFromGeckoAccessible( 256 mGeckoAccessible->AsLocal()->RootAccessible()); 257 } 258 259 return GetObjectOrRepresentedView(nativeParent); 260 261 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 262 } 263 264 // gets all our native children lazily, including those that are ignored. 265 - (NSArray*)moxChildren { 266 MOZ_ASSERT(mGeckoAccessible); 267 268 NSMutableArray* children = [[[NSMutableArray alloc] 269 initWithCapacity:mGeckoAccessible->ChildCount()] autorelease]; 270 271 for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount(); 272 childIdx++) { 273 Accessible* child = mGeckoAccessible->ChildAt(childIdx); 274 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); 275 if (!nativeChild) { 276 continue; 277 } 278 279 [children addObject:nativeChild]; 280 } 281 282 return children; 283 } 284 285 - (NSValue*)moxPosition { 286 CGRect frame = [[self moxFrame] rectValue]; 287 288 return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)]; 289 } 290 291 - (NSValue*)moxSize { 292 CGRect frame = [[self moxFrame] rectValue]; 293 294 return 295 [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)]; 296 } 297 298 - (NSString*)moxRole { 299 if (mRole == roles::ENTRY || 300 (mGeckoAccessible->IsGeneric() && mGeckoAccessible->IsEditableRoot())) { 301 if ([self stateWithMask:states::MULTI_LINE]) { 302 // This is a special case where we have a separate role when an entry is a 303 // multiline text area. 304 return NSAccessibilityTextAreaRole; 305 } 306 307 return NSAccessibilityTextFieldRole; 308 } 309 310 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 311 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 312 nameRule) \ 313 case roles::geckoRole: \ 314 return macRole; 315 316 switch (mRole) { 317 #include "RoleMap.h" 318 default: 319 MOZ_ASSERT_UNREACHABLE("Unknown role."); 320 return NSAccessibilityUnknownRole; 321 } 322 323 #undef ROLE 324 } 325 326 - (nsStaticAtom*)ARIARole { 327 MOZ_ASSERT(mGeckoAccessible); 328 329 if (mGeckoAccessible->HasARIARole()) { 330 const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap(); 331 return roleMap->roleAtom; 332 } 333 334 return nsGkAtoms::_empty; 335 } 336 337 - (NSString*)moxSubrole { 338 MOZ_ASSERT(mGeckoAccessible); 339 340 // Deal with landmarks first 341 // macOS groups the specific landmark types of DPub ARIA into two broad 342 // categories with corresponding subroles: Navigation and region/container. 343 if (mRole == roles::LANDMARK) { 344 nsAtom* landmark = mGeckoAccessible->LandmarkRole(); 345 // HTML Elements treated as landmarks, and ARIA landmarks. 346 if (landmark) { 347 if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner"; 348 if (landmark == nsGkAtoms::complementary) 349 return @"AXLandmarkComplementary"; 350 if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo"; 351 if (landmark == nsGkAtoms::main) return @"AXLandmarkMain"; 352 if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation"; 353 if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch"; 354 } 355 356 // None of the above, so assume DPub ARIA. 357 return @"AXLandmarkRegion"; 358 } 359 360 // Now, deal with widget roles 361 nsStaticAtom* roleAtom = nullptr; 362 363 if (mRole == roles::DIALOG) { 364 roleAtom = [self ARIARole]; 365 366 if (roleAtom == nsGkAtoms::alertdialog) { 367 return @"AXApplicationAlertDialog"; 368 } 369 if (roleAtom == nsGkAtoms::dialog) { 370 return @"AXApplicationDialog"; 371 } 372 } 373 374 if (mRole == roles::FORM) { 375 roleAtom = [self ARIARole]; 376 377 if (roleAtom == nsGkAtoms::form) { 378 return @"AXLandmarkForm"; 379 } 380 } 381 382 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 383 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 384 nameRule) \ 385 case roles::geckoRole: \ 386 if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \ 387 return macSubrole; \ 388 } else { \ 389 break; \ 390 } 391 392 switch (mRole) { 393 #include "RoleMap.h" 394 } 395 396 // These are special. They map to roles::NOTHING 397 // and are instructed by the ARIA map to use the native host role. 398 roleAtom = [self ARIARole]; 399 400 if (roleAtom == nsGkAtoms::log) { 401 return @"AXApplicationLog"; 402 } 403 404 if (roleAtom == nsGkAtoms::timer) { 405 return @"AXApplicationTimer"; 406 } 407 // macOS added an AXSubrole value to distinguish generic AXGroup objects 408 // from those which are AXGroups as a result of an explicit ARIA role, 409 // such as the non-landmark, non-listitem text containers in DPub ARIA. 410 if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) { 411 return @"AXApplicationGroup"; 412 } 413 414 return NSAccessibilityUnknownSubrole; 415 416 #undef ROLE 417 } 418 419 struct RoleDescrMap { 420 NSString* role; 421 const nsLiteralString description; 422 }; 423 424 static constexpr RoleDescrMap sRoleDescrMap[] = { 425 {@"AXApplicationAlert", u"alert"_ns}, 426 {@"AXApplicationAlertDialog", u"alertDialog"_ns}, 427 {@"AXApplicationDialog", u"dialog"_ns}, 428 {@"AXApplicationLog", u"log"_ns}, 429 {@"AXApplicationMarquee", u"marquee"_ns}, 430 {@"AXApplicationStatus", u"status"_ns}, 431 {@"AXApplicationTimer", u"timer"_ns}, 432 {@"AXContentSeparator", u"separator"_ns}, 433 {@"AXDefinition", u"definition"_ns}, 434 {@"AXDetails", u"details"_ns}, 435 {@"AXDocument", u"document"_ns}, 436 {@"AXDocumentArticle", u"article"_ns}, 437 {@"AXDocumentMath", u"math"_ns}, 438 {@"AXDocumentNote", u"note"_ns}, 439 {@"AXLandmarkApplication", u"application"_ns}, 440 {@"AXLandmarkBanner", u"banner"_ns}, 441 {@"AXLandmarkComplementary", u"complementary"_ns}, 442 {@"AXLandmarkContentInfo", u"content"_ns}, 443 {@"AXLandmarkMain", u"main"_ns}, 444 {@"AXLandmarkNavigation", u"navigation"_ns}, 445 {@"AXLandmarkRegion", u"region"_ns}, 446 {@"AXLandmarkSearch", u"search"_ns}, 447 {@"AXSearchField", u"searchTextField"_ns}, 448 {@"AXSummary", u"summary"_ns}, 449 {@"AXTabPanel", u"tabPanel"_ns}, 450 {@"AXTerm", u"term"_ns}, 451 {@"AXUserInterfaceTooltip", u"tooltip"_ns}}; 452 453 struct RoleDescrComparator { 454 const NSString* mRole; 455 explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {} 456 int operator()(const RoleDescrMap& aEntry) const { 457 return [mRole compare:aEntry.role]; 458 } 459 }; 460 461 - (NSString*)moxRoleDescription { 462 if (NSString* ariaRoleDescription = 463 utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) { 464 if ([ariaRoleDescription length]) { 465 return ariaRoleDescription; 466 } 467 } 468 469 if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns); 470 471 if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns); 472 473 if (mRole == roles::MARK) { 474 return utils::LocalizedString(u"highlight"_ns); 475 } 476 477 NSString* subrole = [self moxSubrole]; 478 479 if (subrole) { 480 size_t idx = 0; 481 if (BinarySearchIf(sRoleDescrMap, 0, std::size(sRoleDescrMap), 482 RoleDescrComparator(subrole), &idx)) { 483 return utils::LocalizedString(sRoleDescrMap[idx].description); 484 } 485 } 486 487 return NSAccessibilityRoleDescription([self moxRole], subrole); 488 } 489 490 static bool ProvidesTitle(const Accessible* aAccessible, nsString& aName) { 491 ENameValueFlag flag = aAccessible->Name(aName); 492 493 switch (aAccessible->Role()) { 494 case roles::PAGETAB: 495 case roles::COMBOBOX_OPTION: 496 case roles::OPTION: 497 case roles::PARENT_MENUITEM: 498 case roles::MENUITEM: 499 // These roles always supply a title. 500 return true; 501 case roles::GROUPING: 502 case roles::RADIO_GROUP: 503 case roles::DOCUMENT: 504 case roles::OUTLINE: 505 case roles::ARTICLE: 506 case roles::FIGURE: 507 // These roles never supply a title. 508 return false; 509 default: 510 break; 511 } 512 513 // If the name was calculated from visible text (eg. label or subtree), we 514 // supply a title. 515 return flag != eNameOK; 516 } 517 518 - (NSString*)moxLabel { 519 if ([self isExpired]) { 520 return nil; 521 } 522 523 nsAutoString name; 524 525 if (ProvidesTitle(mGeckoAccessible, name)) { 526 // If it provides title, it is not a description. 527 return nil; 528 } 529 530 return nsCocoaUtils::ToNSString(name); 531 } 532 533 - (NSString*)moxTitle { 534 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 535 536 nsAutoString name; 537 if (!ProvidesTitle(mGeckoAccessible, name)) { 538 return @""; 539 } 540 541 if (nsCoreUtils::IsWhitespaceString(name)) { 542 return @""; 543 } 544 545 return nsCocoaUtils::ToNSString(name); 546 547 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 548 } 549 550 - (id)moxValue { 551 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 552 553 nsAutoString value; 554 mGeckoAccessible->Value(value); 555 556 return nsCocoaUtils::ToNSString(value); 557 558 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 559 } 560 561 - (NSString*)moxHelp { 562 nsAutoString desc; 563 EDescriptionValueFlag descFlag = mGeckoAccessible->Description(desc); 564 565 if (@available(macOS 11.0, *)) { 566 // Provide AXHelp only on non-aria descriptions (eg. title attribute), 567 // or if the accessible is a fieldset or radio group. 568 if (descFlag == eDescriptionFromARIA && 569 mGeckoAccessible->Role() != roles::GROUPING && 570 mGeckoAccessible->Role() != roles::RADIO_GROUP) { 571 return nil; 572 } 573 } 574 575 return nsCocoaUtils::ToNSString(desc); 576 } 577 578 - (NSArray*)moxCustomContent { 579 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 580 581 if (@available(macOS 11.0, *)) { 582 nsAutoString desc; 583 EDescriptionValueFlag descFlag = mGeckoAccessible->Description(desc); 584 585 if (!desc.IsEmpty() && descFlag == eDescriptionFromARIA) { 586 AXCustomContent* contentItem = [AXCustomContent 587 customContentWithLabel:@"description" 588 value:nsCocoaUtils::ToNSString(desc)]; 589 contentItem.importance = AXCustomContentImportanceHigh; 590 return @[ contentItem ]; 591 } 592 } 593 594 return nil; 595 596 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 597 } 598 599 - (NSArray*)moxCustomActions { 600 if (@available(macOS 13.0, *)) { 601 NSMutableArray<NSAccessibilityCustomAction*>* customActions = 602 [[[NSMutableArray alloc] init] autorelease]; 603 Relation relatedActions( 604 mGeckoAccessible->RelationByType(RelationType::ACTION)); 605 while (Accessible* target = relatedActions.Next()) { 606 if (target->HasPrimaryAction()) { 607 // Any ACTION related accesibles should be considered a custom action. 608 mozAccessible* nativeTarget = GetNativeFromGeckoAccessible(target); 609 if (nativeTarget) { 610 nsAutoString name; 611 // Use the name of the target as the action name. 612 target->Name(name); 613 NSAccessibilityCustomAction* action = 614 [[NSAccessibilityCustomAction alloc] 615 initWithName:nsCocoaUtils::ToNSString(name) 616 target:nativeTarget 617 selector:@selector(moxPerformPress)]; 618 [customActions addObject:action]; 619 } 620 } 621 } 622 return customActions; 623 } 624 625 return nil; 626 } 627 628 - (NSWindow*)moxWindow { 629 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 630 631 // Get a pointer to the native window (NSWindow) we reside in. 632 NSWindow* nativeWindow = nil; 633 DocAccessible* docAcc = nullptr; 634 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { 635 docAcc = acc->Document(); 636 } else { 637 RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); 638 LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); 639 if (outerDoc) docAcc = outerDoc->Document(); 640 } 641 642 if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow()); 643 644 MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(), 645 "Couldn't get native window"); 646 return nativeWindow; 647 648 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 649 } 650 651 - (NSNumber*)moxEnabled { 652 if ([self stateWithMask:states::UNAVAILABLE]) { 653 return @NO; 654 } 655 656 if (![self isRoot]) { 657 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; 658 if (![parent isRoot]) { 659 return @(![parent disableChild:self]); 660 } 661 } 662 663 return @YES; 664 } 665 666 - (NSString*)moxInvalid { 667 // For controls that support text input, we will expose 668 // the string value of `aria-invalid` when it exists. 669 // See mozTextAccessible::moxInvalid for that work. 670 // Unfortunately, NSBools do not autoconvert to usable 671 // NSStrings, so we expose "true" and "false" manually. 672 return ([self stateWithMask:states::INVALID] != 0) ? @"true" : @"false"; 673 } 674 675 - (NSArray*)moxErrorMessageElements { 676 if (![[self moxInvalid] isEqualToString:@"false"]) { 677 NSArray* relations = [self getRelationsByType:RelationType::ERRORMSG]; 678 if ([relations count] > 0) { 679 return relations; 680 } 681 } 682 683 return nil; 684 } 685 686 - (NSNumber*)moxFocused { 687 return @([self stateWithMask:states::FOCUSED] != 0); 688 } 689 690 - (NSNumber*)moxSelected { 691 return @NO; 692 } 693 694 - (NSNumber*)moxExpanded { 695 return @([self stateWithMask:states::EXPANDED] != 0); 696 } 697 698 - (NSValue*)moxFrame { 699 MOZ_ASSERT(mGeckoAccessible); 700 701 LayoutDeviceIntRect rect = mGeckoAccessible->Bounds(); 702 NSScreen* screen = utils::GetNSScreenForAcc(self); 703 CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(screen); 704 705 // Regardless of screen selected above, VO is only happy if we use the 706 // main screen height for Y coordinate conversion. This is consistent with 707 // moxHitTest and GeckoTextMarkerRange::Bounds(). 708 NSScreen* mainScreen = [[NSScreen screens] objectAtIndex:0]; 709 CGFloat mainScreenHeight = [mainScreen frame].size.height; 710 711 return [NSValue 712 valueWithRect:NSMakeRect( 713 static_cast<CGFloat>(rect.x) / scaleFactor, 714 mainScreenHeight - 715 static_cast<CGFloat>(rect.y + rect.height) / 716 scaleFactor, 717 static_cast<CGFloat>(rect.width) / scaleFactor, 718 static_cast<CGFloat>(rect.height) / scaleFactor)]; 719 } 720 721 - (NSString*)moxARIACurrent { 722 if (![self stateWithMask:states::CURRENT]) { 723 return nil; 724 } 725 726 return utils::GetAccAttr(self, nsGkAtoms::aria_current); 727 } 728 729 - (NSNumber*)moxARIAAtomic { 730 return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil); 731 } 732 733 - (NSString*)moxARIALive { 734 return utils::GetAccAttr(self, nsGkAtoms::aria_live); 735 } 736 737 - (NSNumber*)moxARIAPosInSet { 738 GroupPos groupPos = mGeckoAccessible->GroupPosition(); 739 return @(groupPos.posInSet); 740 } 741 742 - (NSNumber*)moxARIASetSize { 743 GroupPos groupPos = mGeckoAccessible->GroupPosition(); 744 return @(groupPos.setSize); 745 } 746 747 - (NSString*)moxARIARelevant { 748 if (NSString* relevant = 749 utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) { 750 return relevant; 751 } 752 753 // Default aria-relevant value 754 return @"additions text"; 755 } 756 757 - (NSString*)moxPlaceholderValue { 758 // First, check for plaecholder HTML attribute 759 if (NSString* placeholder = utils::GetAccAttr(self, nsGkAtoms::placeholder)) { 760 return placeholder; 761 } 762 763 // If no placeholder HTML attribute, check for the aria version. 764 return utils::GetAccAttr(self, nsGkAtoms::aria_placeholder); 765 } 766 767 - (id)moxTitleUIElement { 768 MOZ_ASSERT(mGeckoAccessible); 769 770 nsAutoString unused; 771 if (mGeckoAccessible->Name(unused) != eNameFromRelations) { 772 return nil; 773 } 774 775 Relation rel = mGeckoAccessible->RelationByType(RelationType::LABELLED_BY); 776 Accessible* label = rel.Next(); 777 if (!label || rel.Next()) { 778 // Zero or more than one relation. 779 return nil; 780 } 781 782 if (label->IsAncestorOf(mGeckoAccessible)) { 783 // Don't support labelling for a relation that references an ancestor. 784 // VO walks the label's subtree and tries to construct the name for the 785 // control. It does not strip whitespace from the text leaf children, and 786 // since the calculated name of the control is stripped, it sees it as two 787 // different names and inclues both. 788 return nil; 789 } 790 791 if (RefPtr<nsAtom>(label->DisplayStyle()) == nsGkAtoms::block) { 792 TextLeafPoint endPoint = 793 TextLeafPoint(label, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) 794 .FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 795 if (endPoint.IsSpace()) { 796 // A label that is a block element with trailing space causes VO be 797 // unhappy. 798 return nil; 799 } 800 } 801 802 return GetNativeFromGeckoAccessible(label); 803 } 804 805 - (NSString*)moxDOMIdentifier { 806 MOZ_ASSERT(mGeckoAccessible); 807 808 nsAutoString id; 809 mGeckoAccessible->DOMNodeID(id); 810 811 return nsCocoaUtils::ToNSString(id); 812 } 813 814 - (NSNumber*)moxRequired { 815 return @([self stateWithMask:states::REQUIRED] != 0); 816 } 817 818 - (NSNumber*)moxElementBusy { 819 return @([self stateWithMask:states::BUSY] != 0); 820 } 821 822 - (NSArray*)moxLinkedUIElements { 823 return [self getRelationsByType:RelationType::FLOWS_TO]; 824 } 825 826 - (NSArray*)moxARIAControls { 827 return [self getRelationsByType:RelationType::CONTROLLER_FOR]; 828 } 829 830 - (mozAccessible*)topWebArea { 831 Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible); 832 while (doc) { 833 if (doc->IsLocal()) { 834 DocAccessible* docAcc = doc->AsLocal()->AsDoc(); 835 if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) { 836 return GetNativeFromGeckoAccessible(docAcc); 837 } 838 839 doc = docAcc->ParentDocument(); 840 } else { 841 DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc(); 842 if (docProxy->IsTopLevel()) { 843 return GetNativeFromGeckoAccessible(docProxy); 844 } 845 doc = docProxy->ParentDoc(); 846 } 847 } 848 849 return nil; 850 } 851 852 - (void)handleRoleChanged:(mozilla::a11y::role)newRole { 853 mRole = newRole; 854 mARIARole = nullptr; 855 856 // For testing purposes 857 [self moxPostNotification:@"AXMozRoleChanged"]; 858 } 859 860 - (id)moxEditableAncestor { 861 return [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { 862 return [moxAcc moxIsTextField]; 863 }]; 864 } 865 866 - (id)moxHighestEditableAncestor { 867 id highestAncestor = [self moxEditableAncestor]; 868 while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) { 869 id ancestorParent = [highestAncestor moxUnignoredParent]; 870 if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) { 871 break; 872 } 873 874 id higherAncestor = [ancestorParent moxEditableAncestor]; 875 876 if (!higherAncestor) { 877 break; 878 } 879 880 highestAncestor = higherAncestor; 881 } 882 883 return highestAncestor; 884 } 885 886 - (id)moxFocusableAncestor { 887 // XXX: Checking focusable state up the chain can be expensive. For now, 888 // we can just return AXEditableAncestor since the main use case for this 889 // is rich text editing with links. 890 return [self moxEditableAncestor]; 891 } 892 893 - (NSString*)moxLanguage { 894 MOZ_ASSERT(mGeckoAccessible); 895 896 nsAutoString lang; 897 mGeckoAccessible->Language(lang); 898 899 return nsCocoaUtils::ToNSString(lang); 900 } 901 902 - (NSString*)moxKeyShortcutsValue { 903 MOZ_ASSERT(mGeckoAccessible); 904 905 nsAutoString shortcut; 906 907 if (!mGeckoAccessible->GetStringARIAAttr(nsGkAtoms::aria_keyshortcuts, 908 shortcut)) { 909 return nil; 910 } 911 912 return nsCocoaUtils::ToNSString(shortcut); 913 } 914 915 #ifndef RELEASE_OR_BETA 916 - (NSString*)moxMozDebugDescription { 917 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 918 919 if (!mGeckoAccessible) { 920 return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>", 921 NSStringFromClass([self class]), self]; 922 } 923 924 NSMutableString* domInfo = [NSMutableString string]; 925 if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) { 926 [domInfo appendFormat:@" %@", tagName]; 927 NSString* domID = [self moxDOMIdentifier]; 928 if ([domID length]) { 929 [domInfo appendFormat:@"#%@", domID]; 930 } 931 if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) { 932 [domInfo 933 appendFormat:@".%@", 934 [className stringByReplacingOccurrencesOfString:@" " 935 withString:@"."]]; 936 } 937 } 938 939 return [NSString stringWithFormat:@"<%@: %p %@%@>", 940 NSStringFromClass([self class]), self, 941 [self moxRole], domInfo]; 942 943 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 944 } 945 #endif 946 947 - (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate { 948 // Create our search object and set it up with the searchPredicate 949 // params. The init function does additional parsing. We pass a 950 // reference to the web area to use as a start element if one is not 951 // specified. 952 MOXSearchInfo* search = 953 [[[MOXSearchInfo alloc] initWithParameters:searchPredicate 954 andRoot:self] autorelease]; 955 956 return [search performSearch]; 957 } 958 959 - (NSNumber*)moxUIElementCountForSearchPredicate: 960 (NSDictionary*)searchPredicate { 961 return [NSNumber 962 numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] 963 count]]; 964 } 965 966 - (void)moxSetFocused:(NSNumber*)focused { 967 MOZ_ASSERT(mGeckoAccessible); 968 969 if ([focused boolValue]) { 970 mGeckoAccessible->TakeFocus(); 971 } 972 } 973 974 - (void)moxPerformScrollToVisible { 975 MOZ_ASSERT(mGeckoAccessible); 976 mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); 977 } 978 979 - (void)moxPerformShowMenu { 980 MOZ_ASSERT(mGeckoAccessible); 981 982 // We don't need to convert this rect into mac coordinates because the 983 // mouse event synthesizer expects layout (gecko) coordinates. 984 LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds(); 985 986 LocalAccessible* rootAcc = mGeckoAccessible->IsLocal() 987 ? mGeckoAccessible->AsLocal()->RootAccessible() 988 : mGeckoAccessible->AsRemote() 989 ->OuterDocOfRemoteBrowser() 990 ->RootAccessible(); 991 id objOrView = 992 GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc)); 993 994 LayoutDeviceIntPoint p = LayoutDeviceIntPoint( 995 bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2)); 996 nsIWidget* widget = [objOrView widget]; 997 widget->SynthesizeNativeMouseEvent( 998 p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary, 999 nsIWidget::Modifiers::NO_MODIFIERS, nullptr); 1000 } 1001 1002 - (void)moxPerformPress { 1003 MOZ_ASSERT(mGeckoAccessible); 1004 1005 mGeckoAccessible->DoAction(0); 1006 } 1007 1008 #pragma mark - 1009 1010 - (BOOL)disableChild:(mozAccessible*)child { 1011 return NO; 1012 } 1013 1014 - (void)maybePostA11yUtilNotification { 1015 MOZ_ASSERT(mGeckoAccessible); 1016 // Sometimes we use a special live region to make announcements to the user. 1017 // This region is a child of the root document, but doesn't contain any 1018 // content. If we try to fire regular AXLiveRegion changed events through it, 1019 // VoiceOver clips the notifications because it (rightfully) doesn't detect 1020 // focus within the region. We get around this by firing an 1021 // AXAnnouncementRequested notification here instead. 1022 // Verify we're trying to send a notification for the a11yUtils alert (and not 1023 // a random acc with the same ID) by checking: 1024 // - The gecko acc is local, our a11y-announcement lives in browser.xhtml 1025 // - The ID of the gecko acc is "a11y-announcement" 1026 // - The native acc is a direct descendent of the chrome window (ChildView in 1027 // a non-headless context, mozRootAccessible in a headless context). 1028 DocAccessible* maybeRoot = mGeckoAccessible->IsLocal() 1029 ? mGeckoAccessible->AsLocal()->Document() 1030 : nullptr; 1031 if (maybeRoot && maybeRoot->IsRoot() && 1032 [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"]) { 1033 nsAutoString name; 1034 // Our actual announcement should be stored as a child of the alert. 1035 if (Accessible* announcement = mGeckoAccessible->FirstChild()) { 1036 announcement->Name(name); 1037 } else { 1038 // This can happen if a modal dialog is opened, which removes everything 1039 // else from the accessibility tree, and then the modal is dismissed, 1040 // which inserts everything else again. This causes Gecko to fire an alert 1041 // event on a11y-announcement (even though it's empty) since it was just 1042 // shown. 1043 NS_WARNING("A11yUtil event received, but no announcement found"); 1044 } 1045 1046 NSDictionary* info = @{ 1047 NSAccessibilityAnnouncementKey : name.IsEmpty() 1048 ? @("") 1049 : nsCocoaUtils::ToNSString(name), 1050 // High priority means VO will stop what it is currently speaking 1051 // to speak our announcement. 1052 NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh) 1053 }; 1054 1055 // This sends events via nsIObserverService to be consumed by our 1056 // mochitests. Normally we'd fire these events through moxPostNotification 1057 // which takes care of this, but because NSApp isn't derived 1058 // from MOXAccessibleBase, we do this (and post the notification) manually. 1059 // We used to fire this on the window, but per Chrome and Safari these 1060 // notifs get dropped if fired on any non-main window. We now fire on NSApp 1061 // to avoid this. 1062 xpcAccessibleMacEvent::FireEvent( 1063 GetNativeFromGeckoAccessible(maybeRoot), 1064 NSAccessibilityAnnouncementRequestedNotification, info); 1065 NSAccessibilityPostNotificationWithUserInfo( 1066 NSApp, NSAccessibilityAnnouncementRequestedNotification, info); 1067 } 1068 } 1069 1070 - (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType { 1071 NSMutableArray<mozAccessible*>* relations = 1072 [[[NSMutableArray alloc] init] autorelease]; 1073 Relation rel = mGeckoAccessible->RelationByType(relationType); 1074 while (Accessible* relAcc = rel.Next()) { 1075 if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) { 1076 [relations addObject:relNative]; 1077 } 1078 } 1079 1080 return relations; 1081 } 1082 1083 - (void)handleAccessibleEvent:(uint32_t)eventType { 1084 switch (eventType) { 1085 case nsIAccessibleEvent::EVENT_ALERT: 1086 [self maybePostA11yUtilNotification]; 1087 break; 1088 case nsIAccessibleEvent::EVENT_FOCUS: 1089 [self moxPostNotification: 1090 NSAccessibilityFocusedUIElementChangedNotification]; 1091 break; 1092 case nsIAccessibleEvent::EVENT_MENUPOPUP_START: 1093 [self moxPostNotification:@"AXMenuOpened"]; 1094 break; 1095 case nsIAccessibleEvent::EVENT_MENUPOPUP_END: 1096 [self moxPostNotification:@"AXMenuClosed"]; 1097 break; 1098 case nsIAccessibleEvent::EVENT_SELECTION: 1099 case nsIAccessibleEvent::EVENT_SELECTION_ADD: 1100 case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: 1101 case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: 1102 [self moxPostNotification: 1103 NSAccessibilitySelectedChildrenChangedNotification]; 1104 break; 1105 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { 1106 if (![self stateWithMask:states::SELECTABLE_TEXT]) { 1107 break; 1108 } 1109 // We consider any caret move event to be a selected text change event. 1110 // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be 1111 // reduntant. 1112 MOXTextMarkerDelegate* delegate = 1113 static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]); 1114 NSMutableDictionary* userInfo = 1115 [[[delegate selectionChangeInfo] mutableCopy] autorelease]; 1116 userInfo[@"AXTextChangeElement"] = self; 1117 1118 mozAccessible* webArea = [self topWebArea]; 1119 [webArea 1120 moxPostNotification:NSAccessibilitySelectedTextChangedNotification 1121 withUserInfo:userInfo]; 1122 [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification 1123 withUserInfo:userInfo]; 1124 break; 1125 } 1126 case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED: 1127 mIsLiveRegion = true; 1128 [self moxPostNotification:@"AXLiveRegionCreated"]; 1129 break; 1130 case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED: 1131 mIsLiveRegion = false; 1132 break; 1133 case nsIAccessibleEvent::EVENT_NAME_CHANGE: { 1134 // Don't want to passively activate the cache because a name changed. 1135 CacheDomainActivationBlocker cacheBlocker; 1136 nsAutoString nameNotUsed; 1137 if (ProvidesTitle(mGeckoAccessible, nameNotUsed)) { 1138 [self moxPostNotification:NSAccessibilityTitleChangedNotification]; 1139 } 1140 break; 1141 } 1142 case nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED: 1143 MOZ_ASSERT(mIsLiveRegion); 1144 [self moxPostNotification:@"AXLiveRegionChanged"]; 1145 break; 1146 case nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED: { 1147 // aria-errormessage was changed. If aria-invalid != "true", it means that 1148 // VoiceOver should (a) expose a new message or (b) remove an 1149 // old message 1150 if (![[self moxInvalid] isEqualToString:@"false"]) { 1151 [self moxPostNotification:@"AXValidationErrorChanged"]; 1152 } 1153 1154 break; 1155 } 1156 } 1157 } 1158 1159 - (void)maybePostValidationErrorChanged { 1160 NSArray* relations = 1161 [self getRelationsByType:(mozilla::a11y::RelationType::ERRORMSG_FOR)]; 1162 if ([relations count] > 0) { 1163 // only fire AXValidationErrorChanged if related node is not 1164 // `aria-invalid="false"` 1165 for (mozAccessible* related : relations) { 1166 NSString* invalidStr = [related moxInvalid]; 1167 if (![invalidStr isEqualToString:@"false"]) { 1168 [self moxPostNotification:@"AXValidationErrorChanged"]; 1169 break; 1170 } 1171 } 1172 } 1173 } 1174 1175 - (void)expire { 1176 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 1177 1178 mGeckoAccessible = nullptr; 1179 1180 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; 1181 1182 NS_OBJC_END_TRY_IGNORE_BLOCK; 1183 } 1184 1185 - (BOOL)isExpired { 1186 return !mGeckoAccessible; 1187 } 1188 1189 @end