tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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