/* -*- 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(aDoc); doc->AddID(mID, this); } } //----------------------------------------------------- // destruction //----------------------------------------------------- AccessibleWrap::~AccessibleWrap() {} nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { auto accessible = static_cast(aEvent->GetAccessible()); NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE); DocAccessibleWrap* doc = static_cast(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(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(newPos)->PivotTo( java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true, true); } } } break; } case nsIAccessibleEvent::EVENT_SCROLLING_START: { accessible->PivotTo( 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 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 newPosition = static_cast(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(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::PivotTo(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 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 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 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 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 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 AccessibleWrap::AttributeArrayToProperties( const nsTArray& aAttributes) { RefPtr 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 minValue = UnspecifiedNaN(); double maxValue = UnspecifiedNaN(); double step = UnspecifiedNaN(); WrapperRangeInfo(&curValue, &minValue, &maxValue, &step); nsCOMPtr 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 ( 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 children(childCount); for (uint32_t i = 0; i < childCount; i++) { auto child = static_cast(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 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(atomicAncestor)->GetTextEquiv(announcement); } } else { GetTextEquiv(announcement); } announcement.CompressWhitespace(); if (announcement.IsEmpty()) { return false; } announcementTarget->Announce(announcement, priority); return true; }