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 "MOXWebAreaAccessible.h"
9
10#import "MOXSearchInfo.h"
11
12#include "nsCocoaUtils.h"
13#include "DocAccessibleParent.h"
14
15using namespace mozilla::a11y;
16
17@implementation MOXRootGroup
18
19- (id)initWithParent:(MOXWebAreaAccessible*)parent {
20  // The parent is always a MOXWebAreaAccessible
21  mParent = parent;
22  return [super init];
23}
24
25- (NSString*)moxRole {
26  return NSAccessibilityGroupRole;
27}
28
29- (NSString*)moxRoleDescription {
30  if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) {
31    return utils::LocalizedString(u"application"_ns);
32  }
33
34  return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil);
35}
36
37- (id<mozAccessible>)moxParent {
38  return mParent;
39}
40
41- (NSArray*)moxChildren {
42  // Reparent the children of the web area here.
43  return [mParent rootGroupChildren];
44}
45
46- (NSString*)moxIdentifier {
47  // This is mostly for testing purposes to assert that this is the generated
48  // root group.
49  return @"root-group";
50}
51
52- (NSString*)moxSubrole {
53  // Steal the subrole internally mapped to the web area.
54  return [mParent moxSubrole];
55}
56
57- (id)moxHitTest:(NSPoint)point {
58  return [mParent moxHitTest:point];
59}
60
61- (NSValue*)moxPosition {
62  return [mParent moxPosition];
63}
64
65- (NSValue*)moxSize {
66  return [mParent moxSize];
67}
68
69- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
70  MOXSearchInfo* search =
71      [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
72                                         andRoot:self] autorelease];
73
74  return [search performSearch];
75}
76
77- (NSNumber*)moxUIElementCountForSearchPredicate:
78    (NSDictionary*)searchPredicate {
79  return [NSNumber
80      numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
81                           count]];
82}
83
84- (BOOL)disableChild:(id)child {
85  return NO;
86}
87
88- (void)expire {
89  mParent = nil;
90  [super expire];
91}
92
93- (BOOL)isExpired {
94  MOZ_ASSERT((mParent == nil) == mIsExpired);
95
96  return [super isExpired];
97}
98
99@end
100
101@implementation MOXWebAreaAccessible
102
103- (NSString*)moxRole {
104  // The OS role is AXWebArea regardless of the gecko role
105  // (APPLICATION or DOCUMENT).
106  // If the web area has a role of APPLICATION, its root group will
107  // reflect that in a subrole/description.
108  return @"AXWebArea";
109}
110
111- (NSString*)moxRoleDescription {
112  // The role description is "HTML Content" regardless of the gecko role
113  // (APPLICATION or DOCUMENT)
114  return utils::LocalizedString(u"htmlContent"_ns);
115}
116
117- (NSURL*)moxURL {
118  if ([self isExpired]) {
119    return nil;
120  }
121
122  nsAutoString url;
123  if (mGeckoAccessible.IsAccessible()) {
124    MOZ_ASSERT(mGeckoAccessible.AsAccessible()->IsDoc());
125    DocAccessible* acc = mGeckoAccessible.AsAccessible()->AsDoc();
126    acc->URL(url);
127  } else {
128    RemoteAccessible* proxy = mGeckoAccessible.AsProxy();
129    proxy->URL(url);
130  }
131
132  if (url.IsEmpty()) {
133    return nil;
134  }
135
136  return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
137}
138
139- (NSNumber*)moxLoaded {
140  if ([self isExpired]) {
141    return nil;
142  }
143  // We are loaded if we aren't busy or stale
144  return @([self stateWithMask:(states::BUSY & states::STALE)] == 0);
145}
146
147// overrides
148- (NSNumber*)moxLoadingProgress {
149  if ([self isExpired]) {
150    return nil;
151  }
152
153  if ([self stateWithMask:states::STALE] != 0) {
154    // We expose stale state until the document is ready (DOM is loaded and tree
155    // is constructed) so we indicate load hasn't started while this state is
156    // present.
157    return @0.0;
158  }
159
160  if ([self stateWithMask:states::BUSY] != 0) {
161    // We expose state busy until the document and all its subdocuments are
162    // completely loaded, so we indicate partial loading here
163    return @0.5;
164  }
165
166  // if we are not busy and not stale, we are loaded
167  return @1.0;
168}
169
170- (NSArray*)moxLinkUIElements {
171  NSDictionary* searchPredicate = @{
172    @"AXSearchKey" : @"AXLinkSearchKey",
173    @"AXImmediateDescendantsOnly" : @NO,
174    @"AXResultsLimit" : @(-1),
175    @"AXDirection" : @"AXDirectionNext",
176  };
177
178  return [self moxUIElementsForSearchPredicate:searchPredicate];
179}
180
181- (void)handleAccessibleEvent:(uint32_t)eventType {
182  switch (eventType) {
183    case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
184      [self moxPostNotification:
185                NSAccessibilityFocusedUIElementChangedNotification];
186      if ((mGeckoAccessible.IsProxy() && mGeckoAccessible.AsProxy()->IsDoc() &&
187           mGeckoAccessible.AsProxy()->AsDoc()->IsTopLevel()) ||
188          (mGeckoAccessible.IsAccessible() &&
189           !mGeckoAccessible.AsAccessible()->IsRoot() &&
190           mGeckoAccessible.AsAccessible()->AsDoc()->ParentDocument() &&
191           mGeckoAccessible.AsAccessible()
192               ->AsDoc()
193               ->ParentDocument()
194               ->IsRoot())) {
195        // we fire an AXLoadComplete event on top-level documents only
196        [self moxPostNotification:@"AXLoadComplete"];
197      } else {
198        // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess)
199        // and we fire AXLayoutComplete instead
200        [self moxPostNotification:@"AXLayoutComplete"];
201      }
202      break;
203  }
204
205  [super handleAccessibleEvent:eventType];
206}
207
208- (NSArray*)rootGroupChildren {
209  // This method is meant to expose the doc's children to the root group.
210  return [super moxChildren];
211}
212
213- (NSArray*)moxUnignoredChildren {
214  if (id rootGroup = [self rootGroup]) {
215    return @[ [self rootGroup] ];
216  }
217
218  // There is no root group, expose the children here directly.
219  return [super moxUnignoredChildren];
220}
221
222- (BOOL)moxBlockSelector:(SEL)selector {
223  if (selector == @selector(moxSubrole)) {
224    // Never expose a subrole for a web area.
225    return YES;
226  }
227
228  if (selector == @selector(moxElementBusy)) {
229    // Don't confuse aria-busy with a document's busy state.
230    return YES;
231  }
232
233  return [super moxBlockSelector:selector];
234}
235
236- (void)moxPostNotification:(NSString*)notification {
237  if (![notification isEqualToString:@"AXElementBusyChanged"]) {
238    // Suppress AXElementBusyChanged since it uses gecko's BUSY state
239    // to tell VoiceOver about aria-busy changes. We use that state
240    // differently in documents.
241    [super moxPostNotification:notification];
242  }
243}
244
245- (id)rootGroup {
246  NSArray* children = [super moxUnignoredChildren];
247  if (mRole != roles::APPLICATION && [children count] == 1 &&
248      [[[children firstObject] moxUnignoredChildren] count] != 0) {
249    // We only need a root group if our document:
250    // (1) has multiple children, or
251    // (2) a one child that is a leaf, or
252    // (3) has a role of APPLICATION
253    return nil;
254  }
255
256  if (!mRootGroup) {
257    mRootGroup = [[MOXRootGroup alloc] initWithParent:self];
258  }
259
260  return mRootGroup;
261}
262
263- (void)expire {
264  [mRootGroup expire];
265  [super expire];
266}
267
268- (void)dealloc {
269  // This object can only be dealoced after the gecko accessible wrapper
270  // reference is released, and that happens after expire is called.
271  MOZ_ASSERT([self isExpired]);
272  [mRootGroup release];
273
274  [super dealloc];
275}
276
277@end
278