1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "Accessible.h"
8 #include "nsAccUtils.h"
12 #include "mozilla/a11y/FocusManager.h"
13 #include "mozilla/a11y/HyperTextAccessibleBase.h"
14 #include "mozilla/BasicEvents.h"
15 #include "mozilla/Components.h"
16 #include "nsIStringBundle.h"
19 # include "nsAccessibilityService.h"
22 using namespace mozilla
;
23 using namespace mozilla::a11y
;
25 Accessible::Accessible()
26 : mType(static_cast<uint32_t>(0)),
27 mGenericTypes(static_cast<uint32_t>(0)),
28 mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX
) {}
30 Accessible::Accessible(AccType aType
, AccGenericType aGenericTypes
,
31 uint8_t aRoleMapEntryIndex
)
32 : mType(static_cast<uint32_t>(aType
)),
33 mGenericTypes(static_cast<uint32_t>(aGenericTypes
)),
34 mRoleMapEntryIndex(aRoleMapEntryIndex
) {}
36 void Accessible::StaticAsserts() const {
37 static_assert(eLastAccType
<= (1 << kTypeBits
) - 1,
38 "Accessible::mType was oversized by eLastAccType!");
40 eLastAccGenericType
<= (1 << kGenericTypesBits
) - 1,
41 "Accessible::mGenericType was oversized by eLastAccGenericType!");
44 bool Accessible::IsBefore(const Accessible
* aAcc
) const {
45 // Build the chain of parents.
46 const Accessible
* thisP
= this;
47 const Accessible
* otherP
= aAcc
;
48 AutoTArray
<const Accessible
*, 30> thisParents
, otherParents
;
50 thisParents
.AppendElement(thisP
);
51 thisP
= thisP
->Parent();
54 otherParents
.AppendElement(otherP
);
55 otherP
= otherP
->Parent();
58 // Find where the parent chain differs.
59 uint32_t thisPos
= thisParents
.Length(), otherPos
= otherParents
.Length();
60 for (uint32_t len
= std::min(thisPos
, otherPos
); len
> 0; --len
) {
61 const Accessible
* thisChild
= thisParents
.ElementAt(--thisPos
);
62 const Accessible
* otherChild
= otherParents
.ElementAt(--otherPos
);
63 if (thisChild
!= otherChild
) {
64 return thisChild
->IndexInParent() < otherChild
->IndexInParent();
68 // If the ancestries are the same length (both thisPos and otherPos are 0),
69 // we should have returned by now.
70 MOZ_ASSERT(thisPos
!= 0 || otherPos
!= 0);
71 // At this point, one of the ancestries is a superset of the other, so one of
72 // thisPos or otherPos should be 0.
73 MOZ_ASSERT(thisPos
!= otherPos
);
74 // If the other Accessible is deeper than this one (otherPos > 0), this
75 // Accessible comes before the other.
79 Accessible
* Accessible::FocusedChild() {
80 Accessible
* doc
= nsAccUtils::DocumentFor(this);
81 Accessible
* child
= doc
->FocusedChild();
82 if (child
&& (child
== this || child
->Parent() == this)) {
89 const nsRoleMapEntry
* Accessible::ARIARoleMap() const {
90 return aria::GetRoleMapFromIndex(mRoleMapEntryIndex
);
93 bool Accessible::HasARIARole() const {
94 return mRoleMapEntryIndex
!= aria::NO_ROLE_MAP_ENTRY_INDEX
;
97 bool Accessible::IsARIARole(nsAtom
* aARIARole
) const {
98 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
99 return roleMapEntry
&& roleMapEntry
->Is(aARIARole
);
102 bool Accessible::HasStrongARIARole() const {
103 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
104 return roleMapEntry
&& roleMapEntry
->roleRule
== kUseMapRole
;
107 bool Accessible::HasGenericType(AccGenericType aType
) const {
108 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
109 return (mGenericTypes
& aType
) ||
110 (roleMapEntry
&& roleMapEntry
->IsOfType(aType
));
113 nsIntRect
Accessible::BoundsInCSSPixels() const {
114 return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel());
117 LayoutDeviceIntSize
Accessible::Size() const { return Bounds().Size(); }
119 LayoutDeviceIntPoint
Accessible::Position(uint32_t aCoordType
) {
120 LayoutDeviceIntPoint point
= Bounds().TopLeft();
121 nsAccUtils::ConvertScreenCoordsTo(&point
.x
.value
, &point
.y
.value
, aCoordType
,
126 bool Accessible::IsTextRole() {
127 if (!IsHyperText()) {
131 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
132 if (roleMapEntry
&& (roleMapEntry
->role
== roles::GRAPHIC
||
133 roleMapEntry
->role
== roles::IMAGE_MAP
||
134 roleMapEntry
->role
== roles::SLIDER
||
135 roleMapEntry
->role
== roles::PROGRESSBAR
||
136 roleMapEntry
->role
== roles::SEPARATOR
)) {
143 uint32_t Accessible::StartOffset() {
144 MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!");
145 Accessible
* parent
= Parent();
146 HyperTextAccessibleBase
* hyperText
=
147 parent
? parent
->AsHyperTextBase() : nullptr;
148 return hyperText
? hyperText
->GetChildOffset(this) : 0;
151 uint32_t Accessible::EndOffset() {
152 MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!");
153 Accessible
* parent
= Parent();
154 HyperTextAccessibleBase
* hyperText
=
155 parent
? parent
->AsHyperTextBase() : nullptr;
156 return hyperText
? (hyperText
->GetChildOffset(this) + 1) : 0;
159 GroupPos
Accessible::GroupPosition() {
162 // Try aria-row/colcount/index.
164 Accessible
* table
= nsAccUtils::TableFor(this);
166 if (auto count
= table
->GetIntARIAAttr(nsGkAtoms::aria_rowcount
)) {
168 groupPos
.setSize
= *count
;
172 if (auto index
= GetIntARIAAttr(nsGkAtoms::aria_rowindex
)) {
173 groupPos
.posInSet
= *index
;
175 if (groupPos
.setSize
&& groupPos
.posInSet
) {
181 for (table
= Parent(); table
; table
= table
->Parent()) {
182 if (table
->IsTable()) {
187 if (auto count
= table
->GetIntARIAAttr(nsGkAtoms::aria_colcount
)) {
189 groupPos
.setSize
= *count
;
193 if (auto index
= GetIntARIAAttr(nsGkAtoms::aria_colindex
)) {
194 groupPos
.posInSet
= *index
;
196 if (groupPos
.setSize
&& groupPos
.posInSet
) {
201 // Get group position from ARIA attributes.
202 ARIAGroupPosition(&groupPos
.level
, &groupPos
.setSize
, &groupPos
.posInSet
);
204 // If ARIA is missed and the accessible is visible then calculate group
205 // position from hierarchy.
206 if (State() & states::INVISIBLE
) return groupPos
;
208 // Calculate group level if ARIA is missed.
209 if (groupPos
.level
== 0) {
210 groupPos
.level
= GetLevel(false);
213 // Calculate position in group and group size if ARIA is missed.
214 if (groupPos
.posInSet
== 0 || groupPos
.setSize
== 0) {
215 int32_t posInSet
= 0, setSize
= 0;
216 GetPositionAndSetSize(&posInSet
, &setSize
);
217 if (posInSet
!= 0 && setSize
!= 0) {
218 if (groupPos
.posInSet
== 0) groupPos
.posInSet
= posInSet
;
220 if (groupPos
.setSize
== 0) groupPos
.setSize
= setSize
;
227 int32_t Accessible::GetLevel(bool aFast
) const {
229 if (!Parent()) return level
;
231 roles::Role role
= Role();
232 if (role
== roles::OUTLINEITEM
) {
233 // Always expose 'level' attribute for 'outlineitem' accessible. The number
234 // of nested 'grouping' accessibles containing 'outlineitem' accessible is
239 const Accessible
* parent
= this;
240 while ((parent
= parent
->Parent()) && !parent
->IsDoc()) {
241 roles::Role parentRole
= parent
->Role();
243 if (parentRole
== roles::OUTLINE
) break;
244 if (parentRole
== roles::GROUPING
) ++level
;
247 } else if (role
== roles::LISTITEM
&& !aFast
) {
248 // Expose 'level' attribute on nested lists. We support two hierarchies:
249 // a) list -> listitem -> list -> listitem (nested list is a last child
250 // of listitem of the parent list);
251 // b) list -> listitem -> group -> listitem (nested listitems are contained
252 // by group that is a last child of the parent listitem).
254 // Calculate 'level' attribute based on number of parent listitems.
256 const Accessible
* parent
= this;
257 while ((parent
= parent
->Parent()) && !parent
->IsDoc()) {
258 roles::Role parentRole
= parent
->Role();
260 if (parentRole
== roles::LISTITEM
) {
262 } else if (parentRole
!= roles::LIST
&& parentRole
!= roles::GROUPING
) {
268 // If this listitem is on top of nested lists then expose 'level'
271 uint32_t siblingCount
= parent
->ChildCount();
272 for (uint32_t siblingIdx
= 0; siblingIdx
< siblingCount
; siblingIdx
++) {
273 Accessible
* sibling
= parent
->ChildAt(siblingIdx
);
275 Accessible
* siblingChild
= sibling
->LastChild();
277 roles::Role lastChildRole
= siblingChild
->Role();
278 if (lastChildRole
== roles::LIST
||
279 lastChildRole
== roles::GROUPING
) {
285 ++level
; // level is 1-index based
287 } else if (role
== roles::OPTION
|| role
== roles::COMBOBOX_OPTION
) {
288 if (const Accessible
* parent
= Parent()) {
289 if (parent
->IsHTMLOptGroup()) {
293 if (parent
->IsListControl() && !parent
->ARIARoleMap()) {
294 // This is for HTML selects only.
299 for (uint32_t i
= 0, count
= parent
->ChildCount(); i
< count
; ++i
) {
300 if (parent
->ChildAt(i
)->IsHTMLOptGroup()) {
306 } else if (role
== roles::HEADING
) {
307 nsAtom
* tagName
= TagName();
308 if (tagName
== nsGkAtoms::h1
) {
311 if (tagName
== nsGkAtoms::h2
) {
314 if (tagName
== nsGkAtoms::h3
) {
317 if (tagName
== nsGkAtoms::h4
) {
320 if (tagName
== nsGkAtoms::h5
) {
323 if (tagName
== nsGkAtoms::h6
) {
327 const nsRoleMapEntry
* ariaRole
= this->ARIARoleMap();
328 if (ariaRole
&& ariaRole
->Is(nsGkAtoms::heading
)) {
329 // An aria heading with no aria level has a default level of 2.
332 } else if (role
== roles::COMMENT
) {
333 // For comments, count the ancestor elements with the same role to get the
338 const Accessible
* parent
= this;
339 while ((parent
= parent
->Parent()) && !parent
->IsDoc()) {
340 roles::Role parentRole
= parent
->Role();
341 if (parentRole
== roles::COMMENT
) {
346 } else if (role
== roles::ROW
) {
347 // It is a row inside flatten treegrid. Group level is always 1 until it
348 // is overriden by aria-level attribute.
349 const Accessible
* parent
= Parent();
350 if (parent
->Role() == roles::TREE_TABLE
) {
358 void Accessible::GetPositionAndSetSize(int32_t* aPosInSet
, int32_t* aSetSize
) {
359 auto groupInfo
= GetOrCreateGroupInfo();
361 *aPosInSet
= groupInfo
->PosInSet();
362 *aSetSize
= groupInfo
->SetSize();
366 bool Accessible::IsLinkValid() {
367 MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
369 // XXX In order to implement this we would need to follow every link
370 // Perhaps we can get information about invalid links from the cache
371 // In the mean time authors can use role="link" aria-invalid="true"
372 // to force it for links they internally know to be invalid
373 return (0 == (State() & mozilla::a11y::states::INVALID
));
376 uint32_t Accessible::AnchorCount() {
381 MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
385 Accessible
* Accessible::AnchorAt(uint32_t aAnchorIndex
) const {
387 return ChildAt(aAnchorIndex
);
390 MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
391 return aAnchorIndex
== 0 ? const_cast<Accessible
*>(this) : nullptr;
394 already_AddRefed
<nsIURI
> Accessible::AnchorURIAt(uint32_t aAnchorIndex
) const {
395 Accessible
* anchor
= nullptr;
397 if (IsTextLeaf() || IsImage()) {
398 for (Accessible
* parent
= Parent(); parent
&& !parent
->IsOuterDoc();
399 parent
= parent
->Parent()) {
400 if (parent
->IsLink()) {
401 anchor
= parent
->AnchorAt(aAnchorIndex
);
405 anchor
= AnchorAt(aAnchorIndex
);
412 nsresult rv
= NS_NewURI(getter_AddRefs(uri
), spec
);
413 if (NS_SUCCEEDED(rv
)) {
421 bool Accessible::IsSearchbox() const {
422 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
423 if (roleMapEntry
&& roleMapEntry
->Is(nsGkAtoms::searchbox
)) {
427 RefPtr
<nsAtom
> inputType
= InputType();
428 return inputType
== nsGkAtoms::search
;
432 void Accessible::DebugDescription(nsCString
& aDesc
) const {
434 aDesc
.AppendPrintf("%s", IsRemote() ? "Remote" : "Local");
435 aDesc
.AppendPrintf("[%p] ", this);
437 GetAccService()->GetStringRole(Role(), role
);
438 aDesc
.Append(NS_ConvertUTF16toUTF8(role
));
440 if (nsAtom
* tagAtom
= TagName()) {
442 tagAtom
->ToUTF8String(tag
);
443 aDesc
.AppendPrintf(" %s", tag
.get());
449 aDesc
.Append(NS_ConvertUTF16toUTF8(id
));
456 if (!name
.IsEmpty()) {
458 aDesc
.Append(NS_ConvertUTF16toUTF8(name
));
463 void Accessible::DebugPrint(const char* aPrefix
,
464 const Accessible
* aAccessible
) {
467 aAccessible
->DebugDescription(desc
);
469 desc
.AssignLiteral("[null]");
471 # if defined(ANDROID)
472 printf_stderr("%s %s\n", aPrefix
, desc
.get());
474 printf("%s %s\n", aPrefix
, desc
.get());
480 void Accessible::TranslateString(const nsString
& aKey
, nsAString
& aStringOut
) {
481 nsCOMPtr
<nsIStringBundleService
> stringBundleService
=
482 components::StringBundle::Service();
483 if (!stringBundleService
) return;
485 nsCOMPtr
<nsIStringBundle
> stringBundle
;
486 stringBundleService
->CreateBundle(
487 "chrome://global-platform/locale/accessible.properties",
488 getter_AddRefs(stringBundle
));
489 if (!stringBundle
) return;
491 nsAutoString xsValue
;
492 nsresult rv
= stringBundle
->GetStringFromName(
493 NS_ConvertUTF16toUTF8(aKey
).get(), xsValue
);
494 if (NS_SUCCEEDED(rv
)) aStringOut
.Assign(xsValue
);
497 const Accessible
* Accessible::ActionAncestor() const {
498 // We do want to consider a click handler on the document. However, we don't
499 // want to walk outside of this document, so we stop if we see an OuterDoc.
500 for (Accessible
* parent
= Parent(); parent
&& !parent
->IsOuterDoc();
501 parent
= parent
->Parent()) {
502 if (parent
->HasPrimaryAction()) {
510 nsStaticAtom
* Accessible::LandmarkRole() const {
511 nsAtom
* tagName
= TagName();
513 // Either no associated content, or no cache.
517 if (tagName
== nsGkAtoms::nav
) {
518 return nsGkAtoms::navigation
;
521 if (tagName
== nsGkAtoms::aside
) {
522 return nsGkAtoms::complementary
;
525 if (tagName
== nsGkAtoms::main
) {
526 return nsGkAtoms::main
;
529 if (tagName
== nsGkAtoms::header
) {
530 if (Role() == roles::LANDMARK
) {
531 return nsGkAtoms::banner
;
535 if (tagName
== nsGkAtoms::footer
) {
536 if (Role() == roles::LANDMARK
) {
537 return nsGkAtoms::contentinfo
;
541 if (tagName
== nsGkAtoms::section
) {
544 if (!name
.IsEmpty()) {
545 return nsGkAtoms::region
;
549 if (tagName
== nsGkAtoms::form
) {
552 if (!name
.IsEmpty()) {
553 return nsGkAtoms::form
;
557 if (tagName
== nsGkAtoms::search
) {
558 return nsGkAtoms::search
;
561 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
562 return roleMapEntry
&& roleMapEntry
->IsOfType(eLandmark
)
563 ? roleMapEntry
->roleAtom
567 nsStaticAtom
* Accessible::ComputedARIARole() const {
568 const nsRoleMapEntry
* roleMap
= ARIARoleMap();
569 if (roleMap
&& roleMap
->roleAtom
!= nsGkAtoms::_empty
&&
570 // region has its own Gecko role and it needs to be handled specially.
571 roleMap
->roleAtom
!= nsGkAtoms::region
&&
572 (roleMap
->roleRule
== kUseNativeRole
|| roleMap
->IsOfType(eLandmark
) ||
573 roleMap
->roleAtom
== nsGkAtoms::alertdialog
||
574 roleMap
->roleAtom
== nsGkAtoms::feed
||
575 roleMap
->roleAtom
== nsGkAtoms::rowgroup
)) {
576 // Explicit ARIA role (e.g. specified via the role attribute) which does not
577 // map to a unique Gecko role.
578 return roleMap
->roleAtom
;
581 return nsGkAtoms::searchbox
;
583 role geckoRole
= Role();
584 if (geckoRole
== roles::LANDMARK
) {
585 // Landmark role from native markup; e.g. <main>, <nav>.
586 return LandmarkRole();
588 if (geckoRole
== roles::GROUPING
) {
589 // Gecko doesn't differentiate between group and rowgroup. It uses
590 // roles::GROUPING for both.
591 nsAtom
* tag
= TagName();
592 if (tag
== nsGkAtoms::tbody
|| tag
== nsGkAtoms::tfoot
||
593 tag
== nsGkAtoms::thead
) {
594 return nsGkAtoms::rowgroup
;
597 // Role from native markup or layout.
598 #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
599 msaaRole, ia2Role, androidClass, nameRule) \
600 case roles::_geckoRole: \
606 MOZ_ASSERT_UNREACHABLE("Unknown role");
610 void Accessible::ApplyImplicitState(uint64_t& aState
) const {
611 // nsAccessibilityService (and thus FocusManager) can be shut down before
612 // RemoteAccessibles.
613 if (const auto* focusMgr
= FocusMgr()) {
614 if (focusMgr
->IsFocused(this)) {
615 aState
|= states::FOCUSED
;
619 // If this is an ARIA item of the selectable widget and if it's focused and
620 // not marked unselected explicitly (i.e. aria-selected="false") then expose
621 // it as selected to make ARIA widget authors life easier.
622 const nsRoleMapEntry
* roleMapEntry
= ARIARoleMap();
623 if (roleMapEntry
&& !(aState
& states::SELECTED
) &&
624 ARIASelected().valueOr(true)) {
625 // Special case for tabs: focused tab or focus inside related tab panel
626 // implies selected state.
627 if (roleMapEntry
->role
== roles::PAGETAB
) {
628 if (aState
& states::FOCUSED
) {
629 aState
|= states::SELECTED
;
631 // If focus is in a child of the tab panel surely the tab is selected!
632 Relation rel
= RelationByType(RelationType::LABEL_FOR
);
633 Accessible
* relTarget
= nullptr;
634 while ((relTarget
= rel
.Next())) {
635 if (relTarget
->Role() == roles::PROPERTYPAGE
&&
636 FocusMgr()->IsFocusWithin(relTarget
)) {
637 aState
|= states::SELECTED
;
641 } else if (aState
& states::FOCUSED
) {
642 Accessible
* container
= nsAccUtils::GetSelectableContainer(this, aState
);
643 if (container
&& !(container
->State() & states::MULTISELECTABLE
)) {
644 aState
|= states::SELECTED
;
649 if (Opacity() == 1.0f
&& !(aState
& states::INVISIBLE
)) {
650 aState
|= states::OPAQUE1
;
654 ////////////////////////////////////////////////////////////////////////////////
658 uint32_t KeyBinding::AccelModifier() {
659 switch (WidgetInputEvent::AccelModifier()) {
662 case MODIFIER_CONTROL
:
667 MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()");
672 void KeyBinding::ToPlatformFormat(nsAString
& aValue
) const {
673 nsCOMPtr
<nsIStringBundle
> keyStringBundle
;
674 nsCOMPtr
<nsIStringBundleService
> stringBundleService
=
675 mozilla::components::StringBundle::Service();
676 if (stringBundleService
) {
677 stringBundleService
->CreateBundle(
678 "chrome://global-platform/locale/platformKeys.properties",
679 getter_AddRefs(keyStringBundle
));
682 if (!keyStringBundle
) return;
684 nsAutoString separator
;
685 keyStringBundle
->GetStringFromName("MODIFIER_SEPARATOR", separator
);
687 nsAutoString modifierName
;
688 if (mModifierMask
& kControl
) {
689 keyStringBundle
->GetStringFromName("VK_CONTROL", modifierName
);
691 aValue
.Append(modifierName
);
692 aValue
.Append(separator
);
695 if (mModifierMask
& kAlt
) {
696 keyStringBundle
->GetStringFromName("VK_ALT", modifierName
);
698 aValue
.Append(modifierName
);
699 aValue
.Append(separator
);
702 if (mModifierMask
& kShift
) {
703 keyStringBundle
->GetStringFromName("VK_SHIFT", modifierName
);
705 aValue
.Append(modifierName
);
706 aValue
.Append(separator
);
709 if (mModifierMask
& kMeta
) {
710 keyStringBundle
->GetStringFromName("VK_META", modifierName
);
712 aValue
.Append(modifierName
);
713 aValue
.Append(separator
);
719 void KeyBinding::ToAtkFormat(nsAString
& aValue
) const {
720 nsAutoString modifierName
;
721 if (mModifierMask
& kControl
) aValue
.AppendLiteral("<Control>");
723 if (mModifierMask
& kAlt
) aValue
.AppendLiteral("<Alt>");
725 if (mModifierMask
& kShift
) aValue
.AppendLiteral("<Shift>");
727 if (mModifierMask
& kMeta
) aValue
.AppendLiteral("<Meta>");