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 "mozAccessible.h"
9#include "MOXAccessibleBase.h"
10
11#import "MacUtils.h"
12#import "mozView.h"
13#import "MOXSearchInfo.h"
14#import "mozTextAccessible.h"
15
16#include "LocalAccessible-inl.h"
17#include "nsAccUtils.h"
18#include "DocAccessibleParent.h"
19#include "Relation.h"
20#include "Role.h"
21#include "RootAccessible.h"
22#include "TableAccessible.h"
23#include "mozilla/a11y/PDocAccessible.h"
24#include "mozilla/dom/BrowserParent.h"
25#include "OuterDocAccessible.h"
26#include "nsChildView.h"
27
28#include "nsRect.h"
29#include "nsCocoaUtils.h"
30#include "nsCoord.h"
31#include "nsObjCExceptions.h"
32#include "nsWhitespaceTokenizer.h"
33#include <prdtoa.h>
34
35using namespace mozilla;
36using namespace mozilla::a11y;
37
38#pragma mark -
39
40@interface mozAccessible ()
41- (BOOL)providesLabelNotTitle;
42
43- (void)maybePostLiveRegionChanged;
44@end
45
46@implementation mozAccessible
47
48- (id)initWithAccessible:(AccessibleOrProxy)aAccOrProxy {
49  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
50  MOZ_ASSERT(!aAccOrProxy.IsNull(), "Cannot init mozAccessible with null");
51  if ((self = [super init])) {
52    mGeckoAccessible = aAccOrProxy;
53    mRole = aAccOrProxy.Role();
54  }
55
56  return self;
57
58  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
59}
60
61- (void)dealloc {
62  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
63
64  [super dealloc];
65
66  NS_OBJC_END_TRY_IGNORE_BLOCK;
67}
68
69#pragma mark - mozAccessible widget
70
71- (BOOL)hasRepresentedView {
72  return NO;
73}
74
75- (id)representedView {
76  return nil;
77}
78
79- (BOOL)isRoot {
80  return NO;
81}
82
83#pragma mark -
84
85- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
86  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
87    if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
88      if (acc->VisibilityState() & states::INVISIBLE) {
89        return YES;
90      }
91    }
92  }
93
94  return [parent moxIgnoreChild:self];
95}
96
97- (BOOL)moxIgnoreChild:(mozAccessible*)child {
98  return nsAccUtils::MustPrune(mGeckoAccessible);
99}
100
101- (id)childAt:(uint32_t)i {
102  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
103
104  AccessibleOrProxy child = mGeckoAccessible.ChildAt(i);
105  return !child.IsNull() ? GetNativeFromGeckoAccessible(child) : nil;
106
107  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
108}
109
110static const uint64_t kCachedStates =
111    states::CHECKED | states::PRESSED | states::MIXED | states::EXPANDED |
112    states::CURRENT | states::SELECTED | states::TRAVERSED | states::LINKED |
113    states::HASPOPUP | states::BUSY | states::MULTI_LINE;
114static const uint64_t kCacheInitialized = ((uint64_t)0x1) << 63;
115
116- (uint64_t)state {
117  uint64_t state = 0;
118
119  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
120    state = acc->State();
121  }
122
123  if (RemoteAccessible* proxy = mGeckoAccessible.AsProxy()) {
124    state = proxy->State();
125  }
126
127  if (!(mCachedState & kCacheInitialized)) {
128    mCachedState = state & kCachedStates;
129    mCachedState |= kCacheInitialized;
130  }
131
132  return state;
133}
134
135- (uint64_t)stateWithMask:(uint64_t)mask {
136  if ((mask & kCachedStates) == mask &&
137      (mCachedState & kCacheInitialized) != 0) {
138    return mCachedState & mask;
139  }
140
141  return [self state] & mask;
142}
143
144- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
145  if ((state & kCachedStates) != 0) {
146    if (!(mCachedState & kCacheInitialized)) {
147      [self state];
148    } else {
149      if (enabled) {
150        mCachedState |= state;
151      } else {
152        mCachedState &= ~state;
153      }
154    }
155  }
156
157  if (state == states::BUSY) {
158    [self moxPostNotification:@"AXElementBusyChanged"];
159  }
160}
161
162- (void)invalidateState {
163  mCachedState = 0;
164}
165
166- (BOOL)providesLabelNotTitle {
167  // These accessible types are the exception to the rule of label vs. title:
168  // They may be named explicitly, but they still provide a label not a title.
169  return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
170         mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
171         mRole == roles::DOCUMENT || mRole == roles::OUTLINE;
172}
173
174- (mozilla::a11y::AccessibleOrProxy)geckoAccessible {
175  return mGeckoAccessible;
176}
177
178- (mozilla::a11y::AccessibleOrProxy)geckoDocument {
179  MOZ_ASSERT(!mGeckoAccessible.IsNull());
180
181  if (mGeckoAccessible.IsAccessible()) {
182    if (mGeckoAccessible.AsAccessible()->IsDoc()) {
183      return mGeckoAccessible;
184    }
185    return mGeckoAccessible.AsAccessible()->Document();
186  }
187
188  if (mGeckoAccessible.AsProxy()->IsDoc()) {
189    return mGeckoAccessible;
190  }
191
192  return mGeckoAccessible.AsProxy()->Document();
193}
194
195#pragma mark - MOXAccessible protocol
196
197- (BOOL)moxBlockSelector:(SEL)selector {
198  if (selector == @selector(moxPerformPress)) {
199    uint8_t actionCount = mGeckoAccessible.IsAccessible()
200                              ? mGeckoAccessible.AsAccessible()->ActionCount()
201                              : mGeckoAccessible.AsProxy()->ActionCount();
202
203    // If we have no action, we don't support press, so return YES.
204    return actionCount == 0;
205  }
206
207  if (selector == @selector(moxSetFocused:)) {
208    return [self stateWithMask:states::FOCUSABLE] == 0;
209  }
210
211  if (selector == @selector(moxARIALive) ||
212      selector == @selector(moxARIAAtomic) ||
213      selector == @selector(moxARIARelevant)) {
214    return ![self moxIsLiveRegion];
215  }
216
217  return [super moxBlockSelector:selector];
218}
219
220- (id)moxFocusedUIElement {
221  MOZ_ASSERT(!mGeckoAccessible.IsNull());
222
223  LocalAccessible* acc = mGeckoAccessible.AsAccessible();
224  RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
225
226  mozAccessible* focusedChild = nil;
227  if (acc) {
228    LocalAccessible* focusedGeckoChild = acc->FocusedChild();
229    if (focusedGeckoChild) {
230      focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild);
231    } else {
232      dom::BrowserParent* browser = dom::BrowserParent::GetFocused();
233      if (browser) {
234        a11y::DocAccessibleParent* proxyDoc =
235            browser->GetTopLevelDocAccessible();
236        if (proxyDoc) {
237          mozAccessible* nativeRemoteChild =
238              GetNativeFromGeckoAccessible(proxyDoc);
239          return [nativeRemoteChild accessibilityFocusedUIElement];
240        }
241      }
242    }
243  } else if (proxy) {
244    RemoteAccessible* focusedGeckoChild = proxy->FocusedChild();
245    if (focusedGeckoChild) {
246      focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild);
247    }
248  }
249
250  if ([focusedChild isAccessibilityElement]) {
251    return focusedChild;
252  }
253
254  // return ourself if we can't get a native focused child.
255  return self;
256}
257
258- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
259  MOZ_ASSERT(!mGeckoAccessible.IsNull());
260
261  if (mGeckoAccessible.IsAccessible()) {
262    return [MOXTextMarkerDelegate
263        getOrCreateForDoc:mGeckoAccessible.AsAccessible()->Document()];
264  }
265
266  return [MOXTextMarkerDelegate
267      getOrCreateForDoc:mGeckoAccessible.AsProxy()->Document()];
268}
269
270- (BOOL)moxIsLiveRegion {
271  return mIsLiveRegion;
272}
273
274- (id)moxHitTest:(NSPoint)point {
275  MOZ_ASSERT(!mGeckoAccessible.IsNull());
276
277  // Convert the given screen-global point in the cocoa coordinate system (with
278  // origin in the bottom-left corner of the screen) into point in the Gecko
279  // coordinate system (with origin in a top-left screen point).
280  NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
281  NSPoint tmpPoint =
282      NSMakePoint(point.x, [mainView frame].size.height - point.y);
283  LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
284      tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
285
286  AccessibleOrProxy child = mGeckoAccessible.ChildAtPoint(
287      geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
288
289  if (!child.IsNull()) {
290    mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
291    return [nativeChild isAccessibilityElement]
292               ? nativeChild
293               : [nativeChild moxUnignoredParent];
294  }
295
296  // if we didn't find anything, return ourself or child view.
297  return self;
298}
299
300- (id<mozAccessible>)moxParent {
301  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
302  if ([self isExpired]) {
303    return nil;
304  }
305
306  AccessibleOrProxy parent = mGeckoAccessible.Parent();
307
308  if (parent.IsNull()) {
309    return nil;
310  }
311
312  id nativeParent = GetNativeFromGeckoAccessible(parent);
313  if (parent.Role() == roles::DOCUMENT &&
314      [nativeParent respondsToSelector:@selector(rootGroup)]) {
315    // Before returning a WebArea as parent, check to see if
316    // there is a generated root group that is an intermediate container.
317    if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
318      nativeParent = rootGroup;
319    }
320  }
321
322  if (!nativeParent && mGeckoAccessible.IsAccessible()) {
323    // Return native of root accessible if we have no direct parent.
324    // XXX: need to return a sensible fallback in proxy case as well
325    nativeParent = GetNativeFromGeckoAccessible(
326        mGeckoAccessible.AsAccessible()->RootAccessible());
327  }
328
329  return GetObjectOrRepresentedView(nativeParent);
330
331  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
332}
333
334// gets all our native children lazily, including those that are ignored.
335- (NSArray*)moxChildren {
336  MOZ_ASSERT(!mGeckoAccessible.IsNull());
337
338  NSMutableArray* children = [[[NSMutableArray alloc]
339      initWithCapacity:mGeckoAccessible.ChildCount()] autorelease];
340
341  for (uint32_t childIdx = 0; childIdx < mGeckoAccessible.ChildCount();
342       childIdx++) {
343    AccessibleOrProxy child = mGeckoAccessible.ChildAt(childIdx);
344    mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
345    if (!nativeChild) {
346      continue;
347    }
348
349    [children addObject:nativeChild];
350  }
351
352  return children;
353}
354
355- (NSValue*)moxPosition {
356  CGRect frame = [[self moxFrame] rectValue];
357
358  return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
359}
360
361- (NSValue*)moxSize {
362  CGRect frame = [[self moxFrame] rectValue];
363
364  return
365      [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
366}
367
368- (NSString*)moxRole {
369#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
370             ia2Role, androidClass, nameRule)                               \
371  case roles::geckoRole:                                                    \
372    return macRole;
373
374  switch (mRole) {
375#include "RoleMap.h"
376    default:
377      MOZ_ASSERT_UNREACHABLE("Unknown role.");
378      return NSAccessibilityUnknownRole;
379  }
380
381#undef ROLE
382}
383
384- (nsStaticAtom*)ARIARole {
385  MOZ_ASSERT(!mGeckoAccessible.IsNull());
386
387  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
388    if (acc->HasARIARole()) {
389      const nsRoleMapEntry* roleMap = acc->ARIARoleMap();
390      return roleMap->roleAtom;
391    }
392
393    return nsGkAtoms::_empty;
394  }
395
396  if (!mARIARole) {
397    mARIARole = mGeckoAccessible.AsProxy()->ARIARoleAtom();
398    if (!mARIARole) {
399      mARIARole = nsGkAtoms::_empty;
400    }
401  }
402
403  return mARIARole;
404}
405
406- (NSString*)moxSubrole {
407  MOZ_ASSERT(!mGeckoAccessible.IsNull());
408
409  LocalAccessible* acc = mGeckoAccessible.AsAccessible();
410  RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
411
412  // Deal with landmarks first
413  // macOS groups the specific landmark types of DPub ARIA into two broad
414  // categories with corresponding subroles: Navigation and region/container.
415  if (mRole == roles::LANDMARK) {
416    nsAtom* landmark = acc ? acc->LandmarkRole() : proxy->LandmarkRole();
417    // HTML Elements treated as landmarks, and ARIA landmarks.
418    if (landmark) {
419      if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
420      if (landmark == nsGkAtoms::complementary)
421        return @"AXLandmarkComplementary";
422      if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
423      if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
424      if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
425      if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
426    }
427
428    // None of the above, so assume DPub ARIA.
429    return @"AXLandmarkRegion";
430  }
431
432  // Now, deal with widget roles
433  nsStaticAtom* roleAtom = nullptr;
434
435  if (mRole == roles::DIALOG) {
436    roleAtom = [self ARIARole];
437
438    if (roleAtom == nsGkAtoms::alertdialog) {
439      return @"AXApplicationAlertDialog";
440    }
441    if (roleAtom == nsGkAtoms::dialog) {
442      return @"AXApplicationDialog";
443    }
444  }
445
446  if (mRole == roles::FORM) {
447    roleAtom = [self ARIARole];
448
449    if (roleAtom == nsGkAtoms::form) {
450      return @"AXLandmarkForm";
451    }
452  }
453
454#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
455             ia2Role, androidClass, nameRule)                               \
456  case roles::geckoRole:                                                    \
457    if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) {      \
458      return macSubrole;                                                    \
459    } else {                                                                \
460      break;                                                                \
461    }
462
463  switch (mRole) {
464#include "RoleMap.h"
465  }
466
467  // These are special. They map to roles::NOTHING
468  // and are instructed by the ARIA map to use the native host role.
469  roleAtom = [self ARIARole];
470
471  if (roleAtom == nsGkAtoms::log_) {
472    return @"AXApplicationLog";
473  }
474
475  if (roleAtom == nsGkAtoms::timer) {
476    return @"AXApplicationTimer";
477  }
478  // macOS added an AXSubrole value to distinguish generic AXGroup objects
479  // from those which are AXGroups as a result of an explicit ARIA role,
480  // such as the non-landmark, non-listitem text containers in DPub ARIA.
481  if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
482    return @"AXApplicationGroup";
483  }
484
485  return NSAccessibilityUnknownSubrole;
486
487#undef ROLE
488}
489
490struct RoleDescrMap {
491  NSString* role;
492  const nsString description;
493};
494
495static const RoleDescrMap sRoleDescrMap[] = {
496    {@"AXApplicationAlert", u"alert"_ns},
497    {@"AXApplicationAlertDialog", u"alertDialog"_ns},
498    {@"AXApplicationDialog", u"dialog"_ns},
499    {@"AXApplicationLog", u"log"_ns},
500    {@"AXApplicationMarquee", u"marquee"_ns},
501    {@"AXApplicationStatus", u"status"_ns},
502    {@"AXApplicationTimer", u"timer"_ns},
503    {@"AXContentSeparator", u"separator"_ns},
504    {@"AXDefinition", u"definition"_ns},
505    {@"AXDetails", u"details"_ns},
506    {@"AXDocument", u"document"_ns},
507    {@"AXDocumentArticle", u"article"_ns},
508    {@"AXDocumentMath", u"math"_ns},
509    {@"AXDocumentNote", u"note"_ns},
510    {@"AXLandmarkApplication", u"application"_ns},
511    {@"AXLandmarkBanner", u"banner"_ns},
512    {@"AXLandmarkComplementary", u"complementary"_ns},
513    {@"AXLandmarkContentInfo", u"content"_ns},
514    {@"AXLandmarkMain", u"main"_ns},
515    {@"AXLandmarkNavigation", u"navigation"_ns},
516    {@"AXLandmarkRegion", u"region"_ns},
517    {@"AXLandmarkSearch", u"search"_ns},
518    {@"AXSearchField", u"searchTextField"_ns},
519    {@"AXSummary", u"summary"_ns},
520    {@"AXTabPanel", u"tabPanel"_ns},
521    {@"AXTerm", u"term"_ns},
522    {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
523
524struct RoleDescrComparator {
525  const NSString* mRole;
526  explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
527  int operator()(const RoleDescrMap& aEntry) const {
528    return [mRole compare:aEntry.role];
529  }
530};
531
532- (NSString*)moxRoleDescription {
533  if (NSString* ariaRoleDescription =
534          utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
535    if ([ariaRoleDescription length]) {
536      return ariaRoleDescription;
537    }
538  }
539
540  if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
541
542  if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
543
544  if (mRole == roles::MARK) {
545    return utils::LocalizedString(u"highlight"_ns);
546  }
547
548  NSString* subrole = [self moxSubrole];
549
550  if (subrole) {
551    size_t idx = 0;
552    if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
553                       RoleDescrComparator(subrole), &idx)) {
554      return utils::LocalizedString(sRoleDescrMap[idx].description);
555    }
556  }
557
558  return NSAccessibilityRoleDescription([self moxRole], subrole);
559}
560
561- (NSString*)moxLabel {
562  if ([self isExpired]) {
563    return nil;
564  }
565
566  LocalAccessible* acc = mGeckoAccessible.AsAccessible();
567  RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
568  nsAutoString name;
569
570  /* If our accessible is:
571   * 1. Named by invisible text, or
572   * 2. Has more than one labeling relation, or
573   * 3. Is a special role defined in providesLabelNotTitle
574   *   ... return its name as a label (AXDescription).
575   */
576  if (acc) {
577    ENameValueFlag flag = acc->Name(name);
578    if (flag == eNameFromSubtree) {
579      return nil;
580    }
581  } else if (proxy) {
582    uint32_t flag = proxy->Name(name);
583    if (flag == eNameFromSubtree) {
584      return nil;
585    }
586  }
587
588  if (![self providesLabelNotTitle]) {
589    NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
590    if ([relations count] == 1) {
591      return nil;
592    }
593  }
594
595  return nsCocoaUtils::ToNSString(name);
596}
597
598- (NSString*)moxTitle {
599  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
600
601  // In some special cases we provide the name in the label (AXDescription).
602  if ([self providesLabelNotTitle]) {
603    return nil;
604  }
605
606  nsAutoString title;
607  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
608    acc->Name(title);
609  } else {
610    mGeckoAccessible.AsProxy()->Name(title);
611  }
612
613  return nsCocoaUtils::ToNSString(title);
614
615  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
616}
617
618- (id)moxValue {
619  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
620
621  nsAutoString value;
622  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
623    acc->Value(value);
624  } else {
625    mGeckoAccessible.AsProxy()->Value(value);
626  }
627
628  return nsCocoaUtils::ToNSString(value);
629
630  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
631}
632
633- (NSString*)moxHelp {
634  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
635
636  // What needs to go here is actually the accDescription of an item.
637  // The MSAA acc_help method has nothing to do with this one.
638  nsAutoString helpText;
639  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
640    acc->Description(helpText);
641  } else {
642    mGeckoAccessible.AsProxy()->Description(helpText);
643  }
644
645  return nsCocoaUtils::ToNSString(helpText);
646
647  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
648}
649
650- (NSWindow*)moxWindow {
651  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
652
653  // Get a pointer to the native window (NSWindow) we reside in.
654  NSWindow* nativeWindow = nil;
655  DocAccessible* docAcc = nullptr;
656  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
657    docAcc = acc->Document();
658  } else {
659    RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
660    LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
661    if (outerDoc) docAcc = outerDoc->Document();
662  }
663
664  if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
665
666  MOZ_ASSERT(nativeWindow, "Couldn't get native window");
667  return nativeWindow;
668
669  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
670}
671
672- (NSNumber*)moxEnabled {
673  if ([self stateWithMask:states::UNAVAILABLE]) {
674    return @NO;
675  }
676
677  if (![self isRoot]) {
678    mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
679    if (![parent isRoot]) {
680      return @(![parent disableChild:self]);
681    }
682  }
683
684  return @YES;
685}
686
687- (NSNumber*)moxFocused {
688  return @([self stateWithMask:states::FOCUSED] != 0);
689}
690
691- (NSNumber*)moxSelected {
692  return @NO;
693}
694
695- (NSValue*)moxFrame {
696  MOZ_ASSERT(!mGeckoAccessible.IsNull());
697
698  nsIntRect rect = mGeckoAccessible.IsAccessible()
699                       ? mGeckoAccessible.AsAccessible()->Bounds()
700                       : mGeckoAccessible.AsProxy()->Bounds();
701  NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
702  CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
703
704  return [NSValue
705      valueWithRect:NSMakeRect(
706                        static_cast<CGFloat>(rect.x) / scaleFactor,
707                        [mainView frame].size.height -
708                            static_cast<CGFloat>(rect.y + rect.height) /
709                                scaleFactor,
710                        static_cast<CGFloat>(rect.width) / scaleFactor,
711                        static_cast<CGFloat>(rect.height) / scaleFactor)];
712}
713
714- (NSString*)moxARIACurrent {
715  if (![self stateWithMask:states::CURRENT]) {
716    return nil;
717  }
718
719  return utils::GetAccAttr(self, nsGkAtoms::aria_current);
720}
721
722- (NSNumber*)moxARIAAtomic {
723  return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
724}
725
726- (NSString*)moxARIALive {
727  return utils::GetAccAttr(self, nsGkAtoms::aria_live);
728}
729
730- (NSString*)moxARIARelevant {
731  if (NSString* relevant =
732          utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
733    return relevant;
734  }
735
736  // Default aria-relevant value
737  return @"additions text";
738}
739
740- (id)moxTitleUIElement {
741  MOZ_ASSERT(!mGeckoAccessible.IsNull());
742
743  NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
744  if ([relations count] == 1) {
745    return [relations firstObject];
746  }
747
748  return nil;
749}
750
751- (NSString*)moxDOMIdentifier {
752  MOZ_ASSERT(!mGeckoAccessible.IsNull());
753
754  nsAutoString id;
755  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
756    if (acc->GetContent()) {
757      nsCoreUtils::GetID(acc->GetContent(), id);
758    }
759  } else {
760    mGeckoAccessible.AsProxy()->DOMNodeID(id);
761  }
762
763  return nsCocoaUtils::ToNSString(id);
764}
765
766- (NSNumber*)moxRequired {
767  return @([self stateWithMask:states::REQUIRED] != 0);
768}
769
770- (NSNumber*)moxElementBusy {
771  return @([self stateWithMask:states::BUSY] != 0);
772}
773
774- (NSArray*)moxLinkedUIElements {
775  return [self getRelationsByType:RelationType::FLOWS_TO];
776}
777
778- (NSArray*)moxARIAControls {
779  return [self getRelationsByType:RelationType::CONTROLLER_FOR];
780}
781
782- (mozAccessible*)topWebArea {
783  AccessibleOrProxy doc = [self geckoDocument];
784  while (!doc.IsNull()) {
785    if (doc.IsAccessible()) {
786      DocAccessible* docAcc = doc.AsAccessible()->AsDoc();
787      if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
788        return GetNativeFromGeckoAccessible(docAcc);
789      }
790
791      doc = docAcc->ParentDocument();
792    } else {
793      DocAccessibleParent* docProxy = doc.AsProxy()->AsDoc();
794      if (docProxy->IsTopLevel()) {
795        return GetNativeFromGeckoAccessible(docProxy);
796      }
797      doc = docProxy->ParentDoc();
798    }
799  }
800
801  return nil;
802}
803
804- (void)handleRoleChanged:(mozilla::a11y::role)newRole {
805  mRole = newRole;
806  mARIARole = nullptr;
807
808  // For testing purposes
809  [self moxPostNotification:@"AXMozRoleChanged"];
810}
811
812- (id)moxEditableAncestor {
813  return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
814    return [moxAcc isKindOfClass:[mozTextAccessible class]];
815  }];
816}
817
818- (id)moxHighestEditableAncestor {
819  id highestAncestor = [self moxEditableAncestor];
820  while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
821    id ancestorParent = [highestAncestor moxUnignoredParent];
822    if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
823      break;
824    }
825
826    id higherAncestor = [ancestorParent moxEditableAncestor];
827
828    if (!higherAncestor) {
829      break;
830    }
831
832    highestAncestor = higherAncestor;
833  }
834
835  return highestAncestor;
836}
837
838- (id)moxFocusableAncestor {
839  // XXX: Checking focusable state up the chain can be expensive. For now,
840  // we can just return AXEditableAncestor since the main use case for this
841  // is rich text editing with links.
842  return [self moxEditableAncestor];
843}
844
845#ifndef RELEASE_OR_BETA
846- (NSString*)moxMozDebugDescription {
847  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
848
849  NSMutableString* domInfo = [NSMutableString string];
850  if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
851    [domInfo appendFormat:@" %@", tagName];
852    NSString* domID = [self moxDOMIdentifier];
853    if ([domID length]) {
854      [domInfo appendFormat:@"#%@", domID];
855    }
856    if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
857      [domInfo
858          appendFormat:@".%@",
859                       [className stringByReplacingOccurrencesOfString:@" "
860                                                            withString:@"."]];
861    }
862  }
863
864  return [NSString stringWithFormat:@"<%@: %p %@%@>",
865                                    NSStringFromClass([self class]), self,
866                                    [self moxRole], domInfo];
867
868  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
869}
870#endif
871
872- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
873  // Create our search object and set it up with the searchPredicate
874  // params. The init function does additional parsing. We pass a
875  // reference to the web area to use as a start element if one is not
876  // specified.
877  MOXSearchInfo* search =
878      [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
879                                         andRoot:self] autorelease];
880
881  return [search performSearch];
882}
883
884- (NSNumber*)moxUIElementCountForSearchPredicate:
885    (NSDictionary*)searchPredicate {
886  return [NSNumber
887      numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
888                           count]];
889}
890
891- (void)moxSetFocused:(NSNumber*)focused {
892  MOZ_ASSERT(!mGeckoAccessible.IsNull());
893
894  if ([focused boolValue]) {
895    if (mGeckoAccessible.IsAccessible()) {
896      mGeckoAccessible.AsAccessible()->TakeFocus();
897    } else {
898      mGeckoAccessible.AsProxy()->TakeFocus();
899    }
900  }
901}
902
903- (void)moxPerformScrollToVisible {
904  MOZ_ASSERT(!mGeckoAccessible.IsNull());
905
906  if (mGeckoAccessible.IsAccessible()) {
907    // Need strong ref because of MOZ_CAN_RUN_SCRIPT
908    RefPtr<LocalAccessible> acc = mGeckoAccessible.AsAccessible();
909    acc->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
910  } else {
911    mGeckoAccessible.AsProxy()->ScrollTo(
912        nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
913  }
914}
915
916- (void)moxPerformShowMenu {
917  MOZ_ASSERT(!mGeckoAccessible.IsNull());
918
919  nsIntRect bounds = mGeckoAccessible.IsAccessible()
920                         ? mGeckoAccessible.AsAccessible()->Bounds()
921                         : mGeckoAccessible.AsProxy()->Bounds();
922  // We don't need to convert this rect into mac coordinates because the
923  // mouse event synthesizer expects layout (gecko) coordinates.
924  LayoutDeviceIntRect geckoRect = LayoutDeviceIntRect::FromUnknownRect(bounds);
925
926  LocalAccessible* rootAcc =
927      mGeckoAccessible.IsAccessible()
928          ? mGeckoAccessible.AsAccessible()->RootAccessible()
929          : mGeckoAccessible.AsProxy()
930                ->OuterDocOfRemoteBrowser()
931                ->RootAccessible();
932  id objOrView =
933      GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
934
935  LayoutDeviceIntPoint p =
936      LayoutDeviceIntPoint(geckoRect.X() + (geckoRect.Width() / 2),
937                           geckoRect.Y() + (geckoRect.Height() / 2));
938  nsIWidget* widget = [objOrView widget];
939  widget->SynthesizeNativeMouseEvent(
940      p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
941      nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
942}
943
944- (void)moxPerformPress {
945  MOZ_ASSERT(!mGeckoAccessible.IsNull());
946
947  if (mGeckoAccessible.IsAccessible()) {
948    mGeckoAccessible.AsAccessible()->DoAction(0);
949  } else {
950    mGeckoAccessible.AsProxy()->DoAction(0);
951  }
952
953  // Activating accessible may alter its state.
954  [self invalidateState];
955}
956
957#pragma mark -
958
959- (BOOL)disableChild:(mozAccessible*)child {
960  return NO;
961}
962
963- (void)maybePostLiveRegionChanged {
964  id<MOXAccessible> liveRegion =
965      [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
966        return [moxAcc moxIsLiveRegion];
967      }];
968
969  if (liveRegion) {
970    [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
971  }
972}
973
974- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
975  if (LocalAccessible* acc = mGeckoAccessible.AsAccessible()) {
976    NSMutableArray<mozAccessible*>* relations =
977        [[[NSMutableArray alloc] init] autorelease];
978    Relation rel = acc->RelationByType(relationType);
979    while (LocalAccessible* relAcc = rel.Next()) {
980      if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
981        [relations addObject:relNative];
982      }
983    }
984
985    return relations;
986  }
987
988  RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
989  nsTArray<RemoteAccessible*> rel = proxy->RelationByType(relationType);
990  return utils::ConvertToNSArray(rel);
991}
992
993- (void)handleAccessibleTextChangeEvent:(NSString*)change
994                               inserted:(BOOL)isInserted
995                            inContainer:(const AccessibleOrProxy&)container
996                                     at:(int32_t)start {
997}
998
999- (void)handleAccessibleEvent:(uint32_t)eventType {
1000  switch (eventType) {
1001    case nsIAccessibleEvent::EVENT_FOCUS:
1002      [self moxPostNotification:
1003                NSAccessibilityFocusedUIElementChangedNotification];
1004      break;
1005    case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
1006      [self moxPostNotification:@"AXMenuOpened"];
1007      break;
1008    case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
1009      [self moxPostNotification:@"AXMenuClosed"];
1010      break;
1011    case nsIAccessibleEvent::EVENT_SELECTION:
1012    case nsIAccessibleEvent::EVENT_SELECTION_ADD:
1013    case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
1014    case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
1015      [self moxPostNotification:
1016                NSAccessibilitySelectedChildrenChangedNotification];
1017      break;
1018    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
1019      if (![self stateWithMask:states::SELECTABLE_TEXT]) {
1020        break;
1021      }
1022      // We consider any caret move event to be a selected text change event.
1023      // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
1024      // reduntant.
1025      MOXTextMarkerDelegate* delegate =
1026          static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
1027      NSMutableDictionary* userInfo =
1028          [[[delegate selectionChangeInfo] mutableCopy] autorelease];
1029      userInfo[@"AXTextChangeElement"] = self;
1030
1031      mozAccessible* webArea = [self topWebArea];
1032      [webArea
1033          moxPostNotification:NSAccessibilitySelectedTextChangedNotification
1034                 withUserInfo:userInfo];
1035      [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
1036                   withUserInfo:userInfo];
1037      break;
1038    }
1039    case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
1040      mIsLiveRegion = true;
1041      [self moxPostNotification:@"AXLiveRegionCreated"];
1042      break;
1043    case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
1044      mIsLiveRegion = false;
1045      break;
1046    case nsIAccessibleEvent::EVENT_REORDER:
1047      [self maybePostLiveRegionChanged];
1048      break;
1049    case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
1050      if (![self providesLabelNotTitle]) {
1051        [self moxPostNotification:NSAccessibilityTitleChangedNotification];
1052      }
1053      [self maybePostLiveRegionChanged];
1054      break;
1055    }
1056  }
1057}
1058
1059- (void)expire {
1060  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
1061
1062  [self invalidateState];
1063
1064  mGeckoAccessible.SetBits(0);
1065
1066  [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
1067
1068  NS_OBJC_END_TRY_IGNORE_BLOCK;
1069}
1070
1071- (BOOL)isExpired {
1072  return !mGeckoAccessible.AsAccessible() && !mGeckoAccessible.AsProxy();
1073}
1074
1075@end
1076