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#include "DocAccessibleWrap.h"
9#include "nsObjCExceptions.h"
10#include "nsCocoaUtils.h"
11
12#include "LocalAccessible-inl.h"
13#include "nsAccUtils.h"
14#include "Role.h"
15#include "TextRange.h"
16#include "gfxPlatform.h"
17
18#import "MOXLandmarkAccessibles.h"
19#import "MOXMathAccessibles.h"
20#import "MOXTextMarkerDelegate.h"
21#import "MOXWebAreaAccessible.h"
22#import "mozAccessible.h"
23#import "mozActionElements.h"
24#import "mozHTMLAccessible.h"
25#import "mozSelectableElements.h"
26#import "mozTableAccessible.h"
27#import "mozTextAccessible.h"
28
29using namespace mozilla;
30using namespace mozilla::a11y;
31
32AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
33    : LocalAccessible(aContent, aDoc),
34      mNativeObject(nil),
35      mNativeInited(false) {
36  if (aContent && aContent->IsElement() && aDoc) {
37    // Check if this accessible is a live region and queue it
38    // it for dispatching an event after it has been inserted.
39    DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
40    static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
41        nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
42    int32_t attrValue = aContent->AsElement()->FindAttrValueIn(
43        kNameSpaceID_None, nsGkAtoms::aria_live, sLiveRegionValues,
44        eIgnoreCase);
45    if (attrValue == 0) {
46      // aria-live is "off", do nothing.
47    } else if (attrValue > 0) {
48      // aria-live attribute is polite or assertive. It's live!
49      doc->QueueNewLiveRegion(this);
50    } else if (const nsRoleMapEntry* roleMap =
51                   aria::GetRoleMap(aContent->AsElement())) {
52      // aria role defines it as a live region. It's live!
53      if (roleMap->liveAttRule == ePoliteLiveAttr ||
54          roleMap->liveAttRule == eAssertiveLiveAttr) {
55        doc->QueueNewLiveRegion(this);
56      }
57    } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
58                   aContent, nsGkAtoms::aria_live)) {
59      // HTML element defines it as a live region. It's live!
60      if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
61        doc->QueueNewLiveRegion(this);
62      }
63    }
64  }
65}
66
67AccessibleWrap::~AccessibleWrap() {}
68
69mozAccessible* AccessibleWrap::GetNativeObject() {
70  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
71
72  if (!mNativeInited && !mNativeObject) {
73    // We don't creat OSX accessibles for xul tooltips, defunct accessibles,
74    // <br> (whitespace) elements, or pruned children.
75    //
76    // To maintain a scripting environment where the XPCOM accessible hierarchy
77    // look the same on all platforms, we still let the C++ objects be created
78    // though.
79    if (!IsXULTooltip() && !IsDefunct() && Role() != roles::WHITESPACE) {
80      mNativeObject = [[GetNativeType() alloc] initWithAccessible:this];
81    }
82  }
83
84  mNativeInited = true;
85
86  return mNativeObject;
87
88  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
89}
90
91void AccessibleWrap::GetNativeInterface(void** aOutInterface) {
92  *aOutInterface = static_cast<void*>(GetNativeObject());
93}
94
95// overridden in subclasses to create the right kind of object. by default we
96// create a generic 'mozAccessible' node.
97Class AccessibleWrap::GetNativeType() {
98  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
99
100  if (IsXULTabpanels()) {
101    return [mozPaneAccessible class];
102  }
103
104  if (IsTable()) {
105    return [mozTableAccessible class];
106  }
107
108  if (IsTableRow()) {
109    return [mozTableRowAccessible class];
110  }
111
112  if (IsTableCell()) {
113    return [mozTableCellAccessible class];
114  }
115
116  if (IsDoc()) {
117    return [MOXWebAreaAccessible class];
118  }
119
120  return GetTypeFromRole(Role());
121
122  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
123}
124
125// this method is very important. it is fired when an accessible object "dies".
126// after this point the object might still be around (because some 3rd party
127// still has a ref to it), but it is in fact 'dead'.
128void AccessibleWrap::Shutdown() {
129  // this ensure we will not try to re-create the native object.
130  mNativeInited = true;
131
132  // we really intend to access the member directly.
133  if (mNativeObject) {
134    [mNativeObject expire];
135    [mNativeObject release];
136    mNativeObject = nil;
137  }
138
139  LocalAccessible::Shutdown();
140}
141
142nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
143  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
144
145  nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
146  NS_ENSURE_SUCCESS(rv, rv);
147
148  uint32_t eventType = aEvent->GetEventType();
149
150  if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
151    DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document());
152    doc->ProcessNewLiveRegions();
153  }
154
155  if (IPCAccessibilityActive()) {
156    return NS_OK;
157  }
158
159  LocalAccessible* eventTarget = nullptr;
160
161  switch (eventType) {
162    case nsIAccessibleEvent::EVENT_SELECTION:
163    case nsIAccessibleEvent::EVENT_SELECTION_ADD:
164    case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
165      AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
166      // The "widget" is the selected widget's container. In OSX
167      // it is the target of the selection changed event.
168      eventTarget = selEvent->Widget();
169      break;
170    }
171    case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
172    case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
173      LocalAccessible* acc = aEvent->GetAccessible();
174      // If there is a text input ancestor, use it as the event source.
175      while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
176        acc = acc->LocalParent();
177      }
178      eventTarget = acc ? acc : aEvent->GetAccessible();
179      break;
180    }
181    default:
182      eventTarget = aEvent->GetAccessible();
183      break;
184  }
185
186  mozAccessible* nativeAcc = nil;
187  eventTarget->GetNativeInterface((void**)&nativeAcc);
188  if (!nativeAcc) {
189    return NS_ERROR_FAILURE;
190  }
191
192  switch (eventType) {
193    case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
194      AccStateChangeEvent* event = downcast_accEvent(aEvent);
195      [nativeAcc stateChanged:event->GetState()
196                    isEnabled:event->IsStateEnabled()];
197      break;
198    }
199
200    case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: {
201      MOXTextMarkerDelegate* delegate =
202          [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
203      AccTextSelChangeEvent* event = downcast_accEvent(aEvent);
204      AutoTArray<TextRange, 1> ranges;
205      event->SelectionRanges(&ranges);
206
207      if (ranges.Length()) {
208        // Cache selection in delegate.
209        [delegate setSelectionFrom:ranges[0].StartContainer()
210                                at:ranges[0].StartOffset()
211                                to:ranges[0].EndContainer()
212                                at:ranges[0].EndOffset()];
213      }
214
215      [nativeAcc handleAccessibleEvent:eventType];
216      break;
217    }
218
219    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
220      AccCaretMoveEvent* event = downcast_accEvent(aEvent);
221      int32_t caretOffset = event->GetCaretOffset();
222      MOXTextMarkerDelegate* delegate =
223          [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
224      [delegate setCaretOffset:eventTarget at:caretOffset];
225      if (event->IsSelectionCollapsed()) {
226        // If the selection is collapsed, invalidate our text selection cache.
227        [delegate setSelectionFrom:eventTarget
228                                at:caretOffset
229                                to:eventTarget
230                                at:caretOffset];
231      }
232
233      if (mozTextAccessible* textAcc = static_cast<mozTextAccessible*>(
234              [nativeAcc moxEditableAncestor])) {
235        [textAcc
236            handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
237      } else {
238        [nativeAcc
239            handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
240      }
241      break;
242    }
243
244    case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
245    case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
246      AccTextChangeEvent* tcEvent = downcast_accEvent(aEvent);
247      [nativeAcc handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(
248                                                     tcEvent->ModifiedText())
249                                        inserted:tcEvent->IsTextInserted()
250                                     inContainer:aEvent->GetAccessible()
251                                              at:tcEvent->GetStartOffset()];
252      break;
253    }
254
255    case nsIAccessibleEvent::EVENT_FOCUS:
256    case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
257    case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
258    case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
259    case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
260    case nsIAccessibleEvent::EVENT_REORDER:
261    case nsIAccessibleEvent::EVENT_SELECTION:
262    case nsIAccessibleEvent::EVENT_SELECTION_ADD:
263    case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
264    case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
265    case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
266    case nsIAccessibleEvent::EVENT_NAME_CHANGE:
267    case nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED:
268    case nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED:
269      [nativeAcc handleAccessibleEvent:eventType];
270      break;
271
272    default:
273      break;
274  }
275
276  return NS_OK;
277
278  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
279}
280
281bool AccessibleWrap::ApplyPostFilter(const EWhichPostFilter& aSearchKey,
282                                     const nsString& aSearchText) {
283  // We currently only support the eContainsText post filter.
284  MOZ_ASSERT(aSearchKey == EWhichPostFilter::eContainsText,
285             "Only search text supported");
286  nsAutoString name;
287  Name(name);
288  return name.Find(aSearchText, true) != kNotFound;
289}
290
291////////////////////////////////////////////////////////////////////////////////
292// AccessibleWrap protected
293
294Class a11y::GetTypeFromRole(roles::Role aRole) {
295  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
296
297  switch (aRole) {
298    case roles::COMBOBOX:
299      return [mozPopupButtonAccessible class];
300
301    case roles::PUSHBUTTON:
302      return [mozButtonAccessible class];
303
304    case roles::PAGETAB:
305      return [mozTabAccessible class];
306
307    case roles::CHECKBUTTON:
308    case roles::TOGGLE_BUTTON:
309      return [mozCheckboxAccessible class];
310
311    case roles::RADIOBUTTON:
312      return [mozRadioButtonAccessible class];
313
314    case roles::SPINBUTTON:
315    case roles::SLIDER:
316      return [mozIncrementableAccessible class];
317
318    case roles::HEADING:
319      return [mozHeadingAccessible class];
320
321    case roles::PAGETABLIST:
322      return [mozTabGroupAccessible class];
323
324    case roles::ENTRY:
325    case roles::CAPTION:
326    case roles::ACCEL_LABEL:
327    case roles::EDITCOMBOBOX:
328    case roles::PASSWORD_TEXT:
329      // normal textfield (static or editable)
330      return [mozTextAccessible class];
331
332    case roles::TEXT_LEAF:
333    case roles::STATICTEXT:
334      return [mozTextLeafAccessible class];
335
336    case roles::LANDMARK:
337      return [MOXLandmarkAccessible class];
338
339    case roles::LINK:
340      return [mozLinkAccessible class];
341
342    case roles::LISTBOX:
343      return [mozListboxAccessible class];
344
345    case roles::LISTITEM:
346      return [MOXListItemAccessible class];
347
348    case roles::OPTION: {
349      return [mozOptionAccessible class];
350    }
351
352    case roles::RICH_OPTION: {
353      return [mozSelectableChildAccessible class];
354    }
355
356    case roles::COMBOBOX_LIST:
357    case roles::MENUBAR:
358    case roles::MENUPOPUP: {
359      return [mozMenuAccessible class];
360    }
361
362    case roles::COMBOBOX_OPTION:
363    case roles::PARENT_MENUITEM:
364    case roles::MENUITEM: {
365      return [mozMenuItemAccessible class];
366    }
367
368    case roles::MATHML_ROOT:
369      return [MOXMathRootAccessible class];
370
371    case roles::MATHML_SQUARE_ROOT:
372      return [MOXMathSquareRootAccessible class];
373
374    case roles::MATHML_FRACTION:
375      return [MOXMathFractionAccessible class];
376
377    case roles::MATHML_SUB:
378    case roles::MATHML_SUP:
379    case roles::MATHML_SUB_SUP:
380      return [MOXMathSubSupAccessible class];
381
382    case roles::MATHML_UNDER:
383    case roles::MATHML_OVER:
384    case roles::MATHML_UNDER_OVER:
385      return [MOXMathUnderOverAccessible class];
386
387    case roles::SUMMARY:
388      return [MOXSummaryAccessible class];
389
390    case roles::OUTLINE:
391    case roles::TREE_TABLE:
392      return [mozOutlineAccessible class];
393
394    case roles::OUTLINEITEM:
395      return [mozOutlineRowAccessible class];
396
397    default:
398      return [mozAccessible class];
399  }
400
401  return nil;
402
403  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
404}
405