Source code
Revision control
Copy as Markdown
Other Tools
/* clang-format off */
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* clang-format on */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#import "GeckoTextMarker.h"
#import "MacUtils.h"
#include "AccAttributes.h"
#include "DocAccessible.h"
#include "DocAccessibleParent.h"
#include "nsCocoaUtils.h"
#include "HyperTextAccessible.h"
#include "States.h"
#include "nsAccUtils.h"
namespace mozilla {
namespace a11y {
struct TextMarkerData {
TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
: mDoc(aDoc), mID(aID), mOffset(aOffset) {}
TextMarkerData() {}
uintptr_t mDoc;
uintptr_t mID;
int32_t mOffset;
};
// GeckoTextMarker
GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) {
HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase();
if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT &&
aOffset <= static_cast<int32_t>(ht->CharacterCount())) {
mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset);
} else {
mPoint = TextLeafPoint(aAcc, aOffset);
}
}
GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker(
Accessible* aDoc, AXTextMarkerRef aTextMarker) {
MOZ_ASSERT(aDoc);
if (!aTextMarker) {
return GeckoTextMarker();
}
if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) {
MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef");
return GeckoTextMarker();
}
TextMarkerData markerData;
memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker),
sizeof(TextMarkerData));
if (!utils::DocumentExists(aDoc, markerData.mDoc)) {
return GeckoTextMarker();
}
Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc);
MOZ_ASSERT(doc->IsDoc());
int32_t offset = markerData.mOffset;
Accessible* acc = nullptr;
if (doc->IsRemote()) {
acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID);
} else {
acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
reinterpret_cast<void*>(markerData.mID));
}
if (!acc) {
return GeckoTextMarker();
}
return GeckoTextMarker(acc, offset);
}
GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
int32_t aIndex) {
TextLeafRange range(
TextLeafPoint(aRoot, 0),
TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
int32_t index = aIndex;
// Iterate through all segments until we exhausted the index sum
// so we can find the segment the index lives in.
for (TextLeafRange segment : range) {
if (segment.Start().mAcc->IsMenuPopup() &&
(segment.Start().mAcc->State() & states::COLLAPSED)) {
// XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
// them.
continue;
}
if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
// XXX: MacOS expects bullets to be in the range's text, but not in
// the calculated length!
continue;
}
index -= segment.End().mOffset - segment.Start().mOffset;
if (index <= 0) {
// The index is in the current segment.
return GeckoTextMarker(segment.Start().mAcc,
segment.End().mOffset + index);
}
}
return GeckoTextMarker();
}
AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
if (!IsValid()) {
return nil;
}
Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
mPoint.mOffset);
AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData),
sizeof(TextMarkerData));
return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
}
bool GeckoTextMarker::Next() {
TextLeafPoint next =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
if (next && next != mPoint) {
mPoint = next;
return true;
}
return false;
}
bool GeckoTextMarker::Previous() {
TextLeafPoint prev =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
if (prev && mPoint != prev) {
mPoint = prev;
return true;
}
return false;
}
/**
* Return true if the given point is inside editable content.
*/
static bool IsPointInEditable(const TextLeafPoint& aPoint) {
if (aPoint.mAcc) {
if (aPoint.mAcc->State() & states::EDITABLE) {
return true;
}
Accessible* parent = aPoint.mAcc->Parent();
if (parent && (parent->State() & states::EDITABLE)) {
return true;
}
}
return false;
}
GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const {
bool includeCurrentInStart = !mPoint.IsParagraphStart(true);
if (includeCurrentInStart) {
TextLeafPoint prevChar =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
if (!prevChar.IsSpace()) {
includeCurrentInStart = false;
}
}
TextLeafPoint start = mPoint.FindBoundary(
nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
includeCurrentInStart
? (TextLeafPoint::BoundaryFlags::eIncludeOrigin |
TextLeafPoint::BoundaryFlags::eStopInEditable |
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)
: (TextLeafPoint::BoundaryFlags::eStopInEditable |
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker));
TextLeafPoint end;
if (start == mPoint) {
end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable);
}
if (start != mPoint || end == start) {
end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) {
start = end;
end = mPoint;
}
}
return GeckoTextMarkerRange(start < end ? start : end,
start < end ? end : start);
}
GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const {
TextLeafPoint prevChar =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable);
if (prevChar != mPoint && mPoint.IsParagraphStart(true)) {
return GeckoTextMarkerRange(mPoint, mPoint);
}
TextLeafPoint end =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
if (end == mPoint) {
// No word to the right of this point.
return GeckoTextMarkerRange(mPoint, mPoint);
}
TextLeafPoint start =
end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable);
if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable) <
mPoint) {
// Word end is inside of an input to the left of this.
return GeckoTextMarkerRange(mPoint, mPoint);
}
if (mPoint < start) {
end = start;
start = mPoint;
}
return GeckoTextMarkerRange(start < end ? start : end,
start < end ? end : start);
}
GeckoTextMarkerRange GeckoTextMarker::LineRange() const {
TextLeafPoint start = mPoint.FindBoundary(
nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable |
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker |
TextLeafPoint::BoundaryFlags::eIncludeOrigin);
// If this is a blank line containing only a line feed, the start boundary
// is the same as the end boundary. We do not want to walk to the end of the
// next line.
TextLeafPoint end =
start.IsLineFeedChar()
? start
: start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
return GeckoTextMarkerRange(start, end);
}
GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const {
TextLeafPoint start = mPoint.FindBoundary(
nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable |
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
TextLeafPoint end =
start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
return GeckoTextMarkerRange(start, end);
}
GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
TextLeafPoint end =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
TextLeafPoint start =
end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable);
return GeckoTextMarkerRange(start, end);
}
GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const {
// XXX: WebKit gets trapped in inputs. Maybe we shouldn't?
TextLeafPoint end =
mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext,
TextLeafPoint::BoundaryFlags::eStopInEditable);
TextLeafPoint start =
end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious,
TextLeafPoint::BoundaryFlags::eStopInEditable);
return GeckoTextMarkerRange(start, end);
}
GeckoTextMarkerRange GeckoTextMarker::StyleRange() const {
if (mPoint.mOffset == 0) {
// If the marker is on the boundary between two leafs, MacOS expects the
// previous leaf.
TextLeafPoint prev = mPoint.FindBoundary(
nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
if (prev != mPoint) {
return GeckoTextMarker(prev).StyleRange();
}
}
TextLeafPoint start(mPoint.mAcc, 0);
TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc));
return GeckoTextMarkerRange(start, end);
}
Accessible* GeckoTextMarker::Leaf() {
MOZ_ASSERT(mPoint.mAcc);
Accessible* acc = mPoint.mAcc;
if (mPoint.mOffset == 0) {
// If the marker is on the boundary between two leafs, MacOS expects the
// previous leaf.
TextLeafPoint prev = mPoint.FindBoundary(
nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
acc = prev.mAcc;
}
Accessible* parent = acc->Parent();
return parent && nsAccUtils::MustPrune(parent) ? parent : acc;
}
// GeckoTextMarkerRange
GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
mRange = TextLeafRange(
TextLeafPoint(aAccessible, 0),
TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
}
GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
if (!aTextMarkerRange ||
CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
return GeckoTextMarkerRange();
}
AXTextMarkerRef start_marker(
AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
GeckoTextMarker start =
GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker);
GeckoTextMarker end =
GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker);
CFRelease(start_marker);
CFRelease(end_marker);
return GeckoTextMarkerRange(start, end);
}
AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
if (!IsValid()) {
return nil;
}
GeckoTextMarker start = GeckoTextMarker(mRange.Start());
GeckoTextMarker end = GeckoTextMarker(mRange.End());
AXTextMarkerRangeRef cf_text_marker_range =
AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(),
end.CreateAXTextMarker());
return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
cf_text_marker_range)autorelease];
}
NSString* GeckoTextMarkerRange::Text() const {
if (mRange.Start() == mRange.End()) {
return @"";
}
if ((mRange.Start().mAcc == mRange.End().mAcc) &&
(mRange.Start().mAcc->ChildCount() == 0) &&
(mRange.Start().mAcc->State() & states::EDITABLE)) {
return @"";
}
nsAutoString text;
TextLeafPoint prev = mRange.Start().FindBoundary(
nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
TextLeafRange range =
prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
: mRange;
for (TextLeafRange segment : range) {
TextLeafPoint start = segment.Start();
if (start.mAcc->IsMenuPopup() &&
(start.mAcc->State() & states::COLLAPSED)) {
// XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
// them.
continue;
}
if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
continue;
}
start.mAcc->AppendTextTo(text, start.mOffset,
segment.End().mOffset - start.mOffset);
}
return nsCocoaUtils::ToNSString(text);
}
static void AppendTextToAttributedString(
NSMutableAttributedString* aAttributedString, Accessible* aAccessible,
const nsString& aString, AccAttributes* aAttributes) {
NSAttributedString* substr = [[[NSAttributedString alloc]
initWithString:nsCocoaUtils::ToNSString(aString)
attributes:utils::StringAttributesFromAccAttributes(
aAttributes, aAccessible)] autorelease];
[aAttributedString appendAttributedString:substr];
}
NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
NSMutableAttributedString* str =
[[[NSMutableAttributedString alloc] init] autorelease];
if (mRange.Start() == mRange.End()) {
return str;
}
if ((mRange.Start().mAcc == mRange.End().mAcc) &&
(mRange.Start().mAcc->ChildCount() == 0) &&
(mRange.Start().mAcc->IsTextField())) {
return str;
}
TextLeafPoint prev = mRange.Start().FindBoundary(
nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
TextLeafRange range =
prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
: mRange;
nsAutoString text;
RefPtr<AccAttributes> currentRun = range.Start().GetTextAttributes();
Accessible* runAcc = range.Start().mAcc;
for (TextLeafRange segment : range) {
TextLeafPoint start = segment.Start();
TextLeafPoint attributesNext;
if (start.mAcc->IsMenuPopup() &&
(start.mAcc->State() & states::COLLAPSED)) {
// XXX: Menu collapsed XUL menu popups are in our tree and we need to skip
// them.
continue;
}
do {
if (start.mAcc->IsText()) {
attributesNext = start.FindTextAttrsStart(eDirNext, false);
} else {
// If this segment isn't a text leaf, but another kind of inline element
// like a control, just consider this full segment one "attributes run".
attributesNext = segment.End();
}
if (attributesNext == start) {
// XXX: FindTextAttrsStart should not return the same point.
break;
}
RefPtr<AccAttributes> attributes = start.GetTextAttributes();
if (!currentRun || !attributes || !attributes->Equal(currentRun)) {
// If currentRun is null this is a non-text control and we will
// append a run with no text or attributes, just an AXAttachment
// referencing this accessible.
AppendTextToAttributedString(str, runAcc, text, currentRun);
text.Truncate();
currentRun = attributes;
runAcc = start.mAcc;
}
TextLeafPoint end =
attributesNext < segment.End() ? attributesNext : segment.End();
start.mAcc->AppendTextTo(text, start.mOffset,
end.mOffset - start.mOffset);
start = attributesNext;
} while (attributesNext < segment.End());
}
if (!text.IsEmpty()) {
AppendTextToAttributedString(str, runAcc, text, currentRun);
}
return str;
}
int32_t GeckoTextMarkerRange::Length() const {
int32_t length = 0;
for (TextLeafRange segment : mRange) {
if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
// XXX: MacOS expects bullets to be in the range's text, but not in
// the calculated length!
continue;
}
length += segment.End().mOffset - segment.Start().mOffset;
}
return length;
}
NSValue* GeckoTextMarkerRange::Bounds() const {
LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect();
NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
NSRect r =
NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
[mainView frame].size.height -
static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
static_cast<CGFloat>(rect.width) / scaleFactor,
static_cast<CGFloat>(rect.height) / scaleFactor);
return [NSValue valueWithRect:r];
}
void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }
} // namespace a11y
} // namespace mozilla