MUIAccessible.mm (15525B)
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 "MUIAccessible.h" 9 10 #include "nsString.h" 11 #include "RootAccessibleWrap.h" 12 13 using namespace mozilla; 14 using namespace mozilla::a11y; 15 16 #ifdef A11Y_LOG 17 # define DEBUG_HINTS 18 #endif 19 20 #ifdef DEBUG_HINTS 21 static NSString* ToNSString(const nsACString& aCString) { 22 if (aCString.IsEmpty()) { 23 return [NSString string]; 24 } 25 return [[[NSString alloc] initWithBytes:aCString.BeginReading() 26 length:aCString.Length() 27 encoding:NSUTF8StringEncoding] autorelease]; 28 } 29 #endif 30 31 static NSString* ToNSString(const nsAString& aString) { 32 if (aString.IsEmpty()) { 33 return [NSString string]; 34 } 35 return [NSString stringWithCharacters:reinterpret_cast<const unichar*>( 36 aString.BeginReading()) 37 length:aString.Length()]; 38 } 39 40 // These rules offer conditions for whether a gecko accessible 41 // should be considered a UIKit accessibility element. Each role is mapped to a 42 // rule. 43 enum class IsAccessibilityElementRule { 44 // Always yes 45 Yes, 46 // Always no 47 No, 48 // If the accessible has no children. For example an empty header 49 // which is labeled. 50 IfChildless, 51 // If the accessible has no children and it is named and focusable. 52 IfChildlessWithNameAndFocusable, 53 // If this accessible isn't a child of an accessibility element. For example, 54 // a text leaf child of a button. 55 IfParentIsntElementWithName, 56 // If this accessible has multiple leafs that should functionally be 57 // united, for example a link with span elements. 58 IfBrokenUp, 59 }; 60 61 class Trait { 62 public: 63 static const uint64_t None = 0; 64 static const uint64_t Button = ((uint64_t)0x1) << 0; 65 static const uint64_t Link = ((uint64_t)0x1) << 1; 66 static const uint64_t Image = ((uint64_t)0x1) << 2; 67 static const uint64_t Selected = ((uint64_t)0x1) << 3; 68 static const uint64_t PlaysSound = ((uint64_t)0x1) << 4; 69 static const uint64_t KeyboardKey = ((uint64_t)0x1) << 5; 70 static const uint64_t StaticText = ((uint64_t)0x1) << 6; 71 static const uint64_t SummaryElement = ((uint64_t)0x1) << 7; 72 static const uint64_t NotEnabled = ((uint64_t)0x1) << 8; 73 static const uint64_t UpdatesFrequently = ((uint64_t)0x1) << 9; 74 static const uint64_t SearchField = ((uint64_t)0x1) << 10; 75 static const uint64_t StartsMediaSession = ((uint64_t)0x1) << 11; 76 static const uint64_t Adjustable = ((uint64_t)0x1) << 12; 77 static const uint64_t AllowsDirectInteraction = ((uint64_t)0x1) << 13; 78 static const uint64_t CausesPageTurn = ((uint64_t)0x1) << 14; 79 static const uint64_t TabBar = ((uint64_t)0x1) << 15; 80 static const uint64_t Header = ((uint64_t)0x1) << 16; 81 static const uint64_t WebContent = ((uint64_t)0x1) << 17; 82 static const uint64_t TextEntry = ((uint64_t)0x1) << 18; 83 static const uint64_t PickerElement = ((uint64_t)0x1) << 19; 84 static const uint64_t RadioButton = ((uint64_t)0x1) << 20; 85 static const uint64_t IsEditing = ((uint64_t)0x1) << 21; 86 static const uint64_t LaunchIcon = ((uint64_t)0x1) << 22; 87 static const uint64_t StatusBarElement = ((uint64_t)0x1) << 23; 88 static const uint64_t SecureTextField = ((uint64_t)0x1) << 24; 89 static const uint64_t Inactive = ((uint64_t)0x1) << 25; 90 static const uint64_t Footer = ((uint64_t)0x1) << 26; 91 static const uint64_t BackButton = ((uint64_t)0x1) << 27; 92 static const uint64_t TabButton = ((uint64_t)0x1) << 28; 93 static const uint64_t AutoCorrectCandidate = ((uint64_t)0x1) << 29; 94 static const uint64_t DeleteKey = ((uint64_t)0x1) << 30; 95 static const uint64_t SelectionDismissesItem = ((uint64_t)0x1) << 31; 96 static const uint64_t Visited = ((uint64_t)0x1) << 32; 97 static const uint64_t Scrollable = ((uint64_t)0x1) << 33; 98 static const uint64_t Spacer = ((uint64_t)0x1) << 34; 99 static const uint64_t TableIndex = ((uint64_t)0x1) << 35; 100 static const uint64_t Map = ((uint64_t)0x1) << 36; 101 static const uint64_t TextOperationsAvailable = ((uint64_t)0x1) << 37; 102 static const uint64_t Draggable = ((uint64_t)0x1) << 38; 103 static const uint64_t GesturePracticeRegion = ((uint64_t)0x1) << 39; 104 static const uint64_t PopupButton = ((uint64_t)0x1) << 40; 105 static const uint64_t AllowsNativeSliding = ((uint64_t)0x1) << 41; 106 static const uint64_t MathEquation = ((uint64_t)0x1) << 42; 107 static const uint64_t ContainedByTable = ((uint64_t)0x1) << 43; 108 static const uint64_t ContainedByList = ((uint64_t)0x1) << 44; 109 static const uint64_t TouchContainer = ((uint64_t)0x1) << 45; 110 static const uint64_t SupportsZoom = ((uint64_t)0x1) << 46; 111 static const uint64_t TextArea = ((uint64_t)0x1) << 47; 112 static const uint64_t BookContent = ((uint64_t)0x1) << 48; 113 static const uint64_t ContainedByLandmark = ((uint64_t)0x1) << 49; 114 static const uint64_t FolderIcon = ((uint64_t)0x1) << 50; 115 static const uint64_t ReadOnly = ((uint64_t)0x1) << 51; 116 static const uint64_t MenuItem = ((uint64_t)0x1) << 52; 117 static const uint64_t Toggle = ((uint64_t)0x1) << 53; 118 static const uint64_t IgnoreItemChooser = ((uint64_t)0x1) << 54; 119 static const uint64_t SupportsTrackingDetail = ((uint64_t)0x1) << 55; 120 static const uint64_t Alert = ((uint64_t)0x1) << 56; 121 static const uint64_t ContainedByFieldset = ((uint64_t)0x1) << 57; 122 static const uint64_t AllowsLayoutChangeInStatusBar = ((uint64_t)0x1) << 58; 123 }; 124 125 #pragma mark - 126 127 @interface NSObject (AccessibilityPrivate) 128 - (void)_accessibilityUnregister; 129 @end 130 131 @implementation MUIAccessible 132 133 - (id)initWithAccessible:(Accessible*)aAcc { 134 MOZ_ASSERT(aAcc, "Cannot init MUIAccessible with null"); 135 if ((self = [super init])) { 136 mGeckoAccessible = aAcc; 137 } 138 139 return self; 140 } 141 142 - (mozilla::a11y::Accessible*)geckoAccessible { 143 return mGeckoAccessible; 144 } 145 146 - (void)expire { 147 mGeckoAccessible = nullptr; 148 if ([self respondsToSelector:@selector(_accessibilityUnregister)]) { 149 [self _accessibilityUnregister]; 150 } 151 } 152 153 - (void)dealloc { 154 [super dealloc]; 155 } 156 157 static bool isAccessibilityElementInternal(Accessible* aAccessible) { 158 MOZ_ASSERT(aAccessible); 159 IsAccessibilityElementRule rule = IsAccessibilityElementRule::No; 160 161 #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 162 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 163 nameRule) \ 164 case roles::_geckoRole: \ 165 rule = iosIsElement; \ 166 break; 167 switch (aAccessible->Role()) { 168 #include "RoleMap.h" 169 } 170 171 switch (rule) { 172 case IsAccessibilityElementRule::Yes: 173 return true; 174 case IsAccessibilityElementRule::No: 175 return false; 176 case IsAccessibilityElementRule::IfChildless: 177 return aAccessible->ChildCount() == 0; 178 case IsAccessibilityElementRule::IfParentIsntElementWithName: { 179 nsAutoString name; 180 aAccessible->Name(name); 181 name.CompressWhitespace(); 182 if (name.IsEmpty()) { 183 return false; 184 } 185 186 if (isAccessibilityElementInternal(aAccessible->Parent())) { 187 // This is a text leaf that needs to be pruned from a button or the 188 // likes. It should also be ignored in the event of its parent being a 189 // pruned link. 190 return false; 191 } 192 193 return true; 194 } 195 case IsAccessibilityElementRule::IfChildlessWithNameAndFocusable: 196 if (aAccessible->ChildCount() == 0 && 197 (aAccessible->State() & states::FOCUSABLE)) { 198 nsAutoString name; 199 aAccessible->Name(name); 200 name.CompressWhitespace(); 201 return !name.IsEmpty(); 202 } 203 return false; 204 case IsAccessibilityElementRule::IfBrokenUp: { 205 uint32_t childCount = aAccessible->ChildCount(); 206 if (childCount == 1) { 207 // If this is a single child container just use the text leaf and its 208 // traits will be inherited. 209 return false; 210 } 211 212 for (uint32_t idx = 0; idx < childCount; idx++) { 213 Accessible* child = aAccessible->ChildAt(idx); 214 role accRole = child->Role(); 215 if (accRole != roles::STATICTEXT && accRole != roles::TEXT_LEAF && 216 accRole != roles::GRAPHIC) { 217 // If this container contains anything but text leafs and images 218 // ignore this accessible. Its descendants will inherit the 219 // container's traits. 220 return false; 221 } 222 } 223 224 return true; 225 } 226 default: 227 break; 228 } 229 230 MOZ_ASSERT_UNREACHABLE("Unhandled IsAccessibilityElementRule"); 231 232 return false; 233 } 234 235 - (BOOL)isAccessibilityElement { 236 if (!mGeckoAccessible) { 237 return NO; 238 } 239 240 return isAccessibilityElementInternal(mGeckoAccessible) ? YES : NO; 241 } 242 243 - (NSString*)accessibilityLabel { 244 if (!mGeckoAccessible) { 245 return @""; 246 } 247 248 nsAutoString name; 249 mGeckoAccessible->Name(name); 250 251 return ToNSString(name); 252 } 253 254 - (NSString*)accessibilityHint { 255 if (!mGeckoAccessible) { 256 return @""; 257 } 258 259 #ifdef DEBUG_HINTS 260 // Just put in a debug description as the label so we get a clue about which 261 // accessible ends up where. 262 nsAutoCString desc; 263 mGeckoAccessible->DebugDescription(desc); 264 return ToNSString(desc); 265 #else 266 return @""; 267 #endif 268 } 269 270 - (CGRect)accessibilityFrame { 271 RootAccessibleWrap* rootAcc = static_cast<RootAccessibleWrap*>( 272 mGeckoAccessible->IsLocal() 273 ? mGeckoAccessible->AsLocal()->RootAccessible() 274 : mGeckoAccessible->AsRemote() 275 ->OuterDocOfRemoteBrowser() 276 ->RootAccessible()); 277 278 if (!rootAcc) { 279 return CGRectMake(0, 0, 0, 0); 280 } 281 282 LayoutDeviceIntRect rect = mGeckoAccessible->Bounds(); 283 return rootAcc->DevPixelsRectToUIKit(rect); 284 } 285 286 - (NSString*)accessibilityValue { 287 if (!mGeckoAccessible) { 288 return nil; 289 } 290 291 uint64_t state = mGeckoAccessible->State(); 292 if (state & states::LINKED) { 293 // Value returns the URL. We don't want to expose that as the value on iOS. 294 return nil; 295 } 296 297 if (state & states::CHECKABLE) { 298 if (state & states::CHECKED) { 299 return @"1"; 300 } 301 if (state & states::MIXED) { 302 return @"2"; 303 } 304 return @"0"; 305 } 306 307 if (mGeckoAccessible->IsPassword()) { 308 // Accessible::Value returns an empty string. On iOS, we need to return the 309 // masked password so that AT knows how many characters are in the password. 310 Accessible* leaf = mGeckoAccessible->FirstChild(); 311 if (!leaf) { 312 return nil; 313 } 314 nsAutoString masked; 315 leaf->AppendTextTo(masked); 316 return ToNSString(masked); 317 } 318 319 // If there is a heading ancestor, self has the header trait, so value should 320 // be the heading level. 321 for (Accessible* acc = mGeckoAccessible; acc; acc = acc->Parent()) { 322 if (acc->Role() == roles::HEADING) { 323 return [NSString stringWithFormat:@"%d", acc->GroupPosition().level]; 324 } 325 } 326 327 nsAutoString value; 328 mGeckoAccessible->Value(value); 329 return ToNSString(value); 330 } 331 332 static uint64_t GetAccessibilityTraits(Accessible* aAccessible) { 333 uint64_t state = aAccessible->State(); 334 uint64_t traits = Trait::WebContent; 335 switch (aAccessible->Role()) { 336 case roles::LINK: 337 traits |= Trait::Link; 338 break; 339 case roles::GRAPHIC: 340 traits |= Trait::Image; 341 break; 342 case roles::PAGETAB: 343 traits |= Trait::TabButton; 344 break; 345 case roles::PUSHBUTTON: 346 case roles::SUMMARY: 347 case roles::COMBOBOX: 348 case roles::BUTTONMENU: 349 case roles::TOGGLE_BUTTON: 350 case roles::CHECKBUTTON: 351 case roles::SWITCH: 352 traits |= Trait::Button; 353 break; 354 case roles::RADIOBUTTON: 355 traits |= Trait::RadioButton; 356 break; 357 case roles::HEADING: 358 traits |= Trait::Header; 359 break; 360 case roles::STATICTEXT: 361 case roles::TEXT_LEAF: 362 traits |= Trait::StaticText; 363 break; 364 case roles::SLIDER: 365 case roles::SPINBUTTON: 366 traits |= Trait::Adjustable; 367 break; 368 case roles::MENUITEM: 369 case roles::PARENT_MENUITEM: 370 case roles::CHECK_MENU_ITEM: 371 case roles::RADIO_MENU_ITEM: 372 traits |= Trait::MenuItem; 373 break; 374 case roles::PASSWORD_TEXT: 375 traits |= Trait::SecureTextField; 376 break; 377 case roles::SEARCHBOX: 378 traits |= Trait::SearchField; 379 break; 380 default: 381 break; 382 } 383 384 if ((traits & Trait::Link) && (state & states::TRAVERSED)) { 385 traits |= Trait::Visited; 386 } 387 388 if ((traits & Trait::Button) && (state & states::HASPOPUP)) { 389 traits |= Trait::PopupButton; 390 } 391 392 if (state & states::SELECTED) { 393 traits |= Trait::Selected; 394 } 395 396 if (state & states::CHECKABLE) { 397 traits |= Trait::Toggle; 398 } 399 400 if (!(state & states::ENABLED)) { 401 traits |= Trait::NotEnabled; 402 } 403 404 if (state & states::EDITABLE) { 405 traits |= Trait::TextEntry; 406 if (state & states::FOCUSED) { 407 // XXX: Also add "has text cursor" trait 408 traits |= Trait::IsEditing | Trait::TextOperationsAvailable; 409 } 410 411 if (state & states::MULTI_LINE) { 412 traits |= Trait::TextArea; 413 } 414 } 415 416 return traits; 417 } 418 419 - (uint64_t)accessibilityTraits { 420 if (!mGeckoAccessible) { 421 return Trait::None; 422 } 423 424 uint64_t traits = GetAccessibilityTraits(mGeckoAccessible); 425 426 for (Accessible* parent = mGeckoAccessible->Parent(); parent; 427 parent = parent->Parent()) { 428 traits |= GetAccessibilityTraits(parent); 429 } 430 431 return traits; 432 } 433 434 - (NSInteger)accessibilityElementCount { 435 return mGeckoAccessible ? mGeckoAccessible->ChildCount() : 0; 436 } 437 438 - (nullable id)accessibilityElementAtIndex:(NSInteger)index { 439 if (!mGeckoAccessible) { 440 return nil; 441 } 442 443 Accessible* child = mGeckoAccessible->ChildAt(index); 444 return GetNativeFromGeckoAccessible(child); 445 } 446 447 - (NSInteger)indexOfAccessibilityElement:(id)element { 448 Accessible* acc = [(MUIAccessible*)element geckoAccessible]; 449 if (!acc || mGeckoAccessible != acc->Parent()) { 450 return -1; 451 } 452 453 return acc->IndexInParent(); 454 } 455 456 - (NSArray* _Nullable)accessibilityElements { 457 NSMutableArray* children = [[[NSMutableArray alloc] init] autorelease]; 458 uint32_t childCount = mGeckoAccessible->ChildCount(); 459 for (uint32_t i = 0; i < childCount; i++) { 460 if (MUIAccessible* child = 461 GetNativeFromGeckoAccessible(mGeckoAccessible->ChildAt(i))) { 462 [children addObject:child]; 463 } 464 } 465 466 return children; 467 } 468 469 - (UIAccessibilityContainerType)accessibilityContainerType { 470 return UIAccessibilityContainerTypeNone; 471 } 472 473 - (NSRange)_accessibilitySelectedTextRange { 474 if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) { 475 return NSMakeRange(NSNotFound, 0); 476 } 477 // XXX This will only work in simple plain text boxes. It will break horribly 478 // if there are any embedded objects. Also, it only supports caret, not 479 // selection. 480 int32_t caret = mGeckoAccessible->AsHyperTextBase()->CaretOffset(); 481 if (caret != -1) { 482 return NSMakeRange(caret, 0); 483 } 484 return NSMakeRange(NSNotFound, 0); 485 } 486 487 - (void)_accessibilitySetSelectedTextRange:(NSRange)range { 488 if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) { 489 return; 490 } 491 // XXX This will only work in simple plain text boxes. It will break horribly 492 // if there are any embedded objects. Also, it only supports caret, not 493 // selection. 494 mGeckoAccessible->AsHyperTextBase()->SetCaretOffset(range.location); 495 } 496 497 @end