Bug 1850635 [wpt PR 41704] - Implement parseHTMLUnsafe and setHTMLUnsafe, a=testonly
[gecko.git] / accessible / mac / mozAccessible.mm
blob2e1ad991decdcbeee727a2d39363b6c8086577c7
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(moxExpanded)) {
163     return [self stateWithMask:states::EXPANDABLE] == 0;
164   }
166   return [super moxBlockSelector:selector];
169 - (id)moxFocusedUIElement {
170   MOZ_ASSERT(mGeckoAccessible);
171   // This only gets queried on the web area or the root group
172   // so just use the doc's focused child instead of trying to get
173   // the focused child of mGeckoAccessible.
174   Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
175   mozAccessible* focusedChild =
176       GetNativeFromGeckoAccessible(doc->FocusedChild());
178   if ([focusedChild isAccessibilityElement]) {
179     return focusedChild;
180   }
182   // return ourself if we can't get a native focused child.
183   return self;
186 - (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
187   MOZ_ASSERT(mGeckoAccessible);
189   return [MOXTextMarkerDelegate
190       getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)];
193 - (BOOL)moxIsLiveRegion {
194   return mIsLiveRegion;
197 - (id)moxHitTest:(NSPoint)point {
198   MOZ_ASSERT(mGeckoAccessible);
200   // Convert the given screen-global point in the cocoa coordinate system (with
201   // origin in the bottom-left corner of the screen) into point in the Gecko
202   // coordinate system (with origin in a top-left screen point).
203   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
204   NSPoint tmpPoint =
205       NSMakePoint(point.x, [mainView frame].size.height - point.y);
206   LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
207       tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
209   Accessible* child = mGeckoAccessible->ChildAtPoint(
210       geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
212   if (child) {
213     mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
214     return [nativeChild isAccessibilityElement]
215                ? nativeChild
216                : [nativeChild moxUnignoredParent];
217   }
219   // if we didn't find anything, return ourself or child view.
220   return self;
223 - (id<mozAccessible>)moxParent {
224   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
225   if ([self isExpired]) {
226     return nil;
227   }
229   Accessible* parent = mGeckoAccessible->Parent();
231   if (!parent) {
232     return nil;
233   }
235   id nativeParent = GetNativeFromGeckoAccessible(parent);
236   if ([nativeParent isKindOfClass:[MOXWebAreaAccessible class]]) {
237     // Before returning a WebArea as parent, check to see if
238     // there is a generated root group that is an intermediate container.
239     if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
240       nativeParent = rootGroup;
241     }
242   }
244   if (!nativeParent && mGeckoAccessible->IsLocal()) {
245     // Return native of root accessible if we have no direct parent.
246     // XXX: need to return a sensible fallback in proxy case as well
247     nativeParent = GetNativeFromGeckoAccessible(
248         mGeckoAccessible->AsLocal()->RootAccessible());
249   }
251   return GetObjectOrRepresentedView(nativeParent);
253   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
256 // gets all our native children lazily, including those that are ignored.
257 - (NSArray*)moxChildren {
258   MOZ_ASSERT(mGeckoAccessible);
260   NSMutableArray* children = [[[NSMutableArray alloc]
261       initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
263   for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
264        childIdx++) {
265     Accessible* child = mGeckoAccessible->ChildAt(childIdx);
266     mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
267     if (!nativeChild) {
268       continue;
269     }
271     [children addObject:nativeChild];
272   }
274   return children;
277 - (NSValue*)moxPosition {
278   CGRect frame = [[self moxFrame] rectValue];
280   return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
283 - (NSValue*)moxSize {
284   CGRect frame = [[self moxFrame] rectValue];
286   return
287       [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
290 - (NSString*)moxRole {
291 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
292              msaaRole, ia2Role, androidClass, nameRule)                     \
293   case roles::geckoRole:                                                    \
294     return macRole;
296   switch (mRole) {
297 #include "RoleMap.h"
298     default:
299       MOZ_ASSERT_UNREACHABLE("Unknown role.");
300       return NSAccessibilityUnknownRole;
301   }
303 #undef ROLE
306 - (nsStaticAtom*)ARIARole {
307   MOZ_ASSERT(mGeckoAccessible);
309   if (mGeckoAccessible->HasARIARole()) {
310     const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
311     return roleMap->roleAtom;
312   }
314   return nsGkAtoms::_empty;
317 - (NSString*)moxSubrole {
318   MOZ_ASSERT(mGeckoAccessible);
320   // Deal with landmarks first
321   // macOS groups the specific landmark types of DPub ARIA into two broad
322   // categories with corresponding subroles: Navigation and region/container.
323   if (mRole == roles::LANDMARK) {
324     nsAtom* landmark = mGeckoAccessible->LandmarkRole();
325     // HTML Elements treated as landmarks, and ARIA landmarks.
326     if (landmark) {
327       if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
328       if (landmark == nsGkAtoms::complementary)
329         return @"AXLandmarkComplementary";
330       if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
331       if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
332       if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
333       if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
334     }
336     // None of the above, so assume DPub ARIA.
337     return @"AXLandmarkRegion";
338   }
340   // Now, deal with widget roles
341   nsStaticAtom* roleAtom = nullptr;
343   if (mRole == roles::DIALOG) {
344     roleAtom = [self ARIARole];
346     if (roleAtom == nsGkAtoms::alertdialog) {
347       return @"AXApplicationAlertDialog";
348     }
349     if (roleAtom == nsGkAtoms::dialog) {
350       return @"AXApplicationDialog";
351     }
352   }
354   if (mRole == roles::FORM) {
355     roleAtom = [self ARIARole];
357     if (roleAtom == nsGkAtoms::form) {
358       return @"AXLandmarkForm";
359     }
360   }
362 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
363              msaaRole, ia2Role, androidClass, nameRule)                     \
364   case roles::geckoRole:                                                    \
365     if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) {      \
366       return macSubrole;                                                    \
367     } else {                                                                \
368       break;                                                                \
369     }
371   switch (mRole) {
372 #include "RoleMap.h"
373   }
375   // These are special. They map to roles::NOTHING
376   // and are instructed by the ARIA map to use the native host role.
377   roleAtom = [self ARIARole];
379   if (roleAtom == nsGkAtoms::log_) {
380     return @"AXApplicationLog";
381   }
383   if (roleAtom == nsGkAtoms::timer) {
384     return @"AXApplicationTimer";
385   }
386   // macOS added an AXSubrole value to distinguish generic AXGroup objects
387   // from those which are AXGroups as a result of an explicit ARIA role,
388   // such as the non-landmark, non-listitem text containers in DPub ARIA.
389   if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
390     return @"AXApplicationGroup";
391   }
393   return NSAccessibilityUnknownSubrole;
395 #undef ROLE
398 struct RoleDescrMap {
399   NSString* role;
400   const nsString description;
403 static const RoleDescrMap sRoleDescrMap[] = {
404     {@"AXApplicationAlert", u"alert"_ns},
405     {@"AXApplicationAlertDialog", u"alertDialog"_ns},
406     {@"AXApplicationDialog", u"dialog"_ns},
407     {@"AXApplicationLog", u"log"_ns},
408     {@"AXApplicationMarquee", u"marquee"_ns},
409     {@"AXApplicationStatus", u"status"_ns},
410     {@"AXApplicationTimer", u"timer"_ns},
411     {@"AXContentSeparator", u"separator"_ns},
412     {@"AXDefinition", u"definition"_ns},
413     {@"AXDetails", u"details"_ns},
414     {@"AXDocument", u"document"_ns},
415     {@"AXDocumentArticle", u"article"_ns},
416     {@"AXDocumentMath", u"math"_ns},
417     {@"AXDocumentNote", u"note"_ns},
418     {@"AXLandmarkApplication", u"application"_ns},
419     {@"AXLandmarkBanner", u"banner"_ns},
420     {@"AXLandmarkComplementary", u"complementary"_ns},
421     {@"AXLandmarkContentInfo", u"content"_ns},
422     {@"AXLandmarkMain", u"main"_ns},
423     {@"AXLandmarkNavigation", u"navigation"_ns},
424     {@"AXLandmarkRegion", u"region"_ns},
425     {@"AXLandmarkSearch", u"search"_ns},
426     {@"AXSearchField", u"searchTextField"_ns},
427     {@"AXSummary", u"summary"_ns},
428     {@"AXTabPanel", u"tabPanel"_ns},
429     {@"AXTerm", u"term"_ns},
430     {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
432 struct RoleDescrComparator {
433   const NSString* mRole;
434   explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
435   int operator()(const RoleDescrMap& aEntry) const {
436     return [mRole compare:aEntry.role];
437   }
440 - (NSString*)moxRoleDescription {
441   if (NSString* ariaRoleDescription =
442           utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
443     if ([ariaRoleDescription length]) {
444       return ariaRoleDescription;
445     }
446   }
448   if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
450   if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
452   if (mRole == roles::MARK) {
453     return utils::LocalizedString(u"highlight"_ns);
454   }
456   NSString* subrole = [self moxSubrole];
458   if (subrole) {
459     size_t idx = 0;
460     if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
461                        RoleDescrComparator(subrole), &idx)) {
462       return utils::LocalizedString(sRoleDescrMap[idx].description);
463     }
464   }
466   return NSAccessibilityRoleDescription([self moxRole], subrole);
469 - (NSString*)moxLabel {
470   if ([self isExpired]) {
471     return nil;
472   }
474   nsAutoString name;
476   /* If our accessible is:
477    * 1. Named by invisible text, or
478    * 2. Has more than one labeling relation, or
479    * 3. Is a special role defined in providesLabelNotTitle
480    *   ... return its name as a label (AXDescription).
481    */
482   ENameValueFlag flag = mGeckoAccessible->Name(name);
483   if (flag == eNameFromSubtree) {
484     return nil;
485   }
487   if (![self providesLabelNotTitle]) {
488     NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
489     if ([relations count] == 1) {
490       return nil;
491     }
492   }
494   return nsCocoaUtils::ToNSString(name);
497 - (NSString*)moxTitle {
498   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
500   // In some special cases we provide the name in the label (AXDescription).
501   if ([self providesLabelNotTitle]) {
502     return nil;
503   }
505   nsAutoString title;
506   mGeckoAccessible->Name(title);
507   if (nsCoreUtils::IsWhitespaceString(title)) {
508     return @"";
509   }
511   return nsCocoaUtils::ToNSString(title);
513   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
516 - (id)moxValue {
517   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
519   nsAutoString value;
520   mGeckoAccessible->Value(value);
522   return nsCocoaUtils::ToNSString(value);
524   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
527 - (NSString*)moxHelp {
528   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
530   // What needs to go here is actually the accDescription of an item.
531   // The MSAA acc_help method has nothing to do with this one.
532   nsAutoString helpText;
533   mGeckoAccessible->Description(helpText);
535   return nsCocoaUtils::ToNSString(helpText);
537   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
540 - (NSWindow*)moxWindow {
541   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
543   // Get a pointer to the native window (NSWindow) we reside in.
544   NSWindow* nativeWindow = nil;
545   DocAccessible* docAcc = nullptr;
546   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
547     docAcc = acc->Document();
548   } else {
549     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
550     LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
551     if (outerDoc) docAcc = outerDoc->Document();
552   }
554   if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
556   MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
557              "Couldn't get native window");
558   return nativeWindow;
560   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
563 - (NSNumber*)moxEnabled {
564   if ([self stateWithMask:states::UNAVAILABLE]) {
565     return @NO;
566   }
568   if (![self isRoot]) {
569     mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
570     if (![parent isRoot]) {
571       return @(![parent disableChild:self]);
572     }
573   }
575   return @YES;
578 - (NSNumber*)moxFocused {
579   return @([self stateWithMask:states::FOCUSED] != 0);
582 - (NSNumber*)moxSelected {
583   return @NO;
586 - (NSNumber*)moxExpanded {
587   return @([self stateWithMask:states::EXPANDED] != 0);
590 - (NSValue*)moxFrame {
591   MOZ_ASSERT(mGeckoAccessible);
593   LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
594   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
595   CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
597   return [NSValue
598       valueWithRect:NSMakeRect(
599                         static_cast<CGFloat>(rect.x) / scaleFactor,
600                         [mainView frame].size.height -
601                             static_cast<CGFloat>(rect.y + rect.height) /
602                                 scaleFactor,
603                         static_cast<CGFloat>(rect.width) / scaleFactor,
604                         static_cast<CGFloat>(rect.height) / scaleFactor)];
607 - (NSString*)moxARIACurrent {
608   if (![self stateWithMask:states::CURRENT]) {
609     return nil;
610   }
612   return utils::GetAccAttr(self, nsGkAtoms::aria_current);
615 - (NSNumber*)moxARIAAtomic {
616   return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
619 - (NSString*)moxARIALive {
620   return utils::GetAccAttr(self, nsGkAtoms::aria_live);
623 - (NSString*)moxARIARelevant {
624   if (NSString* relevant =
625           utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
626     return relevant;
627   }
629   // Default aria-relevant value
630   return @"additions text";
633 - (id)moxTitleUIElement {
634   MOZ_ASSERT(mGeckoAccessible);
636   NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
637   if ([relations count] == 1) {
638     return [relations firstObject];
639   }
641   return nil;
644 - (NSString*)moxDOMIdentifier {
645   MOZ_ASSERT(mGeckoAccessible);
647   nsAutoString id;
648   mGeckoAccessible->DOMNodeID(id);
650   return nsCocoaUtils::ToNSString(id);
653 - (NSNumber*)moxRequired {
654   return @([self stateWithMask:states::REQUIRED] != 0);
657 - (NSNumber*)moxElementBusy {
658   return @([self stateWithMask:states::BUSY] != 0);
661 - (NSArray*)moxLinkedUIElements {
662   return [self getRelationsByType:RelationType::FLOWS_TO];
665 - (NSArray*)moxARIAControls {
666   return [self getRelationsByType:RelationType::CONTROLLER_FOR];
669 - (mozAccessible*)topWebArea {
670   Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
671   while (doc) {
672     if (doc->IsLocal()) {
673       DocAccessible* docAcc = doc->AsLocal()->AsDoc();
674       if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
675         return GetNativeFromGeckoAccessible(docAcc);
676       }
678       doc = docAcc->ParentDocument();
679     } else {
680       DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
681       if (docProxy->IsTopLevel()) {
682         return GetNativeFromGeckoAccessible(docProxy);
683       }
684       doc = docProxy->ParentDoc();
685     }
686   }
688   return nil;
691 - (void)handleRoleChanged:(mozilla::a11y::role)newRole {
692   mRole = newRole;
693   mARIARole = nullptr;
695   // For testing purposes
696   [self moxPostNotification:@"AXMozRoleChanged"];
699 - (id)moxEditableAncestor {
700   return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
701     return [moxAcc isKindOfClass:[mozTextAccessible class]];
702   }];
705 - (id)moxHighestEditableAncestor {
706   id highestAncestor = [self moxEditableAncestor];
707   while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
708     id ancestorParent = [highestAncestor moxUnignoredParent];
709     if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
710       break;
711     }
713     id higherAncestor = [ancestorParent moxEditableAncestor];
715     if (!higherAncestor) {
716       break;
717     }
719     highestAncestor = higherAncestor;
720   }
722   return highestAncestor;
725 - (id)moxFocusableAncestor {
726   // XXX: Checking focusable state up the chain can be expensive. For now,
727   // we can just return AXEditableAncestor since the main use case for this
728   // is rich text editing with links.
729   return [self moxEditableAncestor];
732 #ifndef RELEASE_OR_BETA
733 - (NSString*)moxMozDebugDescription {
734   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
736   if (!mGeckoAccessible) {
737     return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
738                                       NSStringFromClass([self class]), self];
739   }
741   NSMutableString* domInfo = [NSMutableString string];
742   if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
743     [domInfo appendFormat:@" %@", tagName];
744     NSString* domID = [self moxDOMIdentifier];
745     if ([domID length]) {
746       [domInfo appendFormat:@"#%@", domID];
747     }
748     if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
749       [domInfo
750           appendFormat:@".%@",
751                        [className stringByReplacingOccurrencesOfString:@" "
752                                                             withString:@"."]];
753     }
754   }
756   return [NSString stringWithFormat:@"<%@: %p %@%@>",
757                                     NSStringFromClass([self class]), self,
758                                     [self moxRole], domInfo];
760   NS_OBJC_END_TRY_BLOCK_RETURN(nil);
762 #endif
764 - (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
765   // Create our search object and set it up with the searchPredicate
766   // params. The init function does additional parsing. We pass a
767   // reference to the web area to use as a start element if one is not
768   // specified.
769   MOXSearchInfo* search =
770       [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
771                                          andRoot:self] autorelease];
773   return [search performSearch];
776 - (NSNumber*)moxUIElementCountForSearchPredicate:
777     (NSDictionary*)searchPredicate {
778   return [NSNumber
779       numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
780                            count]];
783 - (void)moxSetFocused:(NSNumber*)focused {
784   MOZ_ASSERT(mGeckoAccessible);
786   if ([focused boolValue]) {
787     mGeckoAccessible->TakeFocus();
788   }
791 - (void)moxPerformScrollToVisible {
792   MOZ_ASSERT(mGeckoAccessible);
793   mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
796 - (void)moxPerformShowMenu {
797   MOZ_ASSERT(mGeckoAccessible);
799   // We don't need to convert this rect into mac coordinates because the
800   // mouse event synthesizer expects layout (gecko) coordinates.
801   LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds();
803   LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
804                                  ? mGeckoAccessible->AsLocal()->RootAccessible()
805                                  : mGeckoAccessible->AsRemote()
806                                        ->OuterDocOfRemoteBrowser()
807                                        ->RootAccessible();
808   id objOrView =
809       GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
811   LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
812       bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
813   nsIWidget* widget = [objOrView widget];
814   widget->SynthesizeNativeMouseEvent(
815       p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
816       nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
819 - (void)moxPerformPress {
820   MOZ_ASSERT(mGeckoAccessible);
822   mGeckoAccessible->DoAction(0);
825 #pragma mark -
827 - (BOOL)disableChild:(mozAccessible*)child {
828   return NO;
831 - (void)maybePostLiveRegionChanged {
832   id<MOXAccessible> liveRegion =
833       [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
834         return [moxAcc moxIsLiveRegion];
835       }];
837   if (liveRegion) {
838     [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
839   }
842 - (void)maybePostA11yUtilNotification {
843   MOZ_ASSERT(mGeckoAccessible);
844   // Sometimes we use a special live region to make announcements to the user.
845   // This region is a child of the root document, but doesn't contain any
846   // content. If we try to fire regular AXLiveRegion changed events through it,
847   // VoiceOver clips the notifications because it (rightfully) doesn't detect
848   // focus within the region. We get around this by firing an
849   // AXAnnouncementRequested notification here instead.
850   // Verify we're trying to send a notification for the a11yUtils alert (and not
851   // a random acc with the same ID) by checking:
852   //  - The gecko acc is local, our a11y-announcement lives in browser.xhtml
853   //  - The ID of the gecko acc is "a11y-announcement"
854   //  - The native acc is a direct descendent of the root
855   if (mGeckoAccessible->IsLocal() &&
856       [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
857       [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
858     // Our actual announcement should be stored as a child of the alert,
859     // so we verify a child exists, and then query that child below.
860     NSArray* children = [self moxChildren];
861     MOZ_ASSERT([children count] == 1 && children[0],
862                "A11yUtil event recieved, but no announcement found?");
864     mozAccessible* announcement = children[0];
865     NSString* key;
866     if ([announcement providesLabelNotTitle]) {
867       key = [announcement moxLabel];
868     } else {
869       key = [announcement moxTitle];
870     }
872     NSDictionary* info = @{
873       NSAccessibilityAnnouncementKey : key ? key : @(""),
874       NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
875     };
877     id window = [self moxWindow];
879     // This sends events via nsIObserverService to be consumed by our
880     // mochitests. Normally we'd fire these events through moxPostNotification
881     // which takes care of this, but because the window we fetch above isn't
882     // derrived from MOXAccessibleBase, we do this (and post the notification)
883     // manually.
884     xpcAccessibleMacEvent::FireEvent(
885         window, NSAccessibilityAnnouncementRequestedNotification, info);
886     NSAccessibilityPostNotificationWithUserInfo(
887         window, NSAccessibilityAnnouncementRequestedNotification, info);
888   }
891 - (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
892   NSMutableArray<mozAccessible*>* relations =
893       [[[NSMutableArray alloc] init] autorelease];
894   Relation rel = mGeckoAccessible->RelationByType(relationType);
895   while (Accessible* relAcc = rel.Next()) {
896     if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
897       [relations addObject:relNative];
898     }
899   }
901   return relations;
904 - (void)handleAccessibleTextChangeEvent:(NSString*)change
905                                inserted:(BOOL)isInserted
906                             inContainer:(Accessible*)container
907                                      at:(int32_t)start {
910 - (void)handleAccessibleEvent:(uint32_t)eventType {
911   switch (eventType) {
912     case nsIAccessibleEvent::EVENT_ALERT:
913       [self maybePostA11yUtilNotification];
914       break;
915     case nsIAccessibleEvent::EVENT_FOCUS:
916       [self moxPostNotification:
917                 NSAccessibilityFocusedUIElementChangedNotification];
918       break;
919     case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
920       [self moxPostNotification:@"AXMenuOpened"];
921       break;
922     case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
923       [self moxPostNotification:@"AXMenuClosed"];
924       break;
925     case nsIAccessibleEvent::EVENT_SELECTION:
926     case nsIAccessibleEvent::EVENT_SELECTION_ADD:
927     case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
928     case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
929       [self moxPostNotification:
930                 NSAccessibilitySelectedChildrenChangedNotification];
931       break;
932     case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
933       if (![self stateWithMask:states::SELECTABLE_TEXT]) {
934         break;
935       }
936       // We consider any caret move event to be a selected text change event.
937       // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
938       // reduntant.
939       MOXTextMarkerDelegate* delegate =
940           static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
941       NSMutableDictionary* userInfo =
942           [[[delegate selectionChangeInfo] mutableCopy] autorelease];
943       userInfo[@"AXTextChangeElement"] = self;
945       mozAccessible* webArea = [self topWebArea];
946       [webArea
947           moxPostNotification:NSAccessibilitySelectedTextChangedNotification
948                  withUserInfo:userInfo];
949       [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
950                    withUserInfo:userInfo];
951       break;
952     }
953     case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
954       mIsLiveRegion = true;
955       [self moxPostNotification:@"AXLiveRegionCreated"];
956       break;
957     case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
958       mIsLiveRegion = false;
959       break;
960     case nsIAccessibleEvent::EVENT_REORDER:
961       [self maybePostLiveRegionChanged];
962       break;
963     case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
964       if (![self providesLabelNotTitle]) {
965         [self moxPostNotification:NSAccessibilityTitleChangedNotification];
966       }
967       [self maybePostLiveRegionChanged];
968       break;
969     }
970   }
973 - (void)expire {
974   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
976   mGeckoAccessible = nullptr;
978   [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
980   NS_OBJC_END_TRY_IGNORE_BLOCK;
983 - (BOOL)isExpired {
984   return !mGeckoAccessible;
987 @end