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