1/* clang-format off */ 2/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 3/* clang-format on */ 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/. */ 7 8#import "MOXWebAreaAccessible.h" 9 10#import "MOXSearchInfo.h" 11 12#include "nsCocoaUtils.h" 13#include "DocAccessibleParent.h" 14 15using namespace mozilla::a11y; 16 17@implementation MOXRootGroup 18 19- (id)initWithParent:(MOXWebAreaAccessible*)parent { 20 // The parent is always a MOXWebAreaAccessible 21 mParent = parent; 22 return [super init]; 23} 24 25- (NSString*)moxRole { 26 return NSAccessibilityGroupRole; 27} 28 29- (NSString*)moxRoleDescription { 30 if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) { 31 return utils::LocalizedString(u"application"_ns); 32 } 33 34 return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil); 35} 36 37- (id<mozAccessible>)moxParent { 38 return mParent; 39} 40 41- (NSArray*)moxChildren { 42 // Reparent the children of the web area here. 43 return [mParent rootGroupChildren]; 44} 45 46- (NSString*)moxIdentifier { 47 // This is mostly for testing purposes to assert that this is the generated 48 // root group. 49 return @"root-group"; 50} 51 52- (NSString*)moxSubrole { 53 // Steal the subrole internally mapped to the web area. 54 return [mParent moxSubrole]; 55} 56 57- (id)moxHitTest:(NSPoint)point { 58 return [mParent moxHitTest:point]; 59} 60 61- (NSValue*)moxPosition { 62 return [mParent moxPosition]; 63} 64 65- (NSValue*)moxSize { 66 return [mParent moxSize]; 67} 68 69- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate { 70 MOXSearchInfo* search = 71 [[[MOXSearchInfo alloc] initWithParameters:searchPredicate 72 andRoot:self] autorelease]; 73 74 return [search performSearch]; 75} 76 77- (NSNumber*)moxUIElementCountForSearchPredicate: 78 (NSDictionary*)searchPredicate { 79 return [NSNumber 80 numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate] 81 count]]; 82} 83 84- (BOOL)disableChild:(id)child { 85 return NO; 86} 87 88- (void)expire { 89 mParent = nil; 90 [super expire]; 91} 92 93- (BOOL)isExpired { 94 MOZ_ASSERT((mParent == nil) == mIsExpired); 95 96 return [super isExpired]; 97} 98 99@end 100 101@implementation MOXWebAreaAccessible 102 103- (NSString*)moxRole { 104 // The OS role is AXWebArea regardless of the gecko role 105 // (APPLICATION or DOCUMENT). 106 // If the web area has a role of APPLICATION, its root group will 107 // reflect that in a subrole/description. 108 return @"AXWebArea"; 109} 110 111- (NSString*)moxRoleDescription { 112 // The role description is "HTML Content" regardless of the gecko role 113 // (APPLICATION or DOCUMENT) 114 return utils::LocalizedString(u"htmlContent"_ns); 115} 116 117- (NSURL*)moxURL { 118 if ([self isExpired]) { 119 return nil; 120 } 121 122 nsAutoString url; 123 if (mGeckoAccessible.IsAccessible()) { 124 MOZ_ASSERT(mGeckoAccessible.AsAccessible()->IsDoc()); 125 DocAccessible* acc = mGeckoAccessible.AsAccessible()->AsDoc(); 126 acc->URL(url); 127 } else { 128 RemoteAccessible* proxy = mGeckoAccessible.AsProxy(); 129 proxy->URL(url); 130 } 131 132 if (url.IsEmpty()) { 133 return nil; 134 } 135 136 return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)]; 137} 138 139- (NSNumber*)moxLoaded { 140 if ([self isExpired]) { 141 return nil; 142 } 143 // We are loaded if we aren't busy or stale 144 return @([self stateWithMask:(states::BUSY & states::STALE)] == 0); 145} 146 147// overrides 148- (NSNumber*)moxLoadingProgress { 149 if ([self isExpired]) { 150 return nil; 151 } 152 153 if ([self stateWithMask:states::STALE] != 0) { 154 // We expose stale state until the document is ready (DOM is loaded and tree 155 // is constructed) so we indicate load hasn't started while this state is 156 // present. 157 return @0.0; 158 } 159 160 if ([self stateWithMask:states::BUSY] != 0) { 161 // We expose state busy until the document and all its subdocuments are 162 // completely loaded, so we indicate partial loading here 163 return @0.5; 164 } 165 166 // if we are not busy and not stale, we are loaded 167 return @1.0; 168} 169 170- (NSArray*)moxLinkUIElements { 171 NSDictionary* searchPredicate = @{ 172 @"AXSearchKey" : @"AXLinkSearchKey", 173 @"AXImmediateDescendantsOnly" : @NO, 174 @"AXResultsLimit" : @(-1), 175 @"AXDirection" : @"AXDirectionNext", 176 }; 177 178 return [self moxUIElementsForSearchPredicate:searchPredicate]; 179} 180 181- (void)handleAccessibleEvent:(uint32_t)eventType { 182 switch (eventType) { 183 case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE: 184 [self moxPostNotification: 185 NSAccessibilityFocusedUIElementChangedNotification]; 186 if ((mGeckoAccessible.IsProxy() && mGeckoAccessible.AsProxy()->IsDoc() && 187 mGeckoAccessible.AsProxy()->AsDoc()->IsTopLevel()) || 188 (mGeckoAccessible.IsAccessible() && 189 !mGeckoAccessible.AsAccessible()->IsRoot() && 190 mGeckoAccessible.AsAccessible()->AsDoc()->ParentDocument() && 191 mGeckoAccessible.AsAccessible() 192 ->AsDoc() 193 ->ParentDocument() 194 ->IsRoot())) { 195 // we fire an AXLoadComplete event on top-level documents only 196 [self moxPostNotification:@"AXLoadComplete"]; 197 } else { 198 // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess) 199 // and we fire AXLayoutComplete instead 200 [self moxPostNotification:@"AXLayoutComplete"]; 201 } 202 break; 203 } 204 205 [super handleAccessibleEvent:eventType]; 206} 207 208- (NSArray*)rootGroupChildren { 209 // This method is meant to expose the doc's children to the root group. 210 return [super moxChildren]; 211} 212 213- (NSArray*)moxUnignoredChildren { 214 if (id rootGroup = [self rootGroup]) { 215 return @[ [self rootGroup] ]; 216 } 217 218 // There is no root group, expose the children here directly. 219 return [super moxUnignoredChildren]; 220} 221 222- (BOOL)moxBlockSelector:(SEL)selector { 223 if (selector == @selector(moxSubrole)) { 224 // Never expose a subrole for a web area. 225 return YES; 226 } 227 228 if (selector == @selector(moxElementBusy)) { 229 // Don't confuse aria-busy with a document's busy state. 230 return YES; 231 } 232 233 return [super moxBlockSelector:selector]; 234} 235 236- (void)moxPostNotification:(NSString*)notification { 237 if (![notification isEqualToString:@"AXElementBusyChanged"]) { 238 // Suppress AXElementBusyChanged since it uses gecko's BUSY state 239 // to tell VoiceOver about aria-busy changes. We use that state 240 // differently in documents. 241 [super moxPostNotification:notification]; 242 } 243} 244 245- (id)rootGroup { 246 NSArray* children = [super moxUnignoredChildren]; 247 if (mRole != roles::APPLICATION && [children count] == 1 && 248 [[[children firstObject] moxUnignoredChildren] count] != 0) { 249 // We only need a root group if our document: 250 // (1) has multiple children, or 251 // (2) a one child that is a leaf, or 252 // (3) has a role of APPLICATION 253 return nil; 254 } 255 256 if (!mRootGroup) { 257 mRootGroup = [[MOXRootGroup alloc] initWithParent:self]; 258 } 259 260 return mRootGroup; 261} 262 263- (void)expire { 264 [mRootGroup expire]; 265 [super expire]; 266} 267 268- (void)dealloc { 269 // This object can only be dealoced after the gecko accessible wrapper 270 // reference is released, and that happens after expire is called. 271 MOZ_ASSERT([self isExpired]); 272 [mRootGroup release]; 273 274 [super dealloc]; 275} 276 277@end 278