/* -*- 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/. */ #include "TraversalRule.h" #include "mozilla/ArrayUtils.h" #include "mozilla/a11y/Accessible.h" #include "mozilla/a11y/Role.h" #include "HTMLListAccessible.h" #include "SessionAccessibility.h" #include "nsAccUtils.h" #include "nsIAccessiblePivot.h" using namespace mozilla; using namespace mozilla::a11y; TraversalRule::TraversalRule() : TraversalRule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true) {} TraversalRule::TraversalRule(int32_t aGranularity, bool aIsLocal) : mGranularity(aGranularity), mIsLocal(aIsLocal) {} uint16_t TraversalRule::Match(Accessible* aAcc) { MOZ_ASSERT(aAcc); if (mIsLocal && aAcc->IsRemote()) { // If we encounter a remote accessible in a local rule, we should // ignore the subtree because we won't encounter anymore local accessibles // in it. return nsIAccessibleTraversalRule::FILTER_IGNORE | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } else if (!mIsLocal && aAcc->IsLocal()) { // If we encounter a local accessible in a remote rule we are likely // traversing backwards/upwards, we don't ignore its subtree because it is // likely the outer doc root of the remote tree. return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; if (nsAccUtils::MustPrune(aAcc)) { result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } uint64_t state = aAcc->State(); if ((state & states::INVISIBLE) != 0) { return result; } if (aAcc->Opacity() == 0.0f) { return result | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } switch (mGranularity) { case java::SessionAccessibility::HTML_GRANULARITY_LINK: result |= LinkMatch(aAcc); break; case java::SessionAccessibility::HTML_GRANULARITY_CONTROL: result |= ControlMatch(aAcc); break; case java::SessionAccessibility::HTML_GRANULARITY_SECTION: result |= SectionMatch(aAcc); break; case java::SessionAccessibility::HTML_GRANULARITY_HEADING: result |= HeadingMatch(aAcc); break; case java::SessionAccessibility::HTML_GRANULARITY_LANDMARK: result |= LandmarkMatch(aAcc); break; default: result |= DefaultMatch(aAcc); break; } return result; } bool TraversalRule::IsSingleLineage(Accessible* aAccessible) { Accessible* child = aAccessible; while (child) { switch (child->ChildCount()) { case 0: return true; case 1: child = child->FirstChild(); break; case 2: if (IsListItemBullet(child->FirstChild())) { child = child->LastChild(); } else { return false; } break; default: return false; } } return true; } bool TraversalRule::IsListItemBullet(const Accessible* aAccessible) { return aAccessible->Role() == roles::LISTITEM_MARKER; } bool TraversalRule::IsFlatSubtree(const Accessible* aAccessible) { for (auto child = aAccessible->FirstChild(); child; child = child->NextSibling()) { roles::Role role = child->Role(); if (role == roles::TEXT_LEAF || role == roles::STATICTEXT) { continue; } if (child->ChildCount() > 0 || child->ActionCount() > 0) { return false; } } return true; } bool TraversalRule::HasName(const Accessible* aAccessible) { nsAutoString name; aAccessible->Name(name); name.CompressWhitespace(); return !name.IsEmpty(); } uint16_t TraversalRule::LinkMatch(Accessible* aAccessible) { if (aAccessible->Role() == roles::LINK && (aAccessible->State() & states::LINKED) != 0) { return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t TraversalRule::HeadingMatch(Accessible* aAccessible) { if (aAccessible->Role() == roles::HEADING && aAccessible->ChildCount()) { return nsIAccessibleTraversalRule::FILTER_MATCH; } return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t TraversalRule::SectionMatch(Accessible* aAccessible) { roles::Role role = aAccessible->Role(); if (role == roles::HEADING || role == roles::LANDMARK || aAccessible->LandmarkRole()) { return nsIAccessibleTraversalRule::FILTER_MATCH; } return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t TraversalRule::LandmarkMatch(Accessible* aAccessible) { if (aAccessible->LandmarkRole()) { return nsIAccessibleTraversalRule::FILTER_MATCH; } return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t TraversalRule::ControlMatch(Accessible* aAccessible) { switch (aAccessible->Role()) { case roles::PUSHBUTTON: case roles::SPINBUTTON: case roles::TOGGLE_BUTTON: case roles::BUTTONDROPDOWN: case roles::COMBOBOX: case roles::LISTBOX: case roles::ENTRY: case roles::PASSWORD_TEXT: case roles::PAGETAB: case roles::RADIOBUTTON: case roles::RADIO_MENU_ITEM: case roles::SLIDER: case roles::CHECKBUTTON: case roles::CHECK_MENU_ITEM: case roles::SWITCH: case roles::MENUITEM: return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; case roles::LINK: return LinkMatch(aAccessible); case roles::EDITCOMBOBOX: if (aAccessible->State() & states::EDITABLE) { // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is // editable. If it's a 1.1 combobox, the combobox is just a container; // we want to stop on the textbox inside it, not the container. return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } break; default: break; } return nsIAccessibleTraversalRule::FILTER_IGNORE; } uint16_t TraversalRule::DefaultMatch(Accessible* aAccessible) { switch (aAccessible->Role()) { case roles::COMBOBOX: // We don't want to ignore the subtree because this is often // where the list box hangs out. return nsIAccessibleTraversalRule::FILTER_MATCH; case roles::EDITCOMBOBOX: if (aAccessible->State() & states::EDITABLE) { // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is // editable. If it's a 1.1 combobox, the combobox is just a container; // we want to stop on the textbox inside it. return nsIAccessibleTraversalRule::FILTER_MATCH; } break; case roles::TEXT_LEAF: case roles::GRAPHIC: // Nameless text leaves are boring, skip them. if (HasName(aAccessible)) { return nsIAccessibleTraversalRule::FILTER_MATCH; } break; case roles::STATICTEXT: // Ignore list bullets if (!IsListItemBullet(aAccessible)) { return nsIAccessibleTraversalRule::FILTER_MATCH; } break; case roles::HEADING: case roles::COLUMNHEADER: case roles::ROWHEADER: case roles::STATUSBAR: if ((aAccessible->ChildCount() > 0 || HasName(aAccessible)) && (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible))) { return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } break; case roles::GRID_CELL: if (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible)) { return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } break; case roles::LABEL: if (IsFlatSubtree(aAccessible)) { // Match if this is a label with text but no nested controls. return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } break; case roles::MENUITEM: case roles::LINK: case roles::PAGETAB: case roles::PUSHBUTTON: case roles::CHECKBUTTON: case roles::RADIOBUTTON: case roles::PROGRESSBAR: case roles::BUTTONDROPDOWN: case roles::BUTTONMENU: case roles::CHECK_MENU_ITEM: case roles::PASSWORD_TEXT: case roles::RADIO_MENU_ITEM: case roles::TOGGLE_BUTTON: case roles::ENTRY: case roles::KEY: case roles::SLIDER: case roles::SPINBUTTON: case roles::OPTION: case roles::SWITCH: case roles::MATHML_MATH: // Ignore the subtree, if there is one. So that we don't land on // the same content that was already presented by its parent. return nsIAccessibleTraversalRule::FILTER_MATCH | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; default: break; } return nsIAccessibleTraversalRule::FILTER_IGNORE; }