diff options
Diffstat (limited to 'accessible/mac/MOXWebAreaAccessible.mm')
-rw-r--r-- | accessible/mac/MOXWebAreaAccessible.mm | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm new file mode 100644 index 0000000000..148d391542 --- /dev/null +++ b/accessible/mac/MOXWebAreaAccessible.mm @@ -0,0 +1,274 @@ +/* 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" + +#include "nsAccUtils.h" +#include "nsCocoaUtils.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 |