зеркало из https://github.com/mozilla/gecko-dev.git
943 строки
30 KiB
C++
943 строки
30 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* 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/. */
|
|
|
|
#include "AccessibleWrap.h"
|
|
|
|
#include "JavaBuiltins.h"
|
|
#include "Accessible-inl.h"
|
|
#include "HyperTextAccessible-inl.h"
|
|
#include "AccEvent.h"
|
|
#include "AndroidInputType.h"
|
|
#include "DocAccessibleWrap.h"
|
|
#include "IDSet.h"
|
|
#include "SessionAccessibility.h"
|
|
#include "TextLeafAccessible.h"
|
|
#include "TraversalRule.h"
|
|
#include "Pivot.h"
|
|
#include "Platform.h"
|
|
#include "nsAccessibilityService.h"
|
|
#include "nsEventShell.h"
|
|
#include "nsPersistentProperties.h"
|
|
#include "nsIAccessibleAnnouncementEvent.h"
|
|
#include "nsAccUtils.h"
|
|
#include "nsTextEquivUtils.h"
|
|
#include "nsWhitespaceTokenizer.h"
|
|
#include "RootAccessible.h"
|
|
|
|
#include "mozilla/a11y/PDocAccessibleChild.h"
|
|
#include "mozilla/jni/GeckoBundleUtils.h"
|
|
|
|
// icu TRUE conflicting with java::sdk::Boolean::TRUE()
|
|
// https://searchfox.org/mozilla-central/rev/ce02064d8afc8673cef83c92896ee873bd35e7ae/intl/icu/source/common/unicode/umachine.h#265
|
|
// https://searchfox.org/mozilla-central/source/__GENERATED__/widget/android/bindings/JavaBuiltins.h#78
|
|
#ifdef TRUE
|
|
# undef TRUE
|
|
#endif
|
|
|
|
using namespace mozilla::a11y;
|
|
|
|
// IDs should be a positive 32bit integer.
|
|
IDSet sIDSet(31UL);
|
|
|
|
//-----------------------------------------------------
|
|
// construction
|
|
//-----------------------------------------------------
|
|
AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
|
|
: Accessible(aContent, aDoc) {
|
|
if (aDoc) {
|
|
mID = AcquireID();
|
|
DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
|
|
doc->AddID(mID, this);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// destruction
|
|
//-----------------------------------------------------
|
|
AccessibleWrap::~AccessibleWrap() {}
|
|
|
|
nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
|
|
auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
|
|
NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
|
|
DocAccessibleWrap* doc =
|
|
static_cast<DocAccessibleWrap*>(accessible->Document());
|
|
if (doc) {
|
|
switch (aEvent->GetEventType()) {
|
|
case nsIAccessibleEvent::EVENT_FOCUS: {
|
|
if (DocAccessibleWrap* topContentDoc =
|
|
doc->GetTopLevelContentDoc(accessible)) {
|
|
topContentDoc->CacheFocusPath(accessible);
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
|
|
AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
|
|
auto newPosition =
|
|
static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
|
|
if (newPosition) {
|
|
if (DocAccessibleWrap* topContentDoc =
|
|
doc->GetTopLevelContentDoc(accessible)) {
|
|
topContentDoc->CacheFocusPath(newPosition);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_REORDER: {
|
|
if (DocAccessibleWrap* topContentDoc =
|
|
doc->GetTopLevelContentDoc(accessible)) {
|
|
topContentDoc->CacheViewport(true);
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
|
|
if (accessible != aEvent->Document() && !aEvent->IsFromUserInput()) {
|
|
AccCaretMoveEvent* caretEvent = downcast_accEvent(aEvent);
|
|
HyperTextAccessible* ht = AsHyperText();
|
|
// Pivot to the caret's position if it has an expanded selection.
|
|
// This is used mostly for find in page.
|
|
if ((ht && ht->SelectionCount())) {
|
|
DOMPoint point =
|
|
AsHyperText()->OffsetToDOMPoint(caretEvent->GetCaretOffset());
|
|
if (Accessible* newPos =
|
|
doc->GetAccessibleOrContainer(point.node)) {
|
|
static_cast<AccessibleWrap*>(newPos)->Pivot(
|
|
java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
|
|
true);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_SCROLLING_START: {
|
|
accessible->Pivot(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
|
|
true, true);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
nsresult rv = Accessible::HandleAccEvent(aEvent);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
accessible->HandleLiveRegionEvent(aEvent);
|
|
|
|
if (IPCAccessibilityActive()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// The accessible can become defunct if we have an xpcom event listener
|
|
// which decides it would be fun to change the DOM and flush layout.
|
|
if (accessible->IsDefunct() || !accessible->IsBoundToParent()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
if (doc) {
|
|
if (!nsCoreUtils::IsContentDocument(doc->DocumentNode())) {
|
|
return NS_OK;
|
|
}
|
|
}
|
|
|
|
RefPtr<SessionAccessibility> sessionAcc =
|
|
SessionAccessibility::GetInstanceFor(accessible);
|
|
if (!sessionAcc) {
|
|
return NS_OK;
|
|
}
|
|
|
|
switch (aEvent->GetEventType()) {
|
|
case nsIAccessibleEvent::EVENT_FOCUS:
|
|
sessionAcc->SendFocusEvent(accessible);
|
|
break;
|
|
case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
|
|
AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
|
|
if (!vcEvent->IsFromUserInput()) {
|
|
break;
|
|
}
|
|
|
|
RefPtr<AccessibleWrap> newPosition =
|
|
static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
|
|
if (sessionAcc && newPosition) {
|
|
if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) {
|
|
sessionAcc->SendHoverEnterEvent(newPosition);
|
|
} else if (vcEvent->BoundaryType() == nsIAccessiblePivot::NO_BOUNDARY) {
|
|
sessionAcc->SendAccessibilityFocusedEvent(newPosition);
|
|
}
|
|
|
|
if (vcEvent->BoundaryType() != nsIAccessiblePivot::NO_BOUNDARY) {
|
|
sessionAcc->SendTextTraversedEvent(
|
|
newPosition, vcEvent->NewStartOffset(), vcEvent->NewEndOffset());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
|
|
AccCaretMoveEvent* event = downcast_accEvent(aEvent);
|
|
sessionAcc->SendTextSelectionChangedEvent(accessible,
|
|
event->GetCaretOffset());
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
|
|
case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
|
|
AccTextChangeEvent* event = downcast_accEvent(aEvent);
|
|
sessionAcc->SendTextChangedEvent(
|
|
accessible, event->ModifiedText(), event->GetStartOffset(),
|
|
event->GetLength(), event->IsTextInserted(),
|
|
event->IsFromUserInput());
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
|
|
AccStateChangeEvent* event = downcast_accEvent(aEvent);
|
|
auto state = event->GetState();
|
|
if (state & states::CHECKED) {
|
|
sessionAcc->SendClickedEvent(
|
|
accessible, java::SessionAccessibility::FLAG_CHECKABLE |
|
|
(event->IsStateEnabled()
|
|
? java::SessionAccessibility::FLAG_CHECKED
|
|
: 0));
|
|
}
|
|
|
|
if (state & states::EXPANDED) {
|
|
sessionAcc->SendClickedEvent(
|
|
accessible, java::SessionAccessibility::FLAG_EXPANDABLE |
|
|
(event->IsStateEnabled()
|
|
? java::SessionAccessibility::FLAG_EXPANDED
|
|
: 0));
|
|
}
|
|
|
|
if (state & states::SELECTED) {
|
|
sessionAcc->SendSelectedEvent(accessible, event->IsStateEnabled());
|
|
}
|
|
|
|
if (state & states::BUSY) {
|
|
sessionAcc->SendWindowStateChangedEvent(accessible);
|
|
}
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_SCROLLING: {
|
|
AccScrollingEvent* event = downcast_accEvent(aEvent);
|
|
sessionAcc->SendScrollingEvent(accessible, event->ScrollX(),
|
|
event->ScrollY(), event->MaxScrollX(),
|
|
event->MaxScrollY());
|
|
break;
|
|
}
|
|
case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
|
|
AccAnnouncementEvent* event = downcast_accEvent(aEvent);
|
|
sessionAcc->SendAnnouncementEvent(accessible, event->Announcement(),
|
|
event->Priority());
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void AccessibleWrap::Shutdown() {
|
|
if (mDoc) {
|
|
if (mID > 0) {
|
|
if (auto doc = static_cast<DocAccessibleWrap*>(mDoc.get())) {
|
|
doc->RemoveID(mID);
|
|
}
|
|
ReleaseID(mID);
|
|
mID = 0;
|
|
}
|
|
}
|
|
|
|
Accessible::Shutdown();
|
|
}
|
|
|
|
bool AccessibleWrap::DoAction(uint8_t aIndex) const {
|
|
if (ActionCount()) {
|
|
return Accessible::DoAction(aIndex);
|
|
}
|
|
|
|
if (mContent) {
|
|
// We still simulate a click on an accessible even if there is no
|
|
// known actions. For the sake of bad markup.
|
|
DoCommand();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int32_t AccessibleWrap::AcquireID() { return sIDSet.GetID(); }
|
|
|
|
void AccessibleWrap::ReleaseID(int32_t aID) { sIDSet.ReleaseID(aID); }
|
|
|
|
void AccessibleWrap::SetTextContents(const nsAString& aText) {
|
|
if (IsHyperText()) {
|
|
AsHyperText()->ReplaceText(aText);
|
|
}
|
|
}
|
|
|
|
void AccessibleWrap::GetTextContents(nsAString& aText) {
|
|
// For now it is a simple wrapper for getting entire range of TextSubstring.
|
|
// In the future this may be smarter and retrieve a flattened string.
|
|
if (IsHyperText()) {
|
|
AsHyperText()->TextSubstring(0, -1, aText);
|
|
} else if (IsTextLeaf()) {
|
|
aText = AsTextLeaf()->Text();
|
|
}
|
|
}
|
|
|
|
bool AccessibleWrap::GetSelectionBounds(int32_t* aStartOffset,
|
|
int32_t* aEndOffset) {
|
|
if (IsHyperText()) {
|
|
return AsHyperText()->SelectionBoundsAt(0, aStartOffset, aEndOffset);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void AccessibleWrap::Pivot(int32_t aGranularity, bool aForward,
|
|
bool aInclusive) {
|
|
AccessibleOrProxy accOrProxyRoot = AccessibleOrProxy(RootAccessible());
|
|
a11y::Pivot pivot(accOrProxyRoot);
|
|
TraversalRule rule(aGranularity);
|
|
AccessibleOrProxy accOrProxy = AccessibleOrProxy(this);
|
|
Accessible* result =
|
|
aForward ? pivot.Next(accOrProxy, rule, aInclusive).AsAccessible()
|
|
: pivot.Prev(accOrProxy, rule, aInclusive).AsAccessible();
|
|
if (result && (result != this || aInclusive)) {
|
|
PivotMoveReason reason = aForward ? nsIAccessiblePivot::REASON_NEXT
|
|
: nsIAccessiblePivot::REASON_PREV;
|
|
RefPtr<AccEvent> event = new AccVCChangeEvent(
|
|
result->Document(), this, -1, -1, result, -1, -1, reason,
|
|
nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput);
|
|
nsEventShell::FireEvent(event);
|
|
}
|
|
}
|
|
|
|
void AccessibleWrap::ExploreByTouch(float aX, float aY) {
|
|
a11y::Pivot pivot(RootAccessible());
|
|
TraversalRule rule;
|
|
|
|
Accessible* result = pivot.AtPoint(aX, aY, rule).AsAccessible();
|
|
|
|
if (result && result != this) {
|
|
RefPtr<AccEvent> event =
|
|
new AccVCChangeEvent(result->Document(), this, -1, -1, result, -1, -1,
|
|
nsIAccessiblePivot::REASON_POINT,
|
|
nsIAccessiblePivot::NO_BOUNDARY, eFromUserInput);
|
|
nsEventShell::FireEvent(event);
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
if (IsTextLeaf() && newAnchor == Parent()) {
|
|
// 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);
|
|
}
|
|
|
|
// 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::SetSelection(int32_t aStart, int32_t aEnd) {
|
|
if (HyperTextAccessible* textAcc = AsHyperText()) {
|
|
if (aStart == aEnd) {
|
|
textAcc->SetCaretOffset(aStart);
|
|
} else {
|
|
textAcc->SetSelectionBoundsAt(0, aStart, aEnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AccessibleWrap::Cut() {
|
|
if ((State() & states::EDITABLE) == 0) {
|
|
return;
|
|
}
|
|
|
|
if (HyperTextAccessible* textAcc = AsHyperText()) {
|
|
int32_t startSel, endSel;
|
|
GetSelectionOrCaret(&startSel, &endSel);
|
|
textAcc->CutText(startSel, endSel);
|
|
}
|
|
}
|
|
|
|
void AccessibleWrap::Copy() {
|
|
if (HyperTextAccessible* textAcc = AsHyperText()) {
|
|
int32_t startSel, endSel;
|
|
GetSelectionOrCaret(&startSel, &endSel);
|
|
textAcc->CopyText(startSel, endSel);
|
|
}
|
|
}
|
|
|
|
void AccessibleWrap::Paste() {
|
|
if ((State() & states::EDITABLE) == 0) {
|
|
return;
|
|
}
|
|
|
|
if (IsHyperText()) {
|
|
RefPtr<HyperTextAccessible> textAcc = AsHyperText();
|
|
int32_t startSel, endSel;
|
|
GetSelectionOrCaret(&startSel, &endSel);
|
|
if (startSel != endSel) {
|
|
textAcc->DeleteText(startSel, endSel);
|
|
}
|
|
textAcc->PasteText(startSel);
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (aState & states::CHECKABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_CHECKABLE;
|
|
}
|
|
|
|
if (aState & states::CHECKED) {
|
|
flags |= java::SessionAccessibility::FLAG_CHECKED;
|
|
}
|
|
|
|
if (aState & states::INVALID) {
|
|
flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
|
|
}
|
|
|
|
if (aState & states::EDITABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_EDITABLE;
|
|
}
|
|
|
|
if (aActionCount && aRole != roles::TEXT_LEAF) {
|
|
flags |= java::SessionAccessibility::FLAG_CLICKABLE;
|
|
}
|
|
|
|
if (aState & states::ENABLED) {
|
|
flags |= java::SessionAccessibility::FLAG_ENABLED;
|
|
}
|
|
|
|
if (aState & states::FOCUSABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
|
|
}
|
|
|
|
if (aState & states::FOCUSED) {
|
|
flags |= java::SessionAccessibility::FLAG_FOCUSED;
|
|
}
|
|
|
|
if (aState & states::MULTI_LINE) {
|
|
flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
|
|
}
|
|
|
|
if (aState & states::SELECTABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_SELECTABLE;
|
|
}
|
|
|
|
if (aState & states::SELECTED) {
|
|
flags |= java::SessionAccessibility::FLAG_SELECTED;
|
|
}
|
|
|
|
if (aState & states::EXPANDABLE) {
|
|
flags |= java::SessionAccessibility::FLAG_EXPANDABLE;
|
|
}
|
|
|
|
if (aState & states::EXPANDED) {
|
|
flags |= java::SessionAccessibility::FLAG_EXPANDED;
|
|
}
|
|
|
|
if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
|
|
flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
|
|
}
|
|
|
|
if (aRole == roles::PASSWORD_TEXT) {
|
|
flags |= java::SessionAccessibility::FLAG_PASSWORD;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
void AccessibleWrap::GetRoleDescription(role aRole,
|
|
nsIPersistentProperties* aAttributes,
|
|
nsAString& aGeckoRole,
|
|
nsAString& aRoleDescription) {
|
|
if (aRole == roles::HEADING && aAttributes) {
|
|
// The heading level is an attribute, so we need that.
|
|
AutoTArray<nsString, 1> formatString;
|
|
nsresult rv = aAttributes->GetStringProperty("level"_ns,
|
|
*formatString.AppendElement());
|
|
if (NS_SUCCEEDED(rv) &&
|
|
LocalizeString("headingLevel", aRoleDescription, formatString)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((aRole == roles::LANDMARK || aRole == roles::REGION) && aAttributes) {
|
|
nsAutoString xmlRoles;
|
|
if (NS_SUCCEEDED(
|
|
aAttributes->GetStringProperty("xml-roles"_ns, xmlRoles))) {
|
|
nsWhitespaceTokenizer tokenizer(xmlRoles);
|
|
while (tokenizer.hasMoreTokens()) {
|
|
if (LocalizeString(NS_ConvertUTF16toUTF8(tokenizer.nextToken()).get(),
|
|
aRoleDescription)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GetAccService()->GetStringRole(aRole, aGeckoRole);
|
|
LocalizeString(NS_ConvertUTF16toUTF8(aGeckoRole).get(), aRoleDescription);
|
|
}
|
|
|
|
already_AddRefed<nsIPersistentProperties>
|
|
AccessibleWrap::AttributeArrayToProperties(
|
|
const nsTArray<Attribute>& aAttributes) {
|
|
RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
|
|
nsAutoString unused;
|
|
|
|
for (size_t i = 0; i < aAttributes.Length(); i++) {
|
|
props->SetStringProperty(aAttributes.ElementAt(i).Name(),
|
|
aAttributes.ElementAt(i).Value(), unused);
|
|
}
|
|
|
|
return props.forget();
|
|
}
|
|
|
|
int32_t AccessibleWrap::GetAndroidClass(role aRole) {
|
|
#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \
|
|
ia2Role, androidClass, nameRule) \
|
|
case roles::geckoRole: \
|
|
return androidClass;
|
|
|
|
switch (aRole) {
|
|
#include "RoleMap.h"
|
|
default:
|
|
return java::SessionAccessibility::CLASSNAME_VIEW;
|
|
}
|
|
|
|
#undef ROLE
|
|
}
|
|
|
|
int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
|
|
if (aInputTypeAttr.EqualsIgnoreCase("email")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("number")) {
|
|
return java::sdk::InputType::TYPE_CLASS_NUMBER;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("password")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_PASSWORD;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("tel")) {
|
|
return java::sdk::InputType::TYPE_CLASS_PHONE;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("text")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
|
|
}
|
|
|
|
if (aInputTypeAttr.EqualsIgnoreCase("url")) {
|
|
return java::sdk::InputType::TYPE_CLASS_TEXT |
|
|
java::sdk::InputType::TYPE_TEXT_VARIATION_URI;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AccessibleWrap::WrapperDOMNodeID(nsString& aDOMNodeID) {
|
|
if (mContent) {
|
|
nsAtom* id = mContent->GetID();
|
|
if (id) {
|
|
id->ToString(aDOMNodeID);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AccessibleWrap::WrapperRangeInfo(double* aCurVal, double* aMinVal,
|
|
double* aMaxVal, double* aStep) {
|
|
if (HasNumericValue()) {
|
|
*aCurVal = CurValue();
|
|
*aMinVal = MinValue();
|
|
*aMaxVal = MaxValue();
|
|
*aStep = Step();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
mozilla::java::GeckoBundle::LocalRef AccessibleWrap::ToBundle(bool aSmall) {
|
|
nsAutoString name;
|
|
Name(name);
|
|
nsAutoString textValue;
|
|
Value(textValue);
|
|
nsAutoString nodeID;
|
|
WrapperDOMNodeID(nodeID);
|
|
nsAutoString description;
|
|
Description(description);
|
|
|
|
if (aSmall) {
|
|
return ToBundle(State(), Bounds(), ActionCount(), name, textValue, nodeID,
|
|
description);
|
|
}
|
|
|
|
double curValue = UnspecifiedNaN<double>();
|
|
double minValue = UnspecifiedNaN<double>();
|
|
double maxValue = UnspecifiedNaN<double>();
|
|
double step = UnspecifiedNaN<double>();
|
|
WrapperRangeInfo(&curValue, &minValue, &maxValue, &step);
|
|
|
|
nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
|
|
|
|
return ToBundle(State(), Bounds(), ActionCount(), name, textValue, nodeID,
|
|
description, curValue, minValue, maxValue, step, attributes);
|
|
}
|
|
|
|
mozilla::java::GeckoBundle::LocalRef AccessibleWrap::ToBundle(
|
|
const uint64_t aState, const nsIntRect& aBounds, const uint8_t aActionCount,
|
|
const nsString& aName, const nsString& aTextValue,
|
|
const nsString& aDOMNodeID, const nsString& aDescription,
|
|
const double& aCurVal, const double& aMinVal, const double& aMaxVal,
|
|
const double& aStep, nsIPersistentProperties* aAttributes) {
|
|
if (!IsProxy() && IsDefunct()) {
|
|
return nullptr;
|
|
}
|
|
|
|
GECKOBUNDLE_START(nodeInfo);
|
|
GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(VirtualViewID()));
|
|
|
|
AccessibleWrap* parent = WrapperParent();
|
|
GECKOBUNDLE_PUT(
|
|
nodeInfo, "parentId",
|
|
java::sdk::Integer::ValueOf(parent ? parent->VirtualViewID() : 0));
|
|
|
|
role role = WrapperRole();
|
|
if (role == roles::LINK && !(aState & states::LINKED)) {
|
|
// A link without the linked state (<a> with no href) shouldn't be presented
|
|
// as a link.
|
|
role = roles::TEXT;
|
|
}
|
|
|
|
uint32_t flags = GetFlags(role, aState, aActionCount);
|
|
GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags));
|
|
GECKOBUNDLE_PUT(nodeInfo, "className",
|
|
java::sdk::Integer::ValueOf(AndroidClass()));
|
|
|
|
nsAutoString hint;
|
|
if (aState & states::EDITABLE) {
|
|
// An editable field's name is populated in the hint.
|
|
hint.Assign(aName);
|
|
GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
|
|
} else {
|
|
if (role == roles::LINK || role == roles::HEADING) {
|
|
GECKOBUNDLE_PUT(nodeInfo, "description", jni::StringParam(aName));
|
|
} else {
|
|
GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
|
|
}
|
|
}
|
|
|
|
if (!aDescription.IsEmpty()) {
|
|
if (!hint.IsEmpty()) {
|
|
// If this is an editable, the description is concatenated with a
|
|
// whitespace directly after the name.
|
|
hint.AppendLiteral(" ");
|
|
}
|
|
hint.Append(aDescription);
|
|
}
|
|
|
|
if ((aState & states::REQUIRED) != 0) {
|
|
nsAutoString requiredString;
|
|
if (LocalizeString("stateRequired", requiredString)) {
|
|
if (!hint.IsEmpty()) {
|
|
// If the hint is non-empty, concatenate with a comma for a brief pause.
|
|
hint.AppendLiteral(", ");
|
|
}
|
|
hint.Append(requiredString);
|
|
}
|
|
}
|
|
|
|
if (!hint.IsEmpty()) {
|
|
GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(hint));
|
|
}
|
|
|
|
nsAutoString geckoRole;
|
|
nsAutoString roleDescription;
|
|
if (VirtualViewID() != kNoID) {
|
|
GetRoleDescription(role, aAttributes, geckoRole, roleDescription);
|
|
}
|
|
|
|
GECKOBUNDLE_PUT(nodeInfo, "roleDescription",
|
|
jni::StringParam(roleDescription));
|
|
GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
|
|
|
|
if (!aDOMNodeID.IsEmpty()) {
|
|
GECKOBUNDLE_PUT(nodeInfo, "viewIdResourceName",
|
|
jni::StringParam(aDOMNodeID));
|
|
}
|
|
|
|
const int32_t data[4] = {aBounds.x, aBounds.y, aBounds.x + aBounds.width,
|
|
aBounds.y + aBounds.height};
|
|
GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4));
|
|
|
|
if (HasNumericValue()) {
|
|
GECKOBUNDLE_START(rangeInfo);
|
|
if (aMaxVal == 1 && aMinVal == 0) {
|
|
GECKOBUNDLE_PUT(rangeInfo, "type",
|
|
java::sdk::Integer::ValueOf(2)); // percent
|
|
} else if (std::round(aStep) != aStep) {
|
|
GECKOBUNDLE_PUT(rangeInfo, "type",
|
|
java::sdk::Integer::ValueOf(1)); // float
|
|
} else {
|
|
GECKOBUNDLE_PUT(rangeInfo, "type",
|
|
java::sdk::Integer::ValueOf(0)); // integer
|
|
}
|
|
|
|
if (!IsNaN(aCurVal)) {
|
|
GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal));
|
|
}
|
|
if (!IsNaN(aMinVal)) {
|
|
GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal));
|
|
}
|
|
if (!IsNaN(aMaxVal)) {
|
|
GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
|
|
}
|
|
|
|
GECKOBUNDLE_FINISH(rangeInfo);
|
|
GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
|
|
}
|
|
|
|
if (aAttributes) {
|
|
nsString inputTypeAttr;
|
|
nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType,
|
|
inputTypeAttr);
|
|
int32_t inputType = GetInputType(inputTypeAttr);
|
|
if (inputType) {
|
|
GECKOBUNDLE_PUT(nodeInfo, "inputType",
|
|
java::sdk::Integer::ValueOf(inputType));
|
|
}
|
|
|
|
nsString posinset;
|
|
nsresult rv = aAttributes->GetStringProperty("posinset"_ns, posinset);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
int32_t rowIndex;
|
|
if (sscanf(NS_ConvertUTF16toUTF8(posinset).get(), "%d", &rowIndex) > 0) {
|
|
GECKOBUNDLE_START(collectionItemInfo);
|
|
GECKOBUNDLE_PUT(collectionItemInfo, "rowIndex",
|
|
java::sdk::Integer::ValueOf(rowIndex));
|
|
GECKOBUNDLE_PUT(collectionItemInfo, "columnIndex",
|
|
java::sdk::Integer::ValueOf(0));
|
|
GECKOBUNDLE_PUT(collectionItemInfo, "rowSpan",
|
|
java::sdk::Integer::ValueOf(1));
|
|
GECKOBUNDLE_PUT(collectionItemInfo, "columnSpan",
|
|
java::sdk::Integer::ValueOf(1));
|
|
GECKOBUNDLE_FINISH(collectionItemInfo);
|
|
|
|
GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo);
|
|
}
|
|
}
|
|
|
|
nsString colSize;
|
|
rv = aAttributes->GetStringProperty("child-item-count"_ns, colSize);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
int32_t rowCount;
|
|
if (sscanf(NS_ConvertUTF16toUTF8(colSize).get(), "%d", &rowCount) > 0) {
|
|
GECKOBUNDLE_START(collectionInfo);
|
|
GECKOBUNDLE_PUT(collectionInfo, "rowCount",
|
|
java::sdk::Integer::ValueOf(rowCount));
|
|
GECKOBUNDLE_PUT(collectionInfo, "columnCount",
|
|
java::sdk::Integer::ValueOf(1));
|
|
|
|
nsString unused;
|
|
rv = aAttributes->GetStringProperty("hierarchical"_ns, unused);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
GECKOBUNDLE_PUT(collectionInfo, "isHierarchical",
|
|
java::sdk::Boolean::TRUE());
|
|
}
|
|
|
|
if (IsSelect()) {
|
|
int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1;
|
|
GECKOBUNDLE_PUT(collectionInfo, "selectionMode",
|
|
java::sdk::Integer::ValueOf(selectionMode));
|
|
}
|
|
|
|
GECKOBUNDLE_FINISH(collectionInfo);
|
|
GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool mustPrune =
|
|
IsProxy() ? nsAccUtils::MustPrune(Proxy()) : nsAccUtils::MustPrune(this);
|
|
if (!mustPrune) {
|
|
auto childCount = ChildCount();
|
|
nsTArray<int32_t> children(childCount);
|
|
for (uint32_t i = 0; i < childCount; i++) {
|
|
auto child = static_cast<AccessibleWrap*>(GetChildAt(i));
|
|
children.AppendElement(child->VirtualViewID());
|
|
}
|
|
|
|
GECKOBUNDLE_PUT(nodeInfo, "children",
|
|
jni::IntArray::New(children.Elements(), children.Length()));
|
|
}
|
|
|
|
GECKOBUNDLE_FINISH(nodeInfo);
|
|
|
|
return nodeInfo;
|
|
}
|
|
|
|
void AccessibleWrap::GetTextEquiv(nsString& aText) {
|
|
if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
|
|
// This is an accessible that normally doesn't get its name from its
|
|
// subtree, so we collect the text equivalent explicitly.
|
|
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
|
|
} else {
|
|
Name(aText);
|
|
}
|
|
}
|
|
|
|
bool AccessibleWrap::HandleLiveRegionEvent(AccEvent* aEvent) {
|
|
auto eventType = aEvent->GetEventType();
|
|
if (eventType != nsIAccessibleEvent::EVENT_TEXT_INSERTED &&
|
|
eventType != nsIAccessibleEvent::EVENT_NAME_CHANGE) {
|
|
// XXX: Right now only announce text inserted events. aria-relevant=removals
|
|
// is potentially on the chopping block[1]. We also don't support editable
|
|
// text because we currently can't descern the source of the change[2].
|
|
// 1. https://github.com/w3c/aria/issues/712
|
|
// 2. https://bugzilla.mozilla.org/show_bug.cgi?id=1531189
|
|
return false;
|
|
}
|
|
|
|
if (aEvent->IsFromUserInput()) {
|
|
return false;
|
|
}
|
|
|
|
nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
|
|
nsString live;
|
|
nsresult rv = attributes->GetStringProperty("container-live"_ns, live);
|
|
if (!NS_SUCCEEDED(rv)) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t priority = live.EqualsIgnoreCase("assertive")
|
|
? nsIAccessibleAnnouncementEvent::ASSERTIVE
|
|
: nsIAccessibleAnnouncementEvent::POLITE;
|
|
|
|
nsString atomic;
|
|
rv = attributes->GetStringProperty("container-atomic"_ns, atomic);
|
|
|
|
Accessible* announcementTarget = this;
|
|
nsAutoString announcement;
|
|
if (atomic.EqualsIgnoreCase("true")) {
|
|
Accessible* atomicAncestor = nullptr;
|
|
for (Accessible* parent = announcementTarget; parent;
|
|
parent = parent->Parent()) {
|
|
dom::Element* element = parent->Elm();
|
|
if (element &&
|
|
element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_atomic,
|
|
nsGkAtoms::_true, eCaseMatters)) {
|
|
atomicAncestor = parent;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (atomicAncestor) {
|
|
announcementTarget = atomicAncestor;
|
|
static_cast<AccessibleWrap*>(atomicAncestor)->GetTextEquiv(announcement);
|
|
}
|
|
} else {
|
|
GetTextEquiv(announcement);
|
|
}
|
|
|
|
announcement.CompressWhitespace();
|
|
if (announcement.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
announcementTarget->Announce(announcement, priority);
|
|
return true;
|
|
}
|