no bug - Correct some typos in the comments. a=typo-fix
[gecko.git] / accessible / mac / mozAccessible.mm
blob0609cc7afcbd47e78b36cc0d0e471905d8049b41
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, uiaControlType, \
299              nameRule)                                                      \
300   case roles::geckoRole:                                                    \
301     return macRole;
303   switch (mRole) {
304 #include "RoleMap.h"
305     default:
306       MOZ_ASSERT_UNREACHABLE("Unknown role.");
307       return NSAccessibilityUnknownRole;
308   }
310 #undef ROLE
313 - (nsStaticAtom*)ARIARole {
314   MOZ_ASSERT(mGeckoAccessible);
316   if (mGeckoAccessible->HasARIARole()) {
317     const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
318     return roleMap->roleAtom;
319   }
321   return nsGkAtoms::_empty;
324 - (NSString*)moxSubrole {
325   MOZ_ASSERT(mGeckoAccessible);
327   // Deal with landmarks first
328   // macOS groups the specific landmark types of DPub ARIA into two broad
329   // categories with corresponding subroles: Navigation and region/container.
330   if (mRole == roles::LANDMARK) {
331     nsAtom* landmark = mGeckoAccessible->LandmarkRole();
332     // HTML Elements treated as landmarks, and ARIA landmarks.
333     if (landmark) {
334       if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
335       if (landmark == nsGkAtoms::complementary)
336         return @"AXLandmarkComplementary";
337       if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
338       if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
339       if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
340       if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
341     }
343     // None of the above, so assume DPub ARIA.
344     return @"AXLandmarkRegion";
345   }
347   // Now, deal with widget roles
348   nsStaticAtom* roleAtom = nullptr;
350   if (mRole == roles::DIALOG) {
351     roleAtom = [self ARIARole];
353     if (roleAtom == nsGkAtoms::alertdialog) {
354       return @"AXApplicationAlertDialog";
355     }
356     if (roleAtom == nsGkAtoms::dialog) {
357       return @"AXApplicationDialog";
358     }
359   }
361   if (mRole == roles::FORM) {
362     roleAtom = [self ARIARole];
364     if (roleAtom == nsGkAtoms::form) {
365       return @"AXLandmarkForm";
366     }
367   }
369 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
370              msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
371              nameRule)                                                      \
372   case roles::geckoRole:                                                    \
373     if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) {      \
374       return macSubrole;                                                    \
375     } else {                                                                \
376       break;                                                                \
377     }
379   switch (mRole) {
380 #include "RoleMap.h"
381   }
383   // These are special. They map to roles::NOTHING
384   // and are instructed by the ARIA map to use the native host role.
385   roleAtom = [self ARIARole];
387   if (roleAtom == nsGkAtoms::log_) {
388     return @"AXApplicationLog";
389   }
391   if (roleAtom == nsGkAtoms::timer) {
392     return @"AXApplicationTimer";
393   }
394   // macOS added an AXSubrole value to distinguish generic AXGroup objects
395   // from those which are AXGroups as a result of an explicit ARIA role,
396   // such as the non-landmark, non-listitem text containers in DPub ARIA.
397   if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
398     return @"AXApplicationGroup";
399   }
401   return NSAccessibilityUnknownSubrole;
403 #undef ROLE
406 struct RoleDescrMap {
407   NSString* role;
408   const nsString description;
411 static const RoleDescrMap sRoleDescrMap[] = {
412     {@"AXApplicationAlert", u"alert"_ns},
413     {@"AXApplicationAlertDialog", u"alertDialog"_ns},
414     {@"AXApplicationDialog", u"dialog"_ns},
415     {@"AXApplicationLog", u"log"_ns},
416     {@"AXApplicationMarquee", u"marquee"_ns},
417     {@"AXApplicationStatus", u"status"_ns},
418     {@"AXApplicationTimer", u"timer"_ns},
419     {@"AXContentSeparator", u"separator"_ns},
420     {@"AXDefinition", u"definition"_ns},
421     {@"AXDetails", u"details"_ns},
422     {@"AXDocument", u"document"_ns},
423     {@"AXDocumentArticle", u"article"_ns},
424     {@"AXDocumentMath", u"math"_ns},
425     {@"AXDocumentNote", u"note"_ns},
426     {@"AXLandmarkApplication", u"application"_ns},
427     {@"AXLandmarkBanner", u"banner"_ns},
428     {@"AXLandmarkComplementary", u"complementary"_ns},
429     {@"AXLandmarkContentInfo", u"content"_ns},
430     {@"AXLandmarkMain", u"main"_ns},
431     {@"AXLandmarkNavigation", u"navigation"_ns},
432     {@"AXLandmarkRegion", u"region"_ns},
433     {@"AXLandmarkSearch", u"search"_ns},
434     {@"AXSearchField", u"searchTextField"_ns},
435     {@"AXSummary", u"summary"_ns},
436     {@"AXTabPanel", u"tabPanel"_ns},
437     {@"AXTerm", u"term"_ns},
438     {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
440 struct RoleDescrComparator {
441   const NSString* mRole;
442   explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
443   int operator()(const RoleDescrMap& aEntry) const {
444     return [mRole compare:aEntry.role];
445   }
448 - (NSString*)moxRoleDescription {
449   if (NSString* ariaRoleDescription =
450           utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
451     if ([ariaRoleDescription length]) {
452       return ariaRoleDescription;
453     }
454   }
456   if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
458   if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
460   if (mRole == roles::MARK) {
461     return utils::LocalizedString(u"highlight"_ns);
462   }
464   NSString* subrole = [self moxSubrole];
466   if (subrole) {
467     size_t idx = 0;
468     if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
469                        RoleDescrComparator(subrole), &idx)) {
470       return utils::LocalizedString(sRoleDescrMap[idx].description);
471     }
472   }
474   return NSAccessibilityRoleDescription([self moxRole], subrole);
477 - (NSString*)moxLabel {
478   if ([self isExpired]) {
479     return nil;
480   }
482   nsAutoString name;
484   /* If our accessible is:
485    * 1. Named by invisible text, or
486    * 2. Has more than one labeling relation, or
487    * 3. Is a special role defined in providesLabelNotTitle
488    *   ... return its name as a label (AXDescription).
489    */
490   ENameValueFlag flag = mGeckoAccessible->Name(name);
491   if (flag == eNameFromSubtree) {
492     return nil;
493   }
495   if (![self providesLabelNotTitle]) {
496     NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
497     if ([relations count] == 1) {
498       return nil;
499     }
500   }
502   return nsCocoaUtils::ToNSString(name);
505 - (NSString*)moxTitle {
506   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
508   // In some special cases we provide the name in the label (AXDescription).
509   if ([self providesLabelNotTitle]) {
510     return nil;
511   }
513   nsAutoString title;
514   mGeckoAccessible->Name(title);
515   if (nsCoreUtils::IsWhitespaceString(title)) {
516     return @"";
517   }
519   return nsCocoaUtils::ToNSString(title);
521   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
524 - (id)moxValue {
525   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
527   nsAutoString value;
528   mGeckoAccessible->Value(value);
530   return nsCocoaUtils::ToNSString(value);
532   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
535 - (NSString*)moxHelp {
536   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
538   // What needs to go here is actually the accDescription of an item.
539   // The MSAA acc_help method has nothing to do with this one.
540   nsAutoString helpText;
541   mGeckoAccessible->Description(helpText);
543   return nsCocoaUtils::ToNSString(helpText);
545   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
548 - (NSWindow*)moxWindow {
549   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
551   // Get a pointer to the native window (NSWindow) we reside in.
552   NSWindow* nativeWindow = nil;
553   DocAccessible* docAcc = nullptr;
554   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
555     docAcc = acc->Document();
556   } else {
557     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
558     LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
559     if (outerDoc) docAcc = outerDoc->Document();
560   }
562   if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
564   MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
565              "Couldn't get native window");
566   return nativeWindow;
568   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
571 - (NSNumber*)moxEnabled {
572   if ([self stateWithMask:states::UNAVAILABLE]) {
573     return @NO;
574   }
576   if (![self isRoot]) {
577     mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
578     if (![parent isRoot]) {
579       return @(![parent disableChild:self]);
580     }
581   }
583   return @YES;
586 - (NSNumber*)moxFocused {
587   return @([self stateWithMask:states::FOCUSED] != 0);
590 - (NSNumber*)moxSelected {
591   return @NO;
594 - (NSNumber*)moxExpanded {
595   return @([self stateWithMask:states::EXPANDED] != 0);
598 - (NSValue*)moxFrame {
599   MOZ_ASSERT(mGeckoAccessible);
601   LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
602   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
603   CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
605   return [NSValue
606       valueWithRect:NSMakeRect(
607                         static_cast<CGFloat>(rect.x) / scaleFactor,
608                         [mainView frame].size.height -
609                             static_cast<CGFloat>(rect.y + rect.height) /
610                                 scaleFactor,
611                         static_cast<CGFloat>(rect.width) / scaleFactor,
612                         static_cast<CGFloat>(rect.height) / scaleFactor)];
615 - (NSString*)moxARIACurrent {
616   if (![self stateWithMask:states::CURRENT]) {
617     return nil;
618   }
620   return utils::GetAccAttr(self, nsGkAtoms::aria_current);
623 - (NSNumber*)moxARIAAtomic {
624   return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
627 - (NSString*)moxARIALive {
628   return utils::GetAccAttr(self, nsGkAtoms::aria_live);
631 - (NSNumber*)moxARIAPosInSet {
632   GroupPos groupPos = mGeckoAccessible->GroupPosition();
633   return @(groupPos.posInSet);
636 - (NSNumber*)moxARIASetSize {
637   GroupPos groupPos = mGeckoAccessible->GroupPosition();
638   return @(groupPos.setSize);
641 - (NSString*)moxARIARelevant {
642   if (NSString* relevant =
643           utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
644     return relevant;
645   }
647   // Default aria-relevant value
648   return @"additions text";
651 - (NSString*)moxPlaceholderValue {
652   // First, check for plaecholder HTML attribute
653   if (NSString* placeholder = utils::GetAccAttr(self, nsGkAtoms::placeholder)) {
654     return placeholder;
655   }
657   // If no placeholder HTML attribute, check for the aria version.
658   return utils::GetAccAttr(self, nsGkAtoms::aria_placeholder);
661 - (id)moxTitleUIElement {
662   MOZ_ASSERT(mGeckoAccessible);
664   NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
665   if ([relations count] == 1) {
666     return [relations firstObject];
667   }
669   return nil;
672 - (NSString*)moxDOMIdentifier {
673   MOZ_ASSERT(mGeckoAccessible);
675   nsAutoString id;
676   mGeckoAccessible->DOMNodeID(id);
678   return nsCocoaUtils::ToNSString(id);
681 - (NSNumber*)moxRequired {
682   return @([self stateWithMask:states::REQUIRED] != 0);
685 - (NSNumber*)moxElementBusy {
686   return @([self stateWithMask:states::BUSY] != 0);
689 - (NSArray*)moxLinkedUIElements {
690   return [self getRelationsByType:RelationType::FLOWS_TO];
693 - (NSArray*)moxARIAControls {
694   return [self getRelationsByType:RelationType::CONTROLLER_FOR];
697 - (mozAccessible*)topWebArea {
698   Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
699   while (doc) {
700     if (doc->IsLocal()) {
701       DocAccessible* docAcc = doc->AsLocal()->AsDoc();
702       if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
703         return GetNativeFromGeckoAccessible(docAcc);
704       }
706       doc = docAcc->ParentDocument();
707     } else {
708       DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
709       if (docProxy->IsTopLevel()) {
710         return GetNativeFromGeckoAccessible(docProxy);
711       }
712       doc = docProxy->ParentDoc();
713     }
714   }
716   return nil;
719 - (void)handleRoleChanged:(mozilla::a11y::role)newRole {
720   mRole = newRole;
721   mARIARole = nullptr;
723   // For testing purposes
724   [self moxPostNotification:@"AXMozRoleChanged"];
727 - (id)moxEditableAncestor {
728   return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
729     return [moxAcc isKindOfClass:[mozTextAccessible class]];
730   }];
733 - (id)moxHighestEditableAncestor {
734   id highestAncestor = [self moxEditableAncestor];
735   while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
736     id ancestorParent = [highestAncestor moxUnignoredParent];
737     if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
738       break;
739     }
741     id higherAncestor = [ancestorParent moxEditableAncestor];
743     if (!higherAncestor) {
744       break;
745     }
747     highestAncestor = higherAncestor;
748   }
750   return highestAncestor;
753 - (id)moxFocusableAncestor {
754   // XXX: Checking focusable state up the chain can be expensive. For now,
755   // we can just return AXEditableAncestor since the main use case for this
756   // is rich text editing with links.
757   return [self moxEditableAncestor];
760 #ifndef RELEASE_OR_BETA
761 - (NSString*)moxMozDebugDescription {
762   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
764   if (!mGeckoAccessible) {
765     return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
766                                       NSStringFromClass([self class]), self];
767   }
769   NSMutableString* domInfo = [NSMutableString string];
770   if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
771     [domInfo appendFormat:@" %@", tagName];
772     NSString* domID = [self moxDOMIdentifier];
773     if ([domID length]) {
774       [domInfo appendFormat:@"#%@", domID];
775     }
776     if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
777       [domInfo
778           appendFormat:@".%@",
779                        [className stringByReplacingOccurrencesOfString:@" "
780                                                             withString:@"."]];
781     }
782   }
784   return [NSString stringWithFormat:@"<%@: %p %@%@>",
785                                     NSStringFromClass([self class]), self,
786                                     [self moxRole], domInfo];
788   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
790 #endif
792 - (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
793   // Create our search object and set it up with the searchPredicate
794   // params. The init function does additional parsing. We pass a
795   // reference to the web area to use as a start element if one is not
796   // specified.
797   MOXSearchInfo* search =
798       [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
799                                          andRoot:self] autorelease];
801   return [search performSearch];
804 - (NSNumber*)moxUIElementCountForSearchPredicate:
805     (NSDictionary*)searchPredicate {
806   return [NSNumber
807       numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
808                            count]];
811 - (void)moxSetFocused:(NSNumber*)focused {
812   MOZ_ASSERT(mGeckoAccessible);
814   if ([focused boolValue]) {
815     mGeckoAccessible->TakeFocus();
816   }
819 - (void)moxPerformScrollToVisible {
820   MOZ_ASSERT(mGeckoAccessible);
821   mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
824 - (void)moxPerformShowMenu {
825   MOZ_ASSERT(mGeckoAccessible);
827   // We don't need to convert this rect into mac coordinates because the
828   // mouse event synthesizer expects layout (gecko) coordinates.
829   LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds();
831   LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
832                                  ? mGeckoAccessible->AsLocal()->RootAccessible()
833                                  : mGeckoAccessible->AsRemote()
834                                        ->OuterDocOfRemoteBrowser()
835                                        ->RootAccessible();
836   id objOrView =
837       GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
839   LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
840       bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
841   nsIWidget* widget = [objOrView widget];
842   widget->SynthesizeNativeMouseEvent(
843       p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
844       nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
847 - (void)moxPerformPress {
848   MOZ_ASSERT(mGeckoAccessible);
850   mGeckoAccessible->DoAction(0);
853 #pragma mark -
855 - (BOOL)disableChild:(mozAccessible*)child {
856   return NO;
859 - (void)maybePostLiveRegionChanged {
860   id<MOXAccessible> liveRegion =
861       [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
862         return [moxAcc moxIsLiveRegion];
863       }];
865   if (liveRegion) {
866     [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
867   }
870 - (void)maybePostA11yUtilNotification {
871   MOZ_ASSERT(mGeckoAccessible);
872   // Sometimes we use a special live region to make announcements to the user.
873   // This region is a child of the root document, but doesn't contain any
874   // content. If we try to fire regular AXLiveRegion changed events through it,
875   // VoiceOver clips the notifications because it (rightfully) doesn't detect
876   // focus within the region. We get around this by firing an
877   // AXAnnouncementRequested notification here instead.
878   // Verify we're trying to send a notification for the a11yUtils alert (and not
879   // a random acc with the same ID) by checking:
880   //  - The gecko acc is local, our a11y-announcement lives in browser.xhtml
881   //  - The ID of the gecko acc is "a11y-announcement"
882   //  - The native acc is a direct descendent of the root
883   if (mGeckoAccessible->IsLocal() &&
884       [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
885       [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
886     // Our actual announcement should be stored as a child of the alert,
887     // so we verify a child exists, and then query that child below.
888     NSArray* children = [self moxChildren];
889     MOZ_ASSERT([children count] == 1 && children[0],
890                "A11yUtil event received, but no announcement found?");
892     mozAccessible* announcement = children[0];
893     NSString* key;
894     if ([announcement providesLabelNotTitle]) {
895       key = [announcement moxLabel];
896     } else {
897       key = [announcement moxTitle];
898     }
900     NSDictionary* info = @{
901       NSAccessibilityAnnouncementKey : key ? key : @(""),
902       NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
903     };
905     id window = [self moxWindow];
907     // This sends events via nsIObserverService to be consumed by our
908     // mochitests. Normally we'd fire these events through moxPostNotification
909     // which takes care of this, but because the window we fetch above isn't
910     // derrived from MOXAccessibleBase, we do this (and post the notification)
911     // manually.
912     xpcAccessibleMacEvent::FireEvent(
913         window, NSAccessibilityAnnouncementRequestedNotification, info);
914     NSAccessibilityPostNotificationWithUserInfo(
915         window, NSAccessibilityAnnouncementRequestedNotification, info);
916   }
919 - (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
920   NSMutableArray<mozAccessible*>* relations =
921       [[[NSMutableArray alloc] init] autorelease];
922   Relation rel = mGeckoAccessible->RelationByType(relationType);
923   while (Accessible* relAcc = rel.Next()) {
924     if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
925       [relations addObject:relNative];
926     }
927   }
929   return relations;
932 - (void)handleAccessibleTextChangeEvent:(NSString*)change
933                                inserted:(BOOL)isInserted
934                             inContainer:(Accessible*)container
935                                      at:(int32_t)start {
938 - (void)handleAccessibleEvent:(uint32_t)eventType {
939   switch (eventType) {
940     case nsIAccessibleEvent::EVENT_ALERT:
941       [self maybePostA11yUtilNotification];
942       break;
943     case nsIAccessibleEvent::EVENT_FOCUS:
944       [self moxPostNotification:
945                 NSAccessibilityFocusedUIElementChangedNotification];
946       break;
947     case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
948       [self moxPostNotification:@"AXMenuOpened"];
949       break;
950     case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
951       [self moxPostNotification:@"AXMenuClosed"];
952       break;
953     case nsIAccessibleEvent::EVENT_SELECTION:
954     case nsIAccessibleEvent::EVENT_SELECTION_ADD:
955     case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
956     case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
957       [self moxPostNotification:
958                 NSAccessibilitySelectedChildrenChangedNotification];
959       break;
960     case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
961       if (![self stateWithMask:states::SELECTABLE_TEXT]) {
962         break;
963       }
964       // We consider any caret move event to be a selected text change event.
965       // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
966       // reduntant.
967       MOXTextMarkerDelegate* delegate =
968           static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
969       NSMutableDictionary* userInfo =
970           [[[delegate selectionChangeInfo] mutableCopy] autorelease];
971       userInfo[@"AXTextChangeElement"] = self;
973       mozAccessible* webArea = [self topWebArea];
974       [webArea
975           moxPostNotification:NSAccessibilitySelectedTextChangedNotification
976                  withUserInfo:userInfo];
977       [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
978                    withUserInfo:userInfo];
979       break;
980     }
981     case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
982       mIsLiveRegion = true;
983       [self moxPostNotification:@"AXLiveRegionCreated"];
984       break;
985     case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
986       mIsLiveRegion = false;
987       break;
988     case nsIAccessibleEvent::EVENT_REORDER:
989       [self maybePostLiveRegionChanged];
990       break;
991     case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
992       if (![self providesLabelNotTitle]) {
993         [self moxPostNotification:NSAccessibilityTitleChangedNotification];
994       }
995       [self maybePostLiveRegionChanged];
996       break;
997     }
998   }
1001 - (void)expire {
1002   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
1004   mGeckoAccessible = nullptr;
1006   [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
1008   NS_OBJC_END_TRY_IGNORE_BLOCK;
1011 - (BOOL)isExpired {
1012   return !mGeckoAccessible;
1015 @end