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 "mozAccessible.h" 9#include "MOXAccessibleBase.h" 10 11#import "MacUtils.h" 12#import "mozView.h" 13#import "MOXSearchInfo.h" 14#import "mozTextAccessible.h" 15 16#include "LocalAccessible-inl.h" 17#include "nsAccUtils.h" 18#include "DocAccessibleParent.h" 19#include "Relation.h" 20#include "Role.h" 21#include "RootAccessible.h" 22#include "TableAccessible.h" 23#include "mozilla/a11y/PDocAccessible.h" 24#include "mozilla/dom/BrowserParent.h" 25#include "OuterDocAccessible.h" 26#include "nsChildView.h" 27 28#include "nsRect.h" 29#include "nsCocoaUtils.h" 30#include "nsCoord.h" 31#include "nsObjCExceptions.h" 32#include "nsWhitespaceTokenizer.h" 33#include <prdtoa.h> 34 35using namespace mozilla; 36using namespace mozilla::a11y; 37 38#pragma mark - 39 40@interface mozAccessible () 41- (BOOL)providesLabelNotTitle; 42 43- (void)maybePostLiveRegionChanged; 44@end 45 46@implementation mozAccessible 47 48- (id)initWithAccessible:(AccessibleOrProxy)aAccOrProxy { 49 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 50 MOZ_ASSERT(!aAccOrProxy.IsNull(), "Cannot init mozAccessible with null"); 51 if ((self = [super init])) { 52 mGeckoAccessible = aAccOrProxy; 53 mRole = aAccOrProxy.Role(); 54 } 55 56 return self; 57 58 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 59} 60 61- (void)dealloc { 62 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 63 64 [super dealloc]; 65 66 NS_OBJC_END_TRY_IGNORE_BLOCK; 67} 68 69#pragma mark - mozAccessible widget 70 71- (BOOL)hasRepresentedView { 72 return NO; 73} 74 75- (id)representedView { 76 return nil; 77} 78 79- (BOOL)isRoot { 80 return NO; 81} 82 83#pragma mark - 84 85- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent { 86 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 87 if (acc->IsContent() && acc->GetContent()->IsXULElement()) { 88 if (acc->VisibilityState() & states::INVISIBLE) { 89 return YES; 90 } 91 } 92 } 93 94 return [parent moxIgnoreChild:self]; 95} 96 97- (BOOL)moxIgnoreChild:(mozAccessible*)child { 98 return nsAccUtils::MustPrune(mGeckoAccessible); 99} 100 101- (id)childAt:(uint32_t)i { 102 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 103 104 AccessibleOrProxy child = mGeckoAccessible.ChildAt(i); 105 return !child.IsNull() ? GetNativeFromGeckoAccessible(child) : nil; 106 107 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 108} 109 110static const uint64_t kCachedStates = 111 states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED | 112 states::CURRENT | states::SELECTED | states::TRAVERSED | states::LINKED | 113 states::HASPOPUP | states::BUSY | states::MULTI_LINE; 114static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63; 115 116- (uint64_t)state { 117 uint64_t state = 0; 118 119 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 120 state = acc->State(); 121 } 122 123 if (RemoteAccessible* proxy = mGeckoAccessible.AsProxy()) { 124 state = proxy->State(); 125 } 126 127 if (!(mCachedState & kCacheInitialized)) { 128 mCachedState = state & kCachedStates; 129 mCachedState |= kCacheInitialized; 130 } 131 132 return state; 133} 134 135- (uint64_t)stateWithMask:(uint64_t)mask { 136 if ((mask & kCachedStates) == mask && 137 (mCachedState & kCacheInitialized) != 0) { 138 return mCachedState & mask; 139 } 140 141 return [self state] & mask; 142} 143 144- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { 145 if ((state & kCachedStates) != 0) { 146 if (!(mCachedState & kCacheInitialized)) { 147 [self state]; 148 } else { 149 if (enabled) { 150 mCachedState |= state; 151 } else { 152 mCachedState &= ~state; 153 } 154 } 155 } 156 157 if (state == states::BUSY) { 158 [self moxPostNotification:@"AXElementBusyChanged"]; 159 } 160} 161 162- (void)invalidateState { 163 mCachedState = 0; 164} 165 166- (BOOL)providesLabelNotTitle { 167 // These accessible types are the exception to the rule of label vs. title: 168 // They may be named explicitly, but they still provide a label not a title. 169 return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP || 170 mRole == roles::FIGURE || mRole == roles::GRAPHIC || 171 mRole == roles::DOCUMENT || mRole == roles::OUTLINE; 172} 173 174- (mozilla::a11y::AccessibleOrProxy)geckoAccessible { 175 return mGeckoAccessible; 176} 177 178- (mozilla::a11y::AccessibleOrProxy)geckoDocument { 179 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 180 181 if (mGeckoAccessible.IsAccessible()) { 182 if (mGeckoAccessible.AsAccessible()->IsDoc()) { 183 return mGeckoAccessible; 184 } 185 return mGeckoAccessible.AsAccessible()->Document(); 186 } 187 188 if (mGeckoAccessible.AsProxy()->IsDoc()) { 189 return mGeckoAccessible; 190 } 191 192 return mGeckoAccessible.AsProxy()->Document(); 193} 194 195#pragma mark - MOXAccessible protocol 196 197- (BOOL)moxBlockSelector:(SEL)selector { 198 if (selector == @selector(moxPerformPress)) { 199 uint8_t actionCount = mGeckoAccessible.IsAccessible() 200 ? mGeckoAccessible.AsAccessible()->ActionCount() 201 : mGeckoAccessible.AsProxy()->ActionCount(); 202 203 // If we have no action, we don't support press, so return YES. 204 return actionCount == 0; 205 } 206 207 if (selector == @selector(moxSetFocused:)) { 208 return [self stateWithMask:states::FOCUSABLE] == 0; 209 } 210 211 if (selector == @selector(moxARIALive) || 212 selector == @selector(moxARIAAtomic) || 213 selector == @selector(moxARIARelevant)) { 214 return ![self moxIsLiveRegion]; 215 } 216 217 return [super moxBlockSelector:selector]; 218} 219 220- (id)moxFocusedUIElement { 221 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 222 223 LocalAccessible* acc = mGeckoAccessible.AsAccessible(); 224 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 225 226 mozAccessible* focusedChild = nil; 227 if (acc) { 228 LocalAccessible* focusedGeckoChild = acc->FocusedChild(); 229 if (focusedGeckoChild) { 230 focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild); 231 } else { 232 dom::BrowserParent* browser = dom::BrowserParent::GetFocused(); 233 if (browser) { 234 a11y::DocAccessibleParent* proxyDoc = 235 browser->GetTopLevelDocAccessible(); 236 if (proxyDoc) { 237 mozAccessible* nativeRemoteChild = 238 GetNativeFromGeckoAccessible(proxyDoc); 239 return [nativeRemoteChild accessibilityFocusedUIElement]; 240 } 241 } 242 } 243 } else if (proxy) { 244 RemoteAccessible* focusedGeckoChild = proxy->FocusedChild(); 245 if (focusedGeckoChild) { 246 focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild); 247 } 248 } 249 250 if ([focusedChild isAccessibilityElement]) { 251 return focusedChild; 252 } 253 254 // return ourself if we can't get a native focused child. 255 return self; 256} 257 258- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate { 259 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 260 261 if (mGeckoAccessible.IsAccessible()) { 262 return [MOXTextMarkerDelegate 263 getOrCreateForDoc:mGeckoAccessible.AsAccessible()->Document()]; 264 } 265 266 return [MOXTextMarkerDelegate 267 getOrCreateForDoc:mGeckoAccessible.AsProxy()->Document()]; 268} 269 270- (BOOL)moxIsLiveRegion { 271 return mIsLiveRegion; 272} 273 274- (id)moxHitTest:(NSPoint)point { 275 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 276 277 // Convert the given screen-global point in the cocoa coordinate system (with 278 // origin in the bottom-left corner of the screen) into point in the Gecko 279 // coordinate system (with origin in a top-left screen point). 280 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; 281 NSPoint tmpPoint = 282 NSMakePoint(point.x, [mainView frame].size.height - point.y); 283 LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels( 284 tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView)); 285 286 AccessibleOrProxy child = mGeckoAccessible.ChildAtPoint( 287 geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild); 288 289 if (!child.IsNull()) { 290 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); 291 return [nativeChild isAccessibilityElement] 292 ? nativeChild 293 : [nativeChild moxUnignoredParent]; 294 } 295 296 // if we didn't find anything, return ourself or child view. 297 return self; 298} 299 300- (id<mozAccessible>)moxParent { 301 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 302 if ([self isExpired]) { 303 return nil; 304 } 305 306 AccessibleOrProxy parent = mGeckoAccessible.Parent(); 307 308 if (parent.IsNull()) { 309 return nil; 310 } 311 312 id nativeParent = GetNativeFromGeckoAccessible(parent); 313 if (parent.Role() == roles::DOCUMENT && 314 [nativeParent respondsToSelector:@selector(rootGroup)]) { 315 // Before returning a WebArea as parent, check to see if 316 // there is a generated root group that is an intermediate container. 317 if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) { 318 nativeParent = rootGroup; 319 } 320 } 321 322 if (!nativeParent && mGeckoAccessible.IsAccessible()) { 323 // Return native of root accessible if we have no direct parent. 324 // XXX: need to return a sensible fallback in proxy case as well 325 nativeParent = GetNativeFromGeckoAccessible( 326 mGeckoAccessible.AsAccessible()->RootAccessible()); 327 } 328 329 return GetObjectOrRepresentedView(nativeParent); 330 331 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 332} 333 334// gets all our native children lazily, including those that are ignored. 335- (NSArray*)moxChildren { 336 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 337 338 NSMutableArray* children = [[[NSMutableArray alloc] 339 initWithCapacity:mGeckoAccessible.ChildCount()] autorelease]; 340 341 for (uint32_t childIdx = 0; childIdx < mGeckoAccessible.ChildCount(); 342 childIdx++) { 343 AccessibleOrProxy child = mGeckoAccessible.ChildAt(childIdx); 344 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child); 345 if (!nativeChild) { 346 continue; 347 } 348 349 [children addObject:nativeChild]; 350 } 351 352 return children; 353} 354 355- (NSValue*)moxPosition { 356 CGRect frame = [[self moxFrame] rectValue]; 357 358 return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)]; 359} 360 361- (NSValue*)moxSize { 362 CGRect frame = [[self moxFrame] rectValue]; 363 364 return 365 [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)]; 366} 367 368- (NSString*)moxRole { 369#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \ 370 ia2Role, androidClass, nameRule) \ 371 case roles::geckoRole: \ 372 return macRole; 373 374 switch (mRole) { 375#include "RoleMap.h" 376 default: 377 MOZ_ASSERT_UNREACHABLE("Unknown role."); 378 return NSAccessibilityUnknownRole; 379 } 380 381#undef ROLE 382} 383 384- (nsStaticAtom*)ARIARole { 385 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 386 387 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 388 if (acc->HasARIARole()) { 389 const nsRoleMapEntry* roleMap = acc->ARIARoleMap(); 390 return roleMap->roleAtom; 391 } 392 393 return nsGkAtoms::_empty; 394 } 395 396 if (!mARIARole) { 397 mARIARole = mGeckoAccessible.AsProxy()->ARIARoleAtom(); 398 if (!mARIARole) { 399 mARIARole = nsGkAtoms::_empty; 400 } 401 } 402 403 return mARIARole; 404} 405 406- (NSString*)moxSubrole { 407 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 408 409 LocalAccessible* acc = mGeckoAccessible.AsAccessible(); 410 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 411 412 // Deal with landmarks first 413 // macOS groups the specific landmark types of DPub ARIA into two broad 414 // categories with corresponding subroles: Navigation and region/container. 415 if (mRole == roles::LANDMARK) { 416 nsAtom* landmark = acc ? acc->LandmarkRole() : proxy->LandmarkRole(); 417 // HTML Elements treated as landmarks, and ARIA landmarks. 418 if (landmark) { 419 if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner"; 420 if (landmark == nsGkAtoms::complementary) 421 return @"AXLandmarkComplementary"; 422 if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo"; 423 if (landmark == nsGkAtoms::main) return @"AXLandmarkMain"; 424 if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation"; 425 if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch"; 426 } 427 428 // None of the above, so assume DPub ARIA. 429 return @"AXLandmarkRegion"; 430 } 431 432 // Now, deal with widget roles 433 nsStaticAtom* roleAtom = nullptr; 434 435 if (mRole == roles::DIALOG) { 436 roleAtom = [self ARIARole]; 437 438 if (roleAtom == nsGkAtoms::alertdialog) { 439 return @"AXApplicationAlertDialog"; 440 } 441 if (roleAtom == nsGkAtoms::dialog) { 442 return @"AXApplicationDialog"; 443 } 444 } 445 446 if (mRole == roles::FORM) { 447 roleAtom = [self ARIARole]; 448 449 if (roleAtom == nsGkAtoms::form) { 450 return @"AXLandmarkForm"; 451 } 452 } 453 454#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \ 455 ia2Role, androidClass, nameRule) \ 456 case roles::geckoRole: \ 457 if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \ 458 return macSubrole; \ 459 } else { \ 460 break; \ 461 } 462 463 switch (mRole) { 464#include "RoleMap.h" 465 } 466 467 // These are special. They map to roles::NOTHING 468 // and are instructed by the ARIA map to use the native host role. 469 roleAtom = [self ARIARole]; 470 471 if (roleAtom == nsGkAtoms::log_) { 472 return @"AXApplicationLog"; 473 } 474 475 if (roleAtom == nsGkAtoms::timer) { 476 return @"AXApplicationTimer"; 477 } 478 // macOS added an AXSubrole value to distinguish generic AXGroup objects 479 // from those which are AXGroups as a result of an explicit ARIA role, 480 // such as the non-landmark, non-listitem text containers in DPub ARIA. 481 if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) { 482 return @"AXApplicationGroup"; 483 } 484 485 return NSAccessibilityUnknownSubrole; 486 487#undef ROLE 488} 489 490struct RoleDescrMap { 491 NSString* role; 492 const nsString description; 493}; 494 495static const RoleDescrMap sRoleDescrMap[] = { 496 {@"AXApplicationAlert", u"alert"_ns}, 497 {@"AXApplicationAlertDialog", u"alertDialog"_ns}, 498 {@"AXApplicationDialog", u"dialog"_ns}, 499 {@"AXApplicationLog", u"log"_ns}, 500 {@"AXApplicationMarquee", u"marquee"_ns}, 501 {@"AXApplicationStatus", u"status"_ns}, 502 {@"AXApplicationTimer", u"timer"_ns}, 503 {@"AXContentSeparator", u"separator"_ns}, 504 {@"AXDefinition", u"definition"_ns}, 505 {@"AXDetails", u"details"_ns}, 506 {@"AXDocument", u"document"_ns}, 507 {@"AXDocumentArticle", u"article"_ns}, 508 {@"AXDocumentMath", u"math"_ns}, 509 {@"AXDocumentNote", u"note"_ns}, 510 {@"AXLandmarkApplication", u"application"_ns}, 511 {@"AXLandmarkBanner", u"banner"_ns}, 512 {@"AXLandmarkComplementary", u"complementary"_ns}, 513 {@"AXLandmarkContentInfo", u"content"_ns}, 514 {@"AXLandmarkMain", u"main"_ns}, 515 {@"AXLandmarkNavigation", u"navigation"_ns}, 516 {@"AXLandmarkRegion", u"region"_ns}, 517 {@"AXLandmarkSearch", u"search"_ns}, 518 {@"AXSearchField", u"searchTextField"_ns}, 519 {@"AXSummary", u"summary"_ns}, 520 {@"AXTabPanel", u"tabPanel"_ns}, 521 {@"AXTerm", u"term"_ns}, 522 {@"AXUserInterfaceTooltip", u"tooltip"_ns}}; 523 524struct RoleDescrComparator { 525 const NSString* mRole; 526 explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {} 527 int operator()(const RoleDescrMap& aEntry) const { 528 return [mRole compare:aEntry.role]; 529 } 530}; 531 532- (NSString*)moxRoleDescription { 533 if (NSString* ariaRoleDescription = 534 utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) { 535 if ([ariaRoleDescription length]) { 536 return ariaRoleDescription; 537 } 538 } 539 540 if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns); 541 542 if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns); 543 544 if (mRole == roles::MARK) { 545 return utils::LocalizedString(u"highlight"_ns); 546 } 547 548 NSString* subrole = [self moxSubrole]; 549 550 if (subrole) { 551 size_t idx = 0; 552 if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap), 553 RoleDescrComparator(subrole), &idx)) { 554 return utils::LocalizedString(sRoleDescrMap[idx].description); 555 } 556 } 557 558 return NSAccessibilityRoleDescription([self moxRole], subrole); 559} 560 561- (NSString*)moxLabel { 562 if ([self isExpired]) { 563 return nil; 564 } 565 566 LocalAccessible* acc = mGeckoAccessible.AsAccessible(); 567 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 568 nsAutoString name; 569 570 /* If our accessible is: 571 * 1. Named by invisible text, or 572 * 2. Has more than one labeling relation, or 573 * 3. Is a special role defined in providesLabelNotTitle 574 * ... return its name as a label (AXDescription). 575 */ 576 if (acc) { 577 ENameValueFlag flag = acc->Name(name); 578 if (flag == eNameFromSubtree) { 579 return nil; 580 } 581 } else if (proxy) { 582 uint32_t flag = proxy->Name(name); 583 if (flag == eNameFromSubtree) { 584 return nil; 585 } 586 } 587 588 if (![self providesLabelNotTitle]) { 589 NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY]; 590 if ([relations count] == 1) { 591 return nil; 592 } 593 } 594 595 return nsCocoaUtils::ToNSString(name); 596} 597 598- (NSString*)moxTitle { 599 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 600 601 // In some special cases we provide the name in the label (AXDescription). 602 if ([self providesLabelNotTitle]) { 603 return nil; 604 } 605 606 nsAutoString title; 607 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 608 acc->Name(title); 609 } else { 610 mGeckoAccessible.AsProxy()->Name(title); 611 } 612 613 return nsCocoaUtils::ToNSString(title); 614 615 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 616} 617 618- (id)moxValue { 619 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 620 621 nsAutoString value; 622 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 623 acc->Value(value); 624 } else { 625 mGeckoAccessible.AsProxy()->Value(value); 626 } 627 628 return nsCocoaUtils::ToNSString(value); 629 630 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 631} 632 633- (NSString*)moxHelp { 634 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 635 636 // What needs to go here is actually the accDescription of an item. 637 // The MSAA acc_help method has nothing to do with this one. 638 nsAutoString helpText; 639 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 640 acc->Description(helpText); 641 } else { 642 mGeckoAccessible.AsProxy()->Description(helpText); 643 } 644 645 return nsCocoaUtils::ToNSString(helpText); 646 647 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 648} 649 650- (NSWindow*)moxWindow { 651 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 652 653 // Get a pointer to the native window (NSWindow) we reside in. 654 NSWindow* nativeWindow = nil; 655 DocAccessible* docAcc = nullptr; 656 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 657 docAcc = acc->Document(); 658 } else { 659 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 660 LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); 661 if (outerDoc) docAcc = outerDoc->Document(); 662 } 663 664 if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow()); 665 666 MOZ_ASSERT(nativeWindow, "Couldn't get native window"); 667 return nativeWindow; 668 669 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 670} 671 672- (NSNumber*)moxEnabled { 673 if ([self stateWithMask:states::UNAVAILABLE]) { 674 return @NO; 675 } 676 677 if (![self isRoot]) { 678 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; 679 if (![parent isRoot]) { 680 return @(![parent disableChild:self]); 681 } 682 } 683 684 return @YES; 685} 686 687- (NSNumber*)moxFocused { 688 return @([self stateWithMask:states::FOCUSED] != 0); 689} 690 691- (NSNumber*)moxSelected { 692 return @NO; 693} 694 695- (NSValue*)moxFrame { 696 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 697 698 nsIntRect rect = mGeckoAccessible.IsAccessible() 699 ? mGeckoAccessible.AsAccessible()->Bounds() 700 : mGeckoAccessible.AsProxy()->Bounds(); 701 NSScreen* mainView = [[NSScreen screens] objectAtIndex:0]; 702 CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView); 703 704 return [NSValue 705 valueWithRect:NSMakeRect( 706 static_cast<CGFloat>(rect.x) / scaleFactor, 707 [mainView frame].size.height - 708 static_cast<CGFloat>(rect.y + rect.height) / 709 scaleFactor, 710 static_cast<CGFloat>(rect.width) / scaleFactor, 711 static_cast<CGFloat>(rect.height) / scaleFactor)]; 712} 713 714- (NSString*)moxARIACurrent { 715 if (![self stateWithMask:states::CURRENT]) { 716 return nil; 717 } 718 719 return utils::GetAccAttr(self, nsGkAtoms::aria_current); 720} 721 722- (NSNumber*)moxARIAAtomic { 723 return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil); 724} 725 726- (NSString*)moxARIALive { 727 return utils::GetAccAttr(self, nsGkAtoms::aria_live); 728} 729 730- (NSString*)moxARIARelevant { 731 if (NSString* relevant = 732 utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) { 733 return relevant; 734 } 735 736 // Default aria-relevant value 737 return @"additions text"; 738} 739 740- (id)moxTitleUIElement { 741 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 742 743 NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY]; 744 if ([relations count] == 1) { 745 return [relations firstObject]; 746 } 747 748 return nil; 749} 750 751- (NSString*)moxDOMIdentifier { 752 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 753 754 nsAutoString id; 755 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 756 if (acc->GetContent()) { 757 nsCoreUtils::GetID(acc->GetContent(), id); 758 } 759 } else { 760 mGeckoAccessible.AsProxy()->DOMNodeID(id); 761 } 762 763 return nsCocoaUtils::ToNSString(id); 764} 765 766- (NSNumber*)moxRequired { 767 return @([self stateWithMask:states::REQUIRED] != 0); 768} 769 770- (NSNumber*)moxElementBusy { 771 return @([self stateWithMask:states::BUSY] != 0); 772} 773 774- (NSArray*)moxLinkedUIElements { 775 return [self getRelationsByType:RelationType::FLOWS_TO]; 776} 777 778- (NSArray*)moxARIAControls { 779 return [self getRelationsByType:RelationType::CONTROLLER_FOR]; 780} 781 782- (mozAccessible*)topWebArea { 783 AccessibleOrProxy doc = [self geckoDocument]; 784 while (!doc.IsNull()) { 785 if (doc.IsAccessible()) { 786 DocAccessible* docAcc = doc.AsAccessible()->AsDoc(); 787 if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) { 788 return GetNativeFromGeckoAccessible(docAcc); 789 } 790 791 doc = docAcc->ParentDocument(); 792 } else { 793 DocAccessibleParent* docProxy = doc.AsProxy()->AsDoc(); 794 if (docProxy->IsTopLevel()) { 795 return GetNativeFromGeckoAccessible(docProxy); 796 } 797 doc = docProxy->ParentDoc(); 798 } 799 } 800 801 return nil; 802} 803 804- (void)handleRoleChanged:(mozilla::a11y::role)newRole { 805 mRole = newRole; 806 mARIARole = nullptr; 807 808 // For testing purposes 809 [self moxPostNotification:@"AXMozRoleChanged"]; 810} 811 812- (id)moxEditableAncestor { 813 return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) { 814 return [moxAcc isKindOfClass:[mozTextAccessible class]]; 815 }]; 816} 817 818- (id)moxHighestEditableAncestor { 819 id highestAncestor = [self moxEditableAncestor]; 820 while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) { 821 id ancestorParent = [highestAncestor moxUnignoredParent]; 822 if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) { 823 break; 824 } 825 826 id higherAncestor = [ancestorParent moxEditableAncestor]; 827 828 if (!higherAncestor) { 829 break; 830 } 831 832 highestAncestor = higherAncestor; 833 } 834 835 return highestAncestor; 836} 837 838- (id)moxFocusableAncestor { 839 // XXX: Checking focusable state up the chain can be expensive. For now, 840 // we can just return AXEditableAncestor since the main use case for this 841 // is rich text editing with links. 842 return [self moxEditableAncestor]; 843} 844 845#ifndef RELEASE_OR_BETA 846- (NSString*)moxMozDebugDescription { 847 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 848 849 NSMutableString* domInfo = [NSMutableString string]; 850 if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) { 851 [domInfo appendFormat:@" %@", tagName]; 852 NSString* domID = [self moxDOMIdentifier]; 853 if ([domID length]) { 854 [domInfo appendFormat:@"#%@", domID]; 855 } 856 if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) { 857 [domInfo 858 appendFormat:@".%@", 859 [className stringByReplacingOccurrencesOfString:@" " 860 withString:@"."]]; 861 } 862 } 863 864 return [NSString stringWithFormat:@"<%@: %p %@%@>", 865 NSStringFromClass([self class]), self, 866 [self moxRole], domInfo]; 867 868 NS_OBJC_END_TRY_BLOCK_RETURN(nil); 869} 870#endif 871 872- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate { 873 // Create our search object and set it up with the searchPredicate 874 // params. The init function does additional parsing. We pass a 875 // reference to the web area to use as a start element if one is not 876 // specified. 877 MOXSearchInfo* search = 878 [[[MOXSearchInfo alloc] initWithParameters:searchPredicate 879 andRoot:self] autorelease]; 880 881 return [search performSearch]; 882} 883 884- (NSNumber*)moxUIElementCountForSearchPredicate: 885 (NSDictionary*)searchPredicate { 886 return [NSNumber 887 numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] 888 count]]; 889} 890 891- (void)moxSetFocused:(NSNumber*)focused { 892 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 893 894 if ([focused boolValue]) { 895 if (mGeckoAccessible.IsAccessible()) { 896 mGeckoAccessible.AsAccessible()->TakeFocus(); 897 } else { 898 mGeckoAccessible.AsProxy()->TakeFocus(); 899 } 900 } 901} 902 903- (void)moxPerformScrollToVisible { 904 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 905 906 if (mGeckoAccessible.IsAccessible()) { 907 // Need strong ref because of MOZ_CAN_RUN_SCRIPT 908 RefPtr<LocalAccessible> acc = mGeckoAccessible.AsAccessible(); 909 acc->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); 910 } else { 911 mGeckoAccessible.AsProxy()->ScrollTo( 912 nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE); 913 } 914} 915 916- (void)moxPerformShowMenu { 917 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 918 919 nsIntRect bounds = mGeckoAccessible.IsAccessible() 920 ? mGeckoAccessible.AsAccessible()->Bounds() 921 : mGeckoAccessible.AsProxy()->Bounds(); 922 // We don't need to convert this rect into mac coordinates because the 923 // mouse event synthesizer expects layout (gecko) coordinates. 924 LayoutDeviceIntRect geckoRect = LayoutDeviceIntRect::FromUnknownRect(bounds); 925 926 LocalAccessible* rootAcc = 927 mGeckoAccessible.IsAccessible() 928 ? mGeckoAccessible.AsAccessible()->RootAccessible() 929 : mGeckoAccessible.AsProxy() 930 ->OuterDocOfRemoteBrowser() 931 ->RootAccessible(); 932 id objOrView = 933 GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc)); 934 935 LayoutDeviceIntPoint p = 936 LayoutDeviceIntPoint(geckoRect.X() + (geckoRect.Width() / 2), 937 geckoRect.Y() + (geckoRect.Height() / 2)); 938 nsIWidget* widget = [objOrView widget]; 939 widget->SynthesizeNativeMouseEvent( 940 p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary, 941 nsIWidget::Modifiers::NO_MODIFIERS, nullptr); 942} 943 944- (void)moxPerformPress { 945 MOZ_ASSERT(!mGeckoAccessible.IsNull()); 946 947 if (mGeckoAccessible.IsAccessible()) { 948 mGeckoAccessible.AsAccessible()->DoAction(0); 949 } else { 950 mGeckoAccessible.AsProxy()->DoAction(0); 951 } 952 953 // Activating accessible may alter its state. 954 [self invalidateState]; 955} 956 957#pragma mark - 958 959- (BOOL)disableChild:(mozAccessible*)child { 960 return NO; 961} 962 963- (void)maybePostLiveRegionChanged { 964 id<MOXAccessible> liveRegion = 965 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { 966 return [moxAcc moxIsLiveRegion]; 967 }]; 968 969 if (liveRegion) { 970 [liveRegion moxPostNotification:@"AXLiveRegionChanged"]; 971 } 972} 973 974- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType { 975 if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) { 976 NSMutableArray<mozAccessible*>* relations = 977 [[[NSMutableArray alloc] init] autorelease]; 978 Relation rel = acc->RelationByType(relationType); 979 while (LocalAccessible* relAcc = rel.Next()) { 980 if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) { 981 [relations addObject:relNative]; 982 } 983 } 984 985 return relations; 986 } 987 988 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 989 nsTArray<RemoteAccessible*> rel = proxy->RelationByType(relationType); 990 return utils::ConvertToNSArray(rel); 991} 992 993- (void)handleAccessibleTextChangeEvent:(NSString*)change 994 inserted:(BOOL)isInserted 995 inContainer:(const AccessibleOrProxy&)container 996 at:(int32_t)start { 997} 998 999- (void)handleAccessibleEvent:(uint32_t)eventType { 1000 switch (eventType) { 1001 case nsIAccessibleEvent::EVENT_FOCUS: 1002 [self moxPostNotification: 1003 NSAccessibilityFocusedUIElementChangedNotification]; 1004 break; 1005 case nsIAccessibleEvent::EVENT_MENUPOPUP_START: 1006 [self moxPostNotification:@"AXMenuOpened"]; 1007 break; 1008 case nsIAccessibleEvent::EVENT_MENUPOPUP_END: 1009 [self moxPostNotification:@"AXMenuClosed"]; 1010 break; 1011 case nsIAccessibleEvent::EVENT_SELECTION: 1012 case nsIAccessibleEvent::EVENT_SELECTION_ADD: 1013 case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: 1014 case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: 1015 [self moxPostNotification: 1016 NSAccessibilitySelectedChildrenChangedNotification]; 1017 break; 1018 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: { 1019 if (![self stateWithMask:states::SELECTABLE_TEXT]) { 1020 break; 1021 } 1022 // We consider any caret move event to be a selected text change event. 1023 // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be 1024 // reduntant. 1025 MOXTextMarkerDelegate* delegate = 1026 static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]); 1027 NSMutableDictionary* userInfo = 1028 [[[delegate selectionChangeInfo] mutableCopy] autorelease]; 1029 userInfo[@"AXTextChangeElement"] = self; 1030 1031 mozAccessible* webArea = [self topWebArea]; 1032 [webArea 1033 moxPostNotification:NSAccessibilitySelectedTextChangedNotification 1034 withUserInfo:userInfo]; 1035 [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification 1036 withUserInfo:userInfo]; 1037 break; 1038 } 1039 case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED: 1040 mIsLiveRegion = true; 1041 [self moxPostNotification:@"AXLiveRegionCreated"]; 1042 break; 1043 case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED: 1044 mIsLiveRegion = false; 1045 break; 1046 case nsIAccessibleEvent::EVENT_REORDER: 1047 [self maybePostLiveRegionChanged]; 1048 break; 1049 case nsIAccessibleEvent::EVENT_NAME_CHANGE: { 1050 if (![self providesLabelNotTitle]) { 1051 [self moxPostNotification:NSAccessibilityTitleChangedNotification]; 1052 } 1053 [self maybePostLiveRegionChanged]; 1054 break; 1055 } 1056 } 1057} 1058 1059- (void)expire { 1060 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 1061 1062 [self invalidateState]; 1063 1064 mGeckoAccessible.SetBits(0); 1065 1066 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification]; 1067 1068 NS_OBJC_END_TRY_IGNORE_BLOCK; 1069} 1070 1071- (BOOL)isExpired { 1072 return !mGeckoAccessible.AsAccessible() && !mGeckoAccessible.AsProxy(); 1073} 1074 1075@end 1076