tor-browser

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

mozSelectableElements.mm (11263B)


      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 "mozSelectableElements.h"
      9 #import "MOXWebAreaAccessible.h"
     10 #import "MacUtils.h"
     11 #include "LocalAccessible-inl.h"
     12 #include "nsCocoaUtils.h"
     13 
     14 using namespace mozilla::a11y;
     15 
     16 @implementation mozSelectableAccessible
     17 
     18 /**
     19 * Return the mozAccessibles that are selectable.
     20 */
     21 - (NSArray*)selectableChildren {
     22  NSArray* toFilter;
     23  if ([self isKindOfClass:[mozMenuAccessible class]]) {
     24    // If we are a menu, our children are only selectable if they are visible
     25    // so we filter this array instead of our unignored children list, which may
     26    // contain invisible items.
     27    toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren];
     28  } else {
     29    NSMutableArray* flattened = [[[NSMutableArray alloc] init] autorelease];
     30    void (^__block unnest)(NSArray*);
     31    // Listboxes can contain nested groups, which then contain options.
     32    // In order for VoiceOver to property advertise an option as
     33    // "selected", it needs to appear in AXSelectableChildren, so
     34    // we unnest any groups and present all "leaf" options as
     35    // selectable here. We do this here, instead of relying solely on our
     36    // `ignore` functions because multiple levels of groups could exist, and the
     37    // `ignore` functions only analyse single-level parent-child relationships.
     38    // Additionally, we don't want to override `moxUnignoredChildren` with this
     39    // logic because groups should still be exposed in the tree.
     40    unnest = ^(NSArray* children) {
     41      for (mozAccessible* child : children) {
     42        if ([[child moxRole] isEqualToString:@"AXGroup"]) {
     43          unnest([child moxUnignoredChildren]);
     44        } else {
     45          [flattened addObject:child];
     46        }
     47      }
     48    };
     49    // This will do classic ignore-filtering, ensuring we don't expose accs that
     50    // are invisible or defunct as selectable.
     51    unnest([self moxUnignoredChildren]);
     52    toFilter = flattened;
     53  }
     54  return [toFilter
     55      filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
     56                                                   mozAccessible* child,
     57                                                   NSDictionary* bindings) {
     58        return [child isKindOfClass:[mozSelectableChildAccessible class]];
     59      }]];
     60 }
     61 
     62 - (void)moxSetSelectedChildren:(NSArray*)selectedChildren {
     63  for (id child in [self selectableChildren]) {
     64    BOOL selected =
     65        [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound;
     66    [child moxSetSelected:@(selected)];
     67  }
     68 }
     69 
     70 /**
     71 * Return the mozAccessibles that are actually selected.
     72 */
     73 - (NSArray*)moxSelectedChildren {
     74  return [[self selectableChildren]
     75      filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
     76                                                   mozAccessible* child,
     77                                                   NSDictionary* bindings) {
     78        // Return mozSelectableChildAccessibles that have are selected (truthy
     79        // value).
     80        return [[(mozSelectableChildAccessible*)child moxSelected] boolValue];
     81      }]];
     82 }
     83 
     84 @end
     85 
     86 @implementation mozSelectableChildAccessible
     87 
     88 - (NSNumber*)moxSelected {
     89  return @([self stateWithMask:states::SELECTED] != 0);
     90 }
     91 
     92 - (void)moxSetSelected:(NSNumber*)selected {
     93  // Get SELECTABLE and UNAVAILABLE state.
     94  uint64_t state =
     95      [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)];
     96  if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) {
     97    // The object is either not selectable or is unavailable. Don't do anything.
     98    return;
     99  }
    100 
    101  mGeckoAccessible->SetSelected([selected boolValue]);
    102 }
    103 
    104 @end
    105 
    106 @implementation mozTabGroupAccessible
    107 
    108 - (NSArray*)moxTabs {
    109  return [self selectableChildren];
    110 }
    111 
    112 - (NSArray*)moxContents {
    113  return [self moxUnignoredChildren];
    114 }
    115 
    116 - (id)moxValue {
    117  // The value of a tab group is its selected child. In the case
    118  // of multiple selections this will return the first one.
    119  return [[self moxSelectedChildren] firstObject];
    120 }
    121 
    122 @end
    123 
    124 @implementation mozTabAccessible
    125 
    126 - (NSString*)moxRoleDescription {
    127  return utils::LocalizedString(u"tab"_ns);
    128 }
    129 
    130 - (id)moxValue {
    131  // Retuens 1 if item is selected, 0 if not.
    132  return [self moxSelected];
    133 }
    134 
    135 @end
    136 
    137 @implementation mozListboxAccessible
    138 
    139 - (BOOL)moxIgnoreChild:(mozAccessible*)child {
    140  if (!child || [[child moxRole] isEqualToString:@"AXGroup"]) {
    141    return YES;
    142  }
    143 
    144  return [super moxIgnoreChild:child];
    145 }
    146 
    147 - (BOOL)disableChild:(mozAccessible*)child {
    148  return ![child isKindOfClass:[mozSelectableChildAccessible class]];
    149 }
    150 
    151 - (NSString*)moxOrientation {
    152  return NSAccessibilityUnknownOrientationValue;
    153 }
    154 
    155 @end
    156 
    157 @implementation mozOptionAccessible
    158 
    159 - (NSString*)moxTitle {
    160  return @"";
    161 }
    162 
    163 - (id)moxValue {
    164  // Swap title and value of option so it behaves more like a AXStaticText.
    165  return [super moxTitle];
    166 }
    167 
    168 @end
    169 
    170 @implementation mozMenuAccessible
    171 
    172 - (NSString*)moxTitle {
    173  return @"";
    174 }
    175 
    176 - (NSString*)moxLabel {
    177  return @"";
    178 }
    179 
    180 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
    181  // This helps us generate the correct moxChildren array for
    182  // a sub menu -- that returned array should contain all
    183  // menu items, regardless of if they are visible or not.
    184  // Because moxChildren does ignore filtering, and because
    185  // our base ignore method filters out invisible accessibles,
    186  // we override this method.
    187  if ([parent isKindOfClass:[MOXWebAreaAccessible class]] ||
    188      [parent isKindOfClass:[MOXRootGroup class]]) {
    189    // We are a top level menu. Check our visibility the normal way
    190    return [super moxIgnoreWithParent:parent];
    191  }
    192 
    193  if ([parent isKindOfClass:[mozMenuItemAccessible class]] &&
    194      [parent geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
    195    // We are a submenu. If our parent menu item is in an open menu
    196    // we should not be ignored
    197    id grandparent = [parent moxParent];
    198    if ([grandparent isKindOfClass:[mozMenuAccessible class]]) {
    199      mozMenuAccessible* parentMenu =
    200          static_cast<mozMenuAccessible*>(grandparent);
    201      return ![parentMenu isOpened];
    202    }
    203  }
    204 
    205  // Otherwise, we call into our superclass's ignore method
    206  // to handle menus that are not submenus
    207  return [super moxIgnoreWithParent:parent];
    208 }
    209 
    210 - (NSArray*)moxVisibleChildren {
    211  // VO expects us to expose two lists of children on menus: all children
    212  // (done in moxUnignoredChildren), and children which are visible (here).
    213  // We implement ignoreWithParent for both menus and menu items
    214  // to ensure moxUnignoredChildren returns a complete list of children
    215  // regardless of visibility, see comments in those methods for additional
    216  // info.
    217  return [[self moxChildren]
    218      filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
    219                                                   mozAccessible* child,
    220                                                   NSDictionary* bindings) {
    221        if (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) {
    222          if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
    223            return ((acc->VisibilityState() & states::INVISIBLE) == 0);
    224          }
    225        }
    226        return true;
    227      }]];
    228 }
    229 
    230 - (id)moxTitleUIElement {
    231  id parent = [self moxUnignoredParent];
    232  if (parent && [parent isKindOfClass:[mozAccessible class]]) {
    233    return parent;
    234  }
    235 
    236  return nil;
    237 }
    238 
    239 - (void)moxPostNotification:(NSString*)notification {
    240  if ([notification isEqualToString:@"AXMenuOpened"]) {
    241    mIsOpened = YES;
    242  } else if ([notification isEqualToString:@"AXMenuClosed"]) {
    243    mIsOpened = NO;
    244  }
    245 
    246  [super moxPostNotification:notification];
    247 }
    248 
    249 - (void)expire {
    250  if (mIsOpened) {
    251    // VO needs to receive a menu closed event when the menu goes away.
    252    // If the menu is being destroyed, send a menu closed event first.
    253    [self moxPostNotification:@"AXMenuClosed"];
    254  }
    255 
    256  [super expire];
    257 }
    258 
    259 - (BOOL)isOpened {
    260  return mIsOpened;
    261 }
    262 
    263 @end
    264 
    265 @implementation mozMenuItemAccessible
    266 
    267 - (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
    268  // This helps us generate the correct moxChildren array for
    269  // a mozMenuAccessible; the returned array should contain all
    270  // menu items, regardless of if they are visible or not.
    271  // Because moxChildren does ignore filtering, and because
    272  // our base ignore method filters out invisible accessibles,
    273  // we override this method.
    274  Accessible* parentAcc = [parent geckoAccessible];
    275  if (parentAcc) {
    276    Accessible* grandparentAcc = parentAcc->Parent();
    277    if (mozAccessible* directGrandparent =
    278            GetNativeFromGeckoAccessible(grandparentAcc)) {
    279      if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) {
    280        return [parent moxIgnoreWithParent:directGrandparent];
    281      }
    282    }
    283  }
    284 
    285  id grandparent = [parent moxParent];
    286  if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) {
    287    mozMenuItemAccessible* acc =
    288        static_cast<mozMenuItemAccessible*>(grandparent);
    289    if ([acc geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
    290      mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent);
    291      // if we are a menu item in a submenu, display only when
    292      // parent menu item is open
    293      return ![parentMenu isOpened];
    294    }
    295  }
    296 
    297  // Otherwise, we call into our superclass's method to handle
    298  // menuitems that are not within submenus
    299  return [super moxIgnoreWithParent:parent];
    300 }
    301 
    302 - (NSString*)moxMenuItemMarkChar {
    303  LocalAccessible* acc = mGeckoAccessible->AsLocal();
    304  if (acc && acc->IsContent() &&
    305      acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) {
    306    // We need to provide a marker character. This is the visible "√" you see
    307    // on dropdown menus. In our a11y tree this is a single child text node
    308    // of the menu item.
    309    // We do this only with XUL menuitems that conform to the native theme, and
    310    // not with aria menu items that might have a pseudo element or something.
    311    if (acc->ChildCount() == 1 &&
    312        acc->LocalFirstChild()->Role() == roles::STATICTEXT) {
    313      nsAutoString marker;
    314      acc->LocalFirstChild()->Name(marker);
    315      if (marker.Length() == 1) {
    316        return nsCocoaUtils::ToNSString(marker);
    317      }
    318    }
    319  }
    320 
    321  return nil;
    322 }
    323 
    324 - (NSNumber*)moxSelected {
    325  // Our focused state is equivelent to native selected states for menus.
    326  return @([self stateWithMask:states::FOCUSED] != 0);
    327 }
    328 
    329 - (void)handleAccessibleEvent:(uint32_t)eventType {
    330  switch (eventType) {
    331    case nsIAccessibleEvent::EVENT_FOCUS:
    332      // Our focused state is equivelent to native selected states for menus.
    333      mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
    334      [parent moxPostNotification:
    335                  NSAccessibilitySelectedChildrenChangedNotification];
    336      break;
    337  }
    338 
    339  [super handleAccessibleEvent:eventType];
    340 }
    341 
    342 - (void)moxPerformPress {
    343  [super moxPerformPress];
    344  // when a menu item is pressed (chosen), we need to tell
    345  // VoiceOver about it, so we send this notification
    346  [self moxPostNotification:@"AXMenuItemSelected"];
    347 }
    348 
    349 @end