tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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