Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "MOXTextMarkerDelegate.h"
#import "MOXWebAreaAccessible.h"
#import "mozTextAccessible.h"
#import "mozRootAccessible.h"
#include "LocalAccessible-inl.h"
#include "nsAccUtils.h"
#include "DocAccessibleParent.h"
#include "Relation.h"
#include "mozilla/a11y/Role.h"
#include "RootAccessible.h"
#include "mozilla/a11y/PDocAccessible.h"
#include "mozilla/dom/BrowserParent.h"
#include "OuterDocAccessible.h"
#include "nsChildView.h"
#include "xpcAccessibleMacInterface.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)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);
}
- (uint64_t)state {
return mGeckoAccessible->State();
}
- (uint64_t)stateWithMask:(uint64_t)mask {
return [self state] & mask;
}
- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
if (state == states::BUSY) {
[self moxPostNotification:@"AXElementBusyChanged"];
}
if (state == states::EXPANDED) {
[self moxPostNotification:@"AXExpandedChanged"];
}
}
- (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;
}
#pragma mark - MOXAccessible protocol
- (BOOL)moxBlockSelector:(SEL)selector {
if (selector == @selector(moxPerformPress)) {
uint8_t actionCount = mGeckoAccessible->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(moxARIAPosInSet) || selector == @selector
(moxARIASetSize)) {
GroupPos groupPos = mGeckoAccessible->GroupPosition();
return groupPos.setSize == 0;
}
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);
return [MOXTextMarkerDelegate
getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)];
}
- (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 isKindOfClass:[MOXWebAreaAccessible class]]) {
// 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, ariaRole, atkRole, macRole, macSubrole, \
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
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);
// 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 = mGeckoAccessible->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, ariaRole, atkRole, macRole, macSubrole, \
msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
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;
};
MOZ_RUNINIT 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, std::size(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->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);
}
- (NSNumber*)moxARIAPosInSet {
GroupPos groupPos = mGeckoAccessible->GroupPosition();
return @(groupPos.posInSet);
}
- (NSNumber*)moxARIASetSize {
GroupPos groupPos = mGeckoAccessible->GroupPosition();
return @(groupPos.setSize);
}
- (NSString*)moxARIARelevant {
if (NSString* relevant =
utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
return relevant;
}
// Default aria-relevant value
return @"additions text";
}
- (NSString*)moxPlaceholderValue {
// First, check for plaecholder HTML attribute
if (NSString* placeholder = utils::GetAccAttr(self, nsGkAtoms::placeholder)) {
return placeholder;
}
// If no placeholder HTML attribute, check for the aria version.
return utils::GetAccAttr(self, nsGkAtoms::aria_placeholder);
}
- (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 = nsAccUtils::DocumentFor(mGeckoAccessible);
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];
}
- (NSString*)moxLanguage {
MOZ_ASSERT(mGeckoAccessible);
nsAutoString lang;
mGeckoAccessible->Language(lang);
return nsCocoaUtils::ToNSString(lang);
}
#ifndef RELEASE_OR_BETA
- (NSString*)moxMozDebugDescription {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if (!mGeckoAccessible) {
return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
NSStringFromClass([self class]), self];
}
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->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);
mGeckoAccessible->DoAction(0);
}
#pragma mark -
- (BOOL)disableChild:(mozAccessible*)child {
return NO;
}
- (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 chrome window (ChildView in
// a non-headless context, mozRootAccessible in a headless context).
DocAccessible* maybeRoot = mGeckoAccessible->IsLocal()
? mGeckoAccessible->AsLocal()->Document()
: nullptr;
if (maybeRoot && maybeRoot->IsRoot() &&
[[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"]) {
// 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 received, 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 : @(""),
// High priority means VO will stop what it is currently speaking
// to speak our announcement.
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh)
};
// 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 NSApp isn't derived
// from MOXAccessibleBase, we do this (and post the notification) manually.
// We used to fire this on the window, but per Chrome and Safari these
// notifs get dropped if fired on any non-main window. We now fire on NSApp
// to avoid this.
xpcAccessibleMacEvent::FireEvent(
GetNativeFromGeckoAccessible(maybeRoot),
NSAccessibilityAnnouncementRequestedNotification, info);
NSAccessibilityPostNotificationWithUserInfo(
NSApp, 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_NAME_CHANGE:
if (![self providesLabelNotTitle]) {
[self moxPostNotification:NSAccessibilityTitleChangedNotification];
}
break;
case nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED:
MOZ_ASSERT(mIsLiveRegion);
[self moxPostNotification:@"AXLiveRegionChanged"];
break;
}
}
- (void)expire {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
mGeckoAccessible = nullptr;
[self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
NS_OBJC_END_TRY_IGNORE_BLOCK;
}
- (BOOL)isExpired {
return !mGeckoAccessible;
}
@end