diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/basetypes/Accessible.cpp | 723 | ||||
-rw-r--r-- | accessible/basetypes/Accessible.h | 725 | ||||
-rw-r--r-- | accessible/basetypes/HyperTextAccessibleBase.cpp | 843 | ||||
-rw-r--r-- | accessible/basetypes/HyperTextAccessibleBase.h | 310 | ||||
-rw-r--r-- | accessible/basetypes/TableAccessible.h | 172 | ||||
-rw-r--r-- | accessible/basetypes/TableCellAccessible.h | 68 | ||||
-rw-r--r-- | accessible/basetypes/moz.build | 25 |
7 files changed, 2866 insertions, 0 deletions
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<uint32_t>(0)), + mGenericTypes(static_cast<uint32_t>(0)), + mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {} + +Accessible::Accessible(AccType aType, AccGenericType aGenericTypes, + uint8_t aRoleMapEntryIndex) + : mType(static_cast<uint32_t>(aType)), + mGenericTypes(static_cast<uint32_t>(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<const Accessible*, 30> 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<Accessible*>(this) : nullptr; +} + +already_AddRefed<nsIURI> 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<nsIURI> 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<nsAtom> 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<nsIStringBundleService> stringBundleService = + components::StringBundle::Service(); + if (!stringBundleService) return; + + nsCOMPtr<nsIStringBundle> 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. <main>, <nav>. + return LandmarkRole(); + } + if (geckoRole == roles::GROUPING) { + // Gecko doesn't differentiate between group and rowgroup. It uses + // roles::GROUPING for both. + nsAtom* tag = TagName(); + if (tag == nsGkAtoms::tbody || tag == nsGkAtoms::tfoot || + tag == nsGkAtoms::thead) { + return nsGkAtoms::rowgroup; + } + } + // Role from native markup or layout. +#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ + msaaRole, ia2Role, androidClass, nameRule) \ + case roles::_geckoRole: \ + return ariaRole; + switch (geckoRole) { +#include "RoleMap.h" + } +#undef ROLE + MOZ_ASSERT_UNREACHABLE("Unknown role"); + return nullptr; +} + +void Accessible::ApplyImplicitState(uint64_t& aState) const { + // nsAccessibilityService (and thus FocusManager) can be shut down before + // RemoteAccessibles. + if (const auto* focusMgr = FocusMgr()) { + if (focusMgr->IsFocused(this)) { + aState |= states::FOCUSED; + } + } + + // If this is an ARIA item of the selectable widget and if it's focused and + // not marked unselected explicitly (i.e. aria-selected="false") then expose + // it as selected to make ARIA widget authors life easier. + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && !(aState & states::SELECTED) && + ARIASelected().valueOr(true)) { + // Special case for tabs: focused tab or focus inside related tab panel + // implies selected state. + if (roleMapEntry->role == roles::PAGETAB) { + if (aState & states::FOCUSED) { + aState |= states::SELECTED; + } else { + // If focus is in a child of the tab panel surely the tab is selected! + Relation rel = RelationByType(RelationType::LABEL_FOR); + Accessible* relTarget = nullptr; + while ((relTarget = rel.Next())) { + if (relTarget->Role() == roles::PROPERTYPAGE && + FocusMgr()->IsFocusWithin(relTarget)) { + aState |= states::SELECTED; + } + } + } + } else if (aState & states::FOCUSED) { + Accessible* container = nsAccUtils::GetSelectableContainer(this, aState); + if (container && !(container->State() & states::MULTISELECTABLE)) { + aState |= states::SELECTED; + } + } + } + + if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) { + aState |= states::OPAQUE1; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// KeyBinding class + +// static +uint32_t KeyBinding::AccelModifier() { + switch (WidgetInputEvent::AccelModifier()) { + case MODIFIER_ALT: + return kAlt; + case MODIFIER_CONTROL: + return kControl; + case MODIFIER_META: + return kMeta; + case MODIFIER_OS: + return kOS; + default: + MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()"); + return 0; + } +} + +void KeyBinding::ToPlatformFormat(nsAString& aValue) const { + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + if (stringBundleService) { + stringBundleService->CreateBundle( + "chrome://global-platform/locale/platformKeys.properties", + getter_AddRefs(keyStringBundle)); + } + + if (!keyStringBundle) return; + + nsAutoString separator; + keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator); + + nsAutoString modifierName; + if (mModifierMask & kControl) { + keyStringBundle->GetStringFromName("VK_CONTROL", modifierName); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kAlt) { + keyStringBundle->GetStringFromName("VK_ALT", modifierName); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kShift) { + keyStringBundle->GetStringFromName("VK_SHIFT", modifierName); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + if (mModifierMask & kMeta) { + keyStringBundle->GetStringFromName("VK_META", modifierName); + + aValue.Append(modifierName); + aValue.Append(separator); + } + + aValue.Append(mKey); +} + +void KeyBinding::ToAtkFormat(nsAString& aValue) const { + nsAutoString modifierName; + if (mModifierMask & kControl) aValue.AppendLiteral("<Control>"); + + if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>"); + + if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>"); + + if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>"); + + aValue.Append(mKey); +} diff --git a/accessible/basetypes/Accessible.h b/accessible/basetypes/Accessible.h new file mode 100644 index 0000000000..d0c2c3c2dd --- /dev/null +++ b/accessible/basetypes/Accessible.h @@ -0,0 +1,725 @@ +/* -*- 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/. */ + +#ifndef _Accessible_H_ +#define _Accessible_H_ + +#include "mozilla/a11y/Role.h" +#include "mozilla/a11y/AccTypes.h" +#include "nsString.h" +#include "nsRect.h" +#include "Units.h" + +class nsAtom; +class nsStaticAtom; + +struct nsRoleMapEntry; + +class nsIURI; + +namespace mozilla { +namespace a11y { + +class AccAttributes; +class AccGroupInfo; +class HyperTextAccessibleBase; +class LocalAccessible; +class Relation; +enum class RelationType; +class RemoteAccessible; +class TableAccessible; +class TableCellAccessible; + +/** + * Name type flags. + */ +enum ENameValueFlag { + /** + * Name either + * a) present (not empty): !name.IsEmpty() + * b) no name (was missed): name.IsVoid() + */ + eNameOK, + + /** + * Name was computed from the subtree. + */ + eNameFromSubtree, + + /** + * Tooltip was used as a name. + */ + eNameFromTooltip +}; + +/** + * Group position (level, position in set and set size). + */ +struct GroupPos { + GroupPos() : level(0), posInSet(0), setSize(0) {} + GroupPos(int32_t aLevel, int32_t aPosInSet, int32_t aSetSize) + : level(aLevel), posInSet(aPosInSet), setSize(aSetSize) {} + + int32_t level; + int32_t posInSet; + int32_t setSize; +}; + +/** + * Represent key binding associated with accessible (such as access key and + * global keyboard shortcuts). + */ +class KeyBinding { + public: + /** + * Modifier mask values. + */ + static const uint32_t kShift = 1; + static const uint32_t kControl = 2; + static const uint32_t kAlt = 4; + static const uint32_t kMeta = 8; + static const uint32_t kOS = 16; + + static uint32_t AccelModifier(); + + KeyBinding() : mKey(0), mModifierMask(0) {} + KeyBinding(uint32_t aKey, uint32_t aModifierMask) + : mKey(aKey), mModifierMask(aModifierMask) {} + explicit KeyBinding(uint64_t aSerialized) : mSerialized(aSerialized) {} + + inline bool IsEmpty() const { return !mKey; } + inline uint32_t Key() const { return mKey; } + inline uint32_t ModifierMask() const { return mModifierMask; } + + /** + * Serialize this KeyBinding to a uint64_t for use in the parent process + * cache. This is simpler than custom IPDL serialization for this simple case. + */ + uint64_t Serialize() { return mSerialized; } + + enum Format { ePlatformFormat, eAtkFormat }; + + /** + * Return formatted string for this key binding depending on the given format. + */ + inline void ToString(nsAString& aValue, + Format aFormat = ePlatformFormat) const { + aValue.Truncate(); + AppendToString(aValue, aFormat); + } + inline void AppendToString(nsAString& aValue, + Format aFormat = ePlatformFormat) const { + if (mKey) { + if (aFormat == ePlatformFormat) { + ToPlatformFormat(aValue); + } else { + ToAtkFormat(aValue); + } + } + } + + private: + void ToPlatformFormat(nsAString& aValue) const; + void ToAtkFormat(nsAString& aValue) const; + + union { + struct { + uint32_t mKey; + uint32_t mModifierMask; + }; + uint64_t mSerialized; + }; +}; + +/** + * The base type for an accessibility tree node. Methods and attributes in this + * class are available in both the content process and the parent process. + * Overrides for these methods live primarily in LocalAccessible and + * RemoteAccessibleBase. + */ +class Accessible { + protected: + Accessible(); + + Accessible(AccType aType, AccGenericType aGenericTypes, + uint8_t aRoleMapEntryIndex); + + public: + /** + * Return an id for this Accessible which is unique within the document. + * Use nsAccUtils::GetAccessibleByID to retrieve an Accessible given an id + * returned from this method. + */ + virtual uint64_t ID() const = 0; + + virtual Accessible* Parent() const = 0; + + virtual role Role() const = 0; + + /** + * Return child accessible at the given index. + */ + virtual Accessible* ChildAt(uint32_t aIndex) const = 0; + + virtual Accessible* NextSibling() const = 0; + virtual Accessible* PrevSibling() const = 0; + + virtual uint32_t ChildCount() const = 0; + + virtual int32_t IndexInParent() const = 0; + + bool HasChildren() const { return !!FirstChild(); } + + inline Accessible* FirstChild() const { + return ChildCount() ? ChildAt(0) : nullptr; + } + + inline Accessible* LastChild() const { + uint32_t childCount = ChildCount(); + return childCount ? ChildAt(childCount - 1) : nullptr; + } + + /** + * Return true if this Accessible is before another Accessible in the tree. + */ + bool IsBefore(const Accessible* aAcc) const; + + bool IsAncestorOf(const Accessible* aAcc) const { + for (const Accessible* parent = aAcc->Parent(); parent; + parent = parent->Parent()) { + if (parent == this) { + return true; + } + } + return false; + } + + /** + * Used by ChildAtPoint() method to get direct or deepest child at point. + */ + enum class EWhichChildAtPoint { DirectChild, DeepestChild }; + + virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY, + EWhichChildAtPoint aWhichChild) = 0; + + /** + * Return the focused child if any. + */ + virtual Accessible* FocusedChild(); + + /** + * Return ARIA role map if any. + */ + const nsRoleMapEntry* ARIARoleMap() const; + + /** + * Return true if ARIA role is specified on the element. + */ + bool HasARIARole() const; + bool IsARIARole(nsAtom* aARIARole) const; + bool HasStrongARIARole() const; + + /** + * Return true if the accessible belongs to the given accessible type. + */ + bool HasGenericType(AccGenericType aType) const; + + /** + * Return group position (level, position in set and set size). + */ + virtual GroupPos GroupPosition(); + + /** + * Return embedded accessible children count. + */ + virtual uint32_t EmbeddedChildCount() = 0; + + /** + * Return embedded accessible child at the given index. + */ + virtual Accessible* EmbeddedChildAt(uint32_t aIndex) = 0; + + /** + * Return index of the given embedded accessible child. + */ + virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) = 0; + + // Methods that potentially access a cache. + + /* + * Get the name of this accessible. + */ + virtual ENameValueFlag Name(nsString& aName) const = 0; + + /* + * Get the description of this accessible. + */ + virtual void Description(nsString& aDescription) const = 0; + + /** + * Get the value of this accessible. + */ + virtual void Value(nsString& aValue) const = 0; + + virtual double CurValue() const = 0; + virtual double MinValue() const = 0; + virtual double MaxValue() const = 0; + virtual double Step() const = 0; + virtual bool SetCurValue(double aValue) = 0; + + /** + * Return boundaries in screen coordinates in device pixels. + */ + virtual LayoutDeviceIntRect Bounds() const = 0; + + /** + * Return boundaries in screen coordinates in app units. + */ + virtual nsRect BoundsInAppUnits() const = 0; + + /** + * Return boundaries in screen coordinates in CSS pixels. + */ + virtual nsIntRect BoundsInCSSPixels() const; + + /** + * Returns text of accessible if accessible has text role otherwise empty + * string. + * + * @param aText [in] returned text of the accessible + * @param aStartOffset [in, optional] start offset inside of the accessible, + * if missed entire text is appended + * @param aLength [in, optional] required length of text, if missed + * then text from start offset till the end is appended + */ + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX) = 0; + + /** + * Return all states of accessible (including ARIA states). + */ + virtual uint64_t State() = 0; + + /** + * Return the start offset of the embedded object within the parent + * HyperTextAccessibleBase. + */ + virtual uint32_t StartOffset(); + + /** + * Return the end offset of the link within the parent + * HyperTextAccessibleBase. + */ + virtual uint32_t EndOffset(); + + /** + * Return object attributes for the accessible. + */ + virtual already_AddRefed<AccAttributes> Attributes() = 0; + + virtual already_AddRefed<nsAtom> DisplayStyle() const = 0; + + virtual float Opacity() const = 0; + + /** + * Get the live region attributes (if any) for this single Accessible. This + * does not propagate attributes from ancestors. If any argument is null, that + * attribute is not fetched. + */ + virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant, + Maybe<bool>* aAtomic, + nsAString* aBusy) const = 0; + + /** + * Get the aria-selected state. aria-selected not being specified is not + * always the same as aria-selected="false". If not specified, Nothing() will + * be returned. + */ + virtual Maybe<bool> ARIASelected() const = 0; + + LayoutDeviceIntSize Size() const; + + LayoutDeviceIntPoint Position(uint32_t aCoordType); + + virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const = 0; + + /** + * Get the relation of the given type. + */ + virtual Relation RelationByType(RelationType aType) const = 0; + + /** + * Get the language associated with the accessible. + */ + virtual void Language(nsAString& aLocale) = 0; + + /** + * Get the role of this Accessible as an ARIA role token. This might have been + * set explicitly (e.g. role="button") or it might be implicit in native + * markup (e.g. <button> returns "button"). + */ + nsStaticAtom* ComputedARIARole() const; + + // Methods that interact with content. + + virtual void TakeFocus() const = 0; + + /** + * Scroll the accessible into view. + */ + MOZ_CAN_RUN_SCRIPT + virtual void ScrollTo(uint32_t aHow) const = 0; + + /** + * Return tag name of associated DOM node. + */ + virtual nsAtom* TagName() const = 0; + + /** + * Return input `type` attribute + */ + virtual already_AddRefed<nsAtom> InputType() const = 0; + + /** + * Return a landmark role if applied. + */ + nsStaticAtom* LandmarkRole() const; + + /** + * Return the id of the dom node this accessible represents. + */ + virtual void DOMNodeID(nsString& aID) const = 0; + + ////////////////////////////////////////////////////////////////////////////// + // ActionAccessible + + /** + * Return the number of actions that can be performed on this accessible. + */ + virtual uint8_t ActionCount() const = 0; + + /** + * Return action name at given index. + */ + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) = 0; + + /** + * Default to localized action name. + */ + void ActionDescriptionAt(uint8_t aIndex, nsAString& aDescription) { + nsAutoString name; + ActionNameAt(aIndex, name); + TranslateString(name, aDescription); + } + + /** + * Invoke the accessible action. + */ + virtual bool DoAction(uint8_t aIndex) const = 0; + + /** + * Return access key, such as Alt+D. + */ + virtual KeyBinding AccessKey() const = 0; + + ////////////////////////////////////////////////////////////////////////////// + // SelectAccessible + + /** + * Return an array of selected items. + */ + virtual void SelectedItems(nsTArray<Accessible*>* aItems) = 0; + + /** + * Return the number of selected items. + */ + virtual uint32_t SelectedItemCount() = 0; + + /** + * Return selected item at the given index. + */ + virtual Accessible* GetSelectedItem(uint32_t aIndex) = 0; + + /** + * Determine if item at the given index is selected. + */ + virtual bool IsItemSelected(uint32_t aIndex) = 0; + + /** + * Add item at the given index the selection. Return true if success. + */ + virtual bool AddItemToSelection(uint32_t aIndex) = 0; + + /** + * Remove item at the given index from the selection. Return if success. + */ + virtual bool RemoveItemFromSelection(uint32_t aIndex) = 0; + + /** + * Select all items. Return true if success. + */ + virtual bool SelectAll() = 0; + + /** + * Unselect all items. Return true if success. + */ + virtual bool UnselectAll() = 0; + + virtual void TakeSelection() = 0; + + virtual void SetSelected(bool aSelect) = 0; + + // Type "is" methods + + bool IsDoc() const { return HasGenericType(eDocument); } + + bool IsTableRow() const { return HasGenericType(eTableRow); } + + bool IsTableCell() const { + // The eTableCell type defined in the ARIA map is used in + // nsAccessibilityService::CreateAccessible to specify when + // ARIAGridCellAccessible should be used for object creation. However, an + // invalid table structure might cause this class not to be used after all. + // To make sure we're really dealing with a cell, only check the generic + // type defined by the class, not the type defined in the ARIA map. + return mGenericTypes & eTableCell; + } + + bool IsTable() const { return HasGenericType(eTable); } + + bool IsHyperText() const { return HasGenericType(eHyperText); } + + bool IsSelect() const { return HasGenericType(eSelect); } + + bool IsActionable() const { return HasGenericType(eActionable); } + + bool IsText() const { return mGenericTypes & eText; } + + bool IsImage() const { return mType == eImageType; } + + bool IsApplication() const { return mType == eApplicationType; } + + bool IsAlert() const { return HasGenericType(eAlert); } + + bool IsButton() const { return HasGenericType(eButton); } + + bool IsCombobox() const { return HasGenericType(eCombobox); } + + virtual bool IsLink() const = 0; + + /** + * Return true if the used ARIA role (if any) allows the hypertext accessible + * to expose text interfaces. + */ + bool IsTextRole(); + + bool IsGenericHyperText() const { return mType == eHyperTextType; } + + bool IsHTMLBr() const { return mType == eHTMLBRType; } + bool IsHTMLCaption() const { return mType == eHTMLCaptionType; } + bool IsHTMLCombobox() const { return mType == eHTMLComboboxType; } + bool IsHTMLFileInput() const { return mType == eHTMLFileInputType; } + + bool IsHTMLListItem() const { return mType == eHTMLLiType; } + + bool IsHTMLLink() const { return mType == eHTMLLinkType; } + + bool IsHTMLOptGroup() const { return mType == eHTMLOptGroupType; } + + bool IsHTMLRadioButton() const { return mType == eHTMLRadioButtonType; } + + bool IsHTMLTable() const { return mType == eHTMLTableType; } + bool IsHTMLTableCell() const { return mType == eHTMLTableCellType; } + bool IsHTMLTableRow() const { return mType == eHTMLTableRowType; } + + bool IsImageMap() const { return mType == eImageMapType; } + + bool IsList() const { return HasGenericType(eList); } + + bool IsListControl() const { return HasGenericType(eListControl); } + + bool IsMenuButton() const { return HasGenericType(eMenuButton); } + + bool IsMenuPopup() const { return mType == eMenuPopupType; } + + bool IsOuterDoc() const { return mType == eOuterDocType; } + + bool IsProgress() const { return mType == eProgressType; } + + bool IsRoot() const { return mType == eRootType; } + + bool IsPassword() const { return mType == eHTMLTextPasswordFieldType; } + + bool IsTextLeaf() const { return mType == eTextLeafType; } + + bool IsXULLabel() const { return mType == eXULLabelType; } + + bool IsXULListItem() const { return mType == eXULListItemType; } + + bool IsXULTabpanels() const { return mType == eXULTabpanelsType; } + + bool IsXULTooltip() const { return mType == eXULTooltipType; } + + bool IsXULTree() const { return mType == eXULTreeType; } + + bool IsAutoCompletePopup() const { + return HasGenericType(eAutoCompletePopup); + } + + bool IsTextField() const { + return mType == eHTMLTextFieldType || mType == eHTMLTextPasswordFieldType; + } + + bool IsDateTimeField() const { return mType == eHTMLDateTimeFieldType; } + + bool IsSearchbox() const; + + virtual bool HasNumericValue() const = 0; + + /** + * Returns true if this is a generic container element that has no meaning on + * its own. + */ + bool IsGeneric() const { + role accRole = Role(); + return accRole == roles::TEXT || accRole == roles::TEXT_CONTAINER || + accRole == roles::SECTION; + } + + /** + * Returns the nearest ancestor which is not a generic element. + */ + Accessible* GetNonGenericParent() const { + for (Accessible* parent = Parent(); parent; parent = parent->Parent()) { + if (!parent->IsGeneric()) { + return parent; + } + } + return nullptr; + } + + /** + * Return true if the link is valid (e. g. points to a valid URL). + */ + bool IsLinkValid(); + + /** + * Return the number of anchors within the link. + */ + uint32_t AnchorCount(); + + /** + * Returns an anchor URI at the given index. + */ + virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) const; + + /** + * Returns an anchor accessible at the given index. + */ + Accessible* AnchorAt(uint32_t aAnchorIndex) const; + + // Remote/Local types + + virtual bool IsRemote() const = 0; + RemoteAccessible* AsRemote(); + + bool IsLocal() const { return !IsRemote(); } + LocalAccessible* AsLocal(); + + virtual HyperTextAccessibleBase* AsHyperTextBase() { return nullptr; } + + virtual TableAccessible* AsTable() { return nullptr; } + virtual TableCellAccessible* AsTableCell() { return nullptr; } + +#ifdef A11Y_LOG + /** + * Provide a human readable description of the accessible, + * including memory address, role, name, DOM tag and DOM ID. + */ + void DebugDescription(nsCString& aDesc) const; + + static void DebugPrint(const char* aPrefix, const Accessible* aAccessible); +#endif + + /** + * Return the localized string for the given key. + */ + static void TranslateString(const nsString& aKey, nsAString& aStringOut); + + protected: + // Some abstracted group utility methods. + + /** + * Get ARIA group attributes. + */ + virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, + int32_t* aPosInSet) const = 0; + + /** + * Return group info if there is an up-to-date version. + */ + virtual AccGroupInfo* GetGroupInfo() const = 0; + + /** + * Return group info or create and update. + */ + virtual AccGroupInfo* GetOrCreateGroupInfo() = 0; + + /* + * Return calculated group level based on accessible hierarchy. + * + * @param aFast [in] Don't climb up tree. Calculate level from aria and + * roles. + */ + virtual int32_t GetLevel(bool aFast) const; + + /** + * Calculate position in group and group size ('posinset' and 'setsize') based + * on accessible hierarchy. + * + * @param aPosInSet [out] accessible position in the group + * @param aSetSize [out] the group size + */ + virtual void GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize); + + /** + * Return the nearest ancestor that has a primary action, or null. + */ + const Accessible* ActionAncestor() const; + + /** + * Return true if accessible has a primary action directly related to it, like + * "click", "activate", "press", "jump", "open", "close", etc. A non-primary + * action would be a complementary one like "showlongdesc". + * If an accessible has an action that is associated with an ancestor, it is + * not a primary action either. + */ + virtual bool HasPrimaryAction() const = 0; + + /** + * Apply states which are implied by other information common to both + * LocalAccessible and RemoteAccessible. + */ + void ApplyImplicitState(uint64_t& aState) const; + + private: + static const uint8_t kTypeBits = 6; + static const uint8_t kGenericTypesBits = 18; + + void StaticAsserts() const; + + protected: + uint32_t mType : kTypeBits; + uint32_t mGenericTypes : kGenericTypesBits; + uint8_t mRoleMapEntryIndex; + + friend class DocAccessibleChildBase; + friend class AccGroupInfo; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/basetypes/HyperTextAccessibleBase.cpp b/accessible/basetypes/HyperTextAccessibleBase.cpp new file mode 100644 index 0000000000..e36b6d7209 --- /dev/null +++ b/accessible/basetypes/HyperTextAccessibleBase.cpp @@ -0,0 +1,843 @@ +/* -*- 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 "HyperTextAccessibleBase.h" + +#include "AccAttributes.h" +#include "mozilla/a11y/Accessible.h" +#include "nsAccUtils.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "TextLeafRange.h" +#include "TextRange.h" + +namespace mozilla::a11y { + +int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const { + auto& offsets = + const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets(); + int32_t lastOffset = 0; + const uint32_t offsetCount = offsets.Length(); + + if (offsetCount > 0) { + lastOffset = offsets[offsetCount - 1]; + if (static_cast<int32_t>(aOffset) < lastOffset) { + // We've cached up to aOffset. + size_t index; + if (BinarySearch(offsets, 0, offsetCount, static_cast<int32_t>(aOffset), + &index)) { + // aOffset is the exclusive end of a child, so return the child before + // it. + return static_cast<int32_t>((index < offsetCount - 1) ? index + 1 + : index); + } + if (index == offsetCount) { + // aOffset is past the end of the text. + return -1; + } + // index points at the exclusive end after aOffset. + return static_cast<int32_t>(index); + } + } + + // We haven't yet cached up to aOffset. Find it, caching as we go. + const Accessible* thisAcc = Acc(); + uint32_t childCount = thisAcc->ChildCount(); + // Even though we're only caching up to aOffset, it's likely that we'll + // eventually cache offsets for all children. Pre-allocate thus to minimize + // re-allocations. + offsets.SetCapacity(childCount); + while (offsets.Length() < childCount) { + Accessible* child = thisAcc->ChildAt(offsets.Length()); + lastOffset += static_cast<int32_t>(nsAccUtils::TextLength(child)); + offsets.AppendElement(lastOffset); + if (static_cast<int32_t>(aOffset) < lastOffset) { + return static_cast<int32_t>(offsets.Length() - 1); + } + } + + if (static_cast<int32_t>(aOffset) == lastOffset) { + return static_cast<int32_t>(offsets.Length() - 1); + } + + return -1; +} + +Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const { + const Accessible* thisAcc = Acc(); + return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset)); +} + +int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild, + bool aInvalidateAfter) const { + const Accessible* thisAcc = Acc(); + if (aChild->Parent() != thisAcc) { + return -1; + } + int32_t index = aChild->IndexInParent(); + if (index == -1) { + return -1; + } + return GetChildOffset(index, aInvalidateAfter); +} + +int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex, + bool aInvalidateAfter) const { + auto& offsets = + const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets(); + if (aChildIndex == 0) { + if (aInvalidateAfter) { + offsets.Clear(); + } + return 0; + } + + int32_t countCachedAfterChild = static_cast<int32_t>(offsets.Length()) - + static_cast<int32_t>(aChildIndex); + if (countCachedAfterChild > 0) { + // We've cached up to aChildIndex. + if (aInvalidateAfter) { + offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild); + } + return offsets[aChildIndex - 1]; + } + + // We haven't yet cached up to aChildIndex. Find it, caching as we go. + const Accessible* thisAcc = Acc(); + // Even though we're only caching up to aChildIndex, it's likely that we'll + // eventually cache offsets for all children. Pre-allocate thus to minimize + // re-allocations. + offsets.SetCapacity(thisAcc->ChildCount()); + uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1]; + while (offsets.Length() < aChildIndex) { + Accessible* child = thisAcc->ChildAt(offsets.Length()); + lastOffset += nsAccUtils::TextLength(child); + offsets.AppendElement(lastOffset); + } + + return offsets[aChildIndex - 1]; +} + +uint32_t HyperTextAccessibleBase::CharacterCount() const { + return GetChildOffset(Acc()->ChildCount()); +} + +index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const { + if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) { + return CharacterCount(); + } + + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + return CaretOffset(); + } + + return aOffset; +} + +void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset, + int32_t aEndOffset, + nsAString& aText) const { + aText.Truncate(); + + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + if (!startOffset.IsValid() || !endOffset.IsValid() || + startOffset > endOffset || endOffset > CharacterCount()) { + NS_ERROR("Wrong in offset"); + return; + } + + int32_t startChildIdx = GetChildIndexAtOffset(startOffset); + if (startChildIdx == -1) { + return; + } + + int32_t endChildIdx = GetChildIndexAtOffset(endOffset); + if (endChildIdx == -1) { + return; + } + + const Accessible* thisAcc = Acc(); + if (startChildIdx == endChildIdx) { + int32_t childOffset = GetChildOffset(startChildIdx); + if (childOffset == -1) { + return; + } + + Accessible* child = thisAcc->ChildAt(startChildIdx); + child->AppendTextTo(aText, startOffset - childOffset, + endOffset - startOffset); + return; + } + + int32_t startChildOffset = GetChildOffset(startChildIdx); + if (startChildOffset == -1) { + return; + } + + Accessible* startChild = thisAcc->ChildAt(startChildIdx); + startChild->AppendTextTo(aText, startOffset - startChildOffset); + + for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx; + childIdx++) { + Accessible* child = thisAcc->ChildAt(childIdx); + child->AppendTextTo(aText); + } + + int32_t endChildOffset = GetChildOffset(endChildIdx); + if (endChildOffset == -1) { + return; + } + + Accessible* endChild = thisAcc->ChildAt(endChildIdx); + endChild->AppendTextTo(aText, 0, endOffset - endChildOffset); +} + +bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar, + int32_t* aStartOffset, + int32_t* aEndOffset) { + MOZ_ASSERT(!aStartOffset == !aEndOffset, + "Offsets should be both defined or both undefined!"); + + int32_t childIdx = GetChildIndexAtOffset(aOffset); + if (childIdx == -1) { + return false; + } + + Accessible* child = Acc()->ChildAt(childIdx); + child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1); + + if (aStartOffset && aEndOffset) { + *aStartOffset = aOffset; + *aEndOffset = aOffset + aChar.Length(); + } + return true; +} + +LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset, + uint32_t aCoordType) { + index_t offset = ConvertMagicOffset(aOffset); + if (!offset.IsValid() || offset > CharacterCount()) { + return LayoutDeviceIntRect(); + } + TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false); + if (!point.mAcc) { + return LayoutDeviceIntRect(); + } + + LayoutDeviceIntRect bounds = point.CharBounds(); + if (!bounds.x && !bounds.y && bounds.IsZeroArea()) { + return bounds; + } + nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc()); + return bounds; +} + +LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aCoordType) { + LayoutDeviceIntRect result; + if (CharacterCount() == 0) { + result = Acc()->Bounds(); + nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); + return result; + } + + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + if (!startOffset.IsValid() || startOffset >= endOffset) { + return LayoutDeviceIntRect(); + } + + // Here's where things get complicated. We can't simply query the first + // and last character, and union their bounds. They might reside on different + // lines, and a simple union may yield an incorrect width. We + // should use the length of the longest spanned line for our width. + + TextLeafPoint startPoint = + ToTextLeafPoint(static_cast<int32_t>(startOffset), false); + TextLeafPoint endPoint = + ToTextLeafPoint(static_cast<int32_t>(endOffset), true); + if (!endPoint) { + // The caller provided an invalid offset. + return LayoutDeviceIntRect(); + } + + // Step backwards from the point returned by ToTextLeafPoint above. + // For our purposes, `endPoint` should be inclusive. + endPoint = + endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + if (endPoint < startPoint) { + return result; + } + + if (endPoint == startPoint) { + result = startPoint.CharBounds(); + } else { + TextLeafRange range(startPoint, endPoint); + result = range.Bounds(); + } + + // Calls to TextLeafRange::Bounds() will construct screen coordinates. + // Perform any additional conversions here. + nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc()); + return result; +} + +int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY, + uint32_t aCoordType) { + Accessible* thisAcc = Acc(); + LayoutDeviceIntPoint coords = + nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); + if (!thisAcc->Bounds().Contains(coords.x, coords.y)) { + // The requested point does not exist in this accessible. + // Check if we used fuzzy hittesting to get here and, if + // so, return 0 to indicate this text leaf is a valid match. + LayoutDeviceIntPoint p(aX, aY); + if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) { + p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc); + } + if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) { + Accessible* hittestMatch = doc->ChildAtPoint( + p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild); + if (hittestMatch && thisAcc == hittestMatch->Parent()) { + return 0; + } + } + return -1; + } + + TextLeafPoint startPoint = ToTextLeafPoint(0, false); + // As with TextBounds, we walk to the very end of the text contained in this + // hypertext and then step backwards to make our endPoint inclusive. + TextLeafPoint endPoint = + ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true); + endPoint = + endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + TextLeafPoint point = startPoint; + // XXX: We should create a TextLeafRange object for this hypertext and move + // this search inside the TextLeafRange class. + // If there are no characters in this container, we might have moved endPoint + // before startPoint. In that case, we shouldn't try to move further forward, + // as that might result in an infinite loop. + if (startPoint <= endPoint) { + for (; !point.ContainsPoint(coords.x, coords.y) && point != endPoint; + point = + point.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext)) { + } + } + if (!point.ContainsPoint(coords.x, coords.y)) { + LayoutDeviceIntRect startRect = startPoint.CharBounds(); + if (coords.x < startRect.x || coords.y < startRect.y) { + // Bug 1816601: The point is within the container but above or to the left + // of the rectangle at offset 0. We should really return -1, but we've + // returned 0 for many years due to a bug. Some users have unfortunately + // come to rely on this, so perpetuate this here. + return 0; + } + return -1; + } + DebugOnly<bool> ok = false; + int32_t htOffset; + std::tie(ok, htOffset) = + TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); + MOZ_ASSERT(ok, "point should be a descendant of this"); + return htOffset; +} + +TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset, + bool aDescendToEnd) { + Accessible* thisAcc = Acc(); + if (!thisAcc->HasChildren()) { + return TextLeafPoint(thisAcc, 0); + } + Accessible* child = GetChildAtOffset(aOffset); + if (!child) { + return TextLeafPoint(); + } + if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) { + return childHt->ToTextLeafPoint( + aDescendToEnd ? static_cast<int32_t>(childHt->CharacterCount()) : 0, + aDescendToEnd); + } + int32_t offset = aOffset - GetChildOffset(child); + return TextLeafPoint(child, offset); +} + +std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset( + Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const { + const Accessible* thisAcc = Acc(); + // From the descendant, go up and get the immediate child of this hypertext. + int32_t offset = aOffset; + Accessible* descendant = aDescendant; + while (descendant) { + Accessible* parent = descendant->Parent(); + if (parent == thisAcc) { + return {true, GetChildOffset(descendant) + offset}; + } + + // This offset no longer applies because the passed-in text object is not + // a child of the hypertext. This happens when there are nested hypertexts, + // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset + // to make it relative the hypertext. + // If the end offset is not supposed to be inclusive and the original point + // is not at 0 offset then the returned offset should be after an embedded + // character the original point belongs to. + if (aIsEndOffset) { + offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0; + } else { + offset = 0; + } + + descendant = parent; + } + + // The given a11y point cannot be mapped to an offset relative to this + // hypertext accessible. Return the start or the end depending on whether this + // is a start ofset or an end offset, thus clipping to the relevant endpoint. + return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0}; +} + +void HyperTextAccessibleBase::AdjustOriginIfEndBoundary( + TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType, + bool aAtOffset) const { + if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END && + aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) { + return; + } + TextLeafPoint actualOrig = + aOrigin.IsCaret() ? aOrigin.ActualizeCaret(/* aAdjustAtEndOfLine */ false) + : aOrigin; + if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) { + if (!actualOrig.IsLineFeedChar()) { + return; + } + aOrigin = + actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + } else { // BOUNDARY_WORD_END + if (aAtOffset) { + // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and + // return the word which ends after the origin if the origin is a word end + // boundary. Also, if the caret is at the end of a line, our tests expect + // the word after the caret, not the word before. The reason for that + // is a mystery lost to history. We can do that by explicitly using the + // actualized caret without adjusting for end of line. + aOrigin = actualOrig; + return; + } + if (!actualOrig.IsSpace()) { + return; + } + TextLeafPoint prevChar = + actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + if (prevChar != actualOrig && !prevChar.IsSpace()) { + // aOrigin is a word end boundary. + aOrigin = prevChar; + } + } +} + +void HyperTextAccessibleBase::TextBeforeOffset( + int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || + aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { + return; // Not implemented. + } + + uint32_t adjustedOffset = ConvertMagicOffset(aOffset); + if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { + NS_ERROR("Wrong given offset!"); + return; + } + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { + if (adjustedOffset > 0) { + CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset, + aEndOffset); + } + return; + } + + TextLeafPoint orig; + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + orig = TextLeafPoint::GetCaret(Acc()); + } else { + orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset)); + } + if (!orig) { + // This can happen if aOffset is invalid. + return; + } + AdjustOriginIfEndBoundary(orig, aBoundaryType); + TextLeafPoint end = + orig.FindBoundary(aBoundaryType, eDirPrevious, + TextLeafPoint::BoundaryFlags::eIncludeOrigin); + bool ok; + std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, + /* aIsEndOffset */ true); + if (!ok) { + // There is no previous boundary inside this HyperText. + *aStartOffset = *aEndOffset = 0; + return; + } + TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious); + // If TransformOffset fails because start is outside this HyperText, + // *aStartOffset will be 0, which is what we want. + std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, + /* aIsEndOffset */ false); + TextSubstring(*aStartOffset, *aEndOffset, aText); +} + +void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset, + AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, + int32_t* aEndOffset, + nsAString& aText) { + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || + aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { + return; // Not implemented. + } + + uint32_t adjustedOffset = ConvertMagicOffset(aOffset); + if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { + NS_ERROR("Wrong given offset!"); + return; + } + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + TextLeafPoint caret = TextLeafPoint::GetCaret(Acc()); + if (caret.IsCaretAtEndOfLine()) { + // The caret is at the end of the line. Return no character. + *aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset); + return; + } + } + CharAt(adjustedOffset, aText, aStartOffset, aEndOffset); + return; + } + + TextLeafPoint start, end; + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + start = TextLeafPoint::GetCaret(Acc()); + AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true); + end = start; + } else { + start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset)); + Accessible* childAcc = GetChildAtOffset(adjustedOffset); + if (childAcc && childAcc->IsHyperText()) { + // We're searching for boundaries enclosing an embedded object. + // An embedded object might contain several boundaries itself. + // Thus, we must ensure we search for the end boundary from the last + // text in the subtree, not just the first. + // For example, if the embedded object is a link and it contains two + // words, but the second word expands beyond the link, we want to + // include the part of the second word which is outside of the link. + end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset), + /* aDescendToEnd */ true); + } else { + AdjustOriginIfEndBoundary(start, aBoundaryType, + /* aAtOffset */ true); + end = start; + } + } + if (!start) { + // This can happen if aOffset is invalid. + return; + } + start = start.FindBoundary(aBoundaryType, eDirPrevious, + TextLeafPoint::BoundaryFlags::eIncludeOrigin); + bool ok; + std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, + /* aIsEndOffset */ false); + end = end.FindBoundary(aBoundaryType, eDirNext); + std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, + /* aIsEndOffset */ true); + TextSubstring(*aStartOffset, *aEndOffset, aText); +} + +void HyperTextAccessibleBase::TextAfterOffset( + int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) { + *aStartOffset = *aEndOffset = 0; + aText.Truncate(); + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START || + aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) { + return; // Not implemented. + } + + uint32_t adjustedOffset = ConvertMagicOffset(aOffset); + if (adjustedOffset == std::numeric_limits<uint32_t>::max()) { + NS_ERROR("Wrong given offset!"); + return; + } + + if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 && + TextLeafPoint::GetCaret(Acc()).IsCaretAtEndOfLine()) { + --adjustedOffset; + } + uint32_t count = CharacterCount(); + if (adjustedOffset >= count) { + *aStartOffset = *aEndOffset = static_cast<int32_t>(count); + } else { + CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset, + aEndOffset); + } + return; + } + + TextLeafPoint orig; + if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) { + orig = TextLeafPoint::GetCaret(Acc()); + } else { + orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset), + /* aDescendToEnd */ true); + } + if (!orig) { + // This can happen if aOffset is invalid. + return; + } + AdjustOriginIfEndBoundary(orig, aBoundaryType); + TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext); + bool ok; + std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, + /* aIsEndOffset */ false); + if (!ok) { + // There is no next boundary inside this HyperText. + *aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount()); + return; + } + TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext); + // If TransformOffset fails because end is outside this HyperText, + // *aEndOffset will be CharacterCount(), which is what we want. + std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, + /* aIsEndOffset */ true); + TextSubstring(*aStartOffset, *aEndOffset, aText); +} + +int32_t HyperTextAccessibleBase::CaretOffset() const { + TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc())) + .ActualizeCaret(/* aAdjustAtEndOfLine */ false); + if (point.mOffset == 0 && point.mAcc == Acc()) { + // If a text box is empty, there will be no children, so point.mAcc will be + // this HyperText. + return 0; + } + auto [ok, htOffset] = + TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false); + if (!ok) { + // The caret is not within this HyperText. + return -1; + } + return htOffset; +} + +int32_t HyperTextAccessibleBase::CaretLineNumber() { + TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc())) + .ActualizeCaret(/* aAdjustAtEndOfLine */ false); + if (point.mOffset == 0 && point.mAcc == Acc()) { + MOZ_ASSERT(CharacterCount() == 0); + // If a text box is empty, there will be no children, so point.mAcc will be + // this HyperText. + return 1; + } + + if (!point.mAcc || + (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) { + // The caret is not within this HyperText. + return -1; + } + + TextLeafPoint firstPointInThis = TextLeafPoint(Acc(), 0); + int32_t lineNumber = 1; + for (TextLeafPoint line = point; line && firstPointInThis < line; + line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, + eDirPrevious)) { + lineNumber++; + } + + return lineNumber; +} + +bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) { + index_t offset = ConvertMagicOffset(aOffset); + return offset.IsValid() && offset <= CharacterCount(); +} + +bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset, + int32_t aEndOffset) { + index_t startOffset = ConvertMagicOffset(aStartOffset); + index_t endOffset = ConvertMagicOffset(aEndOffset); + return startOffset.IsValid() && endOffset.IsValid() && + startOffset <= endOffset && endOffset <= CharacterCount(); +} + +uint32_t HyperTextAccessibleBase::LinkCount() { + return Acc()->EmbeddedChildCount(); +} + +Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) { + return Acc()->EmbeddedChildAt(aIndex); +} + +int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) { + return Acc()->IndexOfEmbeddedChild(aLink); +} + +already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes( + bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset, + int32_t* aEndOffset) { + *aStartOffset = *aEndOffset = 0; + index_t offset = ConvertMagicOffset(aOffset); + if (!offset.IsValid() || offset > CharacterCount()) { + NS_ERROR("Wrong in offset!"); + return RefPtr{new AccAttributes()}.forget(); + } + + Accessible* originAcc = GetChildAtOffset(offset); + if (!originAcc) { + // Offset 0 is correct offset when accessible has empty text. Include + // default attributes if they were requested, otherwise return empty set. + if (offset == 0) { + if (aIncludeDefAttrs) { + return DefaultTextAttributes(); + } + } + return RefPtr{new AccAttributes()}.forget(); + } + + if (!originAcc->IsText()) { + // This is an embedded object. One or more consecutive embedded objects + // form a single attrs run with no attributes. + *aStartOffset = aOffset; + *aEndOffset = aOffset + 1; + Accessible* parent = originAcc->Parent(); + if (!parent) { + return RefPtr{new AccAttributes()}.forget(); + } + int32_t originIdx = originAcc->IndexInParent(); + if (originIdx > 0) { + // Check for embedded objects before the origin. + for (uint32_t idx = originIdx - 1;; --idx) { + Accessible* sibling = parent->ChildAt(idx); + if (sibling->IsText()) { + break; + } + --*aStartOffset; + if (idx == 0) { + break; + } + } + } + // Check for embedded objects after the origin. + for (uint32_t idx = originIdx + 1;; ++idx) { + Accessible* sibling = parent->ChildAt(idx); + if (!sibling || sibling->IsText()) { + break; + } + ++*aEndOffset; + } + return RefPtr{new AccAttributes()}.forget(); + } + + TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset)); + TextLeafPoint start = + origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true); + bool ok; + std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset, + /* aIsEndOffset */ false); + TextLeafPoint end = + origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false); + std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset, + /* aIsEndOffset */ true); + return origin.GetTextAttributes(aIncludeDefAttrs); +} + +void HyperTextAccessibleBase::CroppedSelectionRanges( + nsTArray<TextRange>& aRanges) const { + SelectionRanges(&aRanges); + const Accessible* acc = Acc(); + aRanges.RemoveElementsBy([acc](auto& range) { + if (range.StartPoint() == range.EndPoint()) { + return true; // Collapsed, so remove this range. + } + // If this is the document, it contains all ranges, so there's no need to + // crop. + if (!acc->IsDoc()) { + // If we fail to crop, the range is outside acc, so remove it. + return !range.Crop(const_cast<Accessible*>(acc)); + } + return false; + }); +} + +int32_t HyperTextAccessibleBase::SelectionCount() { + nsTArray<TextRange> ranges; + CroppedSelectionRanges(ranges); + return static_cast<int32_t>(ranges.Length()); +} + +bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum, + int32_t* aStartOffset, + int32_t* aEndOffset) { + nsTArray<TextRange> ranges; + CroppedSelectionRanges(ranges); + if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) { + return false; + } + TextRange& range = ranges[aSelectionNum]; + Accessible* thisAcc = Acc(); + if (range.StartContainer() == thisAcc) { + *aStartOffset = range.StartOffset(); + } else { + bool ok; + // range.StartContainer() isn't a text leaf, so don't use its offset. + std::tie(ok, *aStartOffset) = + TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false); + } + if (range.EndContainer() == thisAcc) { + *aEndOffset = range.EndOffset(); + } else { + bool ok; + // range.EndContainer() isn't a text leaf, so don't use its offset. If + // range.EndOffset() is > 0, we want to include this container, so pas + // offset 1. + std::tie(ok, *aEndOffset) = + TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1, + /* aDescendToEnd */ true); + } + return true; +} + +bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum, + int32_t aStartOffset, + int32_t aEndOffset) { + TextLeafRange range(ToTextLeafPoint(aStartOffset), + ToTextLeafPoint(aEndOffset, true)); + if (!range) { + NS_ERROR("Wrong in offset"); + return false; + } + + return range.SetSelection(aSelectionNum); +} + +void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aScrollType) { + TextLeafRange range(ToTextLeafPoint(aStartOffset), + ToTextLeafPoint(aEndOffset, true)); + range.ScrollIntoView(aScrollType); +} + +} // namespace mozilla::a11y diff --git a/accessible/basetypes/HyperTextAccessibleBase.h b/accessible/basetypes/HyperTextAccessibleBase.h new file mode 100644 index 0000000000..1afed52737 --- /dev/null +++ b/accessible/basetypes/HyperTextAccessibleBase.h @@ -0,0 +1,310 @@ +/* -*- 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/. */ + +#ifndef _HyperTextAccessibleBase_H_ +#define _HyperTextAccessibleBase_H_ + +#include "AccAttributes.h" +#include "nsIAccessibleText.h" +#include "nsIAccessibleTypes.h" + +namespace mozilla::a11y { +class Accessible; +class TextLeafPoint; +class TextRange; + +// This character marks where in the text returned via Text interface, +// that embedded object characters exist +const char16_t kEmbeddedObjectChar = 0xfffc; +const char16_t kImaginaryEmbeddedObjectChar = ' '; +const char16_t kForcedNewLineChar = '\n'; + +/** + * An index type. Assert if out of range value was attempted to be used. + */ +class index_t { + public: + MOZ_IMPLICIT index_t(int32_t aVal) : mVal(aVal) {} + + operator uint32_t() const { + MOZ_ASSERT(mVal >= 0, "Attempt to use wrong index!"); + return mVal; + } + + bool IsValid() const { return mVal >= 0; } + + private: + int32_t mVal; +}; + +class HyperTextAccessibleBase { + public: + /** + * Return child accessible at the given text offset. + * + * @param aOffset [in] the given text offset + */ + virtual int32_t GetChildIndexAtOffset(uint32_t aOffset) const; + + /** + * Return child accessible at the given text offset. + * + * @param aOffset [in] the given text offset + */ + virtual Accessible* GetChildAtOffset(uint32_t aOffset) const; + + /** + * Return text offset of the given child accessible within hypertext + * accessible. + * + * @param aChild [in] accessible child to get text offset for + * @param aInvalidateAfter [in, optional] indicates whether to invalidate + * cached offsets for subsequent siblings of the + * child. + */ + int32_t GetChildOffset(const Accessible* aChild, + bool aInvalidateAfter = false) const; + + /** + * Return text offset for the child accessible index. + */ + virtual int32_t GetChildOffset(uint32_t aChildIndex, + bool aInvalidateAfter = false) const; + + /** + * Return character count within the hypertext accessible. + */ + uint32_t CharacterCount() const; + + /** + * Get/set caret offset, if no caret then -1. + */ + virtual int32_t CaretOffset() const; + virtual void SetCaretOffset(int32_t aOffset) = 0; + + /** + * Provide the line number for the caret. + * @return 1-based index for the line number with the caret + */ + virtual int32_t CaretLineNumber(); + + /** + * Transform magic offset into text offset. + */ + index_t ConvertMagicOffset(int32_t aOffset) const; + + /** + * Return text between given offsets. + */ + void TextSubstring(int32_t aStartOffset, int32_t aEndOffset, + nsAString& aText) const; + + /** + * Get a character at the given offset (don't support magic offsets). + */ + bool CharAt(int32_t aOffset, nsAString& aChar, + int32_t* aStartOffset = nullptr, int32_t* aEndOffset = nullptr); + + char16_t CharAt(int32_t aOffset) { + nsAutoString charAtOffset; + CharAt(aOffset, charAtOffset); + return charAtOffset.CharAt(0); + } + + /** + * Return a rect (in dev pixels) for character at given offset relative + * given coordinate system. + */ + LayoutDeviceIntRect CharBounds(int32_t aOffset, uint32_t aCoordType); + + /** + * Return a rect (in dev pixels) of the given text range relative given + * coordinate system. + */ + LayoutDeviceIntRect TextBounds( + int32_t aStartOffset, int32_t aEndOffset, + uint32_t aCoordType = + nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE); + + /** + * Return the offset of the char that contains the given coordinates. + */ + virtual int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType); + + /** + * Get a TextLeafPoint for a given offset in this HyperTextAccessible. + * If the offset points to an embedded object and aDescendToEnd is true, + * the point right at the end of this subtree will be returned instead of the + * start. + */ + TextLeafPoint ToTextLeafPoint(int32_t aOffset, bool aDescendToEnd = false); + + /** + * Return text before/at/after the given offset corresponding to + * the boundary type. + */ + void TextBeforeOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + void TextAtOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + void TextAfterOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType, + int32_t* aStartOffset, int32_t* aEndOffset, + nsAString& aText); + + /** + * Return true if the given offset/range is valid. + */ + bool IsValidOffset(int32_t aOffset); + bool IsValidRange(int32_t aStartOffset, int32_t aEndOffset); + + /** + * Return link count within this hypertext accessible. + */ + uint32_t LinkCount(); + + /** + * Return link accessible at the given index. + */ + Accessible* LinkAt(uint32_t aIndex); + + /** + * Return index for the given link accessible. + */ + int32_t LinkIndexOf(Accessible* aLink); + + /** + * Return link accessible at the given text offset. + */ + int32_t LinkIndexAtOffset(uint32_t aOffset) { + Accessible* child = GetChildAtOffset(aOffset); + return child ? LinkIndexOf(child) : -1; + } + + /** + * Return text attributes for the given text range. + */ + already_AddRefed<AccAttributes> TextAttributes(bool aIncludeDefAttrs, + int32_t aOffset, + int32_t* aStartOffset, + int32_t* aEndOffset); + + /** + * Return text attributes applied to the accessible. + */ + virtual already_AddRefed<AccAttributes> DefaultTextAttributes() = 0; + + /** + * Return an array of disjoint ranges for selected text within the text + * control or the document this accessible belongs to. + */ + virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const = 0; + + /** + * Return selected regions count within the accessible. + */ + virtual int32_t SelectionCount(); + + /** + * Return the start and end offset of the specified selection. + */ + virtual bool SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset, + int32_t* aEndOffset); + + /** + * Changes the start and end offset of the specified selection. + * @return true if succeeded + */ + // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool SetSelectionBoundsAt( + int32_t aSelectionNum, int32_t aStartOffset, int32_t aEndOffset); + + /** + * Adds a selection bounded by the specified offsets. + * @return true if succeeded + */ + bool AddToSelection(int32_t aStartOffset, int32_t aEndOffset) { + return SetSelectionBoundsAt(-1, aStartOffset, aEndOffset); + } + + /** + * Removes the specified selection. + * @return true if succeeded + */ + // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool RemoveFromSelection( + int32_t aSelectionNum) = 0; + + /** + * Scroll the given text range into view. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ScrollSubstringTo(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aScrollType); + + ////////////////////////////////////////////////////////////////////////////// + // EditableTextAccessible + + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void ReplaceText( + const nsAString& aText) = 0; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void InsertText(const nsAString& aText, + int32_t aPosition) = 0; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CopyText(int32_t aStartPos, + int32_t aEndPos) = 0; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CutText(int32_t aStartPos, + int32_t aEndPos) = 0; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void DeleteText(int32_t aStartPos, + int32_t aEndPos) = 0; + MOZ_CAN_RUN_SCRIPT virtual void PasteText(int32_t aPosition) = 0; + + protected: + virtual const Accessible* Acc() const = 0; + Accessible* Acc() { + const Accessible* acc = + const_cast<const HyperTextAccessibleBase*>(this)->Acc(); + return const_cast<Accessible*>(acc); + } + + /** + * Get the cached map of child indexes to HyperText offsets. + * This is an array which contains the exclusive end offset for each child. + * That is, the start offset for child c is array index c - 1. + */ + virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() = 0; + + private: + /** + * Transform the given a11y point into an offset relative to this hypertext. + * Returns {success, offset}, where success is true if successful. + * If unsuccessful, the returned offset will be CharacterCount() if + * aIsEndOffset is true, 0 otherwise. This means most callers can ignore the + * success return value. + */ + std::pair<bool, int32_t> TransformOffset(Accessible* aDescendant, + int32_t aOffset, + bool aIsEndOffset) const; + + /** + * Helper method for TextBefore/At/AfterOffset. + * If BOUNDARY_LINE_END was requested and the origin is itself a line end + * boundary, we must use the line which ends at the origin. We must do + * similarly for BOUNDARY_WORD_END. This method adjusts the origin + * accordingly. + */ + void AdjustOriginIfEndBoundary(TextLeafPoint& aOrigin, + AccessibleTextBoundary aBoundaryType, + bool aAtOffset = false) const; + + /** + * Return text selection ranges cropped to this Accessible (rather than for + * the entire text control or document). This also excludes collapsed ranges. + */ + virtual void CroppedSelectionRanges(nsTArray<TextRange>& aRanges) const; +}; + +} // namespace mozilla::a11y + +#endif diff --git a/accessible/basetypes/TableAccessible.h b/accessible/basetypes/TableAccessible.h new file mode 100644 index 0000000000..fe5b499f75 --- /dev/null +++ b/accessible/basetypes/TableAccessible.h @@ -0,0 +1,172 @@ +/* -*- 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 TABLE_ACCESSIBLE_H +#define TABLE_ACCESSIBLE_H + +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Accessible table interface. + */ +class TableAccessible { + public: + /** + * Return the caption accessible if any for this table. + */ + virtual Accessible* Caption() const { return nullptr; } + + /** + * Get the summary for this table. + */ + virtual void Summary(nsString& aSummary) { aSummary.Truncate(); } + + /** + * Return the number of columns in the table. + */ + virtual uint32_t ColCount() const { return 0; } + + /** + * Return the number of rows in the table. + */ + virtual uint32_t RowCount() { return 0; } + + /** + * Return the accessible for the cell at the given row and column indices. + */ + virtual Accessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) { + return nullptr; + } + + /** + * Return the index of the cell at the given row and column. + */ + virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) { return -1; } + + /** + * Return the column index of the cell with the given index. + * This returns -1 if the column count is 0 or an invalid index is being + * passed in. + */ + virtual int32_t ColIndexAt(uint32_t aCellIdx) { return -1; } + + /** + * Return the row index of the cell with the given index. + * This returns -1 if the column count is 0 or an invalid index is being + * passed in. + */ + virtual int32_t RowIndexAt(uint32_t aCellIdx) { return -1; } + + /** + * Get the row and column indices for the cell at the given index. + * This returns -1 for both output parameters if the column count is 0 or an + * invalid index is being passed in. + */ + virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx, + int32_t* aColIdx) { + *aRowIdx = -1; + *aColIdx = -1; + } + + /** + * Return the number of columns occupied by the cell at the given row and + * column indices. + */ + virtual uint32_t ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; } + + /** + * Return the number of rows occupied by the cell at the given row and column + * indices. + */ + virtual uint32_t RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; } + + /** + * Get the description of the given column. + */ + virtual void ColDescription(uint32_t aColIdx, nsString& aDescription) { + aDescription.Truncate(); + } + + /** + * Get the description for the given row. + */ + virtual void RowDescription(uint32_t aRowIdx, nsString& aDescription) { + aDescription.Truncate(); + } + + /** + * Return true if the given column is selected. + */ + virtual bool IsColSelected(uint32_t aColIdx) { return false; } + + /** + * Return true if the given row is selected. + */ + virtual bool IsRowSelected(uint32_t aRowIdx) { return false; } + + /** + * Return true if the given cell is selected. + */ + virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) { + return false; + } + + /** + * Return the number of selected cells. + */ + virtual uint32_t SelectedCellCount() { return 0; } + + /** + * Return the number of selected columns. + */ + virtual uint32_t SelectedColCount() { return 0; } + + /** + * Return the number of selected rows. + */ + virtual uint32_t SelectedRowCount() { return 0; } + + /** + * Get the set of selected cells. + */ + virtual void SelectedCells(nsTArray<Accessible*>* aCells) {} + + /** + * Get the set of selected cell indices. + */ + virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) {} + + /** + * Get the set of selected column indices. + */ + virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) {} + + /** + * Get the set of selected row indices. + */ + virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) {} + + /** + * Return true if the table is probably for layout. + */ + virtual bool IsProbablyLayoutTable() { return false; } + + /** + * Convert the table to an Accessible*. + */ + virtual Accessible* AsAccessible() = 0; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/basetypes/TableCellAccessible.h b/accessible/basetypes/TableCellAccessible.h new file mode 100644 index 0000000000..3e92a7098b --- /dev/null +++ b/accessible/basetypes/TableCellAccessible.h @@ -0,0 +1,68 @@ +/* -*- 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_TableCellAccessible_h__ +#define mozilla_a11y_TableCellAccessible_h__ + +#include "nsTArray.h" +#include <stdint.h> + +namespace mozilla { +namespace a11y { + +class Accessible; +class TableAccessible; + +/** + * Abstract interface implemented by table cell accessibles. + */ +class TableCellAccessible { + public: + /** + * Return the table this cell is in. + */ + virtual TableAccessible* Table() const = 0; + + /** + * Return the column of the table this cell is in. + */ + virtual uint32_t ColIdx() const = 0; + + /** + * Return the row of the table this cell is in. + */ + virtual uint32_t RowIdx() const = 0; + + /** + * Return the column extent of this cell. + */ + virtual uint32_t ColExtent() const { return 1; } + + /** + * Return the row extent of this cell. + */ + virtual uint32_t RowExtent() const { return 1; } + + /** + * Return the column header cells for this cell. + */ + virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) = 0; + + /** + * Return the row header cells for this cell. + */ + virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) = 0; + + /** + * Returns true if this cell is selected. + */ + virtual bool Selected() = 0; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_TableCellAccessible_h__ diff --git a/accessible/basetypes/moz.build b/accessible/basetypes/moz.build new file mode 100644 index 0000000000..c0e3fdf9ac --- /dev/null +++ b/accessible/basetypes/moz.build @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.a11y += [ + "Accessible.h", + "HyperTextAccessibleBase.h", + "TableAccessible.h", + "TableCellAccessible.h", +] + +UNIFIED_SOURCES += [ + "Accessible.cpp", + "HyperTextAccessibleBase.cpp", +] + +LOCAL_INCLUDES += [ + "/accessible/base", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") |