summaryrefslogtreecommitdiffstats
path: root/accessible/mac/MOXWebAreaAccessible.mm
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/mac/MOXWebAreaAccessible.mm')
-rw-r--r--accessible/mac/MOXWebAreaAccessible.mm276
1 files changed, 276 insertions, 0 deletions
diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm
new file mode 100644
index 0000000000..c1ae585fa1
--- /dev/null
+++ b/accessible/mac/MOXWebAreaAccessible.mm
@@ -0,0 +1,276 @@
+/* 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 "MOXWebAreaAccessible.h"
+
+#import "MOXSearchInfo.h"
+#import "MacUtils.h"
+
+#include "nsAccUtils.h"
+#include "nsCocoaUtils.h"
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+
+using namespace mozilla::a11y;
+
+@implementation MOXRootGroup
+
+- (id)initWithParent:(MOXWebAreaAccessible*)parent {
+ // The parent is always a MOXWebAreaAccessible
+ mParent = parent;
+ return [super init];
+}
+
+- (NSString*)moxRole {
+ return NSAccessibilityGroupRole;
+}
+
+- (NSString*)moxRoleDescription {
+ if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) {
+ return utils::LocalizedString(u"application"_ns);
+ }
+
+ return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil);
+}
+
+- (id<mozAccessible>)moxParent {
+ return mParent;
+}
+
+- (NSArray*)moxChildren {
+ // Reparent the children of the web area here.
+ return [mParent rootGroupChildren];
+}
+
+- (NSString*)moxIdentifier {
+ // This is mostly for testing purposes to assert that this is the generated
+ // root group.
+ return @"root-group";
+}
+
+- (NSString*)moxSubrole {
+ // Steal the subrole internally mapped to the web area.
+ return [mParent moxSubrole];
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ return [mParent moxHitTest:point];
+}
+
+- (NSValue*)moxPosition {
+ return [mParent moxPosition];
+}
+
+- (NSValue*)moxSize {
+ return [mParent moxSize];
+}
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (BOOL)disableChild:(id)child {
+ return NO;
+}
+
+- (void)expire {
+ mParent = nil;
+ [super expire];
+}
+
+- (BOOL)isExpired {
+ MOZ_ASSERT((mParent == nil) == mIsExpired);
+
+ return [super isExpired];
+}
+
+@end
+
+@implementation MOXWebAreaAccessible
+
+- (NSString*)moxRole {
+ // The OS role is AXWebArea regardless of the gecko role
+ // (APPLICATION or DOCUMENT).
+ // If the web area has a role of APPLICATION, its root group will
+ // reflect that in a subrole/description.
+ return @"AXWebArea";
+}
+
+- (NSString*)moxRoleDescription {
+ // The role description is "HTML Content" regardless of the gecko role
+ // (APPLICATION or DOCUMENT)
+ return utils::LocalizedString(u"htmlContent"_ns);
+}
+
+- (NSURL*)moxURL {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString url;
+ MOZ_ASSERT(mGeckoAccessible->IsDoc());
+ nsAccUtils::DocumentURL(mGeckoAccessible, url);
+
+ if (url.IsEmpty()) {
+ return nil;
+ }
+
+ return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
+}
+
+- (NSNumber*)moxLoaded {
+ if ([self isExpired]) {
+ return nil;
+ }
+ // We are loaded if we aren't busy or stale
+ return @([self stateWithMask:(states::BUSY & states::STALE)] == 0);
+}
+
+// overrides
+- (NSNumber*)moxLoadingProgress {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ if ([self stateWithMask:states::STALE] != 0) {
+ // We expose stale state until the document is ready (DOM is loaded and tree
+ // is constructed) so we indicate load hasn't started while this state is
+ // present.
+ return @0.0;
+ }
+
+ if ([self stateWithMask:states::BUSY] != 0) {
+ // We expose state busy until the document and all its subdocuments are
+ // completely loaded, so we indicate partial loading here
+ return @0.5;
+ }
+
+ // if we are not busy and not stale, we are loaded
+ return @1.0;
+}
+
+- (NSArray*)moxLinkUIElements {
+ NSDictionary* searchPredicate = @{
+ @"AXSearchKey" : @"AXLinkSearchKey",
+ @"AXImmediateDescendantsOnly" : @NO,
+ @"AXResultsLimit" : @(-1),
+ @"AXDirection" : @"AXDirectionNext",
+ };
+
+ return [self moxUIElementsForSearchPredicate:searchPredicate];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ MOZ_ASSERT(mGeckoAccessible->IsRemote() ||
+ mGeckoAccessible->AsLocal()->IsRoot() ||
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument(),
+ "Non-root doc without a parent!");
+ if ((mGeckoAccessible->IsRemote() &&
+ mGeckoAccessible->AsRemote()->IsDoc() &&
+ mGeckoAccessible->AsRemote()->AsDoc()->IsTopLevel()) ||
+ (mGeckoAccessible->IsLocal() &&
+ !mGeckoAccessible->AsLocal()->IsRoot() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument()->IsRoot())) {
+ // we fire an AXLoadComplete event on top-level documents only
+ [self moxPostNotification:@"AXLoadComplete"];
+ } else {
+ // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess)
+ // and we fire AXLayoutComplete instead
+ [self moxPostNotification:@"AXLayoutComplete"];
+ }
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (NSArray*)rootGroupChildren {
+ // This method is meant to expose the doc's children to the root group.
+ return [super moxChildren];
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (id rootGroup = [self rootGroup]) {
+ return @[ [self rootGroup] ];
+ }
+
+ // There is no root group, expose the children here directly.
+ return [super moxUnignoredChildren];
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxSubrole)) {
+ // Never expose a subrole for a web area.
+ return YES;
+ }
+
+ if (selector == @selector(moxElementBusy)) {
+ // Don't confuse aria-busy with a document's busy state.
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if (![notification isEqualToString:@"AXElementBusyChanged"]) {
+ // Suppress AXElementBusyChanged since it uses gecko's BUSY state
+ // to tell VoiceOver about aria-busy changes. We use that state
+ // differently in documents.
+ [super moxPostNotification:notification];
+ }
+}
+
+- (id)rootGroup {
+ NSArray* children = [super moxUnignoredChildren];
+ if (mRole == roles::DOCUMENT && [children count] == 1 &&
+ [[[children firstObject] moxUnignoredChildren] count] != 0) {
+ // We only need a root group if our document:
+ // (1) has multiple children, or
+ // (2) a one child that is a leaf, or
+ // (3) has a role other than the default document role
+ return nil;
+ }
+
+ if (!mRootGroup) {
+ mRootGroup = [[MOXRootGroup alloc] initWithParent:self];
+ }
+
+ return mRootGroup;
+}
+
+- (void)expire {
+ [mRootGroup expire];
+ [super expire];
+}
+
+- (void)dealloc {
+ // This object can only be dealoced after the gecko accessible wrapper
+ // reference is released, and that happens after expire is called.
+ MOZ_ASSERT([self isExpired]);
+ [mRootGroup release];
+
+ [super dealloc];
+}
+
+@end