Bug 1233118 - implement IAccessible2_3::selectionRanges, r=yzen

This commit is contained in:
Alexander Surkov 2016-01-20 12:53:03 -05:00
parent 60b138f772
commit 1eda297f31
18 changed files with 466 additions and 66 deletions

View File

@ -0,0 +1,29 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_a11y_TextRange_inl_h__
#define mozilla_a11y_TextRange_inl_h__
#include "TextRange.h"
#include "HyperTextAccessible.h"
namespace mozilla {
namespace a11y {
inline Accessible*
TextRange::Container() const
{
uint32_t pos1 = 0, pos2 = 0;
nsAutoTArray<Accessible*, 30> parents1, parents2;
return CommonParent(mStartContainer, mEndContainer,
&parents1, &pos1, &parents2, &pos2);
}
} // namespace a11y
} // namespace mozilla
#endif

View File

@ -4,10 +4,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "TextRange.h"
#include "TextRange-inl.h"
#include "Accessible-inl.h"
#include "HyperTextAccessible.h"
#include "nsAccUtils.h"
namespace mozilla {
@ -59,42 +58,6 @@ TextRange::TextRange(HyperTextAccessible* aRoot,
{
}
Accessible*
TextRange::Container() const
{
if (mStartContainer == mEndContainer)
return mStartContainer;
// Build the chain of parents
Accessible* p1 = mStartContainer;
Accessible* p2 = mEndContainer;
nsAutoTArray<Accessible*, 30> parents1, parents2;
do {
parents1.AppendElement(p1);
p1 = p1->Parent();
} while (p1);
do {
parents2.AppendElement(p2);
p2 = p2->Parent();
} while (p2);
// Find where the parent chain differs
uint32_t pos1 = parents1.Length();
uint32_t pos2 = parents2.Length();
Accessible* parent = nullptr;
uint32_t len = 0;
for (len = std::min(pos1, pos2); len > 0; --len) {
Accessible* child1 = parents1.ElementAt(--pos1);
Accessible* child2 = parents2.ElementAt(--pos2);
if (child1 != child2)
break;
parent = child1;
}
return parent;
}
void
TextRange::EmbeddedChildren(nsTArray<Accessible*>* aChildren) const
{
@ -111,28 +74,11 @@ TextRange::EmbeddedChildren(nsTArray<Accessible*>* aChildren) const
Accessible* p1 = mStartContainer->GetChildAtOffset(mStartOffset);
Accessible* p2 = mEndContainer->GetChildAtOffset(mEndOffset);
uint32_t pos1 = 0, pos2 = 0;
nsAutoTArray<Accessible*, 30> parents1, parents2;
do {
parents1.AppendElement(p1);
p1 = p1->Parent();
} while (p1);
do {
parents2.AppendElement(p2);
p2 = p2->Parent();
} while (p2);
// Find deepest common container.
uint32_t pos1 = parents1.Length();
uint32_t pos2 = parents2.Length();
Accessible* container = nullptr;
for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
Accessible* child1 = parents1.ElementAt(--pos1);
Accessible* child2 = parents2.ElementAt(--pos2);
if (child1 != child2)
break;
container = child1;
}
Accessible* container =
CommonParent(p1, p2, &parents1, &pos1, &parents2, &pos2);
// Traverse the tree up to the container and collect embedded objects.
for (uint32_t idx = 0; idx < pos1 - 1; idx++) {
@ -196,6 +142,95 @@ TextRange::Normalize(ETextUnit aUnit)
}
bool
TextRange::Crop(Accessible* aContainer)
{
uint32_t boundaryPos = 0, containerPos = 0;
nsAutoTArray<Accessible*, 30> boundaryParents, containerParents;
// Crop the start boundary.
Accessible* container = nullptr;
Accessible* boundary = mStartContainer->GetChildAtOffset(mStartOffset);
if (boundary != aContainer) {
CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos,
&containerParents, &containerPos);
if (boundaryPos == 0) {
if (containerPos != 0) {
// The container is contained by the start boundary, reduce the range to
// the point starting at the container.
aContainer->ToTextPoint(mStartContainer.StartAssignment(), &mStartOffset);
static_cast<Accessible*>(mStartContainer)->AddRef();
}
else {
// The start boundary and the container are siblings.
container = aContainer;
}
}
else if (containerPos != 0) {
// The container does not contain the start boundary.
boundary = boundaryParents[boundaryPos];
container = containerParents[containerPos];
}
if (container) {
// If the range start is after the container, then make the range invalid.
if (boundary->IndexInParent() > container->IndexInParent()) {
return !!(mRoot = nullptr);
}
// If the range starts before the container, then reduce the range to
// the point starting at the container.
if (boundary->IndexInParent() < container->IndexInParent()) {
container->ToTextPoint(mStartContainer.StartAssignment(), &mStartOffset);
mStartContainer.get()->AddRef();
}
}
boundaryParents.SetLengthAndRetainStorage(0);
containerParents.SetLengthAndRetainStorage(0);
}
boundary = mEndContainer->GetChildAtOffset(mEndOffset);
if (boundary == aContainer) {
return true;
}
// Crop the end boundary.
container = nullptr;
CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos,
&containerParents, &containerPos);
if (boundaryPos == 0) {
if (containerPos != 0) {
aContainer->ToTextPoint(mEndContainer.StartAssignment(), &mEndOffset, false);
static_cast<Accessible*>(mEndContainer)->AddRef();
}
else {
container = aContainer;
}
}
else if (containerPos != 0) {
boundary = boundaryParents[boundaryPos];
container = containerParents[containerPos];
}
if (!container) {
return true;
}
if (boundary->IndexInParent() < container->IndexInParent()) {
return !!(mRoot = nullptr);
}
if (boundary->IndexInParent() > container->IndexInParent()) {
container->ToTextPoint(mEndContainer.StartAssignment(), &mEndOffset, false);
static_cast<Accessible*>(mEndContainer)->AddRef();
}
return true;
}
void
TextRange::FindText(const nsAString& aText, EDirection aDirection,
nsCaseTreatment aCaseSensitive, TextRange* aFoundRange) const
@ -296,5 +331,46 @@ TextRange::MoveInternal(ETextUnit aUnit, int32_t aCount,
}
Accessible*
TextRange::CommonParent(Accessible* aAcc1, Accessible* aAcc2,
nsTArray<Accessible*>* aParents1, uint32_t* aPos1,
nsTArray<Accessible*>* aParents2, uint32_t* aPos2) const
{
if (aAcc1 == aAcc2) {
return aAcc1;
}
MOZ_ASSERT(aParents1->Length() == 0 || aParents2->Length() == 0,
"Wrong arguments");
// Build the chain of parents.
Accessible* p1 = aAcc1;
Accessible* p2 = aAcc2;
do {
aParents1->AppendElement(p1);
p1 = p1->Parent();
} while (p1);
do {
aParents2->AppendElement(p2);
p2 = p2->Parent();
} while (p2);
// Find where the parent chain differs
*aPos1 = aParents1->Length();
*aPos2 = aParents2->Length();
Accessible* parent = nullptr;
uint32_t len = 0;
for (len = std::min(*aPos1, *aPos2); len > 0; --len) {
Accessible* child1 = aParents1->ElementAt(--(*aPos1));
Accessible* child2 = aParents2->ElementAt(--(*aPos2));
if (child1 != child2)
break;
parent = child1;
}
return parent;
}
} // namespace a11y
} // namespace mozilla

View File

@ -131,6 +131,12 @@ public:
*/
void Normalize(ETextUnit aUnit);
/**
* Crops the range if it overlaps the given accessible element boundaries,
* returns true if the range was cropped successfully.
*/
bool Crop(Accessible* aContainer);
enum EDirection {
eBackward,
eForward
@ -243,6 +249,14 @@ private:
HyperTextAccessible* aStopContainer = nullptr,
int32_t aStopOffset = 0);
/**
* A helper method returning a common parent for two given accessible
* elements.
*/
Accessible* CommonParent(Accessible* aAcc1, Accessible* aAcc2,
nsTArray<Accessible*>* aParents1, uint32_t* aPos1,
nsTArray<Accessible*>* aParents2, uint32_t* aPos2) const;
RefPtr<HyperTextAccessible> mRoot;
RefPtr<HyperTextAccessible> mStartContainer;
RefPtr<HyperTextAccessible> mEndContainer;

View File

@ -2250,6 +2250,30 @@ Accessible::AnchorURIAt(uint32_t aAnchorIndex)
return nullptr;
}
void
Accessible::ToTextPoint(HyperTextAccessible** aContainer, int32_t* aOffset,
bool aIsBefore) const
{
if (IsHyperText()) {
*aContainer = const_cast<Accessible*>(this)->AsHyperText();
*aOffset = aIsBefore ? 0 : (*aContainer)->CharacterCount();
return;
}
const Accessible* child = nullptr;
const Accessible* parent = this;
do {
child = parent;
parent = parent->Parent();
} while (parent && !parent->IsHyperText());
if (parent) {
*aContainer = const_cast<Accessible*>(parent)->AsHyperText();
*aOffset = (*aContainer)->GetChildOffset(
child->IndexInParent() + static_cast<int32_t>(!aIsBefore));
}
}
////////////////////////////////////////////////////////////////////////////////
// SelectAccessible

View File

@ -758,6 +758,12 @@ public:
*/
virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex);
/**
* Returns a text point for the accessible element.
*/
void ToTextPoint(HyperTextAccessible** aContainer, int32_t* aOffset,
bool aIsBefore = true) const;
//////////////////////////////////////////////////////////////////////////////
// SelectAccessible

View File

@ -1769,7 +1769,7 @@ HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const
void
HyperTextAccessible::SelectionRanges(nsTArray<a11y::TextRange>* aRanges) const
{
NS_ASSERTION(aRanges->Length() != 0, "TextRange array supposed to be empty");
MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty");
dom::Selection* sel = DOMSelection();
if (!sel)

View File

@ -258,7 +258,7 @@ public:
* @param aInvalidateAfter [in, optional] indicates whether invalidate
* cached offsets for next siblings of the child
*/
int32_t GetChildOffset(Accessible* aChild,
int32_t GetChildOffset(const Accessible* aChild,
bool aInvalidateAfter = false) const
{
int32_t index = GetIndexOf(aChild);

View File

@ -11,6 +11,7 @@ GARBAGE += $(MIDL_GENERATED_FILES)
MIDL_INTERFACES = \
Accessible2.idl \
Accessible2_2.idl \
Accessible2_3.idl \
AccessibleAction.idl \
AccessibleApplication.idl \
AccessibleComponent.idl \

View File

@ -13,7 +13,7 @@ interface nsIVariant;
/**
* A range representing a piece of text in the document.
*/
[scriptable, uuid(525b3401-8a67-4822-b35d-661065767cd8)]
[scriptable, uuid(c4515623-55f9-4543-a3d5-c1e9afa588f4)]
interface nsIAccessibleTextRange : nsISupports
{
readonly attribute nsIAccessibleText startContainer;
@ -81,6 +81,11 @@ interface nsIAccessibleTextRange : nsISupports
*/
void normalize(in unsigned long aUnit);
/**
* Crops the range by the given accessible element.
*/
boolean crop(in nsIAccessible aContainer);
/**
* Return range enclosing the found text.
*/

View File

@ -473,6 +473,10 @@ function testTextRange(aRange, aRangeDescr, aStartContainer, aStartOffset,
is(aRange.endOffset, aEndOffset,
"Wrong end offset of " + aRangeDescr);
if (aText === undefined) {
return;
}
is(aRange.text, aText, "Wrong text of " + aRangeDescr);
var children = aRange.embeddedChildren;

View File

@ -1,3 +1,4 @@
[DEFAULT]
[test_general.html]
[test_selection.html]

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>Text Range selection tests</title>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript"
src="../common.js"></script>
<script type="application/javascript"
src="../text.js"></script>
<script type="application/javascript"
src="../layout.js"></script>
<script type="application/javascript">
function doTest()
{
var sel = window.getSelection();
var p = getNode("p1");
var a = getNode("p2_a");
var range = document.createRange();
sel.addRange(range);
// the accessible is contained by the range
range.selectNode(p);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #1", document, 3, document, 4);
ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #1.");
testTextRange(a11yrange, "cropped range #1", a, 0, a, 5);
// the range is contained by the accessible
range.selectNode(a);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #2", p, 5, p, 6);
ok(a11yrange.crop(getAccessible(p)), "Range failed to crop #2.");
testTextRange(a11yrange, "cropped range #2", p, 5, p, 6);
// the range starts before the accessible and ends inside it
range.setStart(p, 0);
range.setEndAfter(a.firstChild, 4);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #3", p, 0, a, 4);
ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #3.");
testTextRange(a11yrange, "cropped range #3", a, 0, a, 4);
// the range starts inside the accessible and ends after it
range.setStart(a.firstChild, 1);
range.setEndAfter(p);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #4", a, 1, document, 4);
ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #4.");
testTextRange(a11yrange, "cropped range #4", a, 1, a, 5);
// the range ends before the accessible
range.setStart(p.firstChild, 0);
range.setEnd(p.firstChild, 4);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #5", p, 0, p, 4);
ok(!a11yrange.crop(getAccessible(a)), "Crop #5 succeeded while it shouldn't");
// the range starts after the accessible
range.setStart(p.lastChild, 0);
range.setEnd(p.lastChild, 4);
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #6", p, 6, p, 10);
ok(!a11yrange.crop(getAccessible(a)), "Crop #6 succeeded while it shouldn't");
// crop a range by a table
range.selectNode(getNode("c2"));
var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
testTextRange(a11yrange, "selection range #7", document, 4, document, 5);
ok(a11yrange.crop(getAccessible("table")), "Range failed to crop #7.");
testTextRange(a11yrange, "cropped range #7", "c2", 5, "c2", 6);
SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();
addA11yLoadEvent(doTest);
</script>
</head>
<body>
<a target="_blank"
title="Implement IAccessible2_3::selectionRanges"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=1233118">Bug 1233118</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
<p id="p1">text <a id="p2_a" href="www">link<img id="p2_img", src="../moz.png"></a> text</p>
<div id="c2">start<table id="table"><tr><td>cell</td></tr></table>end</div>
</body>
</html>

View File

@ -8,6 +8,7 @@
#include "Accessible2_i.c"
#include "Accessible2_2_i.c"
#include "Accessible2_3_i.c"
#include "AccessibleRole.h"
#include "AccessibleStates.h"
@ -18,6 +19,7 @@
#include "nsIAccessibleTypes.h"
#include "mozilla/a11y/PDocAccessible.h"
#include "Relation.h"
#include "TextRange-inl.h"
#include "nsAccessibilityService.h"
#include "nsIPersistentProperties2.h"
@ -40,7 +42,9 @@ ia2Accessible::QueryInterface(REFIID iid, void** ppv)
*ppv = nullptr;
if (IID_IAccessible2_2 == iid)
if (IID_IAccessible2_3 == iid)
*ppv = static_cast<IAccessible2_3*>(this);
else if (IID_IAccessible2_2 == iid)
*ppv = static_cast<IAccessible2_2*>(this);
else if (IID_IAccessible2 == iid && !Compatibility::IsIA2Off())
*ppv = static_cast<IAccessible2*>(this);
@ -749,6 +753,58 @@ ia2Accessible::get_relationTargetsOfType(BSTR aType,
A11Y_TRYBLOCK_END
}
STDMETHODIMP
ia2Accessible::get_selectionRanges(IA2Range** aRanges,
long *aNRanges)
{
A11Y_TRYBLOCK_BEGIN
if (!aRanges || !aNRanges || aNRanges <= 0)
return E_INVALIDARG;
*aNRanges = 0;
AccessibleWrap* acc = static_cast<AccessibleWrap*>(this);
if (acc->IsDefunct())
return CO_E_OBJNOTCONNECTED;
nsAutoTArray<TextRange, 1> ranges;
acc->Document()->SelectionRanges(&ranges);
uint32_t len = ranges.Length();
for (uint32_t idx = 0; idx < len; idx++) {
if (!ranges[idx].Crop(acc)) {
ranges.RemoveElementAt(idx);
}
}
*aNRanges = ranges.Length();
*aRanges = static_cast<IA2Range*>(
::CoTaskMemAlloc(sizeof(IA2Range) * *aNRanges));
if (!*aRanges)
return E_OUTOFMEMORY;
for (uint32_t idx = 0; idx < static_cast<uint32_t>(*aNRanges); idx++) {
AccessibleWrap* anchor =
static_cast<AccessibleWrap*>(ranges[idx].StartContainer());
(*aRanges)[idx].anchor = static_cast<IAccessible2*>(anchor);
anchor->AddRef();
(*aRanges)[idx].anchorOffset = ranges[idx].StartOffset();
AccessibleWrap* active =
static_cast<AccessibleWrap*>(ranges[idx].EndContainer());
(*aRanges)[idx].active = static_cast<IAccessible2*>(active);
active->AddRef();
(*aRanges)[idx].activeOffset = ranges[idx].EndOffset();
}
return S_OK;
A11Y_TRYBLOCK_END
}
////////////////////////////////////////////////////////////////////////////////
// Helpers

View File

@ -9,13 +9,13 @@
#include "nsISupports.h"
#include "Accessible2_2.h"
#include "Accessible2_3.h"
namespace mozilla {
namespace a11y {
class Attribute;
class ia2Accessible : public IAccessible2_2
class ia2Accessible : public IAccessible2_3
{
public:
@ -104,6 +104,11 @@ public:
/* [out, retval] */ long* nTargets
);
// IAccessible2_3
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectionRanges(
/* [out, size_is(,*nRanges)] */ IA2Range** ranges,
/* [out, retval] */ long *nRanges);
// Helper method
static HRESULT ConvertToIA2Attributes(nsIPersistentProperties* aAttributes,
BSTR* aIA2Attributes);

View File

@ -6,7 +6,7 @@
#include "xpcAccessibleTextRange.h"
#include "TextRange.h"
#include "TextRange-inl.h"
#include "xpcAccessibleDocument.h"
#include "nsIMutableArray.h"
@ -170,6 +170,16 @@ xpcAccessibleTextRange::Normalize(uint32_t aUnit)
return NS_OK;
}
NS_IMETHODIMP
xpcAccessibleTextRange::Crop(nsIAccessible* aContainer, bool* aSuccess)
{
Accessible* container = aContainer->ToInternalAccessible();
NS_ENSURE_TRUE(container, NS_ERROR_INVALID_ARG);
*aSuccess = mRange.Crop(container);
return NS_OK;
}
NS_IMETHODIMP
xpcAccessibleTextRange::FindText(const nsAString& aText, bool aIsBackward,
bool aIsIgnoreCase,

View File

@ -49,6 +49,7 @@ public:
NS_IMETHOD MoveStart(uint32_t aUnit, int32_t aCount) final override;
NS_IMETHOD MoveEnd(uint32_t aUnit, int32_t aCount) final override;
NS_IMETHOD Normalize(uint32_t aUnit) final override;
NS_IMETHOD Crop(nsIAccessible* aContainer, bool* aSuccess) final override;
NS_IMETHOD FindText(const nsAString& aText, bool aIsBackward, bool aIsIgnoreCase,
nsIAccessibleTextRange** aRange) final override;
NS_IMETHOD FindAttr(uint32_t aAttr, nsIVariant* aVal, bool aIsBackward,

View File

@ -0,0 +1,47 @@
import "objidl.idl";
import "oaidl.idl";
import "oleacc.idl";
import "Accessible2_2.idl";
/**
* This structure represents a directional range of the content. It is defined
* by two points in the content, where each one is defined by an accessible
* object and an offset relative to it. A typical case of a range point is
* a text accessible and text offset within it.
*
* The "anchor" is one point of the range and typically remains constant.
* The other point is the "active" point, which typically corresponds to
* the user's focus or point of interest. The user moves the active point to
* expand or collapse the range. In most cases, anchor is the start of the range
* and active is the end. However, in case of selection, when selecting
* backwards (e.g. pressing shift+left arrow in a text field), the start of
* the range is the active point, as the user moves this to manipulate
* the selection.
*/
typedef struct IA2Range {
IUnknown* anchor;
long anchorOffset;
IUnknown* active;
long activeOffset;
} IA2Range;
/**
* @brief This interface is an extension of IAccessible2_2 and IAccessible2
* interfaces.
*/
[object, uuid(5BE18059-762E-4E73-9476-ABA294FED411)]
interface IAccessible2_3 : IAccessible2_2
{
/**
* @brief Returns an array of ranges for selections within the accessible.
* @param [out] the array of selection ranges
* @param [out] the array length
* @retval S_OK
* @retval S_FALSE returned if there is no selection within the accessible
*/
[propget] HRESULT selectionRanges
(
[out, size_is(,*nRanges)] IA2Range **ranges,
[out, retval] long *nRanges
);
}

View File

@ -71,6 +71,7 @@ library IAccessible2Lib
importlib ("oleacc.dll");
interface IAccessible2;
interface IAccessible2_2;
interface IAccessible2_3;
interface IAccessibleAction;
interface IAccessibleApplication;
interface IAccessibleComponent;