Bug 1834874 - P1: Make text navigation work in UI thread with cached TextLeafPoint API. r=Jamie,geckoview-reviewers,m_kato

Differential Revision: https://phabricator.services.mozilla.com/D181320
This commit is contained in:
Eitan Isaacson 2023-06-20 18:15:42 +00:00
Родитель 0e87c46b2a
Коммит 85a5ad4790
10 изменённых файлов: 195 добавлений и 159 удалений

Просмотреть файл

@ -24,10 +24,12 @@
#include "nsTextEquivUtils.h"
#include "nsWhitespaceTokenizer.h"
#include "RootAccessible.h"
#include "TextLeafRange.h"
#include "mozilla/a11y/PDocAccessibleChild.h"
#include "mozilla/jni/GeckoBundleUtils.h"
#include "mozilla/a11y/DocAccessibleParent.h"
#include "mozilla/Maybe.h"
// icu TRUE conflicting with java::sdk::Boolean::TRUE()
// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
@ -37,6 +39,7 @@
#endif
using namespace mozilla::a11y;
using mozilla::Maybe;
//-----------------------------------------------------
// construction
@ -298,100 +301,92 @@ void AccessibleWrap::ExploreByTouch(float aX, float aY) {
}
}
void AccessibleWrap::NavigateText(int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) {
a11y::Pivot pivot(RootAccessible());
static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
return ht->ToTextLeafPoint(aOffset);
}
HyperTextAccessible* editable =
(State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr;
return TextLeafPoint(aAccessible, aOffset);
}
Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect) {
int32_t startOffset = aStartOffset;
int32_t endOffset = aEndOffset;
if (startOffset == -1) {
MOZ_ASSERT(endOffset == -1,
"When start offset is unset, end offset should be too");
startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
}
int32_t start = aStartOffset, end = aEndOffset;
// If the accessible is an editable, set the virtual cursor position
// to its caret offset. Otherwise use the document's virtual cursor
// position as a starting offset.
if (editable) {
start = end = editable->CaretOffset();
if (aAccessible->State() & states::EDITABLE) {
startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
}
uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY;
TextLeafRange currentRange =
TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
ToTextLeafPoint(aAccessible, endOffset));
uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
switch (aGranularity) {
case 1: // MOVEMENT_GRANULARITY_CHARACTER
pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY;
startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
break;
case 2: // MOVEMENT_GRANULARITY_WORD
pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY;
startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
break;
default:
break;
}
int32_t newOffset;
Accessible* newAnchorBase = nullptr;
TextLeafRange resultRange;
if (aForward) {
newAnchorBase = pivot.NextText(this, &start, &end, pivotGranularity);
newOffset = end;
resultRange.SetEnd(
currentRange.End().FindBoundary(endBoundaryType, eDirNext));
resultRange.SetStart(
resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
} else {
newAnchorBase = pivot.PrevText(this, &start, &end, pivotGranularity);
newOffset = start;
}
LocalAccessible* newAnchor =
newAnchorBase ? newAnchorBase->AsLocal() : nullptr;
if (newAnchor && (start != aStartOffset || end != aEndOffset)) {
if (IsTextLeaf() && newAnchor == LocalParent()) {
// For paragraphs, divs, spans, etc., we put a11y focus on the text leaf
// node instead of the HyperTextAccessible. However, Pivot will always
// return a HyperTextAccessible. Android doesn't support text navigation
// landing on an accessible which is different to the originating
// accessible. Therefore, if we're still within the same text leaf,
// translate the offsets to the text leaf.
int32_t thisChild = IndexInParent();
HyperTextAccessible* newHyper = newAnchor->AsHyperText();
MOZ_ASSERT(newHyper);
int32_t startChild = newHyper->GetChildIndexAtOffset(start);
// We use end - 1 because the end offset is exclusive, so end itself
// might be associated with the next child.
int32_t endChild = newHyper->GetChildIndexAtOffset(end - 1);
if (startChild == thisChild && endChild == thisChild) {
// We've landed within the same text leaf.
newAnchor = this;
int32_t thisOffset = newHyper->GetChildOffset(thisChild);
start -= thisOffset;
end -= thisOffset;
}
}
RefPtr<AccEvent> event = new AccVCChangeEvent(
newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start,
end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput);
nsEventShell::FireEvent(event);
resultRange.SetStart(
currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
resultRange.SetEnd(
resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
}
// If we are in an editable, move the caret to the new virtual cursor
// offset.
if (editable) {
if (aSelect) {
int32_t anchor = editable->CaretOffset();
if (editable->SelectionCount()) {
int32_t startSel, endSel;
GetSelectionOrCaret(&startSel, &endSel);
anchor = startSel == anchor ? endSel : startSel;
}
editable->SetSelectionBoundsAt(0, anchor, newOffset);
} else {
editable->SetCaretOffset(newOffset);
}
if (!resultRange.Crop(aAccessible)) {
// If the new range does not intersect at all with the given
// accessible/container this navigation has failed or reached an edge.
return Nothing();
}
}
void AccessibleWrap::GetSelectionOrCaret(int32_t* aStartOffset,
int32_t* aEndOffset) {
*aStartOffset = *aEndOffset = -1;
if (HyperTextAccessible* textAcc = AsHyperText()) {
if (!textAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
*aStartOffset = *aEndOffset = textAcc->CaretOffset();
}
if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
// If the result range equals the current range, or if the result range is
// collapsed, we failed or reached an edge.
return Nothing();
}
if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
DebugOnly<bool> ok = false;
std::tie(ok, startOffset) = ht->TransformOffset(
resultRange.Start().mAcc, resultRange.Start().mOffset, false);
MOZ_ASSERT(ok, "Accessible of range start should be in container.");
std::tie(ok, endOffset) = ht->TransformOffset(
resultRange.End().mAcc, resultRange.End().mOffset, false);
MOZ_ASSERT(ok, "Accessible range end should be in container.");
} else {
startOffset = resultRange.Start().mOffset;
endOffset = resultRange.End().mOffset;
}
return Some(std::make_pair(startOffset, endOffset));
}
uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,

Просмотреть файл

@ -30,9 +30,6 @@ class AccessibleWrap : public LocalAccessible {
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual bool PivotTo(int32_t aGranularity, bool aForward, bool aInclusive);
virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect);
void ExploreByTouch(float aX, float aY);
static uint32_t GetFlags(role aRole, uint64_t aState, uint8_t aActionCount);
@ -54,6 +51,10 @@ class AccessibleWrap : public LocalAccessible {
static Accessible* DoPivot(Accessible* aAccessible, int32_t aGranularity,
bool aForward, bool aInclusive);
static Maybe<std::pair<int32_t, int32_t>> NavigateText(
Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect);
protected:
int32_t mID;
@ -61,8 +62,6 @@ class AccessibleWrap : public LocalAccessible {
void GetTextEquiv(nsString& aText);
bool HandleLiveRegionEvent(AccEvent* aEvent);
void GetSelectionOrCaret(int32_t* aStartOffset, int32_t* aEndOffset);
};
} // namespace a11y

Просмотреть файл

@ -225,14 +225,6 @@ void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) {
}
}
void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) {
FORWARD_EXT_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset,
aEndOffset, aForward, aSelect);
}
static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc,
int32_t* aStartOffset, int32_t* aEndOffset) {
if (!aHyperTextAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
@ -240,6 +232,80 @@ static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc,
}
}
static void AdjustCaretToTextNavigation(Accessible* aAccessible,
int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) {
MOZ_ASSERT(NS_IsMainThread());
if (!(aAccessible->State() & states::EDITABLE)) {
return;
}
HyperTextAccessibleBase* editable = aAccessible->AsHyperTextBase();
MOZ_ASSERT(editable);
if (!editable) {
return;
}
int32_t newOffset = aForward ? aEndOffset : aStartOffset;
if (aSelect) {
int32_t anchor = editable->CaretOffset();
if (editable->SelectionCount()) {
int32_t startSel, endSel;
GetSelectionOrCaret(editable, &startSel, &endSel);
anchor = startSel == anchor ? endSel : startSel;
}
editable->SetSelectionBoundsAt(0, anchor, newOffset);
} else {
editable->SetCaretOffset(newOffset);
}
}
bool SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) {
MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
RefPtr<SessionAccessibility> self(this);
if (Accessible* acc = GetAccessibleByID(aID)) {
if (acc->IsLocal()) {
nsAppShell::PostEvent([this, self, aID, aGranularity, aStartOffset,
aEndOffset, aForward, aSelect] {
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
if (Accessible* _acc = GetAccessibleByID(aID)) {
auto result = AccessibleWrap::NavigateText(
_acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
if (result) {
SendTextTraversedEvent(_acc, result->first, result->second);
AdjustCaretToTextNavigation(_acc, result->first, result->second,
aForward, aSelect);
}
}
});
return true;
} else {
auto result = AccessibleWrap::NavigateText(
acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
if (result) {
nsAppShell::PostEvent([this, self, aID, result, aForward, aSelect] {
MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
if (Accessible* _acc = GetAccessibleByID(aID)) {
SendTextTraversedEvent(_acc, result->first, result->second);
AdjustCaretToTextNavigation(_acc, result->first, result->second,
aForward, aSelect);
}
});
}
return !!result;
}
}
return false;
}
void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart,
int32_t aEnd) {
if (Accessible* acc = GetAccessibleByID(aID)) {

Просмотреть файл

@ -57,7 +57,7 @@ class SessionAccessibility final
void Click(int32_t aID);
bool Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive);
void ExploreByTouch(int32_t aID, float aX, float aY);
void NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset,
bool NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect);
void SetSelection(int32_t aID, int32_t aStart, int32_t aEnd);
void Cut(int32_t aID);
@ -107,6 +107,10 @@ class SessionAccessibility final
void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
bool DoNavigateText(Accessible* aAccessible, int32_t aGranularity,
int32_t aStartOffset, int32_t aEndOffset, bool aForward,
bool aSelect);
jni::NativeWeakPtr<widget::GeckoViewSupport> mWindow; // Parent only
java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;

Просмотреть файл

@ -1768,6 +1768,31 @@ bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) {
return CharBounds().Contains(aX, aY);
}
bool TextLeafRange::Crop(Accessible* aContainer) {
TextLeafPoint containerStart(aContainer, 0);
TextLeafPoint containerEnd(aContainer,
nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
if (mEnd < containerStart || containerEnd < mStart) {
// The range ends before the container, or starts after it.
return false;
}
if (mStart < containerStart) {
// If range start is before container start, adjust range start to
// start of container.
mStart = containerStart;
}
if (containerEnd < mEnd) {
// If range end is after container end, adjust range end to end of
// container.
mEnd = containerEnd;
}
return true;
}
LayoutDeviceIntRect TextLeafRange::Bounds() const {
if (mEnd == mStart || mEnd < mStart) {
return LayoutDeviceIntRect();

Просмотреть файл

@ -277,11 +277,17 @@ class TextLeafRange final {
return mEnd != aOther.mEnd || mStart != aOther.mStart;
}
bool operator==(const TextLeafRange& aOther) const {
return mEnd == aOther.mEnd && mStart == aOther.mStart;
}
TextLeafPoint Start() const { return mStart; }
void SetStart(const TextLeafPoint& aStart) { mStart = aStart; }
TextLeafPoint End() const { return mEnd; }
void SetEnd(const TextLeafPoint& aEnd) { mEnd = aEnd; }
bool Crop(Accessible* aContainer);
/**
* Returns a union rect (in dev pixels) of all character bounds in this range.
* This rect is screen-relative and inclusive of mEnd. This function only

Просмотреть файл

@ -184,6 +184,17 @@ class HyperTextAccessibleBase {
return child ? LinkIndexOf(child) : -1;
}
/**
* Transform the given a11y point into an offset relative to this hypertext.
* Returns {success, offset}, where success is true if successful.
* If unsuccessful, the returned offset will be CharacterCount() if
* aIsEndOffset is true, 0 otherwise. This means most callers can ignore the
* success return value.
*/
std::pair<bool, int32_t> TransformOffset(Accessible* aDescendant,
int32_t aOffset,
bool aIsEndOffset) const;
/**
* Return text attributes for the given text range.
*/
@ -276,17 +287,6 @@ class HyperTextAccessibleBase {
virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() = 0;
private:
/**
* Transform the given a11y point into an offset relative to this hypertext.
* Returns {success, offset}, where success is true if successful.
* If unsuccessful, the returned offset will be CharacterCount() if
* aIsEndOffset is true, 0 otherwise. This means most callers can ignore the
* success return value.
*/
std::pair<bool, int32_t> TransformOffset(Accessible* aDescendant,
int32_t aOffset,
bool aIsEndOffset) const;
/**
* Helper method for TextBefore/At/AfterOffset.
* If BOUNDARY_LINE_END was requested and the origin is itself a line end

Просмотреть файл

@ -127,7 +127,7 @@ class GeckoTextMarkerRange final {
* Return true if successfully cropped. false if the range does not intersect
* with the container.
*/
bool Crop(Accessible* aContainer);
bool Crop(Accessible* aContainer) { return mRange.Crop(aContainer); }
TextLeafRange mRange;
};

Просмотреть файл

@ -509,29 +509,5 @@ NSValue* GeckoTextMarkerRange::Bounds() const {
void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }
bool GeckoTextMarkerRange::Crop(Accessible* aContainer) {
TextLeafPoint containerStart(aContainer, 0);
TextLeafPoint containerEnd(aContainer,
nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
if (mRange.End() < containerStart || containerEnd < mRange.Start()) {
// The range ends before the container, or starts after it.
return false;
}
if (mRange.Start() < containerStart) {
// If range start is before container start, adjust range start to
// start of container.
mRange.SetStart(containerStart);
}
if (containerEnd < mRange.End()) {
// If range end is after container end, adjust range end to end of
// container.
mRange.SetEnd(containerEnd);
}
return true;
}
} // namespace a11y
} // namespace mozilla

Просмотреть файл

@ -297,18 +297,7 @@ public class SessionAccessibility {
AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
final boolean next =
action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY;
// We must return false if we're already at the edge.
if (next) {
if (mAtEndOfText) {
return false;
}
if (granularity == AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD && mAtLastWord) {
return false;
}
} else if (mAtStartOfText) {
return false;
}
nativeProvider.navigateText(
return nativeProvider.navigateText(
virtualViewId, granularity, mStartOffset, mEndOffset, next, extendSelection);
}
return true;
@ -401,9 +390,6 @@ public class SessionAccessibility {
private int mFocusedNode = 0;
private int mStartOffset = -1;
private int mEndOffset = -1;
private boolean mAtStartOfText = false;
private boolean mAtEndOfText = false;
private boolean mAtLastWord = false;
private boolean mViewFocusRequested = false;
/* package */ SessionAccessibility(final GeckoSession session) {
@ -623,9 +609,6 @@ public class SessionAccessibility {
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
mStartOffset = -1;
mEndOffset = -1;
mAtStartOfText = false;
mAtEndOfText = false;
mAtLastWord = false;
mAccessibilityFocusedNode = sourceId;
break;
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
@ -638,24 +621,6 @@ public class SessionAccessibility {
case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
mStartOffset = event.getFromIndex();
mEndOffset = event.getToIndex();
// We must synchronously return false for text navigation
// actions if the user attempts to navigate past the edge.
// Because we do navigation async, we can't query this
// on demand when the action is performed. Therefore, we cache
// whether we're at either edge here.
mAtStartOfText = mStartOffset == 0;
final CharSequence text = event.getText().get(0);
mAtEndOfText = mEndOffset >= text.length();
mAtLastWord = mAtEndOfText;
if (!mAtLastWord) {
// Words exclude trailing spaces. To figure out whether
// we're at the last word, we need to get the text after
// our end offset and check if it's just spaces.
final CharSequence afterText = text.subSequence(mEndOffset, text.length());
if (TextUtils.getTrimmedLength(afterText) == 0) {
mAtLastWord = true;
}
}
break;
}
@ -716,8 +681,8 @@ public class SessionAccessibility {
@WrapForJNI(dispatchTo = "gecko")
public native void exploreByTouch(int id, float x, float y);
@WrapForJNI(dispatchTo = "gecko")
public native void navigateText(
@WrapForJNI(dispatchTo = "current")
public native boolean navigateText(
int id, int granularity, int startOffset, int endOffset, boolean forward, boolean select);
@WrapForJNI(dispatchTo = "gecko")