summaryrefslogtreecommitdiffstats
path: root/accessible/mac/mozAccessible.mm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/mac/mozAccessible.mm')
-rw-r--r--accessible/mac/mozAccessible.mm1062
1 files changed, 1062 insertions, 0 deletions
diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm
new file mode 100644
index 0000000000..f8def7a384
--- /dev/null
+++ b/accessible/mac/mozAccessible.mm
@@ -0,0 +1,1062 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "MOXAccessibleBase.h"
+
+#import "MacUtils.h"
+#import "mozView.h"
+#import "MOXSearchInfo.h"
+#import "mozTextAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "DocAccessibleParent.h"
+#include "Relation.h"
+#include "Role.h"
+#include "RootAccessible.h"
+#include "TableAccessible.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "OuterDocAccessible.h"
+#include "nsChildView.h"
+
+#include "nsRect.h"
+#include "nsCocoaUtils.h"
+#include "nsCoord.h"
+#include "nsObjCExceptions.h"
+#include "nsWhitespaceTokenizer.h"
+#include <prdtoa.h>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#pragma mark -
+
+@interface mozAccessible ()
+- (BOOL)providesLabelNotTitle;
+
+- (void)maybePostLiveRegionChanged;
+- (void)maybePostA11yUtilNotification;
+@end
+
+@implementation mozAccessible
+
+- (id)initWithAccessible:(Accessible*)aAcc {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null");
+ if ((self = [super init])) {
+ mGeckoAccessible = aAcc;
+ mRole = aAcc->Role();
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark - mozAccessible widget
+
+- (BOOL)hasRepresentedView {
+ return NO;
+}
+
+- (id)representedView {
+ return nil;
+}
+
+- (BOOL)isRoot {
+ return NO;
+}
+
+#pragma mark -
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ if (acc->VisibilityState() & states::INVISIBLE) {
+ return YES;
+ }
+ }
+ }
+
+ return [parent moxIgnoreChild:self];
+}
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ return nsAccUtils::MustPrune(mGeckoAccessible);
+}
+
+- (id)childAt:(uint32_t)i {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Accessible* child = mGeckoAccessible->ChildAt(i);
+ return child ? GetNativeFromGeckoAccessible(child) : nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+static const uint64_t kCachedStates =
+ states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED |
+ states::EXPANDABLE | states::CURRENT | states::SELECTED |
+ states::TRAVERSED | states::LINKED | states::HASPOPUP | states::BUSY |
+ states::MULTI_LINE | states::CHECKABLE;
+static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63;
+
+- (uint64_t)state {
+ uint64_t state = 0;
+
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ state = acc->State();
+ }
+
+ if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
+ state = proxy->State();
+ }
+
+ if (!(mCachedState & kCacheInitialized)) {
+ mCachedState = state & kCachedStates;
+ mCachedState |= kCacheInitialized;
+ }
+
+ return state;
+}
+
+- (uint64_t)stateWithMask:(uint64_t)mask {
+ if ((mask & kCachedStates) == mask &&
+ (mCachedState & kCacheInitialized) != 0) {
+ return mCachedState & mask;
+ }
+
+ return [self state] & mask;
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ if ((state & kCachedStates) != 0) {
+ if (!(mCachedState & kCacheInitialized)) {
+ [self state];
+ } else {
+ if (enabled) {
+ mCachedState |= state;
+ } else {
+ mCachedState &= ~state;
+ }
+ }
+ }
+
+ if (state == states::BUSY) {
+ [self moxPostNotification:@"AXElementBusyChanged"];
+ }
+}
+
+- (void)invalidateState {
+ mCachedState = 0;
+}
+
+- (BOOL)providesLabelNotTitle {
+ // These accessible types are the exception to the rule of label vs. title:
+ // They may be named explicitly, but they still provide a label not a title.
+ return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
+ mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
+ mRole == roles::DOCUMENT || mRole == roles::OUTLINE ||
+ mRole == roles::ARTICLE || mRole == roles::ENTRY ||
+ mRole == roles::SPINBUTTON;
+}
+
+- (mozilla::a11y::Accessible*)geckoAccessible {
+ return mGeckoAccessible;
+}
+
+- (mozilla::a11y::Accessible*)geckoDocument {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ if (mGeckoAccessible->AsLocal()->IsDoc()) {
+ return mGeckoAccessible;
+ }
+ return mGeckoAccessible->AsLocal()->Document();
+ }
+
+ if (mGeckoAccessible->AsRemote()->IsDoc()) {
+ return mGeckoAccessible;
+ }
+
+ return mGeckoAccessible->AsRemote()->Document();
+}
+
+#pragma mark - MOXAccessible protocol
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxPerformPress)) {
+ uint8_t actionCount = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->ActionCount()
+ : mGeckoAccessible->AsRemote()->ActionCount();
+
+ // If we have no action, we don't support press, so return YES.
+ return actionCount == 0;
+ }
+
+ if (selector == @selector(moxSetFocused:)) {
+ return [self stateWithMask:states::FOCUSABLE] == 0;
+ }
+
+ if (selector == @selector(moxARIALive) ||
+ selector == @selector(moxARIAAtomic) ||
+ selector == @selector(moxARIARelevant)) {
+ return ![self moxIsLiveRegion];
+ }
+
+ if (selector == @selector(moxExpanded)) {
+ return [self stateWithMask:states::EXPANDABLE] == 0;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (id)moxFocusedUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+ // This only gets queried on the web area or the root group
+ // so just use the doc's focused child instead of trying to get
+ // the focused child of mGeckoAccessible.
+ Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
+ mozAccessible* focusedChild =
+ GetNativeFromGeckoAccessible(doc->FocusedChild());
+
+ if ([focusedChild isAccessibilityElement]) {
+ return focusedChild;
+ }
+
+ // return ourself if we can't get a native focused child.
+ return self;
+}
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ return [MOXTextMarkerDelegate
+ getOrCreateForDoc:mGeckoAccessible->AsLocal()->Document()];
+ }
+
+ return [MOXTextMarkerDelegate
+ getOrCreateForDoc:mGeckoAccessible->AsRemote()->Document()];
+}
+
+- (BOOL)moxIsLiveRegion {
+ return mIsLiveRegion;
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // Convert the given screen-global point in the cocoa coordinate system (with
+ // origin in the bottom-left corner of the screen) into point in the Gecko
+ // coordinate system (with origin in a top-left screen point).
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ NSPoint tmpPoint =
+ NSMakePoint(point.x, [mainView frame].size.height - point.y);
+ LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
+ tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
+
+ Accessible* child = mGeckoAccessible->ChildAtPoint(
+ geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
+
+ if (child) {
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ return [nativeChild isAccessibilityElement]
+ ? nativeChild
+ : [nativeChild moxUnignoredParent];
+ }
+
+ // if we didn't find anything, return ourself or child view.
+ return self;
+}
+
+- (id<mozAccessible>)moxParent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ Accessible* parent = mGeckoAccessible->Parent();
+
+ if (!parent) {
+ return nil;
+ }
+
+ id nativeParent = GetNativeFromGeckoAccessible(parent);
+ if ([nativeParent respondsToSelector:@selector(rootGroup)]) {
+ // Before returning a WebArea as parent, check to see if
+ // there is a generated root group that is an intermediate container.
+ if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
+ nativeParent = rootGroup;
+ }
+ }
+
+ if (!nativeParent && mGeckoAccessible->IsLocal()) {
+ // Return native of root accessible if we have no direct parent.
+ // XXX: need to return a sensible fallback in proxy case as well
+ nativeParent = GetNativeFromGeckoAccessible(
+ mGeckoAccessible->AsLocal()->RootAccessible());
+ }
+
+ return GetObjectOrRepresentedView(nativeParent);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// gets all our native children lazily, including those that are ignored.
+- (NSArray*)moxChildren {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSMutableArray* children = [[[NSMutableArray alloc]
+ initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
+
+ for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
+ childIdx++) {
+ Accessible* child = mGeckoAccessible->ChildAt(childIdx);
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ if (!nativeChild) {
+ continue;
+ }
+
+ [children addObject:nativeChild];
+ }
+
+ return children;
+}
+
+- (NSValue*)moxPosition {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
+}
+
+- (NSValue*)moxSize {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return
+ [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
+}
+
+- (NSString*)moxRole {
+#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
+ ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ return macRole;
+
+ switch (mRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown role.");
+ return NSAccessibilityUnknownRole;
+ }
+
+#undef ROLE
+}
+
+- (nsStaticAtom*)ARIARole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->HasARIARole()) {
+ const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
+ return roleMap->roleAtom;
+ }
+
+ return nsGkAtoms::_empty;
+}
+
+- (NSString*)moxSubrole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+
+ // Deal with landmarks first
+ // macOS groups the specific landmark types of DPub ARIA into two broad
+ // categories with corresponding subroles: Navigation and region/container.
+ if (mRole == roles::LANDMARK) {
+ nsAtom* landmark = acc ? acc->LandmarkRole() : proxy->LandmarkRole();
+ // HTML Elements treated as landmarks, and ARIA landmarks.
+ if (landmark) {
+ if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
+ if (landmark == nsGkAtoms::complementary)
+ return @"AXLandmarkComplementary";
+ if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
+ if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
+ if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
+ if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
+ }
+
+ // None of the above, so assume DPub ARIA.
+ return @"AXLandmarkRegion";
+ }
+
+ // Now, deal with widget roles
+ nsStaticAtom* roleAtom = nullptr;
+
+ if (mRole == roles::DIALOG) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::alertdialog) {
+ return @"AXApplicationAlertDialog";
+ }
+ if (roleAtom == nsGkAtoms::dialog) {
+ return @"AXApplicationDialog";
+ }
+ }
+
+ if (mRole == roles::FORM) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::form) {
+ return @"AXLandmarkForm";
+ }
+ }
+
+#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
+ ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \
+ return macSubrole; \
+ } else { \
+ break; \
+ }
+
+ switch (mRole) {
+#include "RoleMap.h"
+ }
+
+ // These are special. They map to roles::NOTHING
+ // and are instructed by the ARIA map to use the native host role.
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::log_) {
+ return @"AXApplicationLog";
+ }
+
+ if (roleAtom == nsGkAtoms::timer) {
+ return @"AXApplicationTimer";
+ }
+ // macOS added an AXSubrole value to distinguish generic AXGroup objects
+ // from those which are AXGroups as a result of an explicit ARIA role,
+ // such as the non-landmark, non-listitem text containers in DPub ARIA.
+ if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
+ return @"AXApplicationGroup";
+ }
+
+ return NSAccessibilityUnknownSubrole;
+
+#undef ROLE
+}
+
+struct RoleDescrMap {
+ NSString* role;
+ const nsString description;
+};
+
+static const RoleDescrMap sRoleDescrMap[] = {
+ {@"AXApplicationAlert", u"alert"_ns},
+ {@"AXApplicationAlertDialog", u"alertDialog"_ns},
+ {@"AXApplicationDialog", u"dialog"_ns},
+ {@"AXApplicationLog", u"log"_ns},
+ {@"AXApplicationMarquee", u"marquee"_ns},
+ {@"AXApplicationStatus", u"status"_ns},
+ {@"AXApplicationTimer", u"timer"_ns},
+ {@"AXContentSeparator", u"separator"_ns},
+ {@"AXDefinition", u"definition"_ns},
+ {@"AXDetails", u"details"_ns},
+ {@"AXDocument", u"document"_ns},
+ {@"AXDocumentArticle", u"article"_ns},
+ {@"AXDocumentMath", u"math"_ns},
+ {@"AXDocumentNote", u"note"_ns},
+ {@"AXLandmarkApplication", u"application"_ns},
+ {@"AXLandmarkBanner", u"banner"_ns},
+ {@"AXLandmarkComplementary", u"complementary"_ns},
+ {@"AXLandmarkContentInfo", u"content"_ns},
+ {@"AXLandmarkMain", u"main"_ns},
+ {@"AXLandmarkNavigation", u"navigation"_ns},
+ {@"AXLandmarkRegion", u"region"_ns},
+ {@"AXLandmarkSearch", u"search"_ns},
+ {@"AXSearchField", u"searchTextField"_ns},
+ {@"AXSummary", u"summary"_ns},
+ {@"AXTabPanel", u"tabPanel"_ns},
+ {@"AXTerm", u"term"_ns},
+ {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
+
+struct RoleDescrComparator {
+ const NSString* mRole;
+ explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
+ int operator()(const RoleDescrMap& aEntry) const {
+ return [mRole compare:aEntry.role];
+ }
+};
+
+- (NSString*)moxRoleDescription {
+ if (NSString* ariaRoleDescription =
+ utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
+ if ([ariaRoleDescription length]) {
+ return ariaRoleDescription;
+ }
+ }
+
+ if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
+
+ if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
+
+ if (mRole == roles::MARK) {
+ return utils::LocalizedString(u"highlight"_ns);
+ }
+
+ NSString* subrole = [self moxSubrole];
+
+ if (subrole) {
+ size_t idx = 0;
+ if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
+ RoleDescrComparator(subrole), &idx)) {
+ return utils::LocalizedString(sRoleDescrMap[idx].description);
+ }
+ }
+
+ return NSAccessibilityRoleDescription([self moxRole], subrole);
+}
+
+- (NSString*)moxLabel {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString name;
+
+ /* If our accessible is:
+ * 1. Named by invisible text, or
+ * 2. Has more than one labeling relation, or
+ * 3. Is a special role defined in providesLabelNotTitle
+ * ... return its name as a label (AXDescription).
+ */
+ ENameValueFlag flag = mGeckoAccessible->Name(name);
+ if (flag == eNameFromSubtree) {
+ return nil;
+ }
+
+ if (![self providesLabelNotTitle]) {
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return nil;
+ }
+ }
+
+ return nsCocoaUtils::ToNSString(name);
+}
+
+- (NSString*)moxTitle {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // In some special cases we provide the name in the label (AXDescription).
+ if ([self providesLabelNotTitle]) {
+ return nil;
+ }
+
+ nsAutoString title;
+ mGeckoAccessible->Name(title);
+ if (nsCoreUtils::IsWhitespaceString(title)) {
+ return @"";
+ }
+
+ return nsCocoaUtils::ToNSString(title);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)moxValue {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+
+ return nsCocoaUtils::ToNSString(value);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSString*)moxHelp {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // What needs to go here is actually the accDescription of an item.
+ // The MSAA acc_help method has nothing to do with this one.
+ nsAutoString helpText;
+ mGeckoAccessible->Description(helpText);
+
+ return nsCocoaUtils::ToNSString(helpText);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSWindow*)moxWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Get a pointer to the native window (NSWindow) we reside in.
+ NSWindow* nativeWindow = nil;
+ DocAccessible* docAcc = nullptr;
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ docAcc = acc->Document();
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
+ if (outerDoc) docAcc = outerDoc->Document();
+ }
+
+ if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
+
+ MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
+ "Couldn't get native window");
+ return nativeWindow;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSNumber*)moxEnabled {
+ if ([self stateWithMask:states::UNAVAILABLE]) {
+ return @NO;
+ }
+
+ if (![self isRoot]) {
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if (![parent isRoot]) {
+ return @(![parent disableChild:self]);
+ }
+ }
+
+ return @YES;
+}
+
+- (NSNumber*)moxFocused {
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (NSNumber*)moxSelected {
+ return @NO;
+}
+
+- (NSNumber*)moxExpanded {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (NSValue*)moxFrame {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ LayoutDeviceIntRect rect = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->Bounds()
+ : mGeckoAccessible->AsRemote()->Bounds();
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
+
+ return [NSValue
+ valueWithRect:NSMakeRect(
+ static_cast<CGFloat>(rect.x) / scaleFactor,
+ [mainView frame].size.height -
+ static_cast<CGFloat>(rect.y + rect.height) /
+ scaleFactor,
+ static_cast<CGFloat>(rect.width) / scaleFactor,
+ static_cast<CGFloat>(rect.height) / scaleFactor)];
+}
+
+- (NSString*)moxARIACurrent {
+ if (![self stateWithMask:states::CURRENT]) {
+ return nil;
+ }
+
+ return utils::GetAccAttr(self, nsGkAtoms::aria_current);
+}
+
+- (NSNumber*)moxARIAAtomic {
+ return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
+}
+
+- (NSString*)moxARIALive {
+ return utils::GetAccAttr(self, nsGkAtoms::aria_live);
+}
+
+- (NSString*)moxARIARelevant {
+ if (NSString* relevant =
+ utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
+ return relevant;
+ }
+
+ // Default aria-relevant value
+ return @"additions text";
+}
+
+- (id)moxTitleUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return [relations firstObject];
+ }
+
+ return nil;
+}
+
+- (NSString*)moxDOMIdentifier {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ nsAutoString id;
+ mGeckoAccessible->DOMNodeID(id);
+
+ return nsCocoaUtils::ToNSString(id);
+}
+
+- (NSNumber*)moxRequired {
+ return @([self stateWithMask:states::REQUIRED] != 0);
+}
+
+- (NSNumber*)moxElementBusy {
+ return @([self stateWithMask:states::BUSY] != 0);
+}
+
+- (NSArray*)moxLinkedUIElements {
+ return [self getRelationsByType:RelationType::FLOWS_TO];
+}
+
+- (NSArray*)moxARIAControls {
+ return [self getRelationsByType:RelationType::CONTROLLER_FOR];
+}
+
+- (mozAccessible*)topWebArea {
+ Accessible* doc = [self geckoDocument];
+ while (doc) {
+ if (doc->IsLocal()) {
+ DocAccessible* docAcc = doc->AsLocal()->AsDoc();
+ if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
+ return GetNativeFromGeckoAccessible(docAcc);
+ }
+
+ doc = docAcc->ParentDocument();
+ } else {
+ DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
+ if (docProxy->IsTopLevel()) {
+ return GetNativeFromGeckoAccessible(docProxy);
+ }
+ doc = docProxy->ParentDoc();
+ }
+ }
+
+ return nil;
+}
+
+- (void)handleRoleChanged:(mozilla::a11y::role)newRole {
+ mRole = newRole;
+ mARIARole = nullptr;
+
+ // For testing purposes
+ [self moxPostNotification:@"AXMozRoleChanged"];
+}
+
+- (id)moxEditableAncestor {
+ return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
+ return [moxAcc isKindOfClass:[mozTextAccessible class]];
+ }];
+}
+
+- (id)moxHighestEditableAncestor {
+ id highestAncestor = [self moxEditableAncestor];
+ while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
+ id ancestorParent = [highestAncestor moxUnignoredParent];
+ if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
+ break;
+ }
+
+ id higherAncestor = [ancestorParent moxEditableAncestor];
+
+ if (!higherAncestor) {
+ break;
+ }
+
+ highestAncestor = higherAncestor;
+ }
+
+ return highestAncestor;
+}
+
+- (id)moxFocusableAncestor {
+ // XXX: Checking focusable state up the chain can be expensive. For now,
+ // we can just return AXEditableAncestor since the main use case for this
+ // is rich text editing with links.
+ return [self moxEditableAncestor];
+}
+
+#ifndef RELEASE_OR_BETA
+- (NSString*)moxMozDebugDescription {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NSMutableString* domInfo = [NSMutableString string];
+ if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
+ [domInfo appendFormat:@" %@", tagName];
+ NSString* domID = [self moxDOMIdentifier];
+ if ([domID length]) {
+ [domInfo appendFormat:@"#%@", domID];
+ }
+ if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
+ [domInfo
+ appendFormat:@".%@",
+ [className stringByReplacingOccurrencesOfString:@" "
+ withString:@"."]];
+ }
+ }
+
+ return [NSString stringWithFormat:@"<%@: %p %@%@>",
+ NSStringFromClass([self class]), self,
+ [self moxRole], domInfo];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+#endif
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ // Create our search object and set it up with the searchPredicate
+ // params. The init function does additional parsing. We pass a
+ // reference to the web area to use as a start element if one is not
+ // specified.
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (void)moxSetFocused:(NSNumber*)focused {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if ([focused boolValue]) {
+ mGeckoAccessible->TakeFocus();
+ }
+}
+
+- (void)moxPerformScrollToVisible {
+ MOZ_ASSERT(mGeckoAccessible);
+ mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+}
+
+- (void)moxPerformShowMenu {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // We don't need to convert this rect into mac coordinates because the
+ // mouse event synthesizer expects layout (gecko) coordinates.
+ LayoutDeviceIntRect bounds = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->Bounds()
+ : mGeckoAccessible->AsRemote()->Bounds();
+
+ LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->RootAccessible()
+ : mGeckoAccessible->AsRemote()
+ ->OuterDocOfRemoteBrowser()
+ ->RootAccessible();
+ id objOrView =
+ GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
+
+ LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
+ bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
+ nsIWidget* widget = [objOrView widget];
+ widget->SynthesizeNativeMouseEvent(
+ p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
+ nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
+}
+
+- (void)moxPerformPress {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->IsLocal()) {
+ mGeckoAccessible->AsLocal()->DoAction(0);
+ } else {
+ mGeckoAccessible->AsRemote()->DoAction(0);
+ }
+
+ // Activating accessible may alter its state.
+ [self invalidateState];
+}
+
+#pragma mark -
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return NO;
+}
+
+- (void)maybePostLiveRegionChanged {
+ id<MOXAccessible> liveRegion =
+ [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [moxAcc moxIsLiveRegion];
+ }];
+
+ if (liveRegion) {
+ [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
+ }
+}
+
+- (void)maybePostA11yUtilNotification {
+ MOZ_ASSERT(mGeckoAccessible);
+ // Sometimes we use a special live region to make announcements to the user.
+ // This region is a child of the root document, but doesn't contain any
+ // content. If we try to fire regular AXLiveRegion changed events through it,
+ // VoiceOver clips the notifications because it (rightfully) doesn't detect
+ // focus within the region. We get around this by firing an
+ // AXAnnouncementRequested notification here instead.
+ // Verify we're trying to send a notification for the a11yUtils alert (and not
+ // a random acc with the same ID) by checking:
+ // - The gecko acc is local, our a11y-announcement lives in browser.xhtml
+ // - The ID of the gecko acc is "a11y-announcement"
+ // - The native acc is a direct descendent of the root
+ if (mGeckoAccessible->IsLocal() &&
+ [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
+ [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
+ // Our actual announcement should be stored as a child of the alert,
+ // so we verify a child exists, and then query that child below.
+ NSArray* children = [self moxChildren];
+ MOZ_ASSERT([children count] == 1 && children[0],
+ "A11yUtil event recieved, but no announcement found?");
+
+ mozAccessible* announcement = children[0];
+ NSString* key;
+ if ([announcement providesLabelNotTitle]) {
+ key = [announcement moxLabel];
+ } else {
+ key = [announcement moxTitle];
+ }
+
+ NSDictionary* info = @{
+ NSAccessibilityAnnouncementKey : key ? key : @(""),
+ NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
+ };
+
+ id window = [self moxWindow];
+
+ // This sends events via nsIObserverService to be consumed by our
+ // mochitests. Normally we'd fire these events through moxPostNotification
+ // which takes care of this, but because the window we fetch above isn't
+ // derrived from MOXAccessibleBase, we do this (and post the notification)
+ // manually.
+ xpcAccessibleMacEvent::FireEvent(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ NSAccessibilityPostNotificationWithUserInfo(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ }
+}
+
+- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
+ NSMutableArray<mozAccessible*>* relations =
+ [[[NSMutableArray alloc] init] autorelease];
+ Relation rel = mGeckoAccessible->RelationByType(relationType);
+ while (Accessible* relAcc = rel.Next()) {
+ if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
+ [relations addObject:relNative];
+ }
+ }
+
+ return relations;
+}
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(Accessible*)container
+ at:(int32_t)start {
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_ALERT:
+ [self maybePostA11yUtilNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
+ [self moxPostNotification:@"AXMenuOpened"];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
+ [self moxPostNotification:@"AXMenuClosed"];
+ break;
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
+ case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
+ [self moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ if (![self stateWithMask:states::SELECTABLE_TEXT]) {
+ break;
+ }
+ // We consider any caret move event to be a selected text change event.
+ // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
+ // reduntant.
+ MOXTextMarkerDelegate* delegate =
+ static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
+ NSMutableDictionary* userInfo =
+ [[[delegate selectionChangeInfo] mutableCopy] autorelease];
+ userInfo[@"AXTextChangeElement"] = self;
+
+ mozAccessible* webArea = [self topWebArea];
+ [webArea
+ moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
+ mIsLiveRegion = true;
+ [self moxPostNotification:@"AXLiveRegionCreated"];
+ break;
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
+ mIsLiveRegion = false;
+ break;
+ case nsIAccessibleEvent::EVENT_REORDER:
+ [self maybePostLiveRegionChanged];
+ break;
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ if (![self providesLabelNotTitle]) {
+ [self moxPostNotification:NSAccessibilityTitleChangedNotification];
+ }
+ [self maybePostLiveRegionChanged];
+ break;
+ }
+ }
+}
+
+- (void)expire {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateState];
+
+ mGeckoAccessible = nullptr;
+
+ [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isExpired {
+ return !mGeckoAccessible;
+}
+
+@end