no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / accessible / mac / mozAccessible.mm
blobaa9849651173f6f69bed24678d35f8c73fa9050a
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/. */
8 #import "mozAccessible.h"
9 #include "MOXAccessibleBase.h"
11 #import "MacUtils.h"
12 #import "mozView.h"
13 #import "MOXSearchInfo.h"
14 #import "MOXTextMarkerDelegate.h"
15 #import "MOXWebAreaAccessible.h"
16 #import "mozTextAccessible.h"
17 #import "mozRootAccessible.h"
19 #include "LocalAccessible-inl.h"
20 #include "nsAccUtils.h"
21 #include "DocAccessibleParent.h"
22 #include "Relation.h"
23 #include "mozilla/a11y/Role.h"
24 #include "RootAccessible.h"
25 #include "mozilla/a11y/PDocAccessible.h"
26 #include "mozilla/dom/BrowserParent.h"
27 #include "OuterDocAccessible.h"
28 #include "nsChildView.h"
29 #include "xpcAccessibleMacInterface.h"
31 #include "nsRect.h"
32 #include "nsCocoaUtils.h"
33 #include "nsCoord.h"
34 #include "nsObjCExceptions.h"
35 #include "nsWhitespaceTokenizer.h"
36 #include <prdtoa.h>
38 using namespace mozilla;
39 using namespace mozilla::a11y;
41 #pragma mark -
43 @interface mozAccessible ()
44 - (BOOL)providesLabelNotTitle;
46 - (void)maybePostLiveRegionChanged;
47 - (void)maybePostA11yUtilNotification;
48 @end
50 @implementation mozAccessible
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   }
60   return self;
62   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
65 - (void)dealloc {
66   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
68   [super dealloc];
70   NS_OBJC_END_TRY_IGNORE_BLOCK;
73 #pragma mark - mozAccessible widget
75 - (BOOL)hasRepresentedView {
76   return NO;
79 - (id)representedView {
80   return nil;
83 - (BOOL)isRoot {
84   return NO;
87 #pragma mark -
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   }
98   return [parent moxIgnoreChild:self];
101 - (BOOL)moxIgnoreChild:(mozAccessible*)child {
102   return nsAccUtils::MustPrune(mGeckoAccessible);
105 - (id)childAt:(uint32_t)i {
106   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
108   Accessible* child = mGeckoAccessible->ChildAt(i);
109   return child ? GetNativeFromGeckoAccessible(child) : nil;
111   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
114 - (uint64_t)state {
115   return mGeckoAccessible->State();
118 - (uint64_t)stateWithMask:(uint64_t)mask {
119   return [self state] & mask;
122 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
123   if (state == states::BUSY) {
124     [self moxPostNotification:@"AXElementBusyChanged"];
125   }
128 - (BOOL)providesLabelNotTitle {
129   // These accessible types are the exception to the rule of label vs. title:
130   // They may be named explicitly, but they still provide a label not a title.
131   return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
132          mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
133          mRole == roles::DOCUMENT || mRole == roles::OUTLINE ||
134          mRole == roles::ARTICLE || mRole == roles::ENTRY ||
135          mRole == roles::SPINBUTTON;
138 - (mozilla::a11y::Accessible*)geckoAccessible {
139   return mGeckoAccessible;
142 #pragma mark - MOXAccessible protocol
144 - (BOOL)moxBlockSelector:(SEL)selector {
145   if (selector == @selector(moxPerformPress)) {
146     uint8_t actionCount = mGeckoAccessible->ActionCount();
148     // If we have no action, we don't support press, so return YES.
149     return actionCount == 0;
150   }
152   if (selector == @selector(moxSetFocused:)) {
153     return [self stateWithMask:states::FOCUSABLE] == 0;
154   }
156   if (selector == @selector(moxARIALive) ||
157       selector == @selector(moxARIAAtomic) ||
158       selector == @selector(moxARIARelevant)) {
159     return ![self moxIsLiveRegion];
160   }
162   if (selector == @selector(moxARIAPosInSet) || selector == @selector
163                                                     (moxARIASetSize)) {
164     GroupPos groupPos = mGeckoAccessible->GroupPosition();
165     return groupPos.setSize == 0;
166   }
168   if (selector == @selector(moxExpanded)) {
169     return [self stateWithMask:states::EXPANDABLE] == 0;
170   }
172   return [super moxBlockSelector:selector];
175 - (id)moxFocusedUIElement {
176   MOZ_ASSERT(mGeckoAccessible);
177   // This only gets queried on the web area or the root group
178   // so just use the doc's focused child instead of trying to get
179   // the focused child of mGeckoAccessible.
180   Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
181   mozAccessible* focusedChild =
182       GetNativeFromGeckoAccessible(doc->FocusedChild());
184   if ([focusedChild isAccessibilityElement]) {
185     return focusedChild;
186   }
188   // return ourself if we can't get a native focused child.
189   return self;
192 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
193   MOZ_ASSERT(mGeckoAccessible);
195   return [MOXTextMarkerDelegate
196       getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)];
199 - (BOOL)moxIsLiveRegion {
200   return mIsLiveRegion;
203 - (id)moxHitTest:(NSPoint)point {
204   MOZ_ASSERT(mGeckoAccessible);
206   // Convert the given screen-global point in the cocoa coordinate system (with
207   // origin in the bottom-left corner of the screen) into point in the Gecko
208   // coordinate system (with origin in a top-left screen point).
209   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
210   NSPoint tmpPoint =
211       NSMakePoint(point.x, [mainView frame].size.height - point.y);
212   LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
213       tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
215   Accessible* child = mGeckoAccessible->ChildAtPoint(
216       geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
218   if (child) {
219     mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
220     return [nativeChild isAccessibilityElement]
221                ? nativeChild
222                : [nativeChild moxUnignoredParent];
223   }
225   // if we didn't find anything, return ourself or child view.
226   return self;
229 - (id<mozAccessible>)moxParent {
230   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
231   if ([self isExpired]) {
232     return nil;
233   }
235   Accessible* parent = mGeckoAccessible->Parent();
237   if (!parent) {
238     return nil;
239   }
241   id nativeParent = GetNativeFromGeckoAccessible(parent);
242   if ([nativeParent isKindOfClass:[MOXWebAreaAccessible class]]) {
243     // Before returning a WebArea as parent, check to see if
244     // there is a generated root group that is an intermediate container.
245     if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
246       nativeParent = rootGroup;
247     }
248   }
250   if (!nativeParent && mGeckoAccessible->IsLocal()) {
251     // Return native of root accessible if we have no direct parent.
252     // XXX: need to return a sensible fallback in proxy case as well
253     nativeParent = GetNativeFromGeckoAccessible(
254         mGeckoAccessible->AsLocal()->RootAccessible());
255   }
257   return GetObjectOrRepresentedView(nativeParent);
259   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
262 // gets all our native children lazily, including those that are ignored.
263 - (NSArray*)moxChildren {
264   MOZ_ASSERT(mGeckoAccessible);
266   NSMutableArray* children = [[[NSMutableArray alloc]
267       initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
269   for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
270        childIdx++) {
271     Accessible* child = mGeckoAccessible->ChildAt(childIdx);
272     mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
273     if (!nativeChild) {
274       continue;
275     }
277     [children addObject:nativeChild];
278   }
280   return children;
283 - (NSValue*)moxPosition {
284   CGRect frame = [[self moxFrame] rectValue];
286   return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
289 - (NSValue*)moxSize {
290   CGRect frame = [[self moxFrame] rectValue];
292   return
293       [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
296 - (NSString*)moxRole {
297 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
298              msaaRole, ia2Role, androidClass, iosIsElement, nameRule)       \
299   case roles::geckoRole:                                                    \
300     return macRole;
302   switch (mRole) {
303 #include "RoleMap.h"
304     default:
305       MOZ_ASSERT_UNREACHABLE("Unknown role.");
306       return NSAccessibilityUnknownRole;
307   }
309 #undef ROLE
312 - (nsStaticAtom*)ARIARole {
313   MOZ_ASSERT(mGeckoAccessible);
315   if (mGeckoAccessible->HasARIARole()) {
316     const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
317     return roleMap->roleAtom;
318   }
320   return nsGkAtoms::_empty;
323 - (NSString*)moxSubrole {
324   MOZ_ASSERT(mGeckoAccessible);
326   // Deal with landmarks first
327   // macOS groups the specific landmark types of DPub ARIA into two broad
328   // categories with corresponding subroles: Navigation and region/container.
329   if (mRole == roles::LANDMARK) {
330     nsAtom* landmark = mGeckoAccessible->LandmarkRole();
331     // HTML Elements treated as landmarks, and ARIA landmarks.
332     if (landmark) {
333       if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
334       if (landmark == nsGkAtoms::complementary)
335         return @"AXLandmarkComplementary";
336       if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
337       if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
338       if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
339       if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
340     }
342     // None of the above, so assume DPub ARIA.
343     return @"AXLandmarkRegion";
344   }
346   // Now, deal with widget roles
347   nsStaticAtom* roleAtom = nullptr;
349   if (mRole == roles::DIALOG) {
350     roleAtom = [self ARIARole];
352     if (roleAtom == nsGkAtoms::alertdialog) {
353       return @"AXApplicationAlertDialog";
354     }
355     if (roleAtom == nsGkAtoms::dialog) {
356       return @"AXApplicationDialog";
357     }
358   }
360   if (mRole == roles::FORM) {
361     roleAtom = [self ARIARole];
363     if (roleAtom == nsGkAtoms::form) {
364       return @"AXLandmarkForm";
365     }
366   }
368 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
369              msaaRole, ia2Role, androidClass, iosIsElement, nameRule)       \
370   case roles::geckoRole:                                                    \
371     if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) {      \
372       return macSubrole;                                                    \
373     } else {                                                                \
374       break;                                                                \
375     }
377   switch (mRole) {
378 #include "RoleMap.h"
379   }
381   // These are special. They map to roles::NOTHING
382   // and are instructed by the ARIA map to use the native host role.
383   roleAtom = [self ARIARole];
385   if (roleAtom == nsGkAtoms::log_) {
386     return @"AXApplicationLog";
387   }
389   if (roleAtom == nsGkAtoms::timer) {
390     return @"AXApplicationTimer";
391   }
392   // macOS added an AXSubrole value to distinguish generic AXGroup objects
393   // from those which are AXGroups as a result of an explicit ARIA role,
394   // such as the non-landmark, non-listitem text containers in DPub ARIA.
395   if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
396     return @"AXApplicationGroup";
397   }
399   return NSAccessibilityUnknownSubrole;
401 #undef ROLE
404 struct RoleDescrMap {
405   NSString* role;
406   const nsString description;
409 static const RoleDescrMap sRoleDescrMap[] = {
410     {@"AXApplicationAlert", u"alert"_ns},
411     {@"AXApplicationAlertDialog", u"alertDialog"_ns},
412     {@"AXApplicationDialog", u"dialog"_ns},
413     {@"AXApplicationLog", u"log"_ns},
414     {@"AXApplicationMarquee", u"marquee"_ns},
415     {@"AXApplicationStatus", u"status"_ns},
416     {@"AXApplicationTimer", u"timer"_ns},
417     {@"AXContentSeparator", u"separator"_ns},
418     {@"AXDefinition", u"definition"_ns},
419     {@"AXDetails", u"details"_ns},
420     {@"AXDocument", u"document"_ns},
421     {@"AXDocumentArticle", u"article"_ns},
422     {@"AXDocumentMath", u"math"_ns},
423     {@"AXDocumentNote", u"note"_ns},
424     {@"AXLandmarkApplication", u"application"_ns},
425     {@"AXLandmarkBanner", u"banner"_ns},
426     {@"AXLandmarkComplementary", u"complementary"_ns},
427     {@"AXLandmarkContentInfo", u"content"_ns},
428     {@"AXLandmarkMain", u"main"_ns},
429     {@"AXLandmarkNavigation", u"navigation"_ns},
430     {@"AXLandmarkRegion", u"region"_ns},
431     {@"AXLandmarkSearch", u"search"_ns},
432     {@"AXSearchField", u"searchTextField"_ns},
433     {@"AXSummary", u"summary"_ns},
434     {@"AXTabPanel", u"tabPanel"_ns},
435     {@"AXTerm", u"term"_ns},
436     {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
438 struct RoleDescrComparator {
439   const NSString* mRole;
440   explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
441   int operator()(const RoleDescrMap& aEntry) const {
442     return [mRole compare:aEntry.role];
443   }
446 - (NSString*)moxRoleDescription {
447   if (NSString* ariaRoleDescription =
448           utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
449     if ([ariaRoleDescription length]) {
450       return ariaRoleDescription;
451     }
452   }
454   if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
456   if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
458   if (mRole == roles::MARK) {
459     return utils::LocalizedString(u"highlight"_ns);
460   }
462   NSString* subrole = [self moxSubrole];
464   if (subrole) {
465     size_t idx = 0;
466     if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
467                        RoleDescrComparator(subrole), &idx)) {
468       return utils::LocalizedString(sRoleDescrMap[idx].description);
469     }
470   }
472   return NSAccessibilityRoleDescription([self moxRole], subrole);
475 - (NSString*)moxLabel {
476   if ([self isExpired]) {
477     return nil;
478   }
480   nsAutoString name;
482   /* If our accessible is:
483    * 1. Named by invisible text, or
484    * 2. Has more than one labeling relation, or
485    * 3. Is a special role defined in providesLabelNotTitle
486    *   ... return its name as a label (AXDescription).
487    */
488   ENameValueFlag flag = mGeckoAccessible->Name(name);
489   if (flag == eNameFromSubtree) {
490     return nil;
491   }
493   if (![self providesLabelNotTitle]) {
494     NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
495     if ([relations count] == 1) {
496       return nil;
497     }
498   }
500   return nsCocoaUtils::ToNSString(name);
503 - (NSString*)moxTitle {
504   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
506   // In some special cases we provide the name in the label (AXDescription).
507   if ([self providesLabelNotTitle]) {
508     return nil;
509   }
511   nsAutoString title;
512   mGeckoAccessible->Name(title);
513   if (nsCoreUtils::IsWhitespaceString(title)) {
514     return @"";
515   }
517   return nsCocoaUtils::ToNSString(title);
519   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
522 - (id)moxValue {
523   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
525   nsAutoString value;
526   mGeckoAccessible->Value(value);
528   return nsCocoaUtils::ToNSString(value);
530   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
533 - (NSString*)moxHelp {
534   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
536   // What needs to go here is actually the accDescription of an item.
537   // The MSAA acc_help method has nothing to do with this one.
538   nsAutoString helpText;
539   mGeckoAccessible->Description(helpText);
541   return nsCocoaUtils::ToNSString(helpText);
543   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
546 - (NSWindow*)moxWindow {
547   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
549   // Get a pointer to the native window (NSWindow) we reside in.
550   NSWindow* nativeWindow = nil;
551   DocAccessible* docAcc = nullptr;
552   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
553     docAcc = acc->Document();
554   } else {
555     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
556     LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
557     if (outerDoc) docAcc = outerDoc->Document();
558   }
560   if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
562   MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
563              "Couldn't get native window");
564   return nativeWindow;
566   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
569 - (NSNumber*)moxEnabled {
570   if ([self stateWithMask:states::UNAVAILABLE]) {
571     return @NO;
572   }
574   if (![self isRoot]) {
575     mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
576     if (![parent isRoot]) {
577       return @(![parent disableChild:self]);
578     }
579   }
581   return @YES;
584 - (NSNumber*)moxFocused {
585   return @([self stateWithMask:states::FOCUSED] != 0);
588 - (NSNumber*)moxSelected {
589   return @NO;
592 - (NSNumber*)moxExpanded {
593   return @([self stateWithMask:states::EXPANDED] != 0);
596 - (NSValue*)moxFrame {
597   MOZ_ASSERT(mGeckoAccessible);
599   LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
600   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
601   CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
603   return [NSValue
604       valueWithRect:NSMakeRect(
605                         static_cast<CGFloat>(rect.x) / scaleFactor,
606                         [mainView frame].size.height -
607                             static_cast<CGFloat>(rect.y + rect.height) /
608                                 scaleFactor,
609                         static_cast<CGFloat>(rect.width) / scaleFactor,
610                         static_cast<CGFloat>(rect.height) / scaleFactor)];
613 - (NSString*)moxARIACurrent {
614   if (![self stateWithMask:states::CURRENT]) {
615     return nil;
616   }
618   return utils::GetAccAttr(self, nsGkAtoms::aria_current);
621 - (NSNumber*)moxARIAAtomic {
622   return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
625 - (NSString*)moxARIALive {
626   return utils::GetAccAttr(self, nsGkAtoms::aria_live);
629 - (NSNumber*)moxARIAPosInSet {
630   GroupPos groupPos = mGeckoAccessible->GroupPosition();
631   return @(groupPos.posInSet);
634 - (NSNumber*)moxARIASetSize {
635   GroupPos groupPos = mGeckoAccessible->GroupPosition();
636   return @(groupPos.setSize);
639 - (NSString*)moxARIARelevant {
640   if (NSString* relevant =
641           utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
642     return relevant;
643   }
645   // Default aria-relevant value
646   return @"additions text";
649 - (id)moxTitleUIElement {
650   MOZ_ASSERT(mGeckoAccessible);
652   NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
653   if ([relations count] == 1) {
654     return [relations firstObject];
655   }
657   return nil;
660 - (NSString*)moxDOMIdentifier {
661   MOZ_ASSERT(mGeckoAccessible);
663   nsAutoString id;
664   mGeckoAccessible->DOMNodeID(id);
666   return nsCocoaUtils::ToNSString(id);
669 - (NSNumber*)moxRequired {
670   return @([self stateWithMask:states::REQUIRED] != 0);
673 - (NSNumber*)moxElementBusy {
674   return @([self stateWithMask:states::BUSY] != 0);
677 - (NSArray*)moxLinkedUIElements {
678   return [self getRelationsByType:RelationType::FLOWS_TO];
681 - (NSArray*)moxARIAControls {
682   return [self getRelationsByType:RelationType::CONTROLLER_FOR];
685 - (mozAccessible*)topWebArea {
686   Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
687   while (doc) {
688     if (doc->IsLocal()) {
689       DocAccessible* docAcc = doc->AsLocal()->AsDoc();
690       if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
691         return GetNativeFromGeckoAccessible(docAcc);
692       }
694       doc = docAcc->ParentDocument();
695     } else {
696       DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
697       if (docProxy->IsTopLevel()) {
698         return GetNativeFromGeckoAccessible(docProxy);
699       }
700       doc = docProxy->ParentDoc();
701     }
702   }
704   return nil;
707 - (void)handleRoleChanged:(mozilla::a11y::role)newRole {
708   mRole = newRole;
709   mARIARole = nullptr;
711   // For testing purposes
712   [self moxPostNotification:@"AXMozRoleChanged"];
715 - (id)moxEditableAncestor {
716   return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
717     return [moxAcc isKindOfClass:[mozTextAccessible class]];
718   }];
721 - (id)moxHighestEditableAncestor {
722   id highestAncestor = [self moxEditableAncestor];
723   while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
724     id ancestorParent = [highestAncestor moxUnignoredParent];
725     if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
726       break;
727     }
729     id higherAncestor = [ancestorParent moxEditableAncestor];
731     if (!higherAncestor) {
732       break;
733     }
735     highestAncestor = higherAncestor;
736   }
738   return highestAncestor;
741 - (id)moxFocusableAncestor {
742   // XXX: Checking focusable state up the chain can be expensive. For now,
743   // we can just return AXEditableAncestor since the main use case for this
744   // is rich text editing with links.
745   return [self moxEditableAncestor];
748 #ifndef RELEASE_OR_BETA
749 - (NSString*)moxMozDebugDescription {
750   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
752   if (!mGeckoAccessible) {
753     return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
754                                       NSStringFromClass([self class]), self];
755   }
757   NSMutableString* domInfo = [NSMutableString string];
758   if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
759     [domInfo appendFormat:@" %@", tagName];
760     NSString* domID = [self moxDOMIdentifier];
761     if ([domID length]) {
762       [domInfo appendFormat:@"#%@", domID];
763     }
764     if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
765       [domInfo
766           appendFormat:@".%@",
767                        [className stringByReplacingOccurrencesOfString:@" "
768                                                             withString:@"."]];
769     }
770   }
772   return [NSString stringWithFormat:@"<%@: %p %@%@>",
773                                     NSStringFromClass([self class]), self,
774                                     [self moxRole], domInfo];
776   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
778 #endif
780 - (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
781   // Create our search object and set it up with the searchPredicate
782   // params. The init function does additional parsing. We pass a
783   // reference to the web area to use as a start element if one is not
784   // specified.
785   MOXSearchInfo* search =
786       [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
787                                          andRoot:self] autorelease];
789   return [search performSearch];
792 - (NSNumber*)moxUIElementCountForSearchPredicate:
793     (NSDictionary*)searchPredicate {
794   return [NSNumber
795       numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
796                            count]];
799 - (void)moxSetFocused:(NSNumber*)focused {
800   MOZ_ASSERT(mGeckoAccessible);
802   if ([focused boolValue]) {
803     mGeckoAccessible->TakeFocus();
804   }
807 - (void)moxPerformScrollToVisible {
808   MOZ_ASSERT(mGeckoAccessible);
809   mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
812 - (void)moxPerformShowMenu {
813   MOZ_ASSERT(mGeckoAccessible);
815   // We don't need to convert this rect into mac coordinates because the
816   // mouse event synthesizer expects layout (gecko) coordinates.
817   LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds();
819   LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
820                                  ? mGeckoAccessible->AsLocal()->RootAccessible()
821                                  : mGeckoAccessible->AsRemote()
822                                        ->OuterDocOfRemoteBrowser()
823                                        ->RootAccessible();
824   id objOrView =
825       GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
827   LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
828       bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
829   nsIWidget* widget = [objOrView widget];
830   widget->SynthesizeNativeMouseEvent(
831       p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
832       nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
835 - (void)moxPerformPress {
836   MOZ_ASSERT(mGeckoAccessible);
838   mGeckoAccessible->DoAction(0);
841 #pragma mark -
843 - (BOOL)disableChild:(mozAccessible*)child {
844   return NO;
847 - (void)maybePostLiveRegionChanged {
848   id<MOXAccessible> liveRegion =
849       [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
850         return [moxAcc moxIsLiveRegion];
851       }];
853   if (liveRegion) {
854     [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
855   }
858 - (void)maybePostA11yUtilNotification {
859   MOZ_ASSERT(mGeckoAccessible);
860   // Sometimes we use a special live region to make announcements to the user.
861   // This region is a child of the root document, but doesn't contain any
862   // content. If we try to fire regular AXLiveRegion changed events through it,
863   // VoiceOver clips the notifications because it (rightfully) doesn't detect
864   // focus within the region. We get around this by firing an
865   // AXAnnouncementRequested notification here instead.
866   // Verify we're trying to send a notification for the a11yUtils alert (and not
867   // a random acc with the same ID) by checking:
868   //  - The gecko acc is local, our a11y-announcement lives in browser.xhtml
869   //  - The ID of the gecko acc is "a11y-announcement"
870   //  - The native acc is a direct descendent of the root
871   if (mGeckoAccessible->IsLocal() &&
872       [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
873       [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
874     // Our actual announcement should be stored as a child of the alert,
875     // so we verify a child exists, and then query that child below.
876     NSArray* children = [self moxChildren];
877     MOZ_ASSERT([children count] == 1 && children[0],
878                "A11yUtil event received, but no announcement found?");
880     mozAccessible* announcement = children[0];
881     NSString* key;
882     if ([announcement providesLabelNotTitle]) {
883       key = [announcement moxLabel];
884     } else {
885       key = [announcement moxTitle];
886     }
888     NSDictionary* info = @{
889       NSAccessibilityAnnouncementKey : key ? key : @(""),
890       NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
891     };
893     id window = [self moxWindow];
895     // This sends events via nsIObserverService to be consumed by our
896     // mochitests. Normally we'd fire these events through moxPostNotification
897     // which takes care of this, but because the window we fetch above isn't
898     // derrived from MOXAccessibleBase, we do this (and post the notification)
899     // manually.
900     xpcAccessibleMacEvent::FireEvent(
901         window, NSAccessibilityAnnouncementRequestedNotification, info);
902     NSAccessibilityPostNotificationWithUserInfo(
903         window, NSAccessibilityAnnouncementRequestedNotification, info);
904   }
907 - (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
908   NSMutableArray<mozAccessible*>* relations =
909       [[[NSMutableArray alloc] init] autorelease];
910   Relation rel = mGeckoAccessible->RelationByType(relationType);
911   while (Accessible* relAcc = rel.Next()) {
912     if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
913       [relations addObject:relNative];
914     }
915   }
917   return relations;
920 - (void)handleAccessibleTextChangeEvent:(NSString*)change
921                                inserted:(BOOL)isInserted
922                             inContainer:(Accessible*)container
923                                      at:(int32_t)start {
926 - (void)handleAccessibleEvent:(uint32_t)eventType {
927   switch (eventType) {
928     case nsIAccessibleEvent::EVENT_ALERT:
929       [self maybePostA11yUtilNotification];
930       break;
931     case nsIAccessibleEvent::EVENT_FOCUS:
932       [self moxPostNotification:
933                 NSAccessibilityFocusedUIElementChangedNotification];
934       break;
935     case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
936       [self moxPostNotification:@"AXMenuOpened"];
937       break;
938     case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
939       [self moxPostNotification:@"AXMenuClosed"];
940       break;
941     case nsIAccessibleEvent::EVENT_SELECTION:
942     case nsIAccessibleEvent::EVENT_SELECTION_ADD:
943     case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
944     case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
945       [self moxPostNotification:
946                 NSAccessibilitySelectedChildrenChangedNotification];
947       break;
948     case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
949       if (![self stateWithMask:states::SELECTABLE_TEXT]) {
950         break;
951       }
952       // We consider any caret move event to be a selected text change event.
953       // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
954       // reduntant.
955       MOXTextMarkerDelegate* delegate =
956           static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
957       NSMutableDictionary* userInfo =
958           [[[delegate selectionChangeInfo] mutableCopy] autorelease];
959       userInfo[@"AXTextChangeElement"] = self;
961       mozAccessible* webArea = [self topWebArea];
962       [webArea
963           moxPostNotification:NSAccessibilitySelectedTextChangedNotification
964                  withUserInfo:userInfo];
965       [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
966                    withUserInfo:userInfo];
967       break;
968     }
969     case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
970       mIsLiveRegion = true;
971       [self moxPostNotification:@"AXLiveRegionCreated"];
972       break;
973     case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
974       mIsLiveRegion = false;
975       break;
976     case nsIAccessibleEvent::EVENT_REORDER:
977       [self maybePostLiveRegionChanged];
978       break;
979     case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
980       if (![self providesLabelNotTitle]) {
981         [self moxPostNotification:NSAccessibilityTitleChangedNotification];
982       }
983       [self maybePostLiveRegionChanged];
984       break;
985     }
986   }
989 - (void)expire {
990   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
992   mGeckoAccessible = nullptr;
994   [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
996   NS_OBJC_END_TRY_IGNORE_BLOCK;
999 - (BOOL)isExpired {
1000   return !mGeckoAccessible;
1003 @end