Bug 1564549 - Implement text navigation natively. r=geckoview-reviewers,Jamie,snorp

Differential Revision: https://phabricator.services.mozilla.com/D45600

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Eitan Isaacson 2019-09-23 21:06:58 +00:00
parent 85d6a5f093
commit ac01020042
11 changed files with 143 additions and 18 deletions

View File

@ -301,6 +301,78 @@ 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());
HyperTextAccessible* editable =
(State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr;
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();
}
uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY;
switch (aGranularity) {
case 1: // MOVEMENT_GRANULARITY_CHARACTER
pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY;
break;
case 2: // MOVEMENT_GRANULARITY_WORD
pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY;
break;
default:
break;
}
int32_t newOffset;
Accessible* newAnchor = nullptr;
if (aForward) {
newAnchor = pivot.NextText(this, &start, &end, pivotGranularity);
newOffset = end;
} else {
newAnchor = pivot.PrevText(this, &start, &end, pivotGranularity);
newOffset = start;
}
if (newAnchor && (start != aStartOffset || end != aEndOffset)) {
RefPtr<AccEvent> event = new AccVCChangeEvent(
newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start,
end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput);
nsEventShell::FireEvent(event);
}
// 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);
}
}
}
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();
}
}
}
uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState,
uint8_t aActionCount) {
uint32_t flags = 0;

View File

@ -39,6 +39,9 @@ class AccessibleWrap : public Accessible {
virtual void ExploreByTouch(float aX, float aY);
virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward, bool aSelect);
mozilla::java::GeckoBundle::LocalRef ToBundle(bool aSmall = false);
mozilla::java::GeckoBundle::LocalRef ToBundle(
@ -89,6 +92,8 @@ class AccessibleWrap : public Accessible {
bool HandleLiveRegionEvent(AccEvent* aEvent);
void GetSelectionOrCaret(int32_t* aStartOffset, int32_t* aEndOffset);
static void GetRoleDescription(role aRole,
nsIPersistentProperties* aAttributes,
nsAString& aGeckoRole,

View File

@ -120,6 +120,13 @@ void ProxyAccessibleWrap::ExploreByTouch(float aX, float aY) {
Proxy()->ID(), aX, aY);
}
void ProxyAccessibleWrap::NavigateText(int32_t aGranularity,
int32_t aStartOffset, int32_t aEndOffset,
bool aForward, bool aSelect) {
Unused << Proxy()->Document()->GetPlatformExtension()->SendNavigateText(
Proxy()->ID(), aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
}
role ProxyAccessibleWrap::WrapperRole() { return Proxy()->Role(); }
AccessibleWrap* ProxyAccessibleWrap::WrapperParent() {

View File

@ -62,6 +62,10 @@ class ProxyAccessibleWrap : public AccessibleWrap {
virtual void Pivot(int32_t aGranularity, bool aForward,
bool aInclusive) override;
virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) override;
virtual void ExploreByTouch(float aX, float aY) override;
virtual void WrapperDOMNodeID(nsString& aDOMNodeID) override;

View File

@ -124,6 +124,14 @@ void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) {
FORWARD_ACTION_TO_ACCESSIBLE(ExploreByTouch, aX, aY);
}
void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect) {
FORWARD_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset,
aEndOffset, aForward, aSelect);
}
SessionAccessibility* SessionAccessibility::GetInstanceFor(
ProxyAccessible* aAccessible) {
auto tab =

View File

@ -56,6 +56,8 @@ class SessionAccessibility final
void Click(int32_t aID);
void 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,
int32_t aEndOffset, bool aForward, bool aSelect);
void StartNativeAccessibility();
// Event methods

View File

@ -22,10 +22,11 @@ mozilla::ipc::IPCResult DocAccessiblePlatformExtChild::RecvPivot(
}
mozilla::ipc::IPCResult DocAccessiblePlatformExtChild::RecvNavigateText(
int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset,
uint64_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset,
bool aForward, bool aSelect) {
if (auto acc = IdToAccessibleWrap(aID)) {
// XXX: Forward to appropriate wrapper method.
acc->NavigateText(aGranularity, aStartOffset, aEndOffset, aForward,
aSelect);
}
return IPC_OK();

View File

@ -19,7 +19,7 @@ class DocAccessiblePlatformExtChild : public PDocAccessiblePlatformExtChild {
mozilla::ipc::IPCResult RecvPivot(uint64_t aID, int32_t aGranularity,
bool aForward, bool aInclusive);
mozilla::ipc::IPCResult RecvNavigateText(int32_t aID, int32_t aGranularity,
mozilla::ipc::IPCResult RecvNavigateText(uint64_t aID, int32_t aGranularity,
int32_t aStartOffset,
int32_t aEndOffset, bool aForward,
bool aSelect);

View File

@ -17,7 +17,7 @@ child:
async Pivot(uint64_t aID, int32_t aGranularity, bool aForward, bool aInclusive);
async NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect);
async NavigateText(uint64_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect);
async SetSelection(int32_t aID, int32_t aStart, int32_t aEnd);

View File

@ -318,14 +318,17 @@ class ZZAccessibilityTest : BaseSessionTest() {
} while (fromIndex != eventFromIndex || toIndex != eventToIndex)
}
private fun waitUntilTextTraversed(fromIndex: Int, toIndex: Int) {
private fun waitUntilTextTraversed(fromIndex: Int, toIndex: Int): Int {
var nodeId: Int = AccessibilityNodeProvider.HOST_VIEW_ID
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onTextTraversal(event: AccessibilityEvent) {
nodeId = getSourceId(event)
assertThat("fromIndex matches", event.fromIndex, equalTo(fromIndex))
assertThat("toIndex matches", event.toIndex, equalTo(toIndex))
}
})
return nodeId
}
private fun waitUntilClick(checked: Boolean) {
@ -414,6 +417,22 @@ class ZZAccessibilityTest : BaseSessionTest() {
assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel cruel"))
}
})
provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(0, 0))
waitUntilTextSelectionChanged(0, 0)
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD, true))
waitUntilTextSelectionChanged(0, 5)
provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CUT, null)
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled
override fun onTextChanged(event: AccessibilityEvent) {
assertThat("text should be cut", event.text[0].toString(), equalTo(" cruel cruel cruel"))
}
})
}
@Test fun testMoveByCharacter() {
@ -433,17 +452,17 @@ class ZZAccessibilityTest : BaseSessionTest() {
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
waitUntilTextTraversed(0, 1) // "L"
nodeId = waitUntilTextTraversed(0, 1) // "L"
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
waitUntilTextTraversed(1, 2) // "o"
nodeId = waitUntilTextTraversed(1, 2) // "o"
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
waitUntilTextTraversed(0, 1) // "L"
nodeId = waitUntilTextTraversed(0, 1) // "L"
}
@Test fun testMoveByWord() {
@ -463,12 +482,12 @@ class ZZAccessibilityTest : BaseSessionTest() {
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
waitUntilTextTraversed(0, 5) // "Lorem"
nodeId = waitUntilTextTraversed(0, 5) // "Lorem"
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
waitUntilTextTraversed(6, 11) // "ipsum"
nodeId = waitUntilTextTraversed(6, 11) // "ipsum"
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
@ -496,17 +515,17 @@ class ZZAccessibilityTest : BaseSessionTest() {
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
nodeId = waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
waitUntilTextTraversed(18, 28) // "sit amet, "
nodeId = waitUntilTextTraversed(18, 28) // "sit amet, "
provider.performAction(nodeId,
AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
nodeId = waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
}
@Test fun testHeadings() {

View File

@ -271,11 +271,7 @@ public class SessionAccessibility {
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
} else if (granularity > 0) {
boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
data = new GeckoBundle(3);
data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
data.putInt("granularity", granularity);
data.putBoolean("select", extendSelection);
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
nativeProvider.navigateText(virtualViewId, granularity, mStartOffset, mEndOffset, action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, extendSelection);
}
return true;
case AccessibilityNodeInfo.ACTION_SET_SELECTION:
@ -548,6 +544,8 @@ public class SessionAccessibility {
private int mAccessibilityFocusedNode = 0;
// The current node with focus
private int mFocusedNode = 0;
private int mStartOffset = -1;
private int mEndOffset = -1;
// Viewport cache
final SparseArray<GeckoBundle> mViewportCache = new SparseArray<>();
// Focus cache
@ -807,6 +805,8 @@ public class SessionAccessibility {
}
break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
mStartOffset = -1;
mEndOffset = -1;
mAccessibilityFocusedNode = sourceId;
break;
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
@ -816,6 +816,10 @@ public class SessionAccessibility {
return;
}
break;
case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
mStartOffset = event.getFromIndex();
mEndOffset = event.getToIndex();
break;
}
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
@ -864,6 +868,9 @@ public class SessionAccessibility {
@WrapForJNI(dispatchTo = "gecko")
public native void exploreByTouch(int id, float x, float y);
@WrapForJNI(dispatchTo = "gecko")
public native void navigateText(int id, int granularity, int startOffset, int endOffset, boolean forward, boolean select);
@WrapForJNI(calledFrom = "gecko", stubName = "SendEvent")
private void sendEventNative(final int eventType, final int sourceId, final int className, final GeckoBundle eventData) {
ThreadUtils.postToUiThread(new Runnable() {