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 <Cocoa/Cocoa.h>
9
10#include "mozilla/Preferences.h"
11
12#import "MOXTextMarkerDelegate.h"
13
14using namespace mozilla::a11y;
15
16#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug"
17
18static nsTHashMap<nsUint64HashKey, MOXTextMarkerDelegate*> sDelegates;
19
20@implementation MOXTextMarkerDelegate
21
22+ (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc {
23  MOZ_ASSERT(!aDoc.IsNull());
24
25  MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits());
26  if (!delegate) {
27    delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
28    sDelegates.InsertOrUpdate(aDoc.Bits(), delegate);
29    [delegate retain];
30  }
31
32  return delegate;
33}
34
35+ (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc {
36  MOZ_ASSERT(!aDoc.IsNull());
37
38  MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits());
39  if (delegate) {
40    sDelegates.Remove(aDoc.Bits());
41    [delegate release];
42  }
43}
44
45- (id)initWithDoc:(AccessibleOrProxy)aDoc {
46  MOZ_ASSERT(!aDoc.IsNull(), "Cannot init MOXTextDelegate with null");
47  if ((self = [super init])) {
48    mGeckoDocAccessible = aDoc;
49  }
50
51  return self;
52}
53
54- (void)dealloc {
55  [self invalidateSelection];
56  [super dealloc];
57}
58
59- (void)setSelectionFrom:(AccessibleOrProxy)startContainer
60                      at:(int32_t)startOffset
61                      to:(AccessibleOrProxy)endContainer
62                      at:(int32_t)endOffset {
63  GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset),
64                                 GeckoTextMarker(endContainer, endOffset));
65
66  // We store it as an AXTextMarkerRange because it is a safe
67  // way to keep a weak reference - when we need to use the
68  // range we can convert it back to a GeckoTextMarkerRange
69  // and check that it's valid.
70  mSelection = [selection.CreateAXTextMarkerRange() retain];
71}
72
73- (void)setCaretOffset:(mozilla::a11y::AccessibleOrProxy)container
74                    at:(int32_t)offset {
75  GeckoTextMarker caretMarker(container, offset);
76
77  mPrevCaret = mCaret;
78  mCaret = [caretMarker.CreateAXTextMarker() retain];
79}
80
81// This returns an info object to pass with AX SelectedTextChanged events.
82// It uses the current and previous caret position to make decisions
83// regarding which attributes to add to the info object.
84- (NSDictionary*)selectionChangeInfo {
85  GeckoTextMarkerRange selectedGeckoRange =
86      GeckoTextMarkerRange(mGeckoDocAccessible, mSelection);
87  int32_t stateChangeType = selectedGeckoRange.mStart == selectedGeckoRange.mEnd
88                                ? AXTextStateChangeTypeSelectionMove
89                                : AXTextStateChangeTypeSelectionExtend;
90
91  // This is the base info object, includes the selected marker range and
92  // the change type depending on the collapsed state of the selection.
93  NSMutableDictionary* info = [[@{
94    @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid() ? mSelection
95                                                                : [NSNull null],
96    @"AXTextStateChangeType" : @(stateChangeType),
97  } mutableCopy] autorelease];
98
99  GeckoTextMarker caretMarker(mGeckoDocAccessible, mCaret);
100  GeckoTextMarker prevCaretMarker(mGeckoDocAccessible, mPrevCaret);
101  if (!caretMarker.IsValid()) {
102    // If the current caret is invalid, stop here and return base info.
103    return info;
104  }
105
106  mozAccessible* caretEditable =
107      [GetNativeFromGeckoAccessible(caretMarker.mContainer)
108          moxEditableAncestor];
109
110  if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) {
111    // If we are not in an editable, VO expects AXTextStateSync to be present
112    // and true.
113    info[@"AXTextStateSync"] = @YES;
114  }
115
116  if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) {
117    // If we have no stored previous marker, stop here.
118    return info;
119  }
120
121  mozAccessible* prevCaretEditable =
122      [GetNativeFromGeckoAccessible(prevCaretMarker.mContainer)
123          moxEditableAncestor];
124
125  if (prevCaretEditable != caretEditable) {
126    // If the caret goes in or out of an editable, consider the
127    // move direction "discontiguous".
128    info[@"AXTextSelectionDirection"] =
129        @(AXTextSelectionDirectionDiscontiguous);
130    if ([[caretEditable moxFocused] boolValue]) {
131      // If the caret is in a new focused editable, VO expects this attribute to
132      // be present and to be true.
133      info[@"AXTextSelectionChangedFocus"] = @YES;
134    }
135
136    return info;
137  }
138
139  bool isForward = prevCaretMarker < caretMarker;
140  uint32_t deltaLength =
141      GeckoTextMarkerRange(isForward ? prevCaretMarker : caretMarker,
142                           isForward ? caretMarker : prevCaretMarker)
143          .Length();
144
145  // Determine selection direction with marker comparison.
146  // If the delta between the two markers is more than one, consider it
147  // a word. Not accurate, but good enough for VO.
148  [info addEntriesFromDictionary:@{
149    @"AXTextSelectionDirection" : isForward
150        ? @(AXTextSelectionDirectionNext)
151        : @(AXTextSelectionDirectionPrevious),
152    @"AXTextSelectionGranularity" : deltaLength == 1
153        ? @(AXTextSelectionGranularityCharacter)
154        : @(AXTextSelectionGranularityWord)
155  }];
156
157  return info;
158}
159
160- (void)invalidateSelection {
161  [mSelection release];
162  [mCaret release];
163  [mPrevCaret release];
164  mSelection = nil;
165}
166
167- (mozilla::a11y::GeckoTextMarkerRange)selection {
168  return mozilla::a11y::GeckoTextMarkerRange(mGeckoDocAccessible, mSelection);
169}
170
171- (id)moxStartTextMarker {
172  GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
173  return geckoTextPoint.CreateAXTextMarker();
174}
175
176- (id)moxEndTextMarker {
177  uint32_t characterCount =
178      mGeckoDocAccessible.IsProxy()
179          ? mGeckoDocAccessible.AsProxy()->CharacterCount()
180          : mGeckoDocAccessible.AsAccessible()
181                ->Document()
182                ->AsHyperText()
183                ->CharacterCount();
184  GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount);
185  return geckoTextPoint.CreateAXTextMarker();
186}
187
188- (id)moxSelectedTextMarkerRange {
189  return mSelection &&
190                 GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid()
191             ? mSelection
192             : nil;
193}
194
195- (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange {
196  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
197                                            textMarkerRange);
198  if (!range.IsValid()) {
199    return @"";
200  }
201
202  return range.Text();
203}
204
205- (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange {
206  return @([[self moxStringForTextMarkerRange:textMarkerRange] length]);
207}
208
209- (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers {
210  if ([textMarkers count] != 2) {
211    // Don't allow anything but a two member array.
212    return nil;
213  }
214
215  GeckoTextMarker p1(mGeckoDocAccessible, textMarkers[0]);
216  GeckoTextMarker p2(mGeckoDocAccessible, textMarkers[1]);
217
218  if (!p1.IsValid() || !p2.IsValid()) {
219    // If either marker is invalid, return nil.
220    return nil;
221  }
222
223  bool ordered = p1 < p2;
224  GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);
225
226  return range.CreateAXTextMarkerRange();
227}
228
229- (id)moxStartTextMarkerForTextMarkerRange:(id)textMarkerRange {
230  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
231                                            textMarkerRange);
232
233  return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil;
234}
235
236- (id)moxEndTextMarkerForTextMarkerRange:(id)textMarkerRange {
237  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
238                                            textMarkerRange);
239
240  return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil;
241}
242
243- (id)moxLeftWordTextMarkerRangeForTextMarker:(id)textMarker {
244  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
245  if (!geckoTextMarker.IsValid()) {
246    return nil;
247  }
248
249  return geckoTextMarker.Range(EWhichRange::eLeftWord)
250      .CreateAXTextMarkerRange();
251}
252
253- (id)moxRightWordTextMarkerRangeForTextMarker:(id)textMarker {
254  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
255  if (!geckoTextMarker.IsValid()) {
256    return nil;
257  }
258
259  return geckoTextMarker.Range(EWhichRange::eRightWord)
260      .CreateAXTextMarkerRange();
261}
262
263- (id)moxLineTextMarkerRangeForTextMarker:(id)textMarker {
264  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
265  if (!geckoTextMarker.IsValid()) {
266    return nil;
267  }
268
269  return geckoTextMarker.Range(EWhichRange::eLine).CreateAXTextMarkerRange();
270}
271
272- (id)moxLeftLineTextMarkerRangeForTextMarker:(id)textMarker {
273  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
274  if (!geckoTextMarker.IsValid()) {
275    return nil;
276  }
277
278  return geckoTextMarker.Range(EWhichRange::eLeftLine)
279      .CreateAXTextMarkerRange();
280}
281
282- (id)moxRightLineTextMarkerRangeForTextMarker:(id)textMarker {
283  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
284  if (!geckoTextMarker.IsValid()) {
285    return nil;
286  }
287
288  return geckoTextMarker.Range(EWhichRange::eRightLine)
289      .CreateAXTextMarkerRange();
290}
291
292- (id)moxParagraphTextMarkerRangeForTextMarker:(id)textMarker {
293  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
294  if (!geckoTextMarker.IsValid()) {
295    return nil;
296  }
297
298  return geckoTextMarker.Range(EWhichRange::eParagraph)
299      .CreateAXTextMarkerRange();
300}
301
302// override
303- (id)moxStyleTextMarkerRangeForTextMarker:(id)textMarker {
304  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
305  if (!geckoTextMarker.IsValid()) {
306    return nil;
307  }
308
309  return geckoTextMarker.Range(EWhichRange::eStyle).CreateAXTextMarkerRange();
310}
311
312- (id)moxNextTextMarkerForTextMarker:(id)textMarker {
313  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
314  if (!geckoTextMarker.IsValid()) {
315    return nil;
316  }
317
318  if (!geckoTextMarker.Next()) {
319    return nil;
320  }
321
322  return geckoTextMarker.CreateAXTextMarker();
323}
324
325- (id)moxPreviousTextMarkerForTextMarker:(id)textMarker {
326  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
327  if (!geckoTextMarker.IsValid()) {
328    return nil;
329  }
330
331  if (!geckoTextMarker.Previous()) {
332    return nil;
333  }
334
335  return geckoTextMarker.CreateAXTextMarker();
336}
337
338- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
339    (id)textMarkerRange {
340  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
341                                            textMarkerRange);
342  if (!range.IsValid()) {
343    return nil;
344  }
345
346  return range.AttributedText();
347}
348
349- (NSValue*)moxBoundsForTextMarkerRange:(id)textMarkerRange {
350  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
351                                            textMarkerRange);
352  if (!range.IsValid()) {
353    return nil;
354  }
355
356  return range.Bounds();
357}
358
359- (NSNumber*)moxIndexForTextMarker:(id)textMarker {
360  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
361  if (!geckoTextMarker.IsValid()) {
362    return nil;
363  }
364
365  GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0),
366                             geckoTextMarker);
367
368  return @(range.Length());
369}
370
371- (id)moxTextMarkerForIndex:(NSNumber*)index {
372  GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex(
373      mGeckoDocAccessible, [index integerValue]);
374  if (!geckoTextMarker.IsValid()) {
375    return nil;
376  }
377
378  return geckoTextMarker.CreateAXTextMarker();
379}
380
381- (id)moxUIElementForTextMarker:(id)textMarker {
382  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
383  if (!geckoTextMarker.IsValid()) {
384    return nil;
385  }
386
387  AccessibleOrProxy leaf = geckoTextMarker.Leaf();
388  if (leaf.IsNull()) {
389    return nil;
390  }
391
392  return GetNativeFromGeckoAccessible(leaf);
393}
394
395- (id)moxTextMarkerRangeForUIElement:(id)element {
396  if (![element isKindOfClass:[mozAccessible class]]) {
397    return nil;
398  }
399
400  GeckoTextMarkerRange range([element geckoAccessible]);
401  return range.CreateAXTextMarkerRange();
402}
403
404- (NSString*)moxMozDebugDescriptionForTextMarker:(id)textMarker {
405  if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
406    return nil;
407  }
408
409  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
410  if (!geckoTextMarker.IsValid()) {
411    return @"<GeckoTextMarker 0x0 [0]>";
412  }
413
414  return [NSString stringWithFormat:@"<GeckoTextMarker 0x%lx [%d]>",
415                                    geckoTextMarker.mContainer.Bits(),
416                                    geckoTextMarker.mOffset];
417}
418
419- (NSString*)moxMozDebugDescriptionForTextMarkerRange:(id)textMarkerRange {
420  if (!Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
421    return nil;
422  }
423
424  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
425                                            textMarkerRange);
426  if (!range.IsValid()) {
427    return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>";
428  }
429
430  return [NSString
431      stringWithFormat:@"<GeckoTextMarkerRange 0x%lx [%d] - 0x%lx [%d]>",
432                       range.mStart.mContainer.Bits(), range.mStart.mOffset,
433                       range.mEnd.mContainer.Bits(), range.mEnd.mOffset];
434}
435
436- (void)moxSetSelectedTextMarkerRange:(id)textMarkerRange {
437  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible,
438                                            textMarkerRange);
439  if (range.IsValid()) {
440    range.Select();
441  }
442}
443
444@end
445