no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / accessible / mac / GeckoTextMarker.mm
blobba3a6e62317671933f8b4bfdf308c0386510697d
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.End().mAcc->Role() == roles::LISTITEM_MARKER) {
92       // XXX: MacOS expects bullets to be in the range's text, but not in
93       // the calculated length!
94       continue;
95     }
97     index -= segment.End().mOffset - segment.Start().mOffset;
98     if (index <= 0) {
99       // The index is in the current segment.
100       return GeckoTextMarker(segment.Start().mAcc,
101                              segment.End().mOffset + index);
102     }
103   }
105   return GeckoTextMarker();
108 AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
109   if (!IsValid()) {
110     return nil;
111   }
113   Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
114   TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
115                             mPoint.mOffset);
116   AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
117       kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData),
118       sizeof(TextMarkerData));
120   return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
123 bool GeckoTextMarker::Next() {
124   TextLeafPoint next =
125       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
126                           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
128   if (next && next != mPoint) {
129     mPoint = next;
130     return true;
131   }
133   return false;
136 bool GeckoTextMarker::Previous() {
137   TextLeafPoint prev =
138       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
139                           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
140   if (prev && mPoint != prev) {
141     mPoint = prev;
142     return true;
143   }
145   return false;
149  * Return true if the given point is inside editable content.
150  */
151 static bool IsPointInEditable(const TextLeafPoint& aPoint) {
152   if (aPoint.mAcc) {
153     if (aPoint.mAcc->State() & states::EDITABLE) {
154       return true;
155     }
157     Accessible* parent = aPoint.mAcc->Parent();
158     if (parent && (parent->State() & states::EDITABLE)) {
159       return true;
160     }
161   }
163   return false;
166 GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const {
167   bool includeCurrentInStart = !mPoint.IsParagraphStart(true);
168   if (includeCurrentInStart) {
169     TextLeafPoint prevChar =
170         mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
171     if (!prevChar.IsSpace()) {
172       includeCurrentInStart = false;
173     }
174   }
176   TextLeafPoint start = mPoint.FindBoundary(
177       nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
178       includeCurrentInStart
179           ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin |
180              TextLeafPoint::BoundaryFlags::eStopInEditable |
181              TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)
182           : (TextLeafPoint::BoundaryFlags::eStopInEditable |
183              TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker));
185   TextLeafPoint end;
186   if (start == mPoint) {
187     end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
188                              TextLeafPoint::BoundaryFlags::eStopInEditable);
189   }
191   if (start != mPoint || end == start) {
192     end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
193                              TextLeafPoint::BoundaryFlags::eStopInEditable);
194     if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) {
195       start = end;
196       end = mPoint;
197     }
198   }
200   return GeckoTextMarkerRange(start < end ? start : end,
201                               start < end ? end : start);
204 GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const {
205   TextLeafPoint prevChar =
206       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
207                           TextLeafPoint::BoundaryFlags::eStopInEditable);
209   if (prevChar != mPoint && mPoint.IsParagraphStart(true)) {
210     return GeckoTextMarkerRange(mPoint, mPoint);
211   }
213   TextLeafPoint end =
214       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
215                           TextLeafPoint::BoundaryFlags::eStopInEditable);
217   if (end == mPoint) {
218     // No word to the right of this point.
219     return GeckoTextMarkerRange(mPoint, mPoint);
220   }
222   TextLeafPoint start =
223       end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
224                        TextLeafPoint::BoundaryFlags::eStopInEditable);
226   if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
227                          TextLeafPoint::BoundaryFlags::eStopInEditable) <
228       mPoint) {
229     // Word end is inside of an input to the left of this.
230     return GeckoTextMarkerRange(mPoint, mPoint);
231   }
233   if (mPoint < start) {
234     end = start;
235     start = mPoint;
236   }
238   return GeckoTextMarkerRange(start < end ? start : end,
239                               start < end ? end : start);
242 GeckoTextMarkerRange GeckoTextMarker::LineRange() const {
243   TextLeafPoint start = mPoint.FindBoundary(
244       nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
245       TextLeafPoint::BoundaryFlags::eStopInEditable |
246           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker |
247           TextLeafPoint::BoundaryFlags::eIncludeOrigin);
248   // If this is a blank line containing only a line feed, the start boundary
249   // is the same as the end boundary. We do not want to walk to the end of the
250   // next line.
251   TextLeafPoint end =
252       start.IsLineFeedChar()
253           ? start
254           : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
255                                TextLeafPoint::BoundaryFlags::eStopInEditable);
257   return GeckoTextMarkerRange(start, end);
260 GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const {
261   TextLeafPoint start = mPoint.FindBoundary(
262       nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
263       TextLeafPoint::BoundaryFlags::eStopInEditable |
264           TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
265   TextLeafPoint end =
266       start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
267                          TextLeafPoint::BoundaryFlags::eStopInEditable);
269   return GeckoTextMarkerRange(start, end);
272 GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
273   TextLeafPoint end =
274       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
275                           TextLeafPoint::BoundaryFlags::eStopInEditable);
276   TextLeafPoint start =
277       end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
278                        TextLeafPoint::BoundaryFlags::eStopInEditable);
280   return GeckoTextMarkerRange(start, end);
283 GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const {
284   // XXX: WebKit gets trapped in inputs. Maybe we shouldn't?
285   TextLeafPoint end =
286       mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext,
287                           TextLeafPoint::BoundaryFlags::eStopInEditable);
288   TextLeafPoint start =
289       end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious,
290                        TextLeafPoint::BoundaryFlags::eStopInEditable);
292   return GeckoTextMarkerRange(start, end);
295 GeckoTextMarkerRange GeckoTextMarker::StyleRange() const {
296   if (mPoint.mOffset == 0) {
297     // If the marker is on the boundary between two leafs, MacOS expects the
298     // previous leaf.
299     TextLeafPoint prev = mPoint.FindBoundary(
300         nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
301         TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
302     if (prev != mPoint) {
303       return GeckoTextMarker(prev).StyleRange();
304     }
305   }
307   TextLeafPoint start(mPoint.mAcc, 0);
308   TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc));
309   return GeckoTextMarkerRange(start, end);
312 Accessible* GeckoTextMarker::Leaf() {
313   MOZ_ASSERT(mPoint.mAcc);
314   Accessible* acc = mPoint.mAcc;
315   if (mPoint.mOffset == 0) {
316     // If the marker is on the boundary between two leafs, MacOS expects the
317     // previous leaf.
318     TextLeafPoint prev = mPoint.FindBoundary(
319         nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
320         TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
321     acc = prev.mAcc;
322   }
324   Accessible* parent = acc->Parent();
325   return parent && nsAccUtils::MustPrune(parent) ? parent : acc;
328 // GeckoTextMarkerRange
330 GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
331   mRange = TextLeafRange(
332       TextLeafPoint(aAccessible, 0),
333       TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
336 GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
337     Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
338   if (!aTextMarkerRange ||
339       CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
340     return GeckoTextMarkerRange();
341   }
343   AXTextMarkerRef start_marker(
344       AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
345   AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
347   GeckoTextMarker start =
348       GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker);
349   GeckoTextMarker end =
350       GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker);
352   CFRelease(start_marker);
353   CFRelease(end_marker);
355   return GeckoTextMarkerRange(start, end);
358 AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
359   if (!IsValid()) {
360     return nil;
361   }
363   GeckoTextMarker start = GeckoTextMarker(mRange.Start());
364   GeckoTextMarker end = GeckoTextMarker(mRange.End());
366   AXTextMarkerRangeRef cf_text_marker_range =
367       AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(),
368                               end.CreateAXTextMarker());
370   return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
371       cf_text_marker_range)autorelease];
374 NSString* GeckoTextMarkerRange::Text() const {
375   if (mRange.Start() == mRange.End()) {
376     return @"";
377   }
379   if ((mRange.Start().mAcc == mRange.End().mAcc) &&
380       (mRange.Start().mAcc->ChildCount() == 0) &&
381       (mRange.Start().mAcc->State() & states::EDITABLE)) {
382     return @"";
383   }
385   nsAutoString text;
386   TextLeafPoint prev = mRange.Start().FindBoundary(
387       nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
388   TextLeafRange range =
389       prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
390           ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
391           : mRange;
393   for (TextLeafRange segment : range) {
394     TextLeafPoint start = segment.Start();
395     if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
396       continue;
397     }
399     start.mAcc->AppendTextTo(text, start.mOffset,
400                              segment.End().mOffset - start.mOffset);
401   }
403   return nsCocoaUtils::ToNSString(text);
406 static void AppendTextToAttributedString(
407     NSMutableAttributedString* aAttributedString, Accessible* aAccessible,
408     const nsString& aString, AccAttributes* aAttributes) {
409   NSAttributedString* substr = [[[NSAttributedString alloc]
410       initWithString:nsCocoaUtils::ToNSString(aString)
411           attributes:utils::StringAttributesFromAccAttributes(
412                          aAttributes, aAccessible)] autorelease];
414   [aAttributedString appendAttributedString:substr];
417 NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
418   NSMutableAttributedString* str =
419       [[[NSMutableAttributedString alloc] init] autorelease];
421   if (mRange.Start() == mRange.End()) {
422     return str;
423   }
425   if ((mRange.Start().mAcc == mRange.End().mAcc) &&
426       (mRange.Start().mAcc->ChildCount() == 0) &&
427       (mRange.Start().mAcc->IsTextField())) {
428     return str;
429   }
431   TextLeafPoint prev = mRange.Start().FindBoundary(
432       nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
433   TextLeafRange range =
434       prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
435           ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
436           : mRange;
438   nsAutoString text;
439   RefPtr<AccAttributes> currentRun = range.Start().GetTextAttributes();
440   Accessible* runAcc = range.Start().mAcc;
441   for (TextLeafRange segment : range) {
442     TextLeafPoint start = segment.Start();
443     TextLeafPoint attributesNext;
444     do {
445       if (start.mAcc->IsText()) {
446         attributesNext = start.FindTextAttrsStart(eDirNext, false);
447       } else {
448         // If this segment isn't a text leaf, but another kind of inline element
449         // like a control, just consider this full segment one "attributes run".
450         attributesNext = segment.End();
451       }
452       if (attributesNext == start) {
453         // XXX: FindTextAttrsStart should not return the same point.
454         break;
455       }
456       RefPtr<AccAttributes> attributes = start.GetTextAttributes();
457       if (!currentRun || !attributes || !attributes->Equal(currentRun)) {
458         // If currentRun is null this is a non-text control and we will
459         // append a run with no text or attributes, just an AXAttachment
460         // referencing this accessible.
461         AppendTextToAttributedString(str, runAcc, text, currentRun);
462         text.Truncate();
463         currentRun = attributes;
464         runAcc = start.mAcc;
465       }
466       TextLeafPoint end =
467           attributesNext < segment.End() ? attributesNext : segment.End();
468       start.mAcc->AppendTextTo(text, start.mOffset,
469                                end.mOffset - start.mOffset);
470       start = attributesNext;
472     } while (attributesNext < segment.End());
473   }
475   if (!text.IsEmpty()) {
476     AppendTextToAttributedString(str, runAcc, text, currentRun);
477   }
479   return str;
482 int32_t GeckoTextMarkerRange::Length() const {
483   int32_t length = 0;
484   for (TextLeafRange segment : mRange) {
485     if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
486       // XXX: MacOS expects bullets to be in the range's text, but not in
487       // the calculated length!
488       continue;
489     }
490     length += segment.End().mOffset - segment.Start().mOffset;
491   }
493   return length;
496 NSValue* GeckoTextMarkerRange::Bounds() const {
497   LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect();
499   NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
500   CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
501   NSRect r =
502       NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
503                  [mainView frame].size.height -
504                      static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
505                  static_cast<CGFloat>(rect.width) / scaleFactor,
506                  static_cast<CGFloat>(rect.height) / scaleFactor);
508   return [NSValue valueWithRect:r];
511 void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }
513 }  // namespace a11y
514 }  // namespace mozilla