From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- accessible/basetypes/Accessible.cpp | 723 ++++++++++++++++++++++++++++++++++++ 1 file changed, 723 insertions(+) create mode 100644 accessible/basetypes/Accessible.cpp (limited to 'accessible/basetypes/Accessible.cpp') diff --git a/accessible/basetypes/Accessible.cpp b/accessible/basetypes/Accessible.cpp new file mode 100644 index 0000000000..cc41cd3a46 --- /dev/null +++ b/accessible/basetypes/Accessible.cpp @@ -0,0 +1,723 @@ +/* -*- 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 "Accessible.h" +#include "AccGroupInfo.h" +#include "ARIAMap.h" +#include "nsAccUtils.h" +#include "nsIURI.h" +#include "Relation.h" +#include "States.h" +#include "mozilla/a11y/FocusManager.h" +#include "mozilla/a11y/HyperTextAccessibleBase.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Components.h" +#include "nsIStringBundle.h" + +#ifdef A11Y_LOG +# include "nsAccessibilityService.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; + +Accessible::Accessible() + : mType(static_cast(0)), + mGenericTypes(static_cast(0)), + mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {} + +Accessible::Accessible(AccType aType, AccGenericType aGenericTypes, + uint8_t aRoleMapEntryIndex) + : mType(static_cast(aType)), + mGenericTypes(static_cast(aGenericTypes)), + mRoleMapEntryIndex(aRoleMapEntryIndex) {} + +void Accessible::StaticAsserts() const { + static_assert(eLastAccType <= (1 << kTypeBits) - 1, + "Accessible::mType was oversized by eLastAccType!"); + static_assert( + eLastAccGenericType <= (1 << kGenericTypesBits) - 1, + "Accessible::mGenericType was oversized by eLastAccGenericType!"); +} + +bool Accessible::IsBefore(const Accessible* aAcc) const { + // Build the chain of parents. + const Accessible* thisP = this; + const Accessible* otherP = aAcc; + AutoTArray thisParents, otherParents; + do { + thisParents.AppendElement(thisP); + thisP = thisP->Parent(); + } while (thisP); + do { + otherParents.AppendElement(otherP); + otherP = otherP->Parent(); + } while (otherP); + + // Find where the parent chain differs. + uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length(); + for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) { + const Accessible* thisChild = thisParents.ElementAt(--thisPos); + const Accessible* otherChild = otherParents.ElementAt(--otherPos); + if (thisChild != otherChild) { + return thisChild->IndexInParent() < otherChild->IndexInParent(); + } + } + + // If the ancestries are the same length (both thisPos and otherPos are 0), + // we should have returned by now. + MOZ_ASSERT(thisPos != 0 || otherPos != 0); + // At this point, one of the ancestries is a superset of the other, so one of + // thisPos or otherPos should be 0. + MOZ_ASSERT(thisPos != otherPos); + // If the other Accessible is deeper than this one (otherPos > 0), this + // Accessible comes before the other. + return otherPos > 0; +} + +Accessible* Accessible::FocusedChild() { + Accessible* doc = nsAccUtils::DocumentFor(this); + Accessible* child = doc->FocusedChild(); + if (child && (child == this || child->Parent() == this)) { + return child; + } + + return nullptr; +} + +const nsRoleMapEntry* Accessible::ARIARoleMap() const { + return aria::GetRoleMapFromIndex(mRoleMapEntryIndex); +} + +bool Accessible::HasARIARole() const { + return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX; +} + +bool Accessible::IsARIARole(nsAtom* aARIARole) const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->Is(aARIARole); +} + +bool Accessible::HasStrongARIARole() const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->roleRule == kUseMapRole; +} + +bool Accessible::HasGenericType(AccGenericType aType) const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return (mGenericTypes & aType) || + (roleMapEntry && roleMapEntry->IsOfType(aType)); +} + +nsIntRect Accessible::BoundsInCSSPixels() const { + return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel()); +} + +LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); } + +LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) { + LayoutDeviceIntPoint point = Bounds().TopLeft(); + nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType, + this); + return point; +} + +bool Accessible::IsTextRole() { + if (!IsHyperText()) { + return false; + } + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC || + roleMapEntry->role == roles::IMAGE_MAP || + roleMapEntry->role == roles::SLIDER || + roleMapEntry->role == roles::PROGRESSBAR || + roleMapEntry->role == roles::SEPARATOR)) { + return false; + } + + return true; +} + +uint32_t Accessible::StartOffset() { + MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!"); + Accessible* parent = Parent(); + HyperTextAccessibleBase* hyperText = + parent ? parent->AsHyperTextBase() : nullptr; + return hyperText ? hyperText->GetChildOffset(this) : 0; +} + +uint32_t Accessible::EndOffset() { + MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!"); + Accessible* parent = Parent(); + HyperTextAccessibleBase* hyperText = + parent ? parent->AsHyperTextBase() : nullptr; + return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0; +} + +GroupPos Accessible::GroupPosition() { + GroupPos groupPos; + + // Try aria-row/colcount/index. + if (IsTableRow()) { + Accessible* table = nsAccUtils::TableFor(this); + if (table) { + if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) { + if (*count >= 0) { + groupPos.setSize = *count; + } + } + } + if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) { + groupPos.posInSet = *index; + } + if (groupPos.setSize && groupPos.posInSet) { + return groupPos; + } + } + if (IsTableCell()) { + Accessible* table; + for (table = Parent(); table; table = table->Parent()) { + if (table->IsTable()) { + break; + } + } + if (table) { + if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) { + if (*count >= 0) { + groupPos.setSize = *count; + } + } + } + if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) { + groupPos.posInSet = *index; + } + if (groupPos.setSize && groupPos.posInSet) { + return groupPos; + } + } + + // Get group position from ARIA attributes. + ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet); + + // If ARIA is missed and the accessible is visible then calculate group + // position from hierarchy. + if (State() & states::INVISIBLE) return groupPos; + + // Calculate group level if ARIA is missed. + if (groupPos.level == 0) { + groupPos.level = GetLevel(false); + } + + // Calculate position in group and group size if ARIA is missed. + if (groupPos.posInSet == 0 || groupPos.setSize == 0) { + int32_t posInSet = 0, setSize = 0; + GetPositionAndSetSize(&posInSet, &setSize); + if (posInSet != 0 && setSize != 0) { + if (groupPos.posInSet == 0) groupPos.posInSet = posInSet; + + if (groupPos.setSize == 0) groupPos.setSize = setSize; + } + } + + return groupPos; +} + +int32_t Accessible::GetLevel(bool aFast) const { + int32_t level = 0; + if (!Parent()) return level; + + roles::Role role = Role(); + if (role == roles::OUTLINEITEM) { + // Always expose 'level' attribute for 'outlineitem' accessible. The number + // of nested 'grouping' accessibles containing 'outlineitem' accessible is + // its level. + level = 1; + + if (!aFast) { + const Accessible* parent = this; + while ((parent = parent->Parent()) && !parent->IsDoc()) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::OUTLINE) break; + if (parentRole == roles::GROUPING) ++level; + } + } + } else if (role == roles::LISTITEM && !aFast) { + // Expose 'level' attribute on nested lists. We support two hierarchies: + // a) list -> listitem -> list -> listitem (nested list is a last child + // of listitem of the parent list); + // b) list -> listitem -> group -> listitem (nested listitems are contained + // by group that is a last child of the parent listitem). + + // Calculate 'level' attribute based on number of parent listitems. + level = 0; + const Accessible* parent = this; + while ((parent = parent->Parent()) && !parent->IsDoc()) { + roles::Role parentRole = parent->Role(); + + if (parentRole == roles::LISTITEM) { + ++level; + } else if (parentRole != roles::LIST && parentRole != roles::GROUPING) { + break; + } + } + + if (level == 0) { + // If this listitem is on top of nested lists then expose 'level' + // attribute. + parent = Parent(); + uint32_t siblingCount = parent->ChildCount(); + for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) { + Accessible* sibling = parent->ChildAt(siblingIdx); + + Accessible* siblingChild = sibling->LastChild(); + if (siblingChild) { + roles::Role lastChildRole = siblingChild->Role(); + if (lastChildRole == roles::LIST || + lastChildRole == roles::GROUPING) { + return 1; + } + } + } + } else { + ++level; // level is 1-index based + } + } else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) { + if (const Accessible* parent = Parent()) { + if (parent->IsHTMLOptGroup()) { + return 2; + } + + if (parent->IsListControl() && !parent->ARIARoleMap()) { + // This is for HTML selects only. + if (aFast) { + return 1; + } + + for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) { + if (parent->ChildAt(i)->IsHTMLOptGroup()) { + return 1; + } + } + } + } + } else if (role == roles::HEADING) { + nsAtom* tagName = TagName(); + if (tagName == nsGkAtoms::h1) { + return 1; + } + if (tagName == nsGkAtoms::h2) { + return 2; + } + if (tagName == nsGkAtoms::h3) { + return 3; + } + if (tagName == nsGkAtoms::h4) { + return 4; + } + if (tagName == nsGkAtoms::h5) { + return 5; + } + if (tagName == nsGkAtoms::h6) { + return 6; + } + + const nsRoleMapEntry* ariaRole = this->ARIARoleMap(); + if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) { + // An aria heading with no aria level has a default level of 2. + return 2; + } + } else if (role == roles::COMMENT) { + // For comments, count the ancestor elements with the same role to get the + // level. + level = 1; + + if (!aFast) { + const Accessible* parent = this; + while ((parent = parent->Parent()) && !parent->IsDoc()) { + roles::Role parentRole = parent->Role(); + if (parentRole == roles::COMMENT) { + ++level; + } + } + } + } else if (role == roles::ROW) { + // It is a row inside flatten treegrid. Group level is always 1 until it + // is overriden by aria-level attribute. + const Accessible* parent = Parent(); + if (parent->Role() == roles::TREE_TABLE) { + return 1; + } + } + + return level; +} + +void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) { + auto groupInfo = GetOrCreateGroupInfo(); + if (groupInfo) { + *aPosInSet = groupInfo->PosInSet(); + *aSetSize = groupInfo->SetSize(); + } +} + +bool Accessible::IsLinkValid() { + MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!"); + + // XXX In order to implement this we would need to follow every link + // Perhaps we can get information about invalid links from the cache + // In the mean time authors can use role="link" aria-invalid="true" + // to force it for links they internally know to be invalid + return (0 == (State() & mozilla::a11y::states::INVALID)); +} + +uint32_t Accessible::AnchorCount() { + if (IsImageMap()) { + return ChildCount(); + } + + MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!"); + return 1; +} + +Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const { + if (IsImageMap()) { + return ChildAt(aAnchorIndex); + } + + MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!"); + return aAnchorIndex == 0 ? const_cast(this) : nullptr; +} + +already_AddRefed Accessible::AnchorURIAt(uint32_t aAnchorIndex) const { + Accessible* anchor = nullptr; + + if (IsTextLeaf() || IsImage()) { + for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc(); + parent = parent->Parent()) { + if (parent->IsLink()) { + anchor = parent->AnchorAt(aAnchorIndex); + } + } + } else { + anchor = AnchorAt(aAnchorIndex); + } + + if (anchor) { + RefPtr uri; + nsAutoString spec; + anchor->Value(spec); + nsresult rv = NS_NewURI(getter_AddRefs(uri), spec); + if (NS_SUCCEEDED(rv)) { + return uri.forget(); + } + } + + return nullptr; +} + +bool Accessible::IsSearchbox() const { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) { + return true; + } + + RefPtr inputType = InputType(); + return inputType == nsGkAtoms::search; +} + +#ifdef A11Y_LOG +void Accessible::DebugDescription(nsCString& aDesc) const { + aDesc.Truncate(); + aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local"); + aDesc.AppendPrintf("[%p] ", this); + nsAutoString role; + GetAccService()->GetStringRole(Role(), role); + aDesc.Append(NS_ConvertUTF16toUTF8(role)); + + if (nsAtom* tagAtom = TagName()) { + nsAutoCString tag; + tagAtom->ToUTF8String(tag); + aDesc.AppendPrintf(" %s", tag.get()); + + nsAutoString id; + DOMNodeID(id); + if (!id.IsEmpty()) { + aDesc.Append("#"); + aDesc.Append(NS_ConvertUTF16toUTF8(id)); + } + } + nsAutoString id; + + nsAutoString name; + Name(name); + if (!name.IsEmpty()) { + aDesc.Append(" '"); + aDesc.Append(NS_ConvertUTF16toUTF8(name)); + aDesc.Append("'"); + } +} + +void Accessible::DebugPrint(const char* aPrefix, + const Accessible* aAccessible) { + nsAutoCString desc; + aAccessible->DebugDescription(desc); +# if defined(ANDROID) + printf_stderr("%s %s\n", aPrefix, desc.get()); +# else + printf("%s %s\n", aPrefix, desc.get()); +# endif +} + +#endif + +void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut) { + nsCOMPtr stringBundleService = + components::StringBundle::Service(); + if (!stringBundleService) return; + + nsCOMPtr stringBundle; + stringBundleService->CreateBundle( + "chrome://global-platform/locale/accessible.properties", + getter_AddRefs(stringBundle)); + if (!stringBundle) return; + + nsAutoString xsValue; + nsresult rv = stringBundle->GetStringFromName( + NS_ConvertUTF16toUTF8(aKey).get(), xsValue); + if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue); +} + +const Accessible* Accessible::ActionAncestor() const { + // We do want to consider a click handler on the document. However, we don't + // want to walk outside of this document, so we stop if we see an OuterDoc. + for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc(); + parent = parent->Parent()) { + if (parent->HasPrimaryAction()) { + return parent; + } + } + + return nullptr; +} + +nsStaticAtom* Accessible::LandmarkRole() const { + nsAtom* tagName = TagName(); + if (!tagName) { + // Either no associated content, or no cache. + return nullptr; + } + + if (tagName == nsGkAtoms::nav) { + return nsGkAtoms::navigation; + } + + if (tagName == nsGkAtoms::aside) { + return nsGkAtoms::complementary; + } + + if (tagName == nsGkAtoms::main) { + return nsGkAtoms::main; + } + + if (tagName == nsGkAtoms::header) { + if (Role() == roles::LANDMARK) { + return nsGkAtoms::banner; + } + } + + if (tagName == nsGkAtoms::footer) { + if (Role() == roles::LANDMARK) { + return nsGkAtoms::contentinfo; + } + } + + if (tagName == nsGkAtoms::section) { + nsAutoString name; + Name(name); + if (!name.IsEmpty()) { + return nsGkAtoms::region; + } + } + + if (tagName == nsGkAtoms::form) { + nsAutoString name; + Name(name); + if (!name.IsEmpty()) { + return nsGkAtoms::form; + } + } + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + return roleMapEntry && roleMapEntry->IsOfType(eLandmark) + ? roleMapEntry->roleAtom + : nullptr; +} + +nsStaticAtom* Accessible::ComputedARIARole() const { + const nsRoleMapEntry* roleMap = ARIARoleMap(); + if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty && + // region has its own Gecko role and it needs to be handled specially. + roleMap->roleAtom != nsGkAtoms::region && + (roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) || + roleMap->roleAtom == nsGkAtoms::alertdialog || + roleMap->roleAtom == nsGkAtoms::feed || + roleMap->roleAtom == nsGkAtoms::rowgroup || + roleMap->roleAtom == nsGkAtoms::searchbox)) { + // Explicit ARIA role (e.g. specified via the role attribute) which does not + // map to a unique Gecko role. + return roleMap->roleAtom; + } + role geckoRole = Role(); + if (geckoRole == roles::LANDMARK) { + // Landmark role from native markup; e.g.
,