2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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"
12 #include "AccAttributes.h"
13 #include "DocAccessible.h"
14 #include "DocAccessibleParent.h"
15 #include "nsCocoaUtils.h"
16 #include "HyperTextAccessible.h"
18 #include "nsAccUtils.h"
23 struct TextMarkerData {
24 TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
25 : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
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);
40 mPoint = TextLeafPoint(aAcc, aOffset);
44 GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker(
45 Accessible* aDoc, AXTextMarkerRef aTextMarker) {
48 return GeckoTextMarker();
51 if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) {
52 MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef");
53 return GeckoTextMarker();
56 TextMarkerData markerData;
57 memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker),
58 sizeof(TextMarkerData));
60 if (!utils::DocumentExists(aDoc, markerData.mDoc)) {
61 return GeckoTextMarker();
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);
71 acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
72 reinterpret_cast<void*>(markerData.mID));
76 return GeckoTextMarker();
79 return GeckoTextMarker(acc, offset);
82 GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
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
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!
104 index -= segment.End().mOffset - segment.Start().mOffset;
106 // The index is in the current segment.
107 return GeckoTextMarker(segment.Start().mAcc,
108 segment.End().mOffset + index);
112 return GeckoTextMarker();
115 AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
120 Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
121 TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
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() {
132 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
133 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
135 if (next && next != mPoint) {
143 bool GeckoTextMarker::Previous() {
145 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
146 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
147 if (prev && mPoint != prev) {
156 * Return true if the given point is inside editable content.
158 static bool IsPointInEditable(const TextLeafPoint& aPoint) {
160 if (aPoint.mAcc->State() & states::EDITABLE) {
164 Accessible* parent = aPoint.mAcc->Parent();
165 if (parent && (parent->State() & states::EDITABLE)) {
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;
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));
193 if (start == mPoint) {
194 end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
195 TextLeafPoint::BoundaryFlags::eStopInEditable);
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)) {
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);
221 mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
222 TextLeafPoint::BoundaryFlags::eStopInEditable);
225 // No word to the right of this point.
226 return GeckoTextMarkerRange(mPoint, mPoint);
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) <
236 // Word end is inside of an input to the left of this.
237 return GeckoTextMarkerRange(mPoint, mPoint);
240 if (mPoint < start) {
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
259 start.IsLineFeedChar()
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);
273 start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
274 TextLeafPoint::BoundaryFlags::eStopInEditable);
276 return GeckoTextMarkerRange(start, end);
279 GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
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?
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
306 TextLeafPoint prev = mPoint.FindBoundary(
307 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
308 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
309 if (prev != mPoint) {
310 return GeckoTextMarker(prev).StyleRange();
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
325 TextLeafPoint prev = mPoint.FindBoundary(
326 nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
327 TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
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();
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() {
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()) {
386 if ((mRange.Start().mAcc == mRange.End().mAcc) &&
387 (mRange.Start().mAcc->ChildCount() == 0) &&
388 (mRange.Start().mAcc->State() & states::EDITABLE)) {
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())
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
408 if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
412 start.mAcc->AppendTextTo(text, start.mOffset,
413 segment.End().mOffset - start.mOffset);
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()) {
438 if ((mRange.Start().mAcc == mRange.End().mAcc) &&
439 (mRange.Start().mAcc->ChildCount() == 0) &&
440 (mRange.Start().mAcc->IsTextField())) {
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())
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
464 if (start.mAcc->IsText()) {
465 attributesNext = start.FindTextAttrsStart(eDirNext, false);
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();
471 if (attributesNext == start) {
472 // XXX: FindTextAttrsStart should not return the same point.
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);
482 currentRun = attributes;
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());
494 if (!text.IsEmpty()) {
495 AppendTextToAttributedString(str, runAcc, text, currentRun);
501 int32_t GeckoTextMarkerRange::Length() const {
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!
509 length += segment.End().mOffset - segment.Start().mOffset;
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);
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); }
533 } // namespace mozilla