no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / accessible / mac / GeckoTextMarker.mm
blobbb787edd28d381de03db4283f42bdae7f7936d67
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 "GeckoTextMarker.h"
10 #import "MacUtils.h"
12 #include "AccAttributes.h"
13 #include "DocAccessible.h"
14 #include "DocAccessibleParent.h"
15 #include "nsCocoaUtils.h"
16 #include "HyperTextAccessible.h"
17 #include "States.h"
18 #include "nsAccUtils.h"
20 namespace mozilla {
21 namespace a11y {
23 struct TextMarkerData {
24   TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
25       : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
26   TextMarkerData() {}
27   uintptr_t mDoc;
28   uintptr_t mID;
29   int32_t mOffset;
32 // GeckoTextMarker
34 GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) {
35   HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase();
36   if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT &&
37       aOffset <= static_cast<int32_t>(ht->CharacterCount())) {
38     mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset);
39   } else {
40     mPoint = TextLeafPoint(aAcc, aOffset);
41   }
44 GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker(
45     Accessible* aDoc, AXTextMarkerRef aTextMarker) {
46   MOZ_ASSERT(aDoc);
47   if (!aTextMarker) {
48     return GeckoTextMarker();
49   }
51   if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) {
52     MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef");
53     return GeckoTextMarker();
54   }
56   TextMarkerData markerData;
57   memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker),
58          sizeof(TextMarkerData));
60   if (!utils::DocumentExists(aDoc, markerData.mDoc)) {
61     return GeckoTextMarker();
62   }
64   Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc);
65   MOZ_ASSERT(doc->IsDoc());
66   int32_t offset = markerData.mOffset;
67   Accessible* acc = nullptr;
68   if (doc->IsRemote()) {
69     acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID);
70   } else {
71     acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
72         reinterpret_cast<void*>(markerData.mID));
73   }
75   if (!acc) {
76     return GeckoTextMarker();
77   }
79   return GeckoTextMarker(acc, offset);
82 GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
83                                                  int32_t aIndex) {
84   TextLeafRange range(
85       TextLeafPoint(aRoot, 0),
86       TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
87   int32_t index = aIndex;
88   // Iterate through all segments until we exhausted the index sum
89   // so we can find the segment the index lives in.
90   for (TextLeafRange segment : range) {
91     if (segment.Start().mAcc->IsMenuPopup() &&
92         (segment.Start().mAcc->State() & states::COLLAPSED)) {
93       // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
94       // them.
95       continue;
96     }
98     if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
99       // XXX: MacOS expects bullets to be in the range's text, but not in
100       // the calculated length!
101       continue;
102     }
104     index -= segment.End().mOffset - segment.Start().mOffset;
105     if (index <= 0) {
106       // The index is in the current segment.
107       return GeckoTextMarker(segment.Start().mAcc,
108                              segment.End().mOffset + index);
109     }
110   }
112   return GeckoTextMarker();
115 AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
116   if (!IsValid()) {
117     return nil;
118   }
120   Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
121   TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
122                             mPoint.mOffset);
123   AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
124       kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData),
125       sizeof(TextMarkerData));
127   return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
130 bool GeckoTextMarker::Next() {
131   TextLeafPoint next =
132       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
133                           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
135   if (next && next != mPoint) {
136     mPoint = next;
137     return true;
138   }
140   return false;
143 bool GeckoTextMarker::Previous() {
144   TextLeafPoint prev =
145       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
146                           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
147   if (prev && mPoint != prev) {
148     mPoint = prev;
149     return true;
150   }
152   return false;
156  * Return true if the given point is inside editable content.
157  */
158 static bool IsPointInEditable(const TextLeafPoint& aPoint) {
159   if (aPoint.mAcc) {
160     if (aPoint.mAcc->State() & states::EDITABLE) {
161       return true;
162     }
164     Accessible* parent = aPoint.mAcc->Parent();
165     if (parent && (parent->State() & states::EDITABLE)) {
166       return true;
167     }
168   }
170   return false;
173 GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const {
174   bool includeCurrentInStart = !mPoint.IsParagraphStart(true);
175   if (includeCurrentInStart) {
176     TextLeafPoint prevChar =
177         mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
178     if (!prevChar.IsSpace()) {
179       includeCurrentInStart = false;
180     }
181   }
183   TextLeafPoint start = mPoint.FindBoundary(
184       nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
185       includeCurrentInStart
186           ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin |
187              TextLeafPoint::BoundaryFlags::eStopInEditable |
188              TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)
189           : (TextLeafPoint::BoundaryFlags::eStopInEditable |
190              TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker));
192   TextLeafPoint end;
193   if (start == mPoint) {
194     end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
195                              TextLeafPoint::BoundaryFlags::eStopInEditable);
196   }
198   if (start != mPoint || end == start) {
199     end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
200                              TextLeafPoint::BoundaryFlags::eStopInEditable);
201     if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) {
202       start = end;
203       end = mPoint;
204     }
205   }
207   return GeckoTextMarkerRange(start < end ? start : end,
208                               start < end ? end : start);
211 GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const {
212   TextLeafPoint prevChar =
213       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
214                           TextLeafPoint::BoundaryFlags::eStopInEditable);
216   if (prevChar != mPoint && mPoint.IsParagraphStart(true)) {
217     return GeckoTextMarkerRange(mPoint, mPoint);
218   }
220   TextLeafPoint end =
221       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
222                           TextLeafPoint::BoundaryFlags::eStopInEditable);
224   if (end == mPoint) {
225     // No word to the right of this point.
226     return GeckoTextMarkerRange(mPoint, mPoint);
227   }
229   TextLeafPoint start =
230       end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
231                        TextLeafPoint::BoundaryFlags::eStopInEditable);
233   if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
234                          TextLeafPoint::BoundaryFlags::eStopInEditable) <
235       mPoint) {
236     // Word end is inside of an input to the left of this.
237     return GeckoTextMarkerRange(mPoint, mPoint);
238   }
240   if (mPoint < start) {
241     end = start;
242     start = mPoint;
243   }
245   return GeckoTextMarkerRange(start < end ? start : end,
246                               start < end ? end : start);
249 GeckoTextMarkerRange GeckoTextMarker::LineRange() const {
250   TextLeafPoint start = mPoint.FindBoundary(
251       nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
252       TextLeafPoint::BoundaryFlags::eStopInEditable |
253           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker |
254           TextLeafPoint::BoundaryFlags::eIncludeOrigin);
255   // If this is a blank line containing only a line feed, the start boundary
256   // is the same as the end boundary. We do not want to walk to the end of the
257   // next line.
258   TextLeafPoint end =
259       start.IsLineFeedChar()
260           ? start
261           : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
262                                TextLeafPoint::BoundaryFlags::eStopInEditable);
264   return GeckoTextMarkerRange(start, end);
267 GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const {
268   TextLeafPoint start = mPoint.FindBoundary(
269       nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
270       TextLeafPoint::BoundaryFlags::eStopInEditable |
271           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
272   TextLeafPoint end =
273       start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
274                          TextLeafPoint::BoundaryFlags::eStopInEditable);
276   return GeckoTextMarkerRange(start, end);
279 GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
280   TextLeafPoint end =
281       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
282                           TextLeafPoint::BoundaryFlags::eStopInEditable);
283   TextLeafPoint start =
284       end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
285                        TextLeafPoint::BoundaryFlags::eStopInEditable);
287   return GeckoTextMarkerRange(start, end);
290 GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const {
291   // XXX: WebKit gets trapped in inputs. Maybe we shouldn't?
292   TextLeafPoint end =
293       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext,
294                           TextLeafPoint::BoundaryFlags::eStopInEditable);
295   TextLeafPoint start =
296       end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious,
297                        TextLeafPoint::BoundaryFlags::eStopInEditable);
299   return GeckoTextMarkerRange(start, end);
302 GeckoTextMarkerRange GeckoTextMarker::StyleRange() const {
303   if (mPoint.mOffset == 0) {
304     // If the marker is on the boundary between two leafs, MacOS expects the
305     // previous leaf.
306     TextLeafPoint prev = mPoint.FindBoundary(
307         nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
308         TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
309     if (prev != mPoint) {
310       return GeckoTextMarker(prev).StyleRange();
311     }
312   }
314   TextLeafPoint start(mPoint.mAcc, 0);
315   TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc));
316   return GeckoTextMarkerRange(start, end);
319 Accessible* GeckoTextMarker::Leaf() {
320   MOZ_ASSERT(mPoint.mAcc);
321   Accessible* acc = mPoint.mAcc;
322   if (mPoint.mOffset == 0) {
323     // If the marker is on the boundary between two leafs, MacOS expects the
324     // previous leaf.
325     TextLeafPoint prev = mPoint.FindBoundary(
326         nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
327         TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
328     acc = prev.mAcc;
329   }
331   Accessible* parent = acc->Parent();
332   return parent && nsAccUtils::MustPrune(parent) ? parent : acc;
335 // GeckoTextMarkerRange
337 GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
338   mRange = TextLeafRange(
339       TextLeafPoint(aAccessible, 0),
340       TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
343 GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
344     Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
345   if (!aTextMarkerRange ||
346       CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
347     return GeckoTextMarkerRange();
348   }
350   AXTextMarkerRef start_marker(
351       AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
352   AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
354   GeckoTextMarker start =
355       GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker);
356   GeckoTextMarker end =
357       GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker);
359   CFRelease(start_marker);
360   CFRelease(end_marker);
362   return GeckoTextMarkerRange(start, end);
365 AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
366   if (!IsValid()) {
367     return nil;
368   }
370   GeckoTextMarker start = GeckoTextMarker(mRange.Start());
371   GeckoTextMarker end = GeckoTextMarker(mRange.End());
373   AXTextMarkerRangeRef cf_text_marker_range =
374       AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(),
375                               end.CreateAXTextMarker());
377   return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
378       cf_text_marker_range)autorelease];
381 NSString* GeckoTextMarkerRange::Text() const {
382   if (mRange.Start() == mRange.End()) {
383     return @"";
384   }
386   if ((mRange.Start().mAcc == mRange.End().mAcc) &&
387       (mRange.Start().mAcc->ChildCount() == 0) &&
388       (mRange.Start().mAcc->State() & states::EDITABLE)) {
389     return @"";
390   }
392   nsAutoString text;
393   TextLeafPoint prev = mRange.Start().FindBoundary(
394       nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
395   TextLeafRange range =
396       prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
397           ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
398           : mRange;
400   for (TextLeafRange segment : range) {
401     TextLeafPoint start = segment.Start();
402     if (start.mAcc->IsMenuPopup() &&
403         (start.mAcc->State() & states::COLLAPSED)) {
404       // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
405       // them.
406       continue;
407     }
408     if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
409       continue;
410     }
412     start.mAcc->AppendTextTo(text, start.mOffset,
413                              segment.End().mOffset - start.mOffset);
414   }
416   return nsCocoaUtils::ToNSString(text);
419 static void AppendTextToAttributedString(
420     NSMutableAttributedString* aAttributedString, Accessible* aAccessible,
421     const nsString& aString, AccAttributes* aAttributes) {
422   NSAttributedString* substr = [[[NSAttributedString alloc]
423       initWithString:nsCocoaUtils::ToNSString(aString)
424           attributes:utils::StringAttributesFromAccAttributes(
425                          aAttributes, aAccessible)] autorelease];
427   [aAttributedString appendAttributedString:substr];
430 NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
431   NSMutableAttributedString* str =
432       [[[NSMutableAttributedString alloc] init] autorelease];
434   if (mRange.Start() == mRange.End()) {
435     return str;
436   }
438   if ((mRange.Start().mAcc == mRange.End().mAcc) &&
439       (mRange.Start().mAcc->ChildCount() == 0) &&
440       (mRange.Start().mAcc->IsTextField())) {
441     return str;
442   }
444   TextLeafPoint prev = mRange.Start().FindBoundary(
445       nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
446   TextLeafRange range =
447       prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
448           ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
449           : mRange;
451   nsAutoString text;
452   RefPtr<AccAttributes> currentRun = range.Start().GetTextAttributes();
453   Accessible* runAcc = range.Start().mAcc;
454   for (TextLeafRange segment : range) {
455     TextLeafPoint start = segment.Start();
456     TextLeafPoint attributesNext;
457     if (start.mAcc->IsMenuPopup() &&
458         (start.mAcc->State() & states::COLLAPSED)) {
459       // XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
460       // them.
461       continue;
462     }
463     do {
464       if (start.mAcc->IsText()) {
465         attributesNext = start.FindTextAttrsStart(eDirNext, false);
466       } else {
467         // If this segment isn't a text leaf, but another kind of inline element
468         // like a control, just consider this full segment one "attributes run".
469         attributesNext = segment.End();
470       }
471       if (attributesNext == start) {
472         // XXX: FindTextAttrsStart should not return the same point.
473         break;
474       }
475       RefPtr<AccAttributes> attributes = start.GetTextAttributes();
476       if (!currentRun || !attributes || !attributes->Equal(currentRun)) {
477         // If currentRun is null this is a non-text control and we will
478         // append a run with no text or attributes, just an AXAttachment
479         // referencing this accessible.
480         AppendTextToAttributedString(str, runAcc, text, currentRun);
481         text.Truncate();
482         currentRun = attributes;
483         runAcc = start.mAcc;
484       }
485       TextLeafPoint end =
486           attributesNext < segment.End() ? attributesNext : segment.End();
487       start.mAcc->AppendTextTo(text, start.mOffset,
488                                end.mOffset - start.mOffset);
489       start = attributesNext;
491     } while (attributesNext < segment.End());
492   }
494   if (!text.IsEmpty()) {
495     AppendTextToAttributedString(str, runAcc, text, currentRun);
496   }
498   return str;
501 int32_t GeckoTextMarkerRange::Length() const {
502   int32_t length = 0;
503   for (TextLeafRange segment : mRange) {
504     if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
505       // XXX: MacOS expects bullets to be in the range's text, but not in
506       // the calculated length!
507       continue;
508     }
509     length += segment.End().mOffset - segment.Start().mOffset;
510   }
512   return length;
515 NSValue* GeckoTextMarkerRange::Bounds() const {
516   LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect();
518   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
519   CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
520   NSRect r =
521       NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
522                  [mainView frame].size.height -
523                      static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
524                  static_cast<CGFloat>(rect.width) / scaleFactor,
525                  static_cast<CGFloat>(rect.height) / scaleFactor);
527   return [NSValue valueWithRect:r];
530 void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }
532 }  // namespace a11y
533 }  // namespace mozilla