Bug 1832284 - Fix rooting hazard in JSObject::swap r=sfink
[gecko.git] / accessible / mac / MOXTextMarkerDelegate.mm
blob3e1e451ddd69fef78ed5d345552a98e880dfe796
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 <Cocoa/Cocoa.h>
9 #include "DocAccessible.h"
11 #import "MOXTextMarkerDelegate.h"
13 #include "mozAccessible.h"
14 #include "mozilla/Preferences.h"
15 #include "nsISelectionListener.h"
17 using namespace mozilla::a11y;
19 #define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug"
21 static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>,
22                   MOXTextMarkerDelegate*>
23     sDelegates;
25 @implementation MOXTextMarkerDelegate
27 + (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc {
28   MOZ_ASSERT(aDoc);
30   MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
31   if (!delegate) {
32     delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
33     sDelegates.InsertOrUpdate(aDoc, delegate);
34     [delegate retain];
35   }
37   return delegate;
40 + (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc {
41   MOZ_ASSERT(aDoc);
43   MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
44   if (delegate) {
45     sDelegates.Remove(aDoc);
46     [delegate release];
47   }
50 - (id)initWithDoc:(Accessible*)aDoc {
51   MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null");
52   if ((self = [super init])) {
53     mGeckoDocAccessible = aDoc;
54   }
56   mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT;
58   return self;
61 - (void)dealloc {
62   [self invalidateSelection];
63   [super dealloc];
66 - (void)setSelectionFrom:(Accessible*)startContainer
67                       at:(int32_t)startOffset
68                       to:(Accessible*)endContainer
69                       at:(int32_t)endOffset {
70   GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset),
71                                  GeckoTextMarker(endContainer, endOffset));
73   // We store it as an AXTextMarkerRange because it is a safe
74   // way to keep a weak reference - when we need to use the
75   // range we can convert it back to a GeckoTextMarkerRange
76   // and check that it's valid.
77   mSelection = selection.CreateAXTextMarkerRange();
78   CFRetain(mSelection);
81 - (void)setCaretOffset:(mozilla::a11y::Accessible*)container
82                     at:(int32_t)offset
83        moveGranularity:(int32_t)granularity {
84   GeckoTextMarker caretMarker(container, offset);
86   mPrevCaret = mCaret;
87   mCaret = caretMarker.CreateAXTextMarker();
88   mCaretMoveGranularity = granularity;
90   CFRetain(mCaret);
93 mozAccessible* GetEditableNativeFromGeckoAccessible(Accessible* aAcc) {
94   // The gecko accessible may not have a native accessible so we need
95   // to walk up the parent chain to find the nearest one.
96   // This happens when caching is enabled and the text marker's accessible
97   // may be a text leaf that is pruned from the platform.
98   for (Accessible* acc = aAcc; acc; acc = acc->Parent()) {
99     if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(acc)) {
100       return [mozAcc moxEditableAncestor];
101     }
102   }
104   return nil;
107 // This returns an info object to pass with AX SelectedTextChanged events.
108 // It uses the current and previous caret position to make decisions
109 // regarding which attributes to add to the info object.
110 - (NSDictionary*)selectionChangeInfo {
111   GeckoTextMarkerRange selectedGeckoRange =
112       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
113           mGeckoDocAccessible, mSelection);
115   int32_t stateChangeType =
116       selectedGeckoRange.Start() == selectedGeckoRange.End()
117           ? AXTextStateChangeTypeSelectionMove
118           : AXTextStateChangeTypeSelectionExtend;
120   // This is the base info object, includes the selected marker range and
121   // the change type depending on the collapsed state of the selection.
122   NSMutableDictionary* info = [[@{
123     @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid()
124         ? (__bridge id)mSelection
125         : [NSNull null],
126     @"AXTextStateChangeType" : @(stateChangeType),
127   } mutableCopy] autorelease];
129   GeckoTextMarker caretMarker =
130       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mCaret);
131   GeckoTextMarker prevCaretMarker =
132       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mPrevCaret);
134   if (!caretMarker.IsValid()) {
135     // If the current caret is invalid, stop here and return base info.
136     return info;
137   }
139   mozAccessible* caretEditable =
140       GetEditableNativeFromGeckoAccessible(caretMarker.Acc());
142   if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) {
143     // If we are not in an editable, VO expects AXTextStateSync to be present
144     // and true.
145     info[@"AXTextStateSync"] = @YES;
146   }
148   if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) {
149     // If we have no stored previous marker, stop here.
150     return info;
151   }
153   mozAccessible* prevCaretEditable =
154       GetEditableNativeFromGeckoAccessible(prevCaretMarker.Acc());
156   if (prevCaretEditable != caretEditable) {
157     // If the caret goes in or out of an editable, consider the
158     // move direction "discontiguous".
159     info[@"AXTextSelectionDirection"] =
160         @(AXTextSelectionDirectionDiscontiguous);
161     if ([[caretEditable moxFocused] boolValue]) {
162       // If the caret is in a new focused editable, VO expects this attribute to
163       // be present and to be true.
164       info[@"AXTextSelectionChangedFocus"] = @YES;
165     }
167     return info;
168   }
170   bool isForward = prevCaretMarker < caretMarker;
171   int direction = isForward ? AXTextSelectionDirectionNext
172                             : AXTextSelectionDirectionPrevious;
174   int32_t granularity = AXTextSelectionGranularityUnknown;
175   switch (mCaretMoveGranularity) {
176     case nsISelectionListener::CHARACTER_AMOUNT:
177     case nsISelectionListener::CLUSTER_AMOUNT:
178       granularity = AXTextSelectionGranularityCharacter;
179       break;
180     case nsISelectionListener::WORD_AMOUNT:
181     case nsISelectionListener::WORDNOSPACE_AMOUNT:
182       granularity = AXTextSelectionGranularityWord;
183       break;
184     case nsISelectionListener::LINE_AMOUNT:
185       granularity = AXTextSelectionGranularityLine;
186       break;
187     case nsISelectionListener::BEGINLINE_AMOUNT:
188       direction = AXTextSelectionDirectionBeginning;
189       granularity = AXTextSelectionGranularityLine;
190       break;
191     case nsISelectionListener::ENDLINE_AMOUNT:
192       direction = AXTextSelectionDirectionEnd;
193       granularity = AXTextSelectionGranularityLine;
194       break;
195     case nsISelectionListener::PARAGRAPH_AMOUNT:
196       granularity = AXTextSelectionGranularityParagraph;
197       break;
198     default:
199       break;
200   }
202   // Determine selection direction with marker comparison.
203   // If the delta between the two markers is more than one, consider it
204   // a word. Not accurate, but good enough for VO.
205   [info addEntriesFromDictionary:@{
206     @"AXTextSelectionDirection" : @(direction),
207     @"AXTextSelectionGranularity" : @(granularity)
208   }];
210   return info;
213 - (void)invalidateSelection {
214   CFRelease(mSelection);
215   CFRelease(mCaret);
216   CFRelease(mPrevCaret);
217   mSelection = nil;
220 - (mozilla::a11y::GeckoTextMarkerRange)selection {
221   return mozilla::a11y::GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
222       mGeckoDocAccessible, mSelection);
225 - (AXTextMarkerRef)moxStartTextMarker {
226   GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
227   return geckoTextPoint.CreateAXTextMarker();
230 - (AXTextMarkerRef)moxEndTextMarker {
231   GeckoTextMarker geckoTextPoint(mGeckoDocAccessible,
232                                  nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
233   return geckoTextPoint.CreateAXTextMarker();
236 - (AXTextMarkerRangeRef)moxSelectedTextMarkerRange {
237   return mSelection && GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
238                            mGeckoDocAccessible, mSelection)
239                            .IsValid()
240              ? mSelection
241              : nil;
244 - (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
245   mozilla::a11y::GeckoTextMarkerRange range =
246       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
247           mGeckoDocAccessible, textMarkerRange);
248   if (!range.IsValid()) {
249     return @"";
250   }
252   return range.Text();
255 - (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
256   mozilla::a11y::GeckoTextMarkerRange range =
257       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
258           mGeckoDocAccessible, textMarkerRange);
259   if (!range.IsValid()) {
260     return @0;
261   }
263   return @(range.Length());
266 - (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
267     (NSArray*)textMarkers {
268   if ([textMarkers count] != 2) {
269     // Don't allow anything but a two member array.
270     return nil;
271   }
273   GeckoTextMarker p1 = GeckoTextMarker::MarkerFromAXTextMarker(
274       mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[0]);
275   GeckoTextMarker p2 = GeckoTextMarker::MarkerFromAXTextMarker(
276       mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[1]);
278   if (!p1.IsValid() || !p2.IsValid()) {
279     // If either marker is invalid, return nil.
280     return nil;
281   }
283   bool ordered = p1 < p2;
284   GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);
286   return range.CreateAXTextMarkerRange();
289 - (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
290     (AXTextMarkerRangeRef)textMarkerRange {
291   mozilla::a11y::GeckoTextMarkerRange range =
292       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
293           mGeckoDocAccessible, textMarkerRange);
295   return range.IsValid() ? range.Start().CreateAXTextMarker() : nil;
298 - (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
299     (AXTextMarkerRangeRef)textMarkerRange {
300   mozilla::a11y::GeckoTextMarkerRange range =
301       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
302           mGeckoDocAccessible, textMarkerRange);
304   return range.IsValid() ? range.End().CreateAXTextMarker() : nil;
307 - (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
308     (AXTextMarkerRef)textMarker {
309   GeckoTextMarker geckoTextMarker =
310       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
311   if (!geckoTextMarker.IsValid()) {
312     return nil;
313   }
315   return geckoTextMarker.LeftWordRange().CreateAXTextMarkerRange();
318 - (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
319     (AXTextMarkerRef)textMarker {
320   GeckoTextMarker geckoTextMarker =
321       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
322   if (!geckoTextMarker.IsValid()) {
323     return nil;
324   }
326   return geckoTextMarker.RightWordRange().CreateAXTextMarkerRange();
329 - (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
330     (AXTextMarkerRef)textMarker {
331   GeckoTextMarker geckoTextMarker =
332       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
333   if (!geckoTextMarker.IsValid()) {
334     return nil;
335   }
337   return geckoTextMarker.LineRange().CreateAXTextMarkerRange();
340 - (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
341     (AXTextMarkerRef)textMarker {
342   GeckoTextMarker geckoTextMarker =
343       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
344   if (!geckoTextMarker.IsValid()) {
345     return nil;
346   }
348   return geckoTextMarker.LeftLineRange().CreateAXTextMarkerRange();
351 - (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
352     (AXTextMarkerRef)textMarker {
353   GeckoTextMarker geckoTextMarker =
354       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
355   if (!geckoTextMarker.IsValid()) {
356     return nil;
357   }
359   return geckoTextMarker.RightLineRange().CreateAXTextMarkerRange();
362 - (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
363     (AXTextMarkerRef)textMarker {
364   GeckoTextMarker geckoTextMarker =
365       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
366   if (!geckoTextMarker.IsValid()) {
367     return nil;
368   }
370   return geckoTextMarker.ParagraphRange().CreateAXTextMarkerRange();
373 // override
374 - (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
375     (AXTextMarkerRef)textMarker {
376   GeckoTextMarker geckoTextMarker =
377       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
378   if (!geckoTextMarker.IsValid()) {
379     return nil;
380   }
382   return geckoTextMarker.StyleRange().CreateAXTextMarkerRange();
385 - (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker {
386   GeckoTextMarker geckoTextMarker =
387       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
388   if (!geckoTextMarker.IsValid()) {
389     return nil;
390   }
392   if (!geckoTextMarker.Next()) {
393     return nil;
394   }
396   return geckoTextMarker.CreateAXTextMarker();
399 - (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
400     (AXTextMarkerRef)textMarker {
401   GeckoTextMarker geckoTextMarker =
402       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
403   if (!geckoTextMarker.IsValid()) {
404     return nil;
405   }
407   if (!geckoTextMarker.Previous()) {
408     return nil;
409   }
411   return geckoTextMarker.CreateAXTextMarker();
414 - (NSAttributedString*)moxAttributedStringForTextMarkerRange:
415     (AXTextMarkerRangeRef)textMarkerRange {
416   mozilla::a11y::GeckoTextMarkerRange range =
417       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
418           mGeckoDocAccessible, textMarkerRange);
419   if (!range.IsValid()) {
420     return nil;
421   }
423   return range.AttributedText();
426 - (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
427   mozilla::a11y::GeckoTextMarkerRange range =
428       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
429           mGeckoDocAccessible, textMarkerRange);
430   if (!range.IsValid()) {
431     return nil;
432   }
434   return range.Bounds();
437 - (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker {
438   GeckoTextMarker geckoTextMarker =
439       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
440   if (!geckoTextMarker.IsValid()) {
441     return nil;
442   }
444   GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0),
445                              geckoTextMarker);
447   return @(range.Length());
450 - (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index {
451   GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex(
452       mGeckoDocAccessible, [index integerValue]);
453   if (!geckoTextMarker.IsValid()) {
454     return nil;
455   }
457   return geckoTextMarker.CreateAXTextMarker();
460 - (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker {
461   GeckoTextMarker geckoTextMarker =
462       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
463   if (!geckoTextMarker.IsValid()) {
464     return nil;
465   }
467   Accessible* leaf = geckoTextMarker.Leaf();
468   if (!leaf) {
469     return nil;
470   }
472   return GetNativeFromGeckoAccessible(leaf);
475 - (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element {
476   if (![element isKindOfClass:[mozAccessible class]]) {
477     return nil;
478   }
480   GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]);
481   return range.CreateAXTextMarkerRange();
484 - (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker {
485   if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
486     return nil;
487   }
489   GeckoTextMarker geckoTextMarker =
490       GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
491   if (!geckoTextMarker.IsValid()) {
492     return @"<GeckoTextMarker 0x0 [0]>";
493   }
495   return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>",
496                                     geckoTextMarker.Acc(),
497                                     geckoTextMarker.Offset()];
500 - (NSString*)moxMozDebugDescriptionForTextMarkerRange:
501     (AXTextMarkerRangeRef)textMarkerRange {
502   if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
503     return nil;
504   }
506   mozilla::a11y::GeckoTextMarkerRange range =
507       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
508           mGeckoDocAccessible, textMarkerRange);
509   if (!range.IsValid()) {
510     return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>";
511   }
513   return [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>",
514                                     range.Start().Acc(), range.Start().Offset(),
515                                     range.End().Acc(), range.End().Offset()];
518 - (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
519   mozilla::a11y::GeckoTextMarkerRange range =
520       GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
521           mGeckoDocAccessible, textMarkerRange);
522   if (range.IsValid()) {
523     range.Select();
524   }
527 @end