no bug - Correct some typos in the comments. a=typo-fix
[gecko.git] / accessible / ios / MUIAccessible.mm
blobd72ed77172a3291d97f26b6cadec50c03b8d1d26
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/. */
8 #import "MUIAccessible.h"
10 #include "nsString.h"
11 #include "RootAccessibleWrap.h"
13 using namespace mozilla;
14 using namespace mozilla::a11y;
16 #ifdef A11Y_LOG
17 #  define DEBUG_HINTS
18 #endif
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];
29 #endif
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()];
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,
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;
125 #pragma mark -
127 @interface NSObject (AccessibilityPrivate)
128 - (void)_accessibilityUnregister;
129 @end
131 @implementation MUIAccessible
133 - (id)initWithAccessible:(Accessible*)aAcc {
134   MOZ_ASSERT(aAcc, "Cannot init MUIAccessible with null");
135   if ((self = [super init])) {
136     mGeckoAccessible = aAcc;
137   }
139   return self;
142 - (mozilla::a11y::Accessible*)geckoAccessible {
143   return mGeckoAccessible;
146 - (void)expire {
147   mGeckoAccessible = nullptr;
148   if ([self respondsToSelector:@selector(_accessibilityUnregister)]) {
149     [self _accessibilityUnregister];
150   }
153 - (void)dealloc {
154   [super dealloc];
157 static bool isAccessibilityElementInternal(Accessible* aAccessible) {
158   MOZ_ASSERT(aAccessible);
159   IsAccessibilityElementRule rule = IsAccessibilityElementRule::No;
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   }
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       }
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       }
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       }
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       }
224       return true;
225     }
226     default:
227       break;
228   }
230   MOZ_ASSERT_UNREACHABLE("Unhandled IsAccessibilityElementRule");
232   return false;
235 - (BOOL)isAccessibilityElement {
236   if (!mGeckoAccessible) {
237     return NO;
238   }
240   return isAccessibilityElementInternal(mGeckoAccessible) ? YES : NO;
243 - (NSString*)accessibilityLabel {
244   if (!mGeckoAccessible) {
245     return @"";
246   }
248   nsAutoString name;
249   mGeckoAccessible->Name(name);
251   return ToNSString(name);
254 - (NSString*)accessibilityHint {
255   if (!mGeckoAccessible) {
256     return @"";
257   }
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
270 - (CGRect)accessibilityFrame {
271   RootAccessibleWrap* rootAcc = static_cast<RootAccessibleWrap*>(
272       mGeckoAccessible->IsLocal()
273           ? mGeckoAccessible->AsLocal()->RootAccessible()
274           : mGeckoAccessible->AsRemote()
275                 ->OuterDocOfRemoteBrowser()
276                 ->RootAccessible());
278   if (!rootAcc) {
279     return CGRectMake(0, 0, 0, 0);
280   }
282   LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
283   return rootAcc->DevPixelsRectToUIKit(rect);
286 - (NSString*)accessibilityValue {
287   if (!mGeckoAccessible) {
288     return nil;
289   }
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   }
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   }
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   }
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   }
327   nsAutoString value;
328   mGeckoAccessible->Value(value);
329   return ToNSString(value);
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     default:
378       break;
379   }
381   if ((traits & Trait::Link) && (state & states::TRAVERSED)) {
382     traits |= Trait::Visited;
383   }
385   if ((traits & Trait::Button) && (state & states::HASPOPUP)) {
386     traits |= Trait::PopupButton;
387   }
389   if (state & states::SELECTED) {
390     traits |= Trait::Selected;
391   }
393   if (state & states::CHECKABLE) {
394     traits |= Trait::Toggle;
395   }
397   if (!(state & states::ENABLED)) {
398     traits |= Trait::NotEnabled;
399   }
401   if (state & states::EDITABLE) {
402     traits |= Trait::TextEntry;
403     if (state & states::FOCUSED) {
404       // XXX: Also add "has text cursor" trait
405       traits |= Trait::IsEditing | Trait::TextOperationsAvailable;
406     }
408     if (aAccessible->IsSearchbox()) {
409       traits |= Trait::SearchField;
410     }
412     if (state & states::MULTI_LINE) {
413       traits |= Trait::TextArea;
414     }
415   }
417   return traits;
420 - (uint64_t)accessibilityTraits {
421   if (!mGeckoAccessible) {
422     return Trait::None;
423   }
425   uint64_t traits = GetAccessibilityTraits(mGeckoAccessible);
427   for (Accessible* parent = mGeckoAccessible->Parent(); parent;
428        parent = parent->Parent()) {
429     traits |= GetAccessibilityTraits(parent);
430   }
432   return traits;
435 - (NSInteger)accessibilityElementCount {
436   return mGeckoAccessible ? mGeckoAccessible->ChildCount() : 0;
439 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
440   if (!mGeckoAccessible) {
441     return nil;
442   }
444   Accessible* child = mGeckoAccessible->ChildAt(index);
445   return GetNativeFromGeckoAccessible(child);
448 - (NSInteger)indexOfAccessibilityElement:(id)element {
449   Accessible* acc = [(MUIAccessible*)element geckoAccessible];
450   if (!acc || mGeckoAccessible != acc->Parent()) {
451     return -1;
452   }
454   return acc->IndexInParent();
457 - (NSArray* _Nullable)accessibilityElements {
458   NSMutableArray* children = [[[NSMutableArray alloc] init] autorelease];
459   uint32_t childCount = mGeckoAccessible->ChildCount();
460   for (uint32_t i = 0; i < childCount; i++) {
461     if (MUIAccessible* child =
462             GetNativeFromGeckoAccessible(mGeckoAccessible->ChildAt(i))) {
463       [children addObject:child];
464     }
465   }
467   return children;
470 - (UIAccessibilityContainerType)accessibilityContainerType {
471   return UIAccessibilityContainerTypeNone;
474 - (NSRange)_accessibilitySelectedTextRange {
475   if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
476     return NSMakeRange(NSNotFound, 0);
477   }
478   // XXX This will only work in simple plain text boxes. It will break horribly
479   // if there are any embedded objects. Also, it only supports caret, not
480   // selection.
481   int32_t caret = mGeckoAccessible->AsHyperTextBase()->CaretOffset();
482   if (caret != -1) {
483     return NSMakeRange(caret, 0);
484   }
485   return NSMakeRange(NSNotFound, 0);
488 - (void)_accessibilitySetSelectedTextRange:(NSRange)range {
489   if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) {
490     return;
491   }
492   // XXX This will only work in simple plain text boxes. It will break horribly
493   // if there are any embedded objects. Also, it only supports caret, not
494   // selection.
495   mGeckoAccessible->AsHyperTextBase()->SetCaretOffset(range.location);
498 @end