2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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"
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"
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"
32 #include "nsCocoaUtils.h"
34 #include "nsObjCExceptions.h"
35 #include "nsWhitespaceTokenizer.h"
38 using namespace mozilla;
39 using namespace mozilla::a11y;
43 @interface mozAccessible ()
44 - (BOOL)providesLabelNotTitle;
46 - (void)maybePostLiveRegionChanged;
47 - (void)maybePostA11yUtilNotification;
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;
62 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
66 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
70 NS_OBJC_END_TRY_IGNORE_BLOCK;
73 #pragma mark - mozAccessible widget
75 - (BOOL)hasRepresentedView {
79 - (id)representedView {
89 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
90 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
91 if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
92 if (acc->VisibilityState() & states::INVISIBLE) {
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);
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"];
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;
152 if (selector == @selector(moxSetFocused:)) {
153 return [self stateWithMask:states::FOCUSABLE] == 0;
156 if (selector == @selector(moxARIALive) ||
157 selector == @selector(moxARIAAtomic) ||
158 selector == @selector(moxARIARelevant)) {
159 return ![self moxIsLiveRegion];
162 if (selector == @selector(moxExpanded)) {
163 return [self stateWithMask:states::EXPANDABLE] == 0;
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]) {
182 // return ourself if we can't get a native focused child.
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];
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);
213 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
214 return [nativeChild isAccessibilityElement]
216 : [nativeChild moxUnignoredParent];
219 // if we didn't find anything, return ourself or child view.
223 - (id<mozAccessible>)moxParent {
224 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
225 if ([self isExpired]) {
229 Accessible* parent = mGeckoAccessible->Parent();
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;
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());
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();
265 Accessible* child = mGeckoAccessible->ChildAt(childIdx);
266 mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
271 [children addObject:nativeChild];
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];
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: \
299 MOZ_ASSERT_UNREACHABLE("Unknown role.");
300 return NSAccessibilityUnknownRole;
306 - (nsStaticAtom*)ARIARole {
307 MOZ_ASSERT(mGeckoAccessible);
309 if (mGeckoAccessible->HasARIARole()) {
310 const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
311 return roleMap->roleAtom;
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.
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";
336 // None of the above, so assume DPub ARIA.
337 return @"AXLandmarkRegion";
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";
349 if (roleAtom == nsGkAtoms::dialog) {
350 return @"AXApplicationDialog";
354 if (mRole == roles::FORM) {
355 roleAtom = [self ARIARole];
357 if (roleAtom == nsGkAtoms::form) {
358 return @"AXLandmarkForm";
362 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
363 msaaRole, ia2Role, androidClass, nameRule) \
364 case roles::geckoRole: \
365 if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \
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";
383 if (roleAtom == nsGkAtoms::timer) {
384 return @"AXApplicationTimer";
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";
393 return NSAccessibilityUnknownSubrole;
398 struct RoleDescrMap {
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];
440 - (NSString*)moxRoleDescription {
441 if (NSString* ariaRoleDescription =
442 utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
443 if ([ariaRoleDescription length]) {
444 return ariaRoleDescription;
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);
456 NSString* subrole = [self moxSubrole];
460 if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
461 RoleDescrComparator(subrole), &idx)) {
462 return utils::LocalizedString(sRoleDescrMap[idx].description);
466 return NSAccessibilityRoleDescription([self moxRole], subrole);
469 - (NSString*)moxLabel {
470 if ([self isExpired]) {
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).
482 ENameValueFlag flag = mGeckoAccessible->Name(name);
483 if (flag == eNameFromSubtree) {
487 if (![self providesLabelNotTitle]) {
488 NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
489 if ([relations count] == 1) {
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]) {
506 mGeckoAccessible->Name(title);
507 if (nsCoreUtils::IsWhitespaceString(title)) {
511 return nsCocoaUtils::ToNSString(title);
513 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
517 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
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();
549 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
550 LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
551 if (outerDoc) docAcc = outerDoc->Document();
554 if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
556 MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
557 "Couldn't get native window");
560 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
563 - (NSNumber*)moxEnabled {
564 if ([self stateWithMask:states::UNAVAILABLE]) {
568 if (![self isRoot]) {
569 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
570 if (![parent isRoot]) {
571 return @(![parent disableChild:self]);
578 - (NSNumber*)moxFocused {
579 return @([self stateWithMask:states::FOCUSED] != 0);
582 - (NSNumber*)moxSelected {
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);
598 valueWithRect:NSMakeRect(
599 static_cast<CGFloat>(rect.x) / scaleFactor,
600 [mainView frame].size.height -
601 static_cast<CGFloat>(rect.y + rect.height) /
603 static_cast<CGFloat>(rect.width) / scaleFactor,
604 static_cast<CGFloat>(rect.height) / scaleFactor)];
607 - (NSString*)moxARIACurrent {
608 if (![self stateWithMask:states::CURRENT]) {
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)) {
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];
644 - (NSString*)moxDOMIdentifier {
645 MOZ_ASSERT(mGeckoAccessible);
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);
672 if (doc->IsLocal()) {
673 DocAccessible* docAcc = doc->AsLocal()->AsDoc();
674 if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
675 return GetNativeFromGeckoAccessible(docAcc);
678 doc = docAcc->ParentDocument();
680 DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
681 if (docProxy->IsTopLevel()) {
682 return GetNativeFromGeckoAccessible(docProxy);
684 doc = docProxy->ParentDoc();
691 - (void)handleRoleChanged:(mozilla::a11y::role)newRole {
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]];
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)]) {
713 id higherAncestor = [ancestorParent moxEditableAncestor];
715 if (!higherAncestor) {
719 highestAncestor = higherAncestor;
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];
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];
748 if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
751 [className stringByReplacingOccurrencesOfString:@" "
756 return [NSString stringWithFormat:@"<%@: %p %@%@>",
757 NSStringFromClass([self class]), self,
758 [self moxRole], domInfo];
760 NS_OBJC_END_TRY_BLOCK_RETURN(nil);
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
769 MOXSearchInfo* search =
770 [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
771 andRoot:self] autorelease];
773 return [search performSearch];
776 - (NSNumber*)moxUIElementCountForSearchPredicate:
777 (NSDictionary*)searchPredicate {
779 numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
783 - (void)moxSetFocused:(NSNumber*)focused {
784 MOZ_ASSERT(mGeckoAccessible);
786 if ([focused boolValue]) {
787 mGeckoAccessible->TakeFocus();
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()
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);
827 - (BOOL)disableChild:(mozAccessible*)child {
831 - (void)maybePostLiveRegionChanged {
832 id<MOXAccessible> liveRegion =
833 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
834 return [moxAcc moxIsLiveRegion];
838 [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
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];
866 if ([announcement providesLabelNotTitle]) {
867 key = [announcement moxLabel];
869 key = [announcement moxTitle];
872 NSDictionary* info = @{
873 NSAccessibilityAnnouncementKey : key ? key : @(""),
874 NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
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)
884 xpcAccessibleMacEvent::FireEvent(
885 window, NSAccessibilityAnnouncementRequestedNotification, info);
886 NSAccessibilityPostNotificationWithUserInfo(
887 window, NSAccessibilityAnnouncementRequestedNotification, info);
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];
904 - (void)handleAccessibleTextChangeEvent:(NSString*)change
905 inserted:(BOOL)isInserted
906 inContainer:(Accessible*)container
910 - (void)handleAccessibleEvent:(uint32_t)eventType {
912 case nsIAccessibleEvent::EVENT_ALERT:
913 [self maybePostA11yUtilNotification];
915 case nsIAccessibleEvent::EVENT_FOCUS:
916 [self moxPostNotification:
917 NSAccessibilityFocusedUIElementChangedNotification];
919 case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
920 [self moxPostNotification:@"AXMenuOpened"];
922 case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
923 [self moxPostNotification:@"AXMenuClosed"];
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];
932 case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
933 if (![self stateWithMask:states::SELECTABLE_TEXT]) {
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
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];
947 moxPostNotification:NSAccessibilitySelectedTextChangedNotification
948 withUserInfo:userInfo];
949 [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
950 withUserInfo:userInfo];
953 case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
954 mIsLiveRegion = true;
955 [self moxPostNotification:@"AXLiveRegionCreated"];
957 case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
958 mIsLiveRegion = false;
960 case nsIAccessibleEvent::EVENT_REORDER:
961 [self maybePostLiveRegionChanged];
963 case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
964 if (![self providesLabelNotTitle]) {
965 [self moxPostNotification:NSAccessibilityTitleChangedNotification];
967 [self maybePostLiveRegionChanged];
974 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
976 mGeckoAccessible = nullptr;
978 [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
980 NS_OBJC_END_TRY_IGNORE_BLOCK;
984 return !mGeckoAccessible;