diff options
Diffstat (limited to 'accessible/base')
66 files changed, 22152 insertions, 0 deletions
diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp new file mode 100644 index 000000000..c29d37873 --- /dev/null +++ b/accessible/base/ARIAMap.cpp @@ -0,0 +1,1008 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 "ARIAMap.h" + +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "Role.h" +#include "States.h" + +#include "nsAttrName.h" +#include "nsWhitespaceTokenizer.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::a11y::aria; + +static const uint32_t kGenericAccType = 0; + +/** + * This list of WAI-defined roles are currently hardcoded. + * Eventually we will most likely be loading an RDF resource that contains this information + * Using RDF will also allow for role extensibility. See bug 280138. + * + * Definition of nsRoleMapEntry contains comments explaining this table. + * + * When no Role enum mapping exists for an ARIA role, the role will be exposed + * via the object attribute "xml-roles". + */ + +static const nsRoleMapEntry sWAIRoleMaps[] = +{ + { // alert + &nsGkAtoms::alert, + roles::ALERT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eAlert, + kNoReqStates + }, + { // alertdialog + &nsGkAtoms::alertdialog, + roles::DIALOG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // application + &nsGkAtoms::application, + roles::APPLICATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // article + &nsGkAtoms::article, + roles::DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // banner + &nsGkAtoms::banner, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // button + &nsGkAtoms::button, + roles::PUSHBUTTON, + kUseMapRole, + eNoValue, + ePressAction, + eNoLiveAttr, + eButton, + kNoReqStates + // eARIAPressed is auto applied on any button + }, + { // cell + &nsGkAtoms::cell, + roles::CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates + }, + { // checkbox + &nsGkAtoms::checkbox, + roles::CHECKBUTTON, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed, + eARIAReadonly + }, + { // columnheader + &nsGkAtoms::columnheader, + roles::COLUMNHEADER, + kUseMapRole, + eNoValue, + eSortAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectableIfDefined, + eARIAReadonlyOrEditableIfDefined + }, + { // combobox + &nsGkAtoms::combobox, + roles::COMBOBOX, + kUseMapRole, + eNoValue, + eOpenCloseAction, + eNoLiveAttr, + kGenericAccType, + states::COLLAPSED | states::HASPOPUP | states::VERTICAL, + eARIAAutoComplete, + eARIAReadonly, + eARIAOrientation + }, + { // complementary + &nsGkAtoms::complementary, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // contentinfo + &nsGkAtoms::contentinfo, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // dialog + &nsGkAtoms::dialog, + roles::DIALOG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // directory + &nsGkAtoms::directory, + roles::LIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eList, + kNoReqStates + }, + { // document + &nsGkAtoms::document, + roles::DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // feed + &nsGkAtoms::feed, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // form + &nsGkAtoms::form, + roles::FORM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // grid + &nsGkAtoms::grid, + roles::TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect | eTable, + kNoReqStates, + eARIAMultiSelectable, + eARIAReadonlyOrEditable, + eFocusableUntilDisabled + }, + { // gridcell + &nsGkAtoms::gridcell, + roles::GRID_CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectable, + eARIAReadonlyOrEditableIfDefined + }, + { // group + &nsGkAtoms::group, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // heading + &nsGkAtoms::heading, + roles::HEADING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // img + &nsGkAtoms::img, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // key + &nsGkAtoms::key, + roles::KEY, + kUseMapRole, + eNoValue, + ePressAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAPressed + }, + { // link + &nsGkAtoms::link, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // list + &nsGkAtoms::list, + roles::LIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eList, + states::READONLY + }, + { // listbox + &nsGkAtoms::listbox, + roles::LISTBOX, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eListControl | eSelect, + states::VERTICAL, + eARIAMultiSelectable, + eARIAReadonly, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // listitem + &nsGkAtoms::listitem, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, // XXX: should depend on state, parent accessible + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // log + &nsGkAtoms::log_, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + ePoliteLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // main + &nsGkAtoms::main, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // marquee + &nsGkAtoms::marquee, + roles::ANIMATION, + kUseMapRole, + eNoValue, + eNoAction, + eOffLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // math + &nsGkAtoms::math, + roles::FLAT_EQUATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // menu + &nsGkAtoms::menu, + roles::MENUPOPUP, + kUseMapRole, + eNoValue, + eNoAction, // XXX: technically accessibles of menupopup role haven't + // any action, but menu can be open or close. + eNoLiveAttr, + kGenericAccType, + states::VERTICAL, + eARIAOrientation + }, + { // menubar + &nsGkAtoms::menubar, + roles::MENUBAR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // menuitem + &nsGkAtoms::menuitem, + roles::MENUITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckedMixed + }, + { // menuitemcheckbox + &nsGkAtoms::menuitemcheckbox, + roles::CHECK_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed + }, + { // menuitemradio + &nsGkAtoms::menuitemradio, + roles::RADIO_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool + }, + { // navigation + &nsGkAtoms::navigation, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // none + &nsGkAtoms::none, + roles::NOTHING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // note + &nsGkAtoms::note_, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // option + &nsGkAtoms::option, + roles::OPTION, + kUseMapRole, + eNoValue, + eSelectAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable, + eARIACheckedMixed + }, + { // presentation + &nsGkAtoms::presentation, + roles::NOTHING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // progressbar + &nsGkAtoms::progressbar, + roles::PROGRESSBAR, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY, + eIndeterminateIfNoValue + }, + { // radio + &nsGkAtoms::radio, + roles::RADIOBUTTON, + kUseMapRole, + eNoValue, + eSelectAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool + }, + { // radiogroup + &nsGkAtoms::radiogroup, + roles::RADIO_GROUP, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAOrientation + }, + { // region + &nsGkAtoms::region, + roles::PANE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // row + &nsGkAtoms::row, + roles::ROW, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableRow, + kNoReqStates, + eARIASelectable + }, + { // rowgroup + &nsGkAtoms::rowgroup, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // rowheader + &nsGkAtoms::rowheader, + roles::ROWHEADER, + kUseMapRole, + eNoValue, + eSortAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectableIfDefined, + eARIAReadonlyOrEditableIfDefined + }, + { // scrollbar + &nsGkAtoms::scrollbar, + roles::SCROLLBAR, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::VERTICAL, + eARIAOrientation, + eARIAReadonly + }, + { // search + &nsGkAtoms::search, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // searchbox + &nsGkAtoms::searchbox, + roles::ENTRY, + kUseMapRole, + eNoValue, + eActivateAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAAutoComplete, + eARIAMultiline, + eARIAReadonlyOrEditable + }, + { // separator + &nsGkAtoms::separator_, + roles::SEPARATOR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // slider + &nsGkAtoms::slider, + roles::SLIDER, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation, + eARIAReadonly + }, + { // spinbutton + &nsGkAtoms::spinbutton, + roles::SPINBUTTON, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAReadonly + }, + { // status + &nsGkAtoms::status, + roles::STATUSBAR, + kUseMapRole, + eNoValue, + eNoAction, + ePoliteLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // switch + &nsGkAtoms::_switch, + roles::SWITCH, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool + }, + { // tab + &nsGkAtoms::tab, + roles::PAGETAB, + kUseMapRole, + eNoValue, + eSwitchAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable + }, + { // table + &nsGkAtoms::table, + roles::TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTable, + kNoReqStates, + eARIASelectable + }, + { // tablist + &nsGkAtoms::tablist, + roles::PAGETABLIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect, + states::HORIZONTAL, + eARIAOrientation + }, + { // tabpanel + &nsGkAtoms::tabpanel, + roles::PROPERTYPAGE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // textbox + &nsGkAtoms::textbox, + roles::ENTRY, + kUseMapRole, + eNoValue, + eActivateAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIAAutoComplete, + eARIAMultiline, + eARIAReadonlyOrEditable + }, + { // timer + &nsGkAtoms::timer, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eOffLiveAttr, + kNoReqStates + }, + { // toolbar + &nsGkAtoms::toolbar, + roles::TOOLBAR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::HORIZONTAL, + eARIAOrientation + }, + { // tooltip + &nsGkAtoms::tooltip, + roles::TOOLTIP, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // tree + &nsGkAtoms::tree, + roles::OUTLINE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect, + states::VERTICAL, + eARIAReadonly, + eARIAMultiSelectable, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // treegrid + &nsGkAtoms::treegrid, + roles::TREE_TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect | eTable, + states::VERTICAL, + eARIAReadonlyOrEditable, + eARIAMultiSelectable, + eFocusableUntilDisabled, + eARIAOrientation + }, + { // treeitem + &nsGkAtoms::treeitem, + roles::OUTLINEITEM, + kUseMapRole, + eNoValue, + eActivateAction, // XXX: should expose second 'expand/collapse' action based + // on states + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIASelectable, + eARIACheckedMixed + } +}; + +static const nsRoleMapEntry sLandmarkRoleMap = { + &nsGkAtoms::_empty, + roles::NOTHING, + kUseNativeRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates +}; + +nsRoleMapEntry aria::gEmptyRoleMap = { + &nsGkAtoms::_empty, + roles::NOTHING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates +}; + +/** + * Universal (Global) states: + * The following state rules are applied to any accessible element, + * whether there is an ARIA role or not: + */ +static const EStateRule sWAIUnivStateMap[] = { + eARIABusy, + eARIADisabled, + eARIAExpanded, // Currently under spec review but precedent exists + eARIAHasPopup, // Note this is technically a "property" + eARIAInvalid, + eARIAModal, + eARIARequired, // XXX not global, Bug 553117 + eARIANone +}; + + +/** + * ARIA attribute map for attribute characteristics. + * @note ARIA attributes that don't have any flags are not included here. + */ + +struct AttrCharacteristics +{ + nsIAtom** attributeName; + const uint8_t characteristics; +}; + +static const AttrCharacteristics gWAIUnivAttrMap[] = { + {&nsGkAtoms::aria_activedescendant, ATTR_BYPASSOBJ }, + {&nsGkAtoms::aria_atomic, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_busy, ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_checked, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, /* exposes checkable obj attr */ + {&nsGkAtoms::aria_controls, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_describedby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_details, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_disabled, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_dropeffect, ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_errormessage, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_expanded, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_flowto, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_grabbed, ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_haspopup, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_hidden, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, /* handled special way */ + {&nsGkAtoms::aria_invalid, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_label, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_labelledby, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_level, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {&nsGkAtoms::aria_live, ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_modal, ATTR_BYPASSOBJ | ATTR_VALTOKEN | ATTR_GLOBAL }, + {&nsGkAtoms::aria_multiline, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_multiselectable, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_owns, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_orientation, ATTR_VALTOKEN }, + {&nsGkAtoms::aria_posinset, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {&nsGkAtoms::aria_pressed, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_readonly, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_relevant, ATTR_BYPASSOBJ | ATTR_GLOBAL }, + {&nsGkAtoms::aria_required, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_selected, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {&nsGkAtoms::aria_setsize, ATTR_BYPASSOBJ }, /* handled via groupPosition */ + {&nsGkAtoms::aria_sort, ATTR_VALTOKEN }, + {&nsGkAtoms::aria_valuenow, ATTR_BYPASSOBJ }, + {&nsGkAtoms::aria_valuemin, ATTR_BYPASSOBJ }, + {&nsGkAtoms::aria_valuemax, ATTR_BYPASSOBJ }, + {&nsGkAtoms::aria_valuetext, ATTR_BYPASSOBJ } +}; + +namespace { + +struct RoleComparator +{ + const nsDependentSubstring& mRole; + explicit RoleComparator(const nsDependentSubstring& aRole) : mRole(aRole) {} + int operator()(const nsRoleMapEntry& aEntry) const { + return Compare(mRole, aEntry.ARIARoleString()); + } +}; + +} + +const nsRoleMapEntry* +aria::GetRoleMap(dom::Element* aEl) +{ + return GetRoleMapFromIndex(GetRoleMapIndex(aEl)); +} + +uint8_t +aria::GetRoleMapIndex(dom::Element* aEl) +{ + nsAutoString roles; + if (!aEl || !aEl->GetAttr(kNameSpaceID_None, nsGkAtoms::role, roles) || + roles.IsEmpty()) { + // We treat role="" as if the role attribute is absent (per aria spec:8.1.1) + return NO_ROLE_MAP_ENTRY_INDEX; + } + + nsWhitespaceTokenizer tokenizer(roles); + while (tokenizer.hasMoreTokens()) { + // Do a binary search through table for the next role in role list + const nsDependentSubstring role = tokenizer.nextToken(); + size_t idx; + if (BinarySearchIf(sWAIRoleMaps, 0, ArrayLength(sWAIRoleMaps), + RoleComparator(role), &idx)) { + return idx; + } + } + + // Always use some entry index if there is a non-empty role string + // To ensure an accessible object is created + return LANDMARK_ROLE_MAP_ENTRY_INDEX; +} + + +const nsRoleMapEntry* +aria::GetRoleMapFromIndex(uint8_t aRoleMapIndex) +{ + switch (aRoleMapIndex) { + case NO_ROLE_MAP_ENTRY_INDEX: + return nullptr; + case EMPTY_ROLE_MAP_ENTRY_INDEX: + return &gEmptyRoleMap; + case LANDMARK_ROLE_MAP_ENTRY_INDEX: + return &sLandmarkRoleMap; + default: + return sWAIRoleMaps + aRoleMapIndex; + } +} + +uint8_t +aria::GetIndexFromRoleMap(const nsRoleMapEntry* aRoleMapEntry) +{ + if (aRoleMapEntry == nullptr) { + return NO_ROLE_MAP_ENTRY_INDEX; + } else if (aRoleMapEntry == &gEmptyRoleMap) { + return EMPTY_ROLE_MAP_ENTRY_INDEX; + } else if (aRoleMapEntry == &sLandmarkRoleMap) { + return LANDMARK_ROLE_MAP_ENTRY_INDEX; + } else { + return aRoleMapEntry - sWAIRoleMaps; + } +} + +uint64_t +aria::UniversalStatesFor(mozilla::dom::Element* aElement) +{ + uint64_t state = 0; + uint32_t index = 0; + while (MapToState(sWAIUnivStateMap[index], aElement, &state)) + index++; + + return state; +} + +uint8_t +aria::AttrCharacteristicsFor(nsIAtom* aAtom) +{ + for (uint32_t i = 0; i < ArrayLength(gWAIUnivAttrMap); i++) + if (*gWAIUnivAttrMap[i].attributeName == aAtom) + return gWAIUnivAttrMap[i].characteristics; + + return 0; +} + +bool +aria::HasDefinedARIAHidden(nsIContent* aContent) +{ + return aContent && + nsAccUtils::HasDefinedARIAToken(aContent, nsGkAtoms::aria_hidden) && + !aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_hidden, + nsGkAtoms::_false, eCaseMatters); +} + +//////////////////////////////////////////////////////////////////////////////// +// AttrIterator class + +bool +AttrIterator::Next(nsAString& aAttrName, nsAString& aAttrValue) +{ + while (mAttrIdx < mAttrCount) { + const nsAttrName* attr = mContent->GetAttrNameAt(mAttrIdx); + mAttrIdx++; + if (attr->NamespaceEquals(kNameSpaceID_None)) { + nsIAtom* attrAtom = attr->Atom(); + nsDependentAtomString attrStr(attrAtom); + if (!StringBeginsWith(attrStr, NS_LITERAL_STRING("aria-"))) + continue; // Not ARIA + + uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom); + if (attrFlags & ATTR_BYPASSOBJ) + continue; // No need to handle exposing as obj attribute here + + if ((attrFlags & ATTR_VALTOKEN) && + !nsAccUtils::HasDefinedARIAToken(mContent, attrAtom)) + continue; // only expose token based attributes if they are defined + + if ((attrFlags & ATTR_BYPASSOBJ_IF_FALSE) && + mContent->AttrValueIs(kNameSpaceID_None, attrAtom, + nsGkAtoms::_false, eCaseMatters)) { + continue; // only expose token based attribute if value is not 'false'. + } + + nsAutoString value; + if (mContent->GetAttr(kNameSpaceID_None, attrAtom, value)) { + aAttrName.Assign(Substring(attrStr, 5)); + aAttrValue.Assign(value); + return true; + } + } + } + + return false; +} + diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h new file mode 100644 index 000000000..4d603c04b --- /dev/null +++ b/accessible/base/ARIAMap.h @@ -0,0 +1,305 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 mozilla_a11y_aria_ARIAMap_h_ +#define mozilla_a11y_aria_ARIAMap_h_ + +#include "ARIAStateMap.h" +#include "mozilla/a11y/AccTypes.h" +#include "mozilla/a11y/Role.h" + +#include "nsIAtom.h" +#include "nsIContent.h" + +class nsINode; + +//////////////////////////////////////////////////////////////////////////////// +// Value constants + +/** + * Used to define if role requires to expose Value interface. + */ +enum EValueRule +{ + /** + * Value interface isn't exposed. + */ + eNoValue, + + /** + * Value interface is implemented, supports value, min and max from + * aria-valuenow, aria-valuemin and aria-valuemax. + */ + eHasValueMinMax +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Action constants + +/** + * Used to define if the role requires to expose action. + */ +enum EActionRule +{ + eNoAction, + eActivateAction, + eClickAction, + ePressAction, + eCheckUncheckAction, + eExpandAction, + eJumpAction, + eOpenCloseAction, + eSelectAction, + eSortAction, + eSwitchAction +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Live region constants + +/** + * Used to define if role exposes default value of aria-live attribute. + */ +enum ELiveAttrRule +{ + eNoLiveAttr, + eOffLiveAttr, + ePoliteLiveAttr +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Role constants + +/** + * ARIA role overrides role from native markup. + */ +const bool kUseMapRole = true; + +/** + * ARIA role doesn't override the role from native markup. + */ +const bool kUseNativeRole = false; + + +//////////////////////////////////////////////////////////////////////////////// +// ARIA attribute characteristic masks + +/** + * This mask indicates the attribute should not be exposed as an object + * attribute via the catch-all logic in Accessible::Attributes(). + * This means it either isn't mean't to be exposed as an object attribute, or + * that it should, but is already handled in other code. + */ +const uint8_t ATTR_BYPASSOBJ = 0x1 << 0; +const uint8_t ATTR_BYPASSOBJ_IF_FALSE = 0x1 << 1; + +/** + * This mask indicates the attribute is expected to have an NMTOKEN or bool value. + * (See for example usage in Accessible::Attributes()) + */ +const uint8_t ATTR_VALTOKEN = 0x1 << 2; + +/** + * Indicate the attribute is global state or property (refer to + * http://www.w3.org/TR/wai-aria/states_and_properties#global_states). + */ +const uint8_t ATTR_GLOBAL = 0x1 << 3; + +//////////////////////////////////////////////////////////////////////////////// +// State map entry + +/** + * Used in nsRoleMapEntry.state if no nsIAccessibleStates are automatic for + * a given role. + */ +#define kNoReqStates 0 + +//////////////////////////////////////////////////////////////////////////////// +// Role map entry + +/** + * For each ARIA role, this maps the nsIAccessible information. + */ +struct nsRoleMapEntry +{ + /** + * Return true if matches to the given ARIA role. + */ + bool Is(nsIAtom* aARIARole) const + { return *roleAtom == aARIARole; } + + /** + * Return true if ARIA role has the given accessible type. + */ + bool IsOfType(mozilla::a11y::AccGenericType aType) const + { return accTypes & aType; } + + /** + * Return ARIA role. + */ + const nsDependentAtomString ARIARoleString() const + { return nsDependentAtomString(*roleAtom); } + + // ARIA role: string representation such as "button" + nsIAtom** roleAtom; + + // Role mapping rule: maps to enum Role + mozilla::a11y::role role; + + // Role rule: whether to use mapped role or native semantics + bool roleRule; + + // Value mapping rule: how to compute accessible value + EValueRule valueRule; + + // Action mapping rule, how to expose accessible action + EActionRule actionRule; + + // 'live' and 'container-live' object attributes mapping rule: how to expose + // these object attributes if ARIA 'live' attribute is missed. + ELiveAttrRule liveAttRule; + + // Accessible types this role belongs to. + uint32_t accTypes; + + // Automatic state mapping rule: always include in states + uint64_t state; // or kNoReqStates if no default state for this role + + // ARIA properties supported for this role (in other words, the aria-foo + // attribute to accessible states mapping rules). + // Currently you cannot have unlimited mappings, because + // a variable sized array would not allow the use of + // C++'s struct initialization feature. + mozilla::a11y::aria::EStateRule attributeMap1; + mozilla::a11y::aria::EStateRule attributeMap2; + mozilla::a11y::aria::EStateRule attributeMap3; + mozilla::a11y::aria::EStateRule attributeMap4; +}; + + +//////////////////////////////////////////////////////////////////////////////// +// ARIA map + +/** + * These provide the mappings for WAI-ARIA roles, states and properties using + * the structs defined in this file and ARIAStateMap files. + */ +namespace mozilla { +namespace a11y { +namespace aria { + +/** + * Empty role map entry. Used by accessibility service to create an accessible + * if the accessible can't use role of used accessible class. For example, + * it is used for table cells that aren't contained by table. + */ +extern nsRoleMapEntry gEmptyRoleMap; + +/** + * Constants for the role map entry index to indicate that the role map entry + * isn't in sWAIRoleMaps, but rather is a special entry: nullptr, + * gEmptyRoleMap, and sLandmarkRoleMap + */ +const uint8_t NO_ROLE_MAP_ENTRY_INDEX = UINT8_MAX - 2; +const uint8_t EMPTY_ROLE_MAP_ENTRY_INDEX = UINT8_MAX - 1; +const uint8_t LANDMARK_ROLE_MAP_ENTRY_INDEX = UINT8_MAX; + +/** + * Get the role map entry for a given DOM node. This will use the first + * ARIA role if the role attribute provides a space delimited list of roles. + * + * @param aEl [in] the DOM node to get the role map entry for + * @return a pointer to the role map entry for the ARIA role, or nullptr + * if none + */ +const nsRoleMapEntry* GetRoleMap(dom::Element* aEl); + +/** + * Get the role map entry pointer's index for a given DOM node. This will use + * the first ARIA role if the role attribute provides a space delimited list of + * roles. + * + * @param aEl [in] the DOM node to get the role map entry for + * @return the index of the pointer to the role map entry for the ARIA + * role, or NO_ROLE_MAP_ENTRY_INDEX if none + */ +uint8_t GetRoleMapIndex(dom::Element* aEl); + +/** + * Get the role map entry pointer for a given role map entry index. + * + * @param aRoleMapIndex [in] the role map index to get the role map entry + * pointer for + * @return a pointer to the role map entry for the ARIA role, + * or nullptr, if none + */ +const nsRoleMapEntry* GetRoleMapFromIndex(uint8_t aRoleMapIndex); + +/** + * Get the role map entry index for a given role map entry pointer. If the role + * map entry is within sWAIRoleMaps, return the index within that array, + * otherwise return one of the special index constants listed above. + * + * @param aRoleMap [in] the role map entry pointer to get the index for + * @return the index of the pointer to the role map entry, or + * NO_ROLE_MAP_ENTRY_INDEX if none + */ +uint8_t GetIndexFromRoleMap(const nsRoleMapEntry* aRoleMap); + +/** + * Return accessible state from ARIA universal states applied to the given + * element. + */ +uint64_t UniversalStatesFor(mozilla::dom::Element* aElement); + +/** + * Get the ARIA attribute characteristics for a given ARIA attribute. + * + * @param aAtom ARIA attribute + * @return A bitflag representing the attribute characteristics + * (see above for possible bit masks, prefixed "ATTR_") + */ +uint8_t AttrCharacteristicsFor(nsIAtom* aAtom); + +/** + * Return true if the element has defined aria-hidden. + */ +bool HasDefinedARIAHidden(nsIContent* aContent); + + /** + * Represents a simple enumerator for iterating through ARIA attributes + * exposed as object attributes on a given accessible. + */ +class AttrIterator +{ +public: + explicit AttrIterator(nsIContent* aContent) : + mContent(aContent), mAttrIdx(0) + { + mAttrCount = mContent->GetAttrCount(); + } + + bool Next(nsAString& aAttrName, nsAString& aAttrValue); + +private: + AttrIterator() = delete; + AttrIterator(const AttrIterator&) = delete; + AttrIterator& operator= (const AttrIterator&) = delete; + + nsIContent* mContent; + uint32_t mAttrIdx; + uint32_t mAttrCount; +}; + +} // namespace aria +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/ARIAStateMap.cpp b/accessible/base/ARIAStateMap.cpp new file mode 100644 index 000000000..f121835d1 --- /dev/null +++ b/accessible/base/ARIAStateMap.cpp @@ -0,0 +1,376 @@ +/* -*- 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 "ARIAMap.h" +#include "nsAccUtils.h" +#include "States.h" + +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::a11y::aria; + +/** + * Used to store state map rule data for ARIA attribute of enum type. + */ +struct EnumTypeData +{ + // ARIA attribute name. + nsIAtom* const mAttrName; + + // States if the attribute value is matched to the enum value. Used as + // nsIContent::AttrValuesArray, last item must be nullptr. + nsIAtom* const* const mValues[4]; + + // States applied if corresponding enum values are matched. + const uint64_t mStates[3]; + + // States to clear in case of match. + const uint64_t mClearState; +}; + +enum ETokenType +{ + eBoolType = 0, + eMixedType = 1, // can take 'mixed' value + eDefinedIfAbsent = 2 // permanent and false state are applied if absent +}; + +/** + * Used to store state map rule data for ARIA attribute of token type (including + * mixed value). + */ +struct TokenTypeData +{ + TokenTypeData(nsIAtom* aAttrName, uint32_t aType, + uint64_t aPermanentState, + uint64_t aTrueState, + uint64_t aFalseState = 0) : + mAttrName(aAttrName), mType(aType), mPermanentState(aPermanentState), + mTrueState(aTrueState), mFalseState(aFalseState) + { } + + // ARIA attribute name. + nsIAtom* const mAttrName; + + // Type. + const uint32_t mType; + + // State applied if the attribute is defined or mType doesn't have + // eDefinedIfAbsent flag set. + const uint64_t mPermanentState; + + // States applied if the attribute value is true/false. + const uint64_t mTrueState; + const uint64_t mFalseState; +}; + +/** + * Map enum type attribute value to accessible state. + */ +static void MapEnumType(dom::Element* aElement, uint64_t* aState, + const EnumTypeData& aData); + +/** + * Map token type attribute value to states. + */ +static void MapTokenType(dom::Element* aContent, uint64_t* aState, + const TokenTypeData& aData); + +bool +aria::MapToState(EStateRule aRule, dom::Element* aElement, uint64_t* aState) +{ + switch (aRule) { + case eARIAAutoComplete: + { + static const EnumTypeData data = { + nsGkAtoms::aria_autocomplete, + { &nsGkAtoms::inlinevalue, + &nsGkAtoms::list, + &nsGkAtoms::both, nullptr }, + { states::SUPPORTS_AUTOCOMPLETION, + states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION, + states::HASPOPUP | states::SUPPORTS_AUTOCOMPLETION }, 0 + }; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIABusy: + { + static const EnumTypeData data = { + nsGkAtoms::aria_busy, + { &nsGkAtoms::_true, + &nsGkAtoms::error, nullptr }, + { states::BUSY, + states::INVALID }, 0 + }; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIACheckableBool: + { + static const TokenTypeData data( + nsGkAtoms::aria_checked, eBoolType | eDefinedIfAbsent, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIACheckableMixed: + { + static const TokenTypeData data( + nsGkAtoms::aria_checked, eMixedType | eDefinedIfAbsent, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIACheckedMixed: + { + static const TokenTypeData data( + nsGkAtoms::aria_checked, eMixedType, + states::CHECKABLE, states::CHECKED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIADisabled: + { + static const TokenTypeData data( + nsGkAtoms::aria_disabled, eBoolType, + 0, states::UNAVAILABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAExpanded: + { + static const TokenTypeData data( + nsGkAtoms::aria_expanded, eBoolType, + 0, states::EXPANDED, states::COLLAPSED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAHasPopup: + { + static const TokenTypeData data( + nsGkAtoms::aria_haspopup, eBoolType, + 0, states::HASPOPUP); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAInvalid: + { + static const TokenTypeData data( + nsGkAtoms::aria_invalid, eBoolType, + 0, states::INVALID); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAModal: + { + static const TokenTypeData data( + nsGkAtoms::aria_modal, eBoolType, + 0, states::MODAL); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAMultiline: + { + static const TokenTypeData data( + nsGkAtoms::aria_multiline, eBoolType | eDefinedIfAbsent, + 0, states::MULTI_LINE, states::SINGLE_LINE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAMultiSelectable: + { + static const TokenTypeData data( + nsGkAtoms::aria_multiselectable, eBoolType, + 0, states::MULTISELECTABLE | states::EXTSELECTABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAOrientation: + { + static const EnumTypeData data = { + nsGkAtoms::aria_orientation, + { &nsGkAtoms::horizontal, + &nsGkAtoms::vertical, nullptr }, + { states::HORIZONTAL, + states::VERTICAL }, + states::HORIZONTAL | states::VERTICAL + }; + + MapEnumType(aElement, aState, data); + return true; + } + + case eARIAPressed: + { + static const TokenTypeData data( + nsGkAtoms::aria_pressed, eMixedType, + 0, states::PRESSED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAReadonly: + { + static const TokenTypeData data( + nsGkAtoms::aria_readonly, eBoolType, + 0, states::READONLY); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAReadonlyOrEditable: + { + static const TokenTypeData data( + nsGkAtoms::aria_readonly, eBoolType | eDefinedIfAbsent, + 0, states::READONLY, states::EDITABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIAReadonlyOrEditableIfDefined: + { + static const TokenTypeData data( + nsGkAtoms::aria_readonly, eBoolType, + 0, states::READONLY, states::EDITABLE); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIARequired: + { + static const TokenTypeData data( + nsGkAtoms::aria_required, eBoolType, + 0, states::REQUIRED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIASelectable: + { + static const TokenTypeData data( + nsGkAtoms::aria_selected, eBoolType | eDefinedIfAbsent, + states::SELECTABLE, states::SELECTED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eARIASelectableIfDefined: + { + static const TokenTypeData data( + nsGkAtoms::aria_selected, eBoolType, + states::SELECTABLE, states::SELECTED); + + MapTokenType(aElement, aState, data); + return true; + } + + case eReadonlyUntilEditable: + { + if (!(*aState & states::EDITABLE)) + *aState |= states::READONLY; + + return true; + } + + case eIndeterminateIfNoValue: + { + if (!aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow) && + !aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext)) + *aState |= states::MIXED; + + return true; + } + + case eFocusableUntilDisabled: + { + if (!nsAccUtils::HasDefinedARIAToken(aElement, nsGkAtoms::aria_disabled) || + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_disabled, + nsGkAtoms::_false, eCaseMatters)) + *aState |= states::FOCUSABLE; + + return true; + } + + default: + return false; + } +} + +static void +MapEnumType(dom::Element* aElement, uint64_t* aState, const EnumTypeData& aData) +{ + switch (aElement->FindAttrValueIn(kNameSpaceID_None, aData.mAttrName, + aData.mValues, eCaseMatters)) { + case 0: + *aState = (*aState & ~aData.mClearState) | aData.mStates[0]; + return; + case 1: + *aState = (*aState & ~aData.mClearState) | aData.mStates[1]; + return; + case 2: + *aState = (*aState & ~aData.mClearState) | aData.mStates[2]; + return; + } +} + +static void +MapTokenType(dom::Element* aElement, uint64_t* aState, + const TokenTypeData& aData) +{ + if (nsAccUtils::HasDefinedARIAToken(aElement, aData.mAttrName)) { + if ((aData.mType & eMixedType) && + aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, + nsGkAtoms::mixed, eCaseMatters)) { + *aState |= aData.mPermanentState | states::MIXED; + return; + } + + if (aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, + nsGkAtoms::_false, eCaseMatters)) { + *aState |= aData.mPermanentState | aData.mFalseState; + return; + } + + *aState |= aData.mPermanentState | aData.mTrueState; + return; + } + + if (aData.mType & eDefinedIfAbsent) + *aState |= aData.mPermanentState | aData.mFalseState; +} diff --git a/accessible/base/ARIAStateMap.h b/accessible/base/ARIAStateMap.h new file mode 100644 index 000000000..41626fcaf --- /dev/null +++ b/accessible/base/ARIAStateMap.h @@ -0,0 +1,67 @@ +/* -*- 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_aria_ARIAStateMap_h_ +#define _mozilla_a11y_aria_ARIAStateMap_h_ + +#include <stdint.h> + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { +namespace aria { + +/** + * List of the ARIA state mapping rules. + */ +enum EStateRule +{ + eARIANone, + eARIAAutoComplete, + eARIABusy, + eARIACheckableBool, + eARIACheckableMixed, + eARIACheckedMixed, + eARIADisabled, + eARIAExpanded, + eARIAHasPopup, + eARIAInvalid, + eARIAModal, + eARIAMultiline, + eARIAMultiSelectable, + eARIAOrientation, + eARIAPressed, + eARIAReadonly, + eARIAReadonlyOrEditable, + eARIAReadonlyOrEditableIfDefined, + eARIARequired, + eARIASelectable, + eARIASelectableIfDefined, + eReadonlyUntilEditable, + eIndeterminateIfNoValue, + eFocusableUntilDisabled +}; + +/** + * Expose the accessible states for the given element accordingly to state + * mapping rule. + * + * @param aRule [in] state mapping rule ID + * @param aElement [in] node of the accessible + * @param aState [in/out] accessible states + * @return true if state map rule ID is valid + */ +bool MapToState(EStateRule aRule, dom::Element* aElement, uint64_t* aState); + +} // namespace aria +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccEvent.cpp b/accessible/base/AccEvent.cpp new file mode 100644 index 000000000..a27ceacb9 --- /dev/null +++ b/accessible/base/AccEvent.cpp @@ -0,0 +1,271 @@ +/* -*- 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 "AccEvent.h" + +#include "nsAccUtils.h" +#include "DocAccessible.h" +#include "xpcAccEvents.h" +#include "States.h" +#include "xpcAccessibleDocument.h" + +#include "mozilla/EventStateManager.h" +#include "mozilla/dom/Selection.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +static_assert(static_cast<bool>(eNoUserInput) == false && + static_cast<bool>(eFromUserInput) == true, + "EIsFromUserInput cannot be casted to bool"); + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent constructors + +AccEvent::AccEvent(uint32_t aEventType, Accessible* aAccessible, + EIsFromUserInput aIsFromUserInput, EEventRule aEventRule) : + mEventType(aEventType), mEventRule(aEventRule), mAccessible(aAccessible) +{ + if (aIsFromUserInput == eAutoDetect) + mIsFromUserInput = EventStateManager::IsHandlingUserInput(); + else + mIsFromUserInput = aIsFromUserInput == eFromUserInput ? true : false; +} + +//////////////////////////////////////////////////////////////////////////////// +// AccEvent cycle collection + +NS_IMPL_CYCLE_COLLECTION_CLASS(AccEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AccEvent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessible) + if (AccTreeMutationEvent* tmEvent = downcast_accEvent(tmp)) { + tmEvent->SetNextEvent(nullptr); + tmEvent->SetPrevEvent(nullptr); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AccEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessible) + if (AccTreeMutationEvent* tmEvent = downcast_accEvent(tmp)) { + CycleCollectionNoteChild(cb, tmEvent->NextEvent(), "mNext"); + CycleCollectionNoteChild(cb, tmEvent->PrevEvent(), "mPrevEvent"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AccEvent, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AccEvent, Release) + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// AccTextChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +// Note: we pass in eAllowDupes to the base class because we don't support text +// events coalescence. We fire delayed text change events in DocAccessible but +// we continue to base the event off the accessible object rather than just the +// node. This means we won't try to create an accessible based on the node when +// we are ready to fire the event and so we will no longer assert at that point +// if the node was removed from the document. Either way, the AT won't work with +// a defunct accessible so the behaviour should be equivalent. +AccTextChangeEvent:: + AccTextChangeEvent(Accessible* aAccessible, int32_t aStart, + const nsAString& aModifiedText, bool aIsInserted, + EIsFromUserInput aIsFromUserInput) + : AccEvent(aIsInserted ? + static_cast<uint32_t>(nsIAccessibleEvent::EVENT_TEXT_INSERTED) : + static_cast<uint32_t>(nsIAccessibleEvent::EVENT_TEXT_REMOVED), + aAccessible, aIsFromUserInput, eAllowDupes) + , mStart(aStart) + , mIsInserted(aIsInserted) + , mModifiedText(aModifiedText) +{ + // XXX We should use IsFromUserInput here, but that isn't always correct + // when the text change isn't related to content insertion or removal. + mIsFromUserInput = mAccessible->State() & + (states::FOCUSED | states::EDITABLE); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccHideEvent +//////////////////////////////////////////////////////////////////////////////// + +AccHideEvent:: + AccHideEvent(Accessible* aTarget, bool aNeedsShutdown) : + AccMutationEvent(::nsIAccessibleEvent::EVENT_HIDE, aTarget), + mNeedsShutdown(aNeedsShutdown) +{ + mNextSibling = mAccessible->NextSibling(); + mPrevSibling = mAccessible->PrevSibling(); +} + + +//////////////////////////////////////////////////////////////////////////////// +// AccShowEvent +//////////////////////////////////////////////////////////////////////////////// + +AccShowEvent:: + AccShowEvent(Accessible* aTarget) : + AccMutationEvent(::nsIAccessibleEvent::EVENT_SHOW, aTarget) +{ + int32_t idx = aTarget->IndexInParent(); + MOZ_ASSERT(idx >= 0); + mInsertionIndex = idx; +} + + +//////////////////////////////////////////////////////////////////////////////// +// AccTextSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccTextSelChangeEvent::AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, + int32_t aReason) : + AccEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, aTarget, + eAutoDetect, eCoalesceTextSelChange), + mSel(aSelection), mReason(aReason) {} + +AccTextSelChangeEvent::~AccTextSelChangeEvent() { } + +bool +AccTextSelChangeEvent::IsCaretMoveOnly() const +{ + return mSel->RangeCount() == 1 && mSel->IsCollapsed() && + ((mReason & (nsISelectionListener::COLLAPSETOSTART_REASON | + nsISelectionListener::COLLAPSETOEND_REASON)) == 0); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccSelChangeEvent:: + AccSelChangeEvent(Accessible* aWidget, Accessible* aItem, + SelChangeType aSelChangeType) : + AccEvent(0, aItem, eAutoDetect, eCoalesceSelectionChange), + mWidget(aWidget), mItem(aItem), mSelChangeType(aSelChangeType), + mPreceedingCount(0), mPackedEvent(nullptr) +{ + if (aSelChangeType == eSelectionAdd) { + if (mWidget->GetSelectedItem(1)) + mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; + else + mEventType = nsIAccessibleEvent::EVENT_SELECTION; + } else { + mEventType = nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// AccTableChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccTableChangeEvent:: + AccTableChangeEvent(Accessible* aAccessible, uint32_t aEventType, + int32_t aRowOrColIndex, int32_t aNumRowsOrCols) : + AccEvent(aEventType, aAccessible), + mRowOrColIndex(aRowOrColIndex), mNumRowsOrCols(aNumRowsOrCols) +{ +} + + +//////////////////////////////////////////////////////////////////////////////// +// AccVCChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccVCChangeEvent:: + AccVCChangeEvent(Accessible* aAccessible, + Accessible* aOldAccessible, + int32_t aOldStart, int32_t aOldEnd, + int16_t aReason, EIsFromUserInput aIsFromUserInput) : + AccEvent(::nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED, aAccessible, + aIsFromUserInput), + mOldAccessible(aOldAccessible), mOldStart(aOldStart), mOldEnd(aOldEnd), + mReason(aReason) +{ +} + +already_AddRefed<nsIAccessibleEvent> +a11y::MakeXPCEvent(AccEvent* aEvent) +{ + DocAccessible* doc = aEvent->Document(); + Accessible* acc = aEvent->GetAccessible(); + nsINode* node = acc->GetNode(); + nsIDOMNode* domNode = node ? node->AsDOMNode() : nullptr; + bool fromUser = aEvent->IsFromUserInput(); + uint32_t type = aEvent->GetEventType(); + uint32_t eventGroup = aEvent->GetEventGroups(); + nsCOMPtr<nsIAccessibleEvent> xpEvent; + + if (eventGroup & (1 << AccEvent::eStateChangeEvent)) { + AccStateChangeEvent* sc = downcast_accEvent(aEvent); + bool extra = false; + uint32_t state = nsAccUtils::To32States(sc->GetState(), &extra); + xpEvent = new xpcAccStateChangeEvent(type, ToXPC(acc), ToXPCDocument(doc), + domNode, fromUser, + state, extra, sc->IsStateEnabled()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eTextChangeEvent)) { + AccTextChangeEvent* tc = downcast_accEvent(aEvent); + nsString text; + tc->GetModifiedText(text); + xpEvent = new xpcAccTextChangeEvent(type, ToXPC(acc), ToXPCDocument(doc), + domNode, fromUser, + tc->GetStartOffset(), tc->GetLength(), + tc->IsTextInserted(), text); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eHideEvent)) { + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + xpEvent = new xpcAccHideEvent(type, ToXPC(acc), ToXPCDocument(doc), + domNode, fromUser, + ToXPC(hideEvent->TargetParent()), + ToXPC(hideEvent->TargetNextSibling()), + ToXPC(hideEvent->TargetPrevSibling())); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eCaretMoveEvent)) { + AccCaretMoveEvent* cm = downcast_accEvent(aEvent); + xpEvent = new xpcAccCaretMoveEvent(type, ToXPC(acc), ToXPCDocument(doc), + domNode, fromUser, + cm->GetCaretOffset()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eVirtualCursorChangeEvent)) { + AccVCChangeEvent* vcc = downcast_accEvent(aEvent); + xpEvent = new xpcAccVirtualCursorChangeEvent(type, + ToXPC(acc), ToXPCDocument(doc), + domNode, fromUser, + ToXPC(vcc->OldAccessible()), + vcc->OldStartOffset(), + vcc->OldEndOffset(), + vcc->Reason()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eObjectAttrChangedEvent)) { + AccObjectAttrChangedEvent* oac = downcast_accEvent(aEvent); + xpEvent = new xpcAccObjectAttributeChangedEvent(type, + ToXPC(acc), + ToXPCDocument(doc), domNode, + fromUser, + oac->GetAttribute()); + return xpEvent.forget(); + } + + xpEvent = new xpcAccEvent(type, ToXPC(acc), ToXPCDocument(doc), domNode, fromUser); + return xpEvent.forget(); + } diff --git a/accessible/base/AccEvent.h b/accessible/base/AccEvent.h new file mode 100644 index 000000000..416224278 --- /dev/null +++ b/accessible/base/AccEvent.h @@ -0,0 +1,570 @@ +/* -*- 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 _AccEvent_H_ +#define _AccEvent_H_ + +#include "nsIAccessibleEvent.h" + +#include "mozilla/a11y/Accessible.h" + +class nsEventShell; +namespace mozilla { + +namespace dom { +class Selection; +} + +namespace a11y { + +class DocAccessible; + +// Constants used to point whether the event is from user input. +enum EIsFromUserInput +{ + // eNoUserInput: event is not from user input + eNoUserInput = 0, + // eFromUserInput: event is from user input + eFromUserInput = 1, + // eAutoDetect: the value should be obtained from event state manager + eAutoDetect = -1 +}; + +/** + * Generic accessible event. + */ +class AccEvent +{ +public: + + // Rule for accessible events. + // The rule will be applied when flushing pending events. + enum EEventRule { + // eAllowDupes : More than one event of the same type is allowed. + // This event will always be emitted. This flag is used for events that + // don't support coalescence. + eAllowDupes, + + // eCoalesceReorder : For reorder events from the same subtree or the same + // node, only the umbrella event on the ancestor will be emitted. + eCoalesceReorder, + + // eCoalesceOfSameType : For events of the same type, only the newest event + // will be processed. + eCoalesceOfSameType, + + // eCoalesceSelectionChange: coalescence of selection change events. + eCoalesceSelectionChange, + + // eCoalesceStateChange: coalesce state change events. + eCoalesceStateChange, + + // eCoalesceTextSelChange: coalescence of text selection change events. + eCoalesceTextSelChange, + + // eRemoveDupes : For repeat events, only the newest event in queue + // will be emitted. + eRemoveDupes, + + // eDoNotEmit : This event is confirmed as a duplicate, do not emit it. + eDoNotEmit + }; + + // Initialize with an accessible. + AccEvent(uint32_t aEventType, Accessible* aAccessible, + EIsFromUserInput aIsFromUserInput = eAutoDetect, + EEventRule aEventRule = eRemoveDupes); + + // AccEvent + uint32_t GetEventType() const { return mEventType; } + EEventRule GetEventRule() const { return mEventRule; } + bool IsFromUserInput() const { return mIsFromUserInput; } + EIsFromUserInput FromUserInput() const + { return static_cast<EIsFromUserInput>(mIsFromUserInput); } + + Accessible* GetAccessible() const { return mAccessible; } + DocAccessible* Document() const { return mAccessible->Document(); } + + /** + * Down casting. + */ + enum EventGroup { + eGenericEvent, + eStateChangeEvent, + eTextChangeEvent, + eTreeMutationEvent, + eMutationEvent, + eReorderEvent, + eHideEvent, + eShowEvent, + eCaretMoveEvent, + eTextSelChangeEvent, + eSelectionChangeEvent, + eTableChangeEvent, + eVirtualCursorChangeEvent, + eObjectAttrChangedEvent + }; + + static const EventGroup kEventGroup = eGenericEvent; + virtual unsigned int GetEventGroups() const + { + return 1U << eGenericEvent; + } + + /** + * Reference counting and cycle collection. + */ + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AccEvent) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(AccEvent) + +protected: + virtual ~AccEvent() {} + + bool mIsFromUserInput; + uint32_t mEventType; + EEventRule mEventRule; + RefPtr<Accessible> mAccessible; + + friend class EventQueue; + friend class EventTree; + friend class ::nsEventShell; + friend class NotificationController; +}; + + +/** + * Accessible state change event. + */ +class AccStateChangeEvent: public AccEvent +{ +public: + AccStateChangeEvent(Accessible* aAccessible, uint64_t aState, + bool aIsEnabled, + EIsFromUserInput aIsFromUserInput = eAutoDetect) : + AccEvent(nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible, + aIsFromUserInput, eCoalesceStateChange), + mState(aState), mIsEnabled(aIsEnabled) { } + + AccStateChangeEvent(Accessible* aAccessible, uint64_t aState) : + AccEvent(::nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible, + eAutoDetect, eCoalesceStateChange), mState(aState) + { mIsEnabled = (mAccessible->State() & mState) != 0; } + + // AccEvent + static const EventGroup kEventGroup = eStateChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eStateChangeEvent); + } + + // AccStateChangeEvent + uint64_t GetState() const { return mState; } + bool IsStateEnabled() const { return mIsEnabled; } + +private: + uint64_t mState; + bool mIsEnabled; + + friend class EventQueue; +}; + + +/** + * Accessible text change event. + */ +class AccTextChangeEvent: public AccEvent +{ +public: + AccTextChangeEvent(Accessible* aAccessible, int32_t aStart, + const nsAString& aModifiedText, bool aIsInserted, + EIsFromUserInput aIsFromUserInput = eAutoDetect); + + // AccEvent + static const EventGroup kEventGroup = eTextChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eTextChangeEvent); + } + + // AccTextChangeEvent + int32_t GetStartOffset() const { return mStart; } + uint32_t GetLength() const { return mModifiedText.Length(); } + bool IsTextInserted() const { return mIsInserted; } + void GetModifiedText(nsAString& aModifiedText) + { aModifiedText = mModifiedText; } + const nsString& ModifiedText() const { return mModifiedText; } + +private: + int32_t mStart; + bool mIsInserted; + nsString mModifiedText; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * A base class for events related to tree mutation, either an AccMutation + * event, or an AccReorderEvent. + */ +class AccTreeMutationEvent : public AccEvent +{ +public: + AccTreeMutationEvent(uint32_t aEventType, Accessible* aTarget) : + AccEvent(aEventType, aTarget, eAutoDetect, eCoalesceReorder), mGeneration(0) {} + + // Event + static const EventGroup kEventGroup = eTreeMutationEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eTreeMutationEvent); + } + + void SetNextEvent(AccTreeMutationEvent* aNext) { mNextEvent = aNext; } + void SetPrevEvent(AccTreeMutationEvent* aPrev) { mPrevEvent = aPrev; } + AccTreeMutationEvent* NextEvent() const { return mNextEvent; } + AccTreeMutationEvent* PrevEvent() const { return mPrevEvent; } + + /** + * A sequence number to know when this event was fired. + */ + uint32_t EventGeneration() const { return mGeneration; } + void SetEventGeneration(uint32_t aGeneration) { mGeneration = aGeneration; } + +private: + RefPtr<AccTreeMutationEvent> mNextEvent; + RefPtr<AccTreeMutationEvent> mPrevEvent; + uint32_t mGeneration; +}; + +/** + * Base class for show and hide accessible events. + */ +class AccMutationEvent: public AccTreeMutationEvent +{ +public: + AccMutationEvent(uint32_t aEventType, Accessible* aTarget) : + AccTreeMutationEvent(aEventType, aTarget) + { + // Don't coalesce these since they are coalesced by reorder event. Coalesce + // contained text change events. + mParent = mAccessible->Parent(); + } + virtual ~AccMutationEvent() { } + + // Event + static const EventGroup kEventGroup = eMutationEvent; + virtual unsigned int GetEventGroups() const override + { + return AccTreeMutationEvent::GetEventGroups() | (1U << eMutationEvent); + } + + // MutationEvent + bool IsShow() const { return mEventType == nsIAccessibleEvent::EVENT_SHOW; } + bool IsHide() const { return mEventType == nsIAccessibleEvent::EVENT_HIDE; } + + Accessible* Parent() const { return mParent; } + +protected: + nsCOMPtr<nsINode> mNode; + RefPtr<Accessible> mParent; + RefPtr<AccTextChangeEvent> mTextChangeEvent; + + friend class EventTree; + friend class NotificationController; +}; + + +/** + * Accessible hide event. + */ +class AccHideEvent: public AccMutationEvent +{ +public: + explicit AccHideEvent(Accessible* aTarget, bool aNeedsShutdown = true); + + // Event + static const EventGroup kEventGroup = eHideEvent; + virtual unsigned int GetEventGroups() const override + { + return AccMutationEvent::GetEventGroups() | (1U << eHideEvent); + } + + // AccHideEvent + Accessible* TargetParent() const { return mParent; } + Accessible* TargetNextSibling() const { return mNextSibling; } + Accessible* TargetPrevSibling() const { return mPrevSibling; } + bool NeedsShutdown() const { return mNeedsShutdown; } + +protected: + bool mNeedsShutdown; + RefPtr<Accessible> mNextSibling; + RefPtr<Accessible> mPrevSibling; + + friend class EventTree; + friend class NotificationController; +}; + + +/** + * Accessible show event. + */ +class AccShowEvent: public AccMutationEvent +{ +public: + explicit AccShowEvent(Accessible* aTarget); + + // Event + static const EventGroup kEventGroup = eShowEvent; + virtual unsigned int GetEventGroups() const override + { + return AccMutationEvent::GetEventGroups() | (1U << eShowEvent); + } + + uint32_t InsertionIndex() const { return mInsertionIndex; } + +private: + nsTArray<RefPtr<AccHideEvent>> mPrecedingEvents; + uint32_t mInsertionIndex; + + friend class EventTree; +}; + + +/** + * Class for reorder accessible event. Takes care about + */ +class AccReorderEvent : public AccTreeMutationEvent +{ +public: + explicit AccReorderEvent(Accessible* aTarget) : + AccTreeMutationEvent(::nsIAccessibleEvent::EVENT_REORDER, aTarget) { } + virtual ~AccReorderEvent() { } + + // Event + static const EventGroup kEventGroup = eReorderEvent; + virtual unsigned int GetEventGroups() const override + { + return AccTreeMutationEvent::GetEventGroups() | (1U << eReorderEvent); + } +}; + + +/** + * Accessible caret move event. + */ +class AccCaretMoveEvent: public AccEvent +{ +public: + AccCaretMoveEvent(Accessible* aAccessible, int32_t aCaretOffset, + EIsFromUserInput aIsFromUserInput = eAutoDetect) : + AccEvent(::nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED, aAccessible, + aIsFromUserInput), + mCaretOffset(aCaretOffset) { } + virtual ~AccCaretMoveEvent() { } + + // AccEvent + static const EventGroup kEventGroup = eCaretMoveEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eCaretMoveEvent); + } + + // AccCaretMoveEvent + int32_t GetCaretOffset() const { return mCaretOffset; } + +private: + int32_t mCaretOffset; +}; + + +/** + * Accessible text selection change event. + */ +class AccTextSelChangeEvent : public AccEvent +{ +public: + AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, + int32_t aReason); + virtual ~AccTextSelChangeEvent(); + + // AccEvent + static const EventGroup kEventGroup = eTextSelChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eTextSelChangeEvent); + } + + // AccTextSelChangeEvent + + /** + * Return true if the text selection change wasn't caused by pure caret move. + */ + bool IsCaretMoveOnly() const; + +private: + RefPtr<dom::Selection> mSel; + int32_t mReason; + + friend class EventQueue; + friend class SelectionManager; +}; + + +/** + * Accessible widget selection change event. + */ +class AccSelChangeEvent : public AccEvent +{ +public: + enum SelChangeType { + eSelectionAdd, + eSelectionRemove + }; + + AccSelChangeEvent(Accessible* aWidget, Accessible* aItem, + SelChangeType aSelChangeType); + + virtual ~AccSelChangeEvent() { } + + // AccEvent + static const EventGroup kEventGroup = eSelectionChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eSelectionChangeEvent); + } + + // AccSelChangeEvent + Accessible* Widget() const { return mWidget; } + +private: + RefPtr<Accessible> mWidget; + RefPtr<Accessible> mItem; + SelChangeType mSelChangeType; + uint32_t mPreceedingCount; + AccSelChangeEvent* mPackedEvent; + + friend class EventQueue; +}; + + +/** + * Accessible table change event. + */ +class AccTableChangeEvent : public AccEvent +{ +public: + AccTableChangeEvent(Accessible* aAccessible, uint32_t aEventType, + int32_t aRowOrColIndex, int32_t aNumRowsOrCols); + + // AccEvent + static const EventGroup kEventGroup = eTableChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eTableChangeEvent); + } + + // AccTableChangeEvent + uint32_t GetIndex() const { return mRowOrColIndex; } + uint32_t GetCount() const { return mNumRowsOrCols; } + +private: + uint32_t mRowOrColIndex; // the start row/column after which the rows are inserted/deleted. + uint32_t mNumRowsOrCols; // the number of inserted/deleted rows/columns +}; + +/** + * Accessible virtual cursor change event. + */ +class AccVCChangeEvent : public AccEvent +{ +public: + AccVCChangeEvent(Accessible* aAccessible, + Accessible* aOldAccessible, + int32_t aOldStart, int32_t aOldEnd, + int16_t aReason, + EIsFromUserInput aIsFromUserInput = eFromUserInput); + + virtual ~AccVCChangeEvent() { } + + // AccEvent + static const EventGroup kEventGroup = eVirtualCursorChangeEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eVirtualCursorChangeEvent); + } + + // AccTableChangeEvent + Accessible* OldAccessible() const { return mOldAccessible; } + int32_t OldStartOffset() const { return mOldStart; } + int32_t OldEndOffset() const { return mOldEnd; } + int32_t Reason() const { return mReason; } + +private: + RefPtr<Accessible> mOldAccessible; + int32_t mOldStart; + int32_t mOldEnd; + int16_t mReason; +}; + +/** + * Accessible object attribute changed event. + */ +class AccObjectAttrChangedEvent: public AccEvent +{ +public: + AccObjectAttrChangedEvent(Accessible* aAccessible, nsIAtom* aAttribute) : + AccEvent(::nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED, aAccessible), + mAttribute(aAttribute) { } + + // AccEvent + static const EventGroup kEventGroup = eObjectAttrChangedEvent; + virtual unsigned int GetEventGroups() const override + { + return AccEvent::GetEventGroups() | (1U << eObjectAttrChangedEvent); + } + + // AccObjectAttrChangedEvent + nsIAtom* GetAttribute() const { return mAttribute; } + +private: + nsCOMPtr<nsIAtom> mAttribute; + + virtual ~AccObjectAttrChangedEvent() { } +}; + +/** + * Downcast the generic accessible event object to derived type. + */ +class downcast_accEvent +{ +public: + explicit downcast_accEvent(AccEvent* e) : mRawPtr(e) { } + + template<class Destination> + operator Destination*() { + if (!mRawPtr) + return nullptr; + + return mRawPtr->GetEventGroups() & (1U << Destination::kEventGroup) ? + static_cast<Destination*>(mRawPtr) : nullptr; + } + +private: + AccEvent* mRawPtr; +}; + +/** + * Return a new xpcom accessible event for the given internal one. + */ +already_AddRefed<nsIAccessibleEvent> +MakeXPCEvent(AccEvent* aEvent); + +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/base/AccGroupInfo.cpp b/accessible/base/AccGroupInfo.cpp new file mode 100644 index 000000000..bef707887 --- /dev/null +++ b/accessible/base/AccGroupInfo.cpp @@ -0,0 +1,227 @@ +/* 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 "AccGroupInfo.h" +#include "nsAccUtils.h" + +#include "Role.h" +#include "States.h" + +using namespace mozilla::a11y; + +AccGroupInfo::AccGroupInfo(Accessible* aItem, role aRole) : + mPosInSet(0), mSetSize(0), mParent(nullptr), mItem(aItem), mRole(aRole) +{ + MOZ_COUNT_CTOR(AccGroupInfo); + Update(); +} + +void +AccGroupInfo::Update() +{ + Accessible* parent = mItem->Parent(); + if (!parent) + return; + + int32_t indexInParent = mItem->IndexInParent(); + uint32_t siblingCount = parent->ChildCount(); + if (indexInParent == -1 || + indexInParent >= static_cast<int32_t>(siblingCount)) { + NS_ERROR("Wrong index in parent! Tree invalidation problem."); + return; + } + + int32_t level = nsAccUtils::GetARIAOrDefaultLevel(mItem); + + // Compute position in set. + mPosInSet = 1; + for (int32_t idx = indexInParent - 1; idx >= 0 ; idx--) { + Accessible* sibling = parent->GetChildAt(idx); + roles::Role siblingRole = sibling->Role(); + + // If the sibling is separator then the group is ended. + if (siblingRole == roles::SEPARATOR) + break; + + // If sibling is not visible and hasn't the same base role. + if (BaseRole(siblingRole) != mRole || sibling->State() & states::INVISIBLE) + continue; + + // Check if it's hierarchical flatten structure, i.e. if the sibling + // level is lesser than this one then group is ended, if the sibling level + // is greater than this one then the group is split by some child elements + // (group will be continued). + int32_t siblingLevel = nsAccUtils::GetARIAOrDefaultLevel(sibling); + if (siblingLevel < level) { + mParent = sibling; + break; + } + + // Skip subset. + if (siblingLevel > level) + continue; + + // If the previous item in the group has calculated group information then + // build group information for this item based on found one. + if (sibling->mBits.groupInfo) { + mPosInSet += sibling->mBits.groupInfo->mPosInSet; + mParent = sibling->mBits.groupInfo->mParent; + mSetSize = sibling->mBits.groupInfo->mSetSize; + return; + } + + mPosInSet++; + } + + // Compute set size. + mSetSize = mPosInSet; + + for (uint32_t idx = indexInParent + 1; idx < siblingCount; idx++) { + Accessible* sibling = parent->GetChildAt(idx); + + roles::Role siblingRole = sibling->Role(); + + // If the sibling is separator then the group is ended. + if (siblingRole == roles::SEPARATOR) + break; + + // If sibling is visible and has the same base role + if (BaseRole(siblingRole) != mRole || sibling->State() & states::INVISIBLE) + continue; + + // and check if it's hierarchical flatten structure. + int32_t siblingLevel = nsAccUtils::GetARIAOrDefaultLevel(sibling); + if (siblingLevel < level) + break; + + // Skip subset. + if (siblingLevel > level) + continue; + + // If the next item in the group has calculated group information then + // build group information for this item based on found one. + if (sibling->mBits.groupInfo) { + mParent = sibling->mBits.groupInfo->mParent; + mSetSize = sibling->mBits.groupInfo->mSetSize; + return; + } + + mSetSize++; + } + + if (mParent) + return; + + roles::Role parentRole = parent->Role(); + if (ShouldReportRelations(mRole, parentRole)) + mParent = parent; + + // ARIA tree and list can be arranged by using ARIA groups to organize levels. + if (parentRole != roles::GROUPING) + return; + + // Way #1 for ARIA tree (not ARIA treegrid): previous sibling of a group is a + // parent. In other words the parent of the tree item will be a group and + // the previous tree item of the group is a conceptual parent of the tree + // item. + if (mRole == roles::OUTLINEITEM) { + Accessible* parentPrevSibling = parent->PrevSibling(); + if (parentPrevSibling && parentPrevSibling->Role() == mRole) { + mParent = parentPrevSibling; + return; + } + } + + // Way #2 for ARIA list and tree: group is a child of an item. In other words + // the parent of the item will be a group and containing item of the group is + // a conceptual parent of the item. + if (mRole == roles::LISTITEM || mRole == roles::OUTLINEITEM) { + Accessible* grandParent = parent->Parent(); + if (grandParent && grandParent->Role() == mRole) + mParent = grandParent; + } +} + +Accessible* +AccGroupInfo::FirstItemOf(Accessible* aContainer) +{ + // ARIA tree can be arranged by ARIA groups case #1 (previous sibling of a + // group is a parent) or by aria-level. + a11y::role containerRole = aContainer->Role(); + Accessible* item = aContainer->NextSibling(); + if (item) { + if (containerRole == roles::OUTLINEITEM && item->Role() == roles::GROUPING) + item = item->FirstChild(); + + if (item) { + AccGroupInfo* itemGroupInfo = item->GetGroupInfo(); + if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) + return item; + } + } + + // ARIA list and tree can be arranged by ARIA groups case #2 (group is + // a child of an item). + item = aContainer->LastChild(); + if (!item) + return nullptr; + + if (item->Role() == roles::GROUPING && + (containerRole == roles::LISTITEM || containerRole == roles::OUTLINEITEM)) { + item = item->FirstChild(); + if (item) { + AccGroupInfo* itemGroupInfo = item->GetGroupInfo(); + if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) + return item; + } + } + + // Otherwise, it can be a direct child if the container is a list or tree. + item = aContainer->FirstChild(); + if (ShouldReportRelations(item->Role(), containerRole)) + return item; + + return nullptr; +} + +Accessible* +AccGroupInfo::NextItemTo(Accessible* aItem) +{ + AccGroupInfo* groupInfo = aItem->GetGroupInfo(); + if (!groupInfo) + return nullptr; + + // If the item in middle of the group then search next item in siblings. + if (groupInfo->PosInSet() >= groupInfo->SetSize()) + return nullptr; + + Accessible* parent = aItem->Parent(); + uint32_t childCount = parent->ChildCount(); + for (uint32_t idx = aItem->IndexInParent() + 1; idx < childCount; idx++) { + Accessible* nextItem = parent->GetChildAt(idx); + AccGroupInfo* nextGroupInfo = nextItem->GetGroupInfo(); + if (nextGroupInfo && + nextGroupInfo->ConceptualParent() == groupInfo->ConceptualParent()) { + return nextItem; + } + } + + NS_NOTREACHED("Item in the middle of the group but there's no next item!"); + return nullptr; +} + +bool +AccGroupInfo::ShouldReportRelations(role aRole, role aParentRole) +{ + // We only want to report hierarchy-based node relations for items in tree or + // list form. ARIA level/owns relations are always reported. + if (aParentRole == roles::OUTLINE && aRole == roles::OUTLINEITEM) + return true; + if (aParentRole == roles::TREE_TABLE && aRole == roles::ROW) + return true; + if (aParentRole == roles::LIST && aRole == roles::LISTITEM) + return true; + + return false; +} diff --git a/accessible/base/AccGroupInfo.h b/accessible/base/AccGroupInfo.h new file mode 100644 index 000000000..093258070 --- /dev/null +++ b/accessible/base/AccGroupInfo.h @@ -0,0 +1,114 @@ +/* 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 AccGroupInfo_h_ +#define AccGroupInfo_h_ + +#include "Accessible-inl.h" + +namespace mozilla { +namespace a11y { + +/** + * Calculate and store group information. + */ +class AccGroupInfo +{ +public: + ~AccGroupInfo() { MOZ_COUNT_DTOR(AccGroupInfo); } + + /** + * Return 1-based position in the group. + */ + uint32_t PosInSet() const { return mPosInSet; } + + /** + * Return a number of items in the group. + */ + uint32_t SetSize() const { return mSetSize; } + + /** + * Return a direct or logical parent of the accessible that this group info is + * created for. + */ + Accessible* ConceptualParent() const { return mParent; } + + /** + * Update group information. + */ + void Update(); + + /** + * Create group info. + */ + static AccGroupInfo* CreateGroupInfo(Accessible* aAccessible) + { + mozilla::a11y::role role = aAccessible->Role(); + if (role != mozilla::a11y::roles::ROW && + role != mozilla::a11y::roles::OUTLINEITEM && + role != mozilla::a11y::roles::OPTION && + role != mozilla::a11y::roles::LISTITEM && + role != mozilla::a11y::roles::MENUITEM && + role != mozilla::a11y::roles::COMBOBOX_OPTION && + role != mozilla::a11y::roles::RICH_OPTION && + role != mozilla::a11y::roles::CHECK_RICH_OPTION && + role != mozilla::a11y::roles::PARENT_MENUITEM && + role != mozilla::a11y::roles::CHECK_MENU_ITEM && + role != mozilla::a11y::roles::RADIO_MENU_ITEM && + role != mozilla::a11y::roles::RADIOBUTTON && + role != mozilla::a11y::roles::PAGETAB) + return nullptr; + + AccGroupInfo* info = new AccGroupInfo(aAccessible, BaseRole(role)); + return info; + } + + /** + * Return a first item for the given container. + */ + static Accessible* FirstItemOf(Accessible* aContainer); + + /** + * Return next item of the same group to the given item. + */ + static Accessible* NextItemTo(Accessible* aItem); + +protected: + AccGroupInfo(Accessible* aItem, a11y::role aRole); + +private: + AccGroupInfo() = delete; + AccGroupInfo(const AccGroupInfo&) = delete; + AccGroupInfo& operator =(const AccGroupInfo&) = delete; + + static mozilla::a11y::role BaseRole(mozilla::a11y::role aRole) + { + if (aRole == mozilla::a11y::roles::CHECK_MENU_ITEM || + aRole == mozilla::a11y::roles::PARENT_MENUITEM || + aRole == mozilla::a11y::roles::RADIO_MENU_ITEM) + return mozilla::a11y::roles::MENUITEM; + + if (aRole == mozilla::a11y::roles::CHECK_RICH_OPTION) + return mozilla::a11y::roles::RICH_OPTION; + + return aRole; + } + + /** + * Return true if the given parent and child roles should have their node + * relations reported. + */ + static bool ShouldReportRelations(a11y::role aRole, a11y::role aParentRole); + + uint32_t mPosInSet; + uint32_t mSetSize; + Accessible* mParent; + Accessible* mItem; + a11y::role mRole; +}; + +} // namespace mozilla +} // namespace a11y + +#endif diff --git a/accessible/base/AccIterator.cpp b/accessible/base/AccIterator.cpp new file mode 100644 index 000000000..f6e890c50 --- /dev/null +++ b/accessible/base/AccIterator.cpp @@ -0,0 +1,414 @@ +/* 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 "AccIterator.h" + +#include "AccGroupInfo.h" +#ifdef MOZ_XUL +#include "XULTreeAccessible.h" +#endif + +#include "mozilla/dom/HTMLLabelElement.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// AccIterator +//////////////////////////////////////////////////////////////////////////////// + +AccIterator::AccIterator(Accessible* aAccessible, + filters::FilterFuncPtr aFilterFunc) : + mFilterFunc(aFilterFunc) +{ + mState = new IteratorState(aAccessible); +} + +AccIterator::~AccIterator() +{ + while (mState) { + IteratorState *tmp = mState; + mState = tmp->mParentState; + delete tmp; + } +} + +Accessible* +AccIterator::Next() +{ + while (mState) { + Accessible* child = mState->mParent->GetChildAt(mState->mIndex++); + if (!child) { + IteratorState* tmp = mState; + mState = mState->mParentState; + delete tmp; + + continue; + } + + uint32_t result = mFilterFunc(child); + if (result & filters::eMatch) + return child; + + if (!(result & filters::eSkipSubtree)) { + IteratorState* childState = new IteratorState(child, mState); + mState = childState; + } + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccIterator::IteratorState + +AccIterator::IteratorState::IteratorState(Accessible* aParent, + IteratorState *mParentState) : + mParent(aParent), mIndex(0), mParentState(mParentState) +{ +} + + +//////////////////////////////////////////////////////////////////////////////// +// RelatedAccIterator +//////////////////////////////////////////////////////////////////////////////// + +RelatedAccIterator:: + RelatedAccIterator(DocAccessible* aDocument, nsIContent* aDependentContent, + nsIAtom* aRelAttr) : + mDocument(aDocument), mRelAttr(aRelAttr), mProviders(nullptr), + mBindingParent(nullptr), mIndex(0) +{ + mBindingParent = aDependentContent->GetBindingParent(); + nsIAtom* IDAttr = mBindingParent ? + nsGkAtoms::anonid : nsGkAtoms::id; + + nsAutoString id; + if (aDependentContent->GetAttr(kNameSpaceID_None, IDAttr, id)) + mProviders = mDocument->mDependentIDsHash.Get(id); +} + +Accessible* +RelatedAccIterator::Next() +{ + if (!mProviders) + return nullptr; + + while (mIndex < mProviders->Length()) { + DocAccessible::AttrRelProvider* provider = (*mProviders)[mIndex++]; + + // Return related accessible for the given attribute and if the provider + // content is in the same binding in the case of XBL usage. + if (provider->mRelAttr == mRelAttr) { + nsIContent* bindingParent = provider->mContent->GetBindingParent(); + bool inScope = mBindingParent == bindingParent || + mBindingParent == provider->mContent; + + if (inScope) { + Accessible* related = mDocument->GetAccessible(provider->mContent); + if (related) + return related; + + // If the document content is pointed by relation then return the document + // itself. + if (provider->mContent == mDocument->GetContent()) + return mDocument; + } + } + } + + return nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// HTMLLabelIterator +//////////////////////////////////////////////////////////////////////////////// + +HTMLLabelIterator:: + HTMLLabelIterator(DocAccessible* aDocument, const Accessible* aAccessible, + LabelFilter aFilter) : + mRelIter(aDocument, aAccessible->GetContent(), nsGkAtoms::_for), + mAcc(aAccessible), mLabelFilter(aFilter) +{ +} + +bool +HTMLLabelIterator::IsLabel(Accessible* aLabel) +{ + dom::HTMLLabelElement* labelEl = + dom::HTMLLabelElement::FromContent(aLabel->GetContent()); + return labelEl && labelEl->GetControl() == mAcc->GetContent(); +} + +Accessible* +HTMLLabelIterator::Next() +{ + // Get either <label for="[id]"> element which explicitly points to given + // element, or <label> ancestor which implicitly point to it. + Accessible* label = nullptr; + while ((label = mRelIter.Next())) { + if (IsLabel(label)) { + return label; + } + } + + // Ignore ancestor label on not widget accessible. + if (mLabelFilter == eSkipAncestorLabel || !mAcc->IsWidget()) + return nullptr; + + // Go up tree to get a name of ancestor label if there is one (an ancestor + // <label> implicitly points to us). Don't go up farther than form or + // document. + Accessible* walkUp = mAcc->Parent(); + while (walkUp && !walkUp->IsDoc()) { + nsIContent* walkUpEl = walkUp->GetContent(); + if (IsLabel(walkUp) && + !walkUpEl->HasAttr(kNameSpaceID_None, nsGkAtoms::_for)) { + mLabelFilter = eSkipAncestorLabel; // prevent infinite loop + return walkUp; + } + + if (walkUpEl->IsHTMLElement(nsGkAtoms::form)) + break; + + walkUp = walkUp->Parent(); + } + + return nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// HTMLOutputIterator +//////////////////////////////////////////////////////////////////////////////// + +HTMLOutputIterator:: +HTMLOutputIterator(DocAccessible* aDocument, nsIContent* aElement) : + mRelIter(aDocument, aElement, nsGkAtoms::_for) +{ +} + +Accessible* +HTMLOutputIterator::Next() +{ + Accessible* output = nullptr; + while ((output = mRelIter.Next())) { + if (output->GetContent()->IsHTMLElement(nsGkAtoms::output)) + return output; + } + + return nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// XULLabelIterator +//////////////////////////////////////////////////////////////////////////////// + +XULLabelIterator:: + XULLabelIterator(DocAccessible* aDocument, nsIContent* aElement) : + mRelIter(aDocument, aElement, nsGkAtoms::control) +{ +} + +Accessible* +XULLabelIterator::Next() +{ + Accessible* label = nullptr; + while ((label = mRelIter.Next())) { + if (label->GetContent()->IsXULElement(nsGkAtoms::label)) + return label; + } + + return nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// XULDescriptionIterator +//////////////////////////////////////////////////////////////////////////////// + +XULDescriptionIterator:: + XULDescriptionIterator(DocAccessible* aDocument, nsIContent* aElement) : + mRelIter(aDocument, aElement, nsGkAtoms::control) +{ +} + +Accessible* +XULDescriptionIterator::Next() +{ + Accessible* descr = nullptr; + while ((descr = mRelIter.Next())) { + if (descr->GetContent()->IsXULElement(nsGkAtoms::description)) + return descr; + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// IDRefsIterator +//////////////////////////////////////////////////////////////////////////////// + +IDRefsIterator:: + IDRefsIterator(DocAccessible* aDoc, nsIContent* aContent, + nsIAtom* aIDRefsAttr) : + mContent(aContent), mDoc(aDoc), mCurrIdx(0) +{ + if (mContent->IsInUncomposedDoc()) + mContent->GetAttr(kNameSpaceID_None, aIDRefsAttr, mIDs); +} + +const nsDependentSubstring +IDRefsIterator::NextID() +{ + for (; mCurrIdx < mIDs.Length(); mCurrIdx++) { + if (!NS_IsAsciiWhitespace(mIDs[mCurrIdx])) + break; + } + + if (mCurrIdx >= mIDs.Length()) + return nsDependentSubstring(); + + nsAString::index_type idStartIdx = mCurrIdx; + while (++mCurrIdx < mIDs.Length()) { + if (NS_IsAsciiWhitespace(mIDs[mCurrIdx])) + break; + } + + return Substring(mIDs, idStartIdx, mCurrIdx++ - idStartIdx); +} + +nsIContent* +IDRefsIterator::NextElem() +{ + while (true) { + const nsDependentSubstring id = NextID(); + if (id.IsEmpty()) + break; + + nsIContent* refContent = GetElem(id); + if (refContent) + return refContent; + } + + return nullptr; +} + +nsIContent* +IDRefsIterator::GetElem(const nsDependentSubstring& aID) +{ + // Get elements in DOM tree by ID attribute if this is an explicit content. + // In case of bound element check its anonymous subtree. + if (!mContent->IsInAnonymousSubtree()) { + dom::Element* refElm = mContent->OwnerDoc()->GetElementById(aID); + if (refElm || !mContent->GetXBLBinding()) + return refElm; + } + + // If content is in anonymous subtree or an element having anonymous subtree + // then use "anonid" attribute to get elements in anonymous subtree. + + // Check inside the binding the element is contained in. + nsIContent* bindingParent = mContent->GetBindingParent(); + if (bindingParent) { + nsIContent* refElm = bindingParent->OwnerDoc()-> + GetAnonymousElementByAttribute(bindingParent, nsGkAtoms::anonid, aID); + + if (refElm) + return refElm; + } + + // Check inside the binding of the element. + if (mContent->GetXBLBinding()) { + return mContent->OwnerDoc()-> + GetAnonymousElementByAttribute(mContent, nsGkAtoms::anonid, aID); + } + + return nullptr; +} + +Accessible* +IDRefsIterator::Next() +{ + nsIContent* nextEl = nullptr; + while ((nextEl = NextElem())) { + Accessible* acc = mDoc->GetAccessible(nextEl); + if (acc) { + return acc; + } + } + return nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// SingleAccIterator +//////////////////////////////////////////////////////////////////////////////// + +Accessible* +SingleAccIterator::Next() +{ + RefPtr<Accessible> nextAcc; + mAcc.swap(nextAcc); + if (!nextAcc || nextAcc->IsDefunct()) { + return nullptr; + } + return nextAcc; +} + + +//////////////////////////////////////////////////////////////////////////////// +// ItemIterator +//////////////////////////////////////////////////////////////////////////////// + +Accessible* +ItemIterator::Next() +{ + if (mContainer) { + mAnchor = AccGroupInfo::FirstItemOf(mContainer); + mContainer = nullptr; + return mAnchor; + } + + return mAnchor ? (mAnchor = AccGroupInfo::NextItemTo(mAnchor)) : nullptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +// XULTreeItemIterator +//////////////////////////////////////////////////////////////////////////////// + +XULTreeItemIterator::XULTreeItemIterator(XULTreeAccessible* aXULTree, + nsITreeView* aTreeView, + int32_t aRowIdx) : + mXULTree(aXULTree), mTreeView(aTreeView), mRowCount(-1), + mContainerLevel(-1), mCurrRowIdx(aRowIdx + 1) +{ + mTreeView->GetRowCount(&mRowCount); + if (aRowIdx != -1) + mTreeView->GetLevel(aRowIdx, &mContainerLevel); +} + +Accessible* +XULTreeItemIterator::Next() +{ + while (mCurrRowIdx < mRowCount) { + int32_t level = 0; + mTreeView->GetLevel(mCurrRowIdx, &level); + + if (level == mContainerLevel + 1) + return mXULTree->GetTreeItemAccessible(mCurrRowIdx++); + + if (level <= mContainerLevel) { // got level up + mCurrRowIdx = mRowCount; + break; + } + + mCurrRowIdx++; + } + + return nullptr; +} diff --git a/accessible/base/AccIterator.h b/accessible/base/AccIterator.h new file mode 100644 index 000000000..362eb3a92 --- /dev/null +++ b/accessible/base/AccIterator.h @@ -0,0 +1,325 @@ +/* -*- 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_AccIterator_h__ +#define mozilla_a11y_AccIterator_h__ + +#include "DocAccessible.h" +#include "Filters.h" + +#include <memory> + +class nsITreeView; + +namespace mozilla { +namespace a11y { + +/** + * AccIterable is a basic interface for iterators over accessibles. + */ +class AccIterable +{ +public: + virtual ~AccIterable() { } + virtual Accessible* Next() = 0; + +private: + friend class Relation; + std::unique_ptr<AccIterable> mNextIter; +}; + +/** + * Allows to iterate through accessible children or subtree complying with + * filter function. + */ +class AccIterator : public AccIterable +{ +public: + AccIterator(Accessible* aRoot, filters::FilterFuncPtr aFilterFunc); + virtual ~AccIterator(); + + /** + * Return next accessible complying with filter function. Return the first + * accessible for the first time. + */ + virtual Accessible* Next() override; + +private: + AccIterator(); + AccIterator(const AccIterator&); + AccIterator& operator =(const AccIterator&); + + struct IteratorState + { + explicit IteratorState(Accessible* aParent, IteratorState* mParentState = nullptr); + + Accessible* mParent; + int32_t mIndex; + IteratorState* mParentState; + }; + + filters::FilterFuncPtr mFilterFunc; + IteratorState* mState; +}; + + +/** + * Allows to traverse through related accessibles that are pointing to the given + * dependent accessible by relation attribute. + */ +class RelatedAccIterator : public AccIterable +{ +public: + /** + * Constructor. + * + * @param aDocument [in] the document accessible the related + * & accessibles belong to. + * @param aDependentContent [in] the content of dependent accessible that + * relations were requested for + * @param aRelAttr [in] relation attribute that relations are + * pointed by + */ + RelatedAccIterator(DocAccessible* aDocument, nsIContent* aDependentContent, + nsIAtom* aRelAttr); + + virtual ~RelatedAccIterator() { } + + /** + * Return next related accessible for the given dependent accessible. + */ + virtual Accessible* Next() override; + +private: + RelatedAccIterator(); + RelatedAccIterator(const RelatedAccIterator&); + RelatedAccIterator& operator = (const RelatedAccIterator&); + + DocAccessible* mDocument; + nsIAtom* mRelAttr; + DocAccessible::AttrRelProviderArray* mProviders; + nsIContent* mBindingParent; + uint32_t mIndex; +}; + + +/** + * Used to iterate through HTML labels associated with the given accessible. + */ +class HTMLLabelIterator : public AccIterable +{ +public: + enum LabelFilter { + eAllLabels, + eSkipAncestorLabel + }; + + HTMLLabelIterator(DocAccessible* aDocument, const Accessible* aAccessible, + LabelFilter aFilter = eAllLabels); + + virtual ~HTMLLabelIterator() { } + + /** + * Return next label accessible associated with the given element. + */ + virtual Accessible* Next() override; + +private: + HTMLLabelIterator(); + HTMLLabelIterator(const HTMLLabelIterator&); + HTMLLabelIterator& operator = (const HTMLLabelIterator&); + + bool IsLabel(Accessible* aLabel); + + RelatedAccIterator mRelIter; + // XXX: replace it on weak reference (bug 678429), it's safe to use raw + // pointer now because iterators life cycle is short. + const Accessible* mAcc; + LabelFilter mLabelFilter; +}; + + +/** + * Used to iterate through HTML outputs associated with the given element. + */ +class HTMLOutputIterator : public AccIterable +{ +public: + HTMLOutputIterator(DocAccessible* aDocument, nsIContent* aElement); + virtual ~HTMLOutputIterator() { } + + /** + * Return next output accessible associated with the given element. + */ + virtual Accessible* Next() override; + +private: + HTMLOutputIterator(); + HTMLOutputIterator(const HTMLOutputIterator&); + HTMLOutputIterator& operator = (const HTMLOutputIterator&); + + RelatedAccIterator mRelIter; +}; + + +/** + * Used to iterate through XUL labels associated with the given element. + */ +class XULLabelIterator : public AccIterable +{ +public: + XULLabelIterator(DocAccessible* aDocument, nsIContent* aElement); + virtual ~XULLabelIterator() { } + + /** + * Return next label accessible associated with the given element. + */ + virtual Accessible* Next() override; + +private: + XULLabelIterator(); + XULLabelIterator(const XULLabelIterator&); + XULLabelIterator& operator = (const XULLabelIterator&); + + RelatedAccIterator mRelIter; +}; + + +/** + * Used to iterate through XUL descriptions associated with the given element. + */ +class XULDescriptionIterator : public AccIterable +{ +public: + XULDescriptionIterator(DocAccessible* aDocument, nsIContent* aElement); + virtual ~XULDescriptionIterator() { } + + /** + * Return next description accessible associated with the given element. + */ + virtual Accessible* Next() override; + +private: + XULDescriptionIterator(); + XULDescriptionIterator(const XULDescriptionIterator&); + XULDescriptionIterator& operator = (const XULDescriptionIterator&); + + RelatedAccIterator mRelIter; +}; + +/** + * Used to iterate through IDs, elements or accessibles pointed by IDRefs + * attribute. Note, any method used to iterate through IDs, elements, or + * accessibles moves iterator to next position. + */ +class IDRefsIterator : public AccIterable +{ +public: + IDRefsIterator(DocAccessible* aDoc, nsIContent* aContent, + nsIAtom* aIDRefsAttr); + virtual ~IDRefsIterator() { } + + /** + * Return next ID. + */ + const nsDependentSubstring NextID(); + + /** + * Return next element. + */ + nsIContent* NextElem(); + + /** + * Return the element with the given ID. + */ + nsIContent* GetElem(const nsDependentSubstring& aID); + + // AccIterable + virtual Accessible* Next() override; + +private: + IDRefsIterator(); + IDRefsIterator(const IDRefsIterator&); + IDRefsIterator operator = (const IDRefsIterator&); + + nsString mIDs; + nsIContent* mContent; + DocAccessible* mDoc; + nsAString::index_type mCurrIdx; +}; + + +/** + * Iterator that points to a single accessible returning it on the first call + * to Next(). + */ +class SingleAccIterator : public AccIterable +{ +public: + explicit SingleAccIterator(Accessible* aTarget): mAcc(aTarget) { } + virtual ~SingleAccIterator() { } + + virtual Accessible* Next() override; + +private: + SingleAccIterator(); + SingleAccIterator(const SingleAccIterator&); + SingleAccIterator& operator = (const SingleAccIterator&); + + RefPtr<Accessible> mAcc; +}; + + +/** + * Used to iterate items of the given item container. + */ +class ItemIterator : public AccIterable +{ +public: + explicit ItemIterator(Accessible* aItemContainer) : + mContainer(aItemContainer), mAnchor(nullptr) { } + virtual ~ItemIterator() { } + + virtual Accessible* Next() override; + +private: + ItemIterator() = delete; + ItemIterator(const ItemIterator&) = delete; + ItemIterator& operator = (const ItemIterator&) = delete; + + Accessible* mContainer; + Accessible* mAnchor; +}; + + +/** + * Used to iterate through XUL tree items of the same level. + */ +class XULTreeItemIterator : public AccIterable +{ +public: + XULTreeItemIterator(XULTreeAccessible* aXULTree, nsITreeView* aTreeView, + int32_t aRowIdx); + virtual ~XULTreeItemIterator() { } + + virtual Accessible* Next() override; + +private: + XULTreeItemIterator() = delete; + XULTreeItemIterator(const XULTreeItemIterator&) = delete; + XULTreeItemIterator& operator = (const XULTreeItemIterator&) = delete; + + XULTreeAccessible* mXULTree; + nsITreeView* mTreeView; + int32_t mRowCount; + int32_t mContainerLevel; + int32_t mCurrRowIdx; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccTypes.h b/accessible/base/AccTypes.h new file mode 100644 index 000000000..856d6978e --- /dev/null +++ b/accessible/base/AccTypes.h @@ -0,0 +1,94 @@ +/* -*- 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_AccTypes_h +#define mozilla_a11y_AccTypes_h + +namespace mozilla { +namespace a11y { + +/** + * Accessible object types. Each accessible class can have own type. + */ +enum AccType { + /** + * This set of types is used for accessible creation, keep them together in + * alphabetical order since they are used in switch statement. + */ + eNoType, + eHTMLBRType, + eHTMLButtonType, + eHTMLCanvasType, + eHTMLCaptionType, + eHTMLCheckboxType, + eHTMLComboboxType, + eHTMLFileInputType, + eHTMLGroupboxType, + eHTMLHRType, + eHTMLImageMapType, + eHTMLLiType, + eHTMLSelectListType, + eHTMLMediaType, + eHTMLRadioButtonType, + eHTMLRangeType, + eHTMLSpinnerType, + eHTMLTableType, + eHTMLTableCellType, + eHTMLTableRowType, + eHTMLTextFieldType, + eHyperTextType, + eImageType, + eOuterDocType, + ePluginType, + eTextLeafType, + + /** + * Other accessible types. + */ + eApplicationType, + eHTMLOptGroupType, + eImageMapType, + eMenuPopupType, + eProxyType, + eProgressType, + eRootType, + eXULLabelType, + eXULListItemType, + eXULTabpanelsType, + eXULTreeType, + + eLastAccType = eXULTreeType +}; + +/** + * Generic accessible type, different accessible classes can share the same + * type, the same accessible class can have several types. + */ +enum AccGenericType { + eAlert = 1 << 0, + eAutoComplete = 1 << 1, + eAutoCompletePopup = 1 << 2, + eButton = 1 << 3, + eCombobox = 1 << 4, + eDocument = 1 << 5, + eHyperText = 1 << 6, + eLandmark = 1 << 7, + eList = 1 << 8, + eListControl = 1 << 9, + eMenuButton = 1 << 10, + eSelect = 1 << 11, + eTable = 1 << 12, + eTableCell = 1 << 13, + eTableRow = 1 << 14, + eText = 1 << 15, + + eLastAccGenericType = eText +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_AccTypes_h diff --git a/accessible/base/AccessibleOrProxy.cpp b/accessible/base/AccessibleOrProxy.cpp new file mode 100644 index 000000000..77fb44a11 --- /dev/null +++ b/accessible/base/AccessibleOrProxy.cpp @@ -0,0 +1,27 @@ +/* -*- 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 "AccessibleOrProxy.h" + +AccessibleOrProxy +AccessibleOrProxy::Parent() const +{ + if (IsAccessible()) { + return AsAccessible()->Parent(); + } + + ProxyAccessible* proxy = AsProxy(); + if (!proxy) { + return nullptr; + } + + if (ProxyAccessible* parent = proxy->Parent()) { + return parent; + } + + // Otherwise this should be the proxy for the tab's top level document. + return proxy->OuterDocOfRemoteBrowser(); +} diff --git a/accessible/base/AccessibleOrProxy.h b/accessible/base/AccessibleOrProxy.h new file mode 100644 index 000000000..0cdf825ca --- /dev/null +++ b/accessible/base/AccessibleOrProxy.h @@ -0,0 +1,123 @@ +/* -*- 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_AccessibleOrProxy_h +#define mozilla_a11y_AccessibleOrProxy_h + +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/ProxyAccessible.h" +#include "mozilla/a11y/Role.h" + +#include <stdint.h> + +namespace mozilla { +namespace a11y { + +/** + * This class stores an Accessible* or a ProxyAccessible* in a safe manner + * with size sizeof(void*). + */ +class AccessibleOrProxy +{ +public: + MOZ_IMPLICIT AccessibleOrProxy(Accessible* aAcc) : + mBits(reinterpret_cast<uintptr_t>(aAcc)) {} + MOZ_IMPLICIT AccessibleOrProxy(ProxyAccessible* aProxy) : + mBits(aProxy ? (reinterpret_cast<uintptr_t>(aProxy) | IS_PROXY) : 0) {} + MOZ_IMPLICIT AccessibleOrProxy(decltype(nullptr)) : mBits(0) {} + + bool IsProxy() const { return mBits & IS_PROXY; } + ProxyAccessible* AsProxy() const + { + if (IsProxy()) { + return reinterpret_cast<ProxyAccessible*>(mBits & ~IS_PROXY); + } + + return nullptr; + } + + bool IsAccessible() const { return !IsProxy(); } + Accessible* AsAccessible() const + { + if (IsAccessible()) { + return reinterpret_cast<Accessible*>(mBits); + } + + return nullptr; + } + + bool IsNull() const { return mBits == 0; } + + uint32_t ChildCount() const + { + if (IsProxy()) { + return AsProxy()->ChildrenCount(); + } + + return AsAccessible()->ChildCount(); + } + + /** + * Return the child object either an accessible or a proxied accessible at + * the given index. + */ + AccessibleOrProxy ChildAt(uint32_t aIdx) + { + if (IsProxy()) { + return AsProxy()->ChildAt(aIdx); + } + + return AsAccessible()->GetChildAt(aIdx); + } + + /** + * Return the first child object. + */ + AccessibleOrProxy FirstChild() + { + if (IsProxy()) { + return AsProxy()->FirstChild(); + } + + return AsAccessible()->FirstChild(); + } + + /** + * Return the first child object. + */ + AccessibleOrProxy LastChild() + { + if (IsProxy()) { + return AsProxy()->LastChild(); + } + + return AsAccessible()->LastChild(); + } + + role Role() const + { + if (IsProxy()) { + return AsProxy()->Role(); + } + + return AsAccessible()->Role(); + } + + AccessibleOrProxy Parent() const; + + // XXX these are implementation details that ideally would not be exposed. + uintptr_t Bits() const { return mBits; } + void SetBits(uintptr_t aBits) { mBits = aBits; } + +private: + uintptr_t mBits; + static const uintptr_t IS_PROXY = 0x1; +}; + +} +} + +#endif diff --git a/accessible/base/Asserts.cpp b/accessible/base/Asserts.cpp new file mode 100644 index 000000000..b97a48ec4 --- /dev/null +++ b/accessible/base/Asserts.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "nsIAccessibleRelation.h" +#include "nsIAccessibleRole.h" +#include "RelationType.h" +#include "Role.h" + +using namespace mozilla::a11y; + +#define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, nameRule) \ + static_assert(static_cast<uint32_t>(roles::geckoRole) \ + == static_cast<uint32_t>(nsIAccessibleRole::ROLE_ ## geckoRole), \ + "internal and xpcom roles differ!"); +#include "RoleMap.h" +#undef ROLE + +#define RELATIONTYPE(geckoType, stringType, atkType, msaaType, ia2Type) \ + static_assert(static_cast<uint32_t>(RelationType::geckoType) \ + == static_cast<uint32_t>(nsIAccessibleRelation::RELATION_ ## geckoType), \ + "internal and xpcom relations differ!"); +#include "RelationTypeMap.h" +#undef RELATIONTYPE diff --git a/accessible/base/DocManager.cpp b/accessible/base/DocManager.cpp new file mode 100644 index 000000000..786ccbbe3 --- /dev/null +++ b/accessible/base/DocManager.cpp @@ -0,0 +1,594 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "DocManager.h" + +#include "ApplicationAccessible.h" +#include "ARIAMap.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "DocAccessibleParent.h" +#include "nsAccessibilityService.h" +#include "Platform.h" +#include "RootAccessibleWrap.h" +#include "xpcAccessibleDocument.h" + +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +#include "mozilla/EventListenerManager.h" +#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent() +#include "nsCURILoader.h" +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIDOMDocument.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebNavigation.h" +#include "nsServiceManagerUtils.h" +#include "nsIWebProgress.h" +#include "nsCoreUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/dom/TabChild.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +StaticAutoPtr<nsTArray<DocAccessibleParent*>> DocManager::sRemoteDocuments; +nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, xpcAccessibleDocument>* +DocManager::sRemoteXPCDocumentCache = nullptr; + +//////////////////////////////////////////////////////////////////////////////// +// DocManager +//////////////////////////////////////////////////////////////////////////////// + +DocManager::DocManager() + : mDocAccessibleCache(2), mXPCDocumentCache(0) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager public + +DocAccessible* +DocManager::GetDocAccessible(nsIDocument* aDocument) +{ + if (!aDocument) + return nullptr; + + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (docAcc) + return docAcc; + + return CreateDocOrRootAccessible(aDocument); +} + +Accessible* +DocManager::FindAccessibleInCache(nsINode* aNode) const +{ + for (auto iter = mDocAccessibleCache.ConstIter(); !iter.Done(); iter.Next()) { + DocAccessible* docAccessible = iter.UserData(); + NS_ASSERTION(docAccessible, + "No doc accessible for the object in doc accessible cache!"); + + if (docAccessible) { + Accessible* accessible = docAccessible->GetAccessible(aNode); + if (accessible) { + return accessible; + } + } + } + return nullptr; +} + +void +DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument, + nsIDocument* aDOMDocument) +{ + // We need to remove listeners in both cases, when document is being shutdown + // or when accessibility service is being shut down as well. + RemoveListeners(aDOMDocument); + + // Document will already be removed when accessibility service is shutting + // down so we do not need to remove it twice. + if (nsAccessibilityService::IsShutdown()) { + return; + } + + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (xpcDoc) { + xpcDoc->Shutdown(); + mXPCDocumentCache.Remove(aDocument); + } + + mDocAccessibleCache.Remove(aDOMDocument); +} + +void +DocManager::NotifyOfRemoteDocShutdown(DocAccessibleParent* aDoc) +{ + xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc); + if (doc) { + doc->Shutdown(); + sRemoteXPCDocumentCache->Remove(aDoc); + } +} + +xpcAccessibleDocument* +DocManager::GetXPCDocument(DocAccessible* aDocument) +{ + if (!aDocument) + return nullptr; + + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (!xpcDoc) { + xpcDoc = new xpcAccessibleDocument(aDocument); + mXPCDocumentCache.Put(aDocument, xpcDoc); + } + return xpcDoc; +} + +xpcAccessibleDocument* +DocManager::GetXPCDocument(DocAccessibleParent* aDoc) +{ + xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc); + if (doc) { + return doc; + } + + if (!sRemoteXPCDocumentCache) { + sRemoteXPCDocumentCache = + new nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, xpcAccessibleDocument>; + } + + doc = + new xpcAccessibleDocument(aDoc, Interfaces::DOCUMENT | Interfaces::HYPERTEXT); + sRemoteXPCDocumentCache->Put(aDoc, doc); + + return doc; +} + +#ifdef DEBUG +bool +DocManager::IsProcessingRefreshDriverNotification() const +{ + for (auto iter = mDocAccessibleCache.ConstIter(); !iter.Done(); iter.Next()) { + DocAccessible* docAccessible = iter.UserData(); + NS_ASSERTION(docAccessible, + "No doc accessible for the object in doc accessible cache!"); + + if (docAccessible && docAccessible->mNotificationController && + docAccessible->mNotificationController->IsUpdating()) { + return true; + } + } + return false; +} +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// DocManager protected + +bool +DocManager::Init() +{ + nsCOMPtr<nsIWebProgress> progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + + if (!progress) + return false; + + progress->AddProgressListener(static_cast<nsIWebProgressListener*>(this), + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + + return true; +} + +void +DocManager::Shutdown() +{ + nsCOMPtr<nsIWebProgress> progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + + if (progress) + progress->RemoveProgressListener(static_cast<nsIWebProgressListener*>(this)); + + ClearDocCache(); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS(DocManager, + nsIWebProgressListener, + nsIDOMEventListener, + nsISupportsWeakReference) + +//////////////////////////////////////////////////////////////////////////////// +// nsIWebProgressListener + +NS_IMETHODIMP +DocManager::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) +{ + NS_ASSERTION(aStateFlags & STATE_IS_DOCUMENT, "Other notifications excluded"); + + if (nsAccessibilityService::IsShutdown() || !aWebProgress || + (aStateFlags & (STATE_START | STATE_STOP)) == 0) + return NS_OK; + + nsCOMPtr<mozIDOMWindowProxy> DOMWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow)); + NS_ENSURE_STATE(DOMWindow); + + nsPIDOMWindowOuter* piWindow = nsPIDOMWindowOuter::From(DOMWindow); + MOZ_ASSERT(piWindow); + + nsCOMPtr<nsIDocument> document = piWindow->GetDoc(); + NS_ENSURE_STATE(document); + + // Document was loaded. + if (aStateFlags & STATE_STOP) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) + logging::DocLoad("document loaded", aWebProgress, aRequest, aStateFlags); +#endif + + // Figure out an event type to notify the document has been loaded. + uint32_t eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED; + + // Some XUL documents get start state and then stop state with failure + // status when everything is ok. Fire document load complete event in this + // case. + if (NS_SUCCEEDED(aStatus) || !nsCoreUtils::IsContentDocument(document)) + eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; + + // If end consumer has been retargeted for loaded content then do not fire + // any event because it means no new document has been loaded, for example, + // it happens when user clicks on file link. + if (aRequest) { + uint32_t loadFlags = 0; + aRequest->GetLoadFlags(&loadFlags); + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) + eventType = 0; + } + + HandleDOMDocumentLoad(document, eventType); + return NS_OK; + } + + // Document loading was started. +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) + logging::DocLoad("start document loading", aWebProgress, aRequest, aStateFlags); +#endif + + DocAccessible* docAcc = GetExistingDocAccessible(document); + if (!docAcc) + return NS_OK; + + nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(DOMWindow)); + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(webNav)); + NS_ENSURE_STATE(docShell); + + bool isReloading = false; + uint32_t loadType; + docShell->GetLoadType(&loadType); + if (loadType == LOAD_RELOAD_NORMAL || + loadType == LOAD_RELOAD_BYPASS_CACHE || + loadType == LOAD_RELOAD_BYPASS_PROXY || + loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE || + loadType == LOAD_RELOAD_ALLOW_MIXED_CONTENT) { + isReloading = true; + } + + docAcc->NotifyOfLoading(isReloading); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +DocManager::HandleEvent(nsIDOMEvent* aEvent) +{ + nsAutoString type; + aEvent->GetType(type); + + nsCOMPtr<nsIDocument> document = + do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget()); + NS_ASSERTION(document, "pagehide or DOMContentLoaded for non document!"); + if (!document) + return NS_OK; + + if (type.EqualsLiteral("pagehide")) { + // 'pagehide' event is registered on every DOM document we create an + // accessible for, process the event for the target. This document + // accessible and all its sub document accessible are shutdown as result of + // processing. + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) + logging::DocDestroy("received 'pagehide' event", document); +#endif + + // Shutdown this one and sub document accessibles. + + // We're allowed to not remove listeners when accessible document is + // shutdown since we don't keep strong reference on chrome event target and + // listeners are removed automatically when chrome event target goes away. + DocAccessible* docAccessible = GetExistingDocAccessible(document); + if (docAccessible) + docAccessible->Shutdown(); + + return NS_OK; + } + + // XXX: handle error pages loading separately since they get neither + // webprogress notifications nor 'pageshow' event. + if (type.EqualsLiteral("DOMContentLoaded") && + nsCoreUtils::IsErrorPage(document)) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) + logging::DocLoad("handled 'DOMContentLoaded' event", document); +#endif + + HandleDOMDocumentLoad(document, + nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager private + +void +DocManager::HandleDOMDocumentLoad(nsIDocument* aDocument, + uint32_t aLoadEventType) +{ + // Document accessible can be created before we were notified the DOM document + // was loaded completely. However if it's not created yet then create it. + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (!docAcc) { + docAcc = CreateDocOrRootAccessible(aDocument); + if (!docAcc) + return; + } + + docAcc->NotifyOfLoad(aLoadEventType); +} + +void +DocManager::AddListeners(nsIDocument* aDocument, + bool aAddDOMContentLoadedListener) +{ + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + EventTarget* target = window->GetChromeEventHandler(); + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->AddEventListenerByType(this, NS_LITERAL_STRING("pagehide"), + TrustedEventsAtCapture()); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) + logging::Text("added 'pagehide' listener"); +#endif + + if (aAddDOMContentLoadedListener) { + elm->AddEventListenerByType(this, NS_LITERAL_STRING("DOMContentLoaded"), + TrustedEventsAtCapture()); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) + logging::Text("added 'DOMContentLoaded' listener"); +#endif + } +} + +void +DocManager::RemoveListeners(nsIDocument* aDocument) +{ + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + if (!window) + return; + + EventTarget* target = window->GetChromeEventHandler(); + if (!target) + return; + + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("pagehide"), + TrustedEventsAtCapture()); + + elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("DOMContentLoaded"), + TrustedEventsAtCapture()); +} + +DocAccessible* +DocManager::CreateDocOrRootAccessible(nsIDocument* aDocument) +{ + // Ignore hiding, resource documents and documents without docshell. + if (!aDocument->IsVisibleConsideringAncestors() || + aDocument->IsResourceDoc() || !aDocument->IsActive()) + return nullptr; + + // Ignore documents without presshell and not having root frame. + nsIPresShell* presShell = aDocument->GetShell(); + if (!presShell || presShell->IsDestroying()) + return nullptr; + + bool isRootDoc = nsCoreUtils::IsRootDocument(aDocument); + + DocAccessible* parentDocAcc = nullptr; + if (!isRootDoc) { + // XXXaaronl: ideally we would traverse the presshell chain. Since there's + // no easy way to do that, we cheat and use the document hierarchy. + parentDocAcc = GetDocAccessible(aDocument->GetParentDocument()); + NS_ASSERTION(parentDocAcc, + "Can't create an accessible for the document!"); + if (!parentDocAcc) + return nullptr; + } + + // We only create root accessibles for the true root, otherwise create a + // doc accessible. + RefPtr<DocAccessible> docAcc = isRootDoc ? + new RootAccessibleWrap(aDocument, presShell) : + new DocAccessibleWrap(aDocument, presShell); + + // Cache the document accessible into document cache. + mDocAccessibleCache.Put(aDocument, docAcc); + + // Initialize the document accessible. + docAcc->Init(); + + // Bind the document to the tree. + if (isRootDoc) { + if (!ApplicationAcc()->AppendChild(docAcc)) { + docAcc->Shutdown(); + return nullptr; + } + + // Fire reorder event to notify new accessible document has been attached to + // the tree. The reorder event is delivered after the document tree is + // constructed because event processing and tree construction are done by + // the same document. + // Note: don't use AccReorderEvent to avoid coalsecense and special reorder + // events processing. + docAcc->FireDelayedEvent(nsIAccessibleEvent::EVENT_REORDER, + ApplicationAcc()); + + if (IPCAccessibilityActive()) { + nsIDocShell* docShell = aDocument->GetDocShell(); + if (docShell) { + nsCOMPtr<nsITabChild> tabChild = docShell->GetTabChild(); + + // XXX We may need to handle the case that we don't have a tab child + // differently. It may be that this will cause us to fail to notify + // the parent process about important accessible documents. + if (tabChild) { + DocAccessibleChild* ipcDoc = new DocAccessibleChild(docAcc); + docAcc->SetIPCDoc(ipcDoc); + +#if defined(XP_WIN) + IAccessibleHolder holder(CreateHolderFromAccessible(docAcc)); +#endif + + static_cast<TabChild*>(tabChild.get())-> + SendPDocAccessibleConstructor(ipcDoc, nullptr, 0, +#if defined(XP_WIN) + AccessibleWrap::GetChildIDFor(docAcc), + holder +#else + 0, 0 +#endif + ); + } + } + } + } else { + parentDocAcc->BindChildDocument(docAcc); + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("document creation finished", aDocument); + logging::Stack(); + } +#endif + + AddListeners(aDocument, isRootDoc); + return docAcc; +} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager static + +void +DocManager::ClearDocCache() +{ + while (mDocAccessibleCache.Count() > 0) { + auto iter = mDocAccessibleCache.Iter(); + MOZ_ASSERT(!iter.Done()); + DocAccessible* docAcc = iter.UserData(); + NS_ASSERTION(docAcc, + "No doc accessible for the object in doc accessible cache!"); + if (docAcc) { + docAcc->Shutdown(); + } + + iter.Remove(); + } + + // Ensure that all xpcom accessible documents are shut down as well. + while (mXPCDocumentCache.Count() > 0) { + auto iter = mXPCDocumentCache.Iter(); + MOZ_ASSERT(!iter.Done()); + xpcAccessibleDocument* xpcDoc = iter.UserData(); + NS_ASSERTION(xpcDoc, "No xpc doc for the object in xpc doc cache!"); + + if (xpcDoc) { + xpcDoc->Shutdown(); + } + + iter.Remove(); + } +} + +void +DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) +{ + if (!sRemoteDocuments) { + sRemoteDocuments = new nsTArray<DocAccessibleParent*>; + ClearOnShutdown(&sRemoteDocuments); + } + + MOZ_ASSERT(!sRemoteDocuments->Contains(aDoc), + "How did we already have the doc!"); + sRemoteDocuments->AppendElement(aDoc); + ProxyCreated(aDoc, Interfaces::DOCUMENT | Interfaces::HYPERTEXT); +} diff --git a/accessible/base/DocManager.h b/accessible/base/DocManager.h new file mode 100644 index 000000000..b07b6eb01 --- /dev/null +++ b/accessible/base/DocManager.h @@ -0,0 +1,189 @@ +/* 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_a11_DocManager_h_ +#define mozilla_a11_DocManager_h_ + +#include "mozilla/ClearOnShutdown.h" +#include "nsIDocument.h" +#include "nsIDOMEventListener.h" +#include "nsRefPtrHashtable.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" +#include "nsIPresShell.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace a11y { + +class Accessible; +class DocAccessible; +class xpcAccessibleDocument; +class DocAccessibleParent; + +/** + * Manage the document accessible life cycle. + */ +class DocManager : public nsIWebProgressListener, + public nsIDOMEventListener, + public nsSupportsWeakReference +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIDOMEVENTLISTENER + + /** + * Return document accessible for the given DOM node. + */ + DocAccessible* GetDocAccessible(nsIDocument* aDocument); + + /** + * Return document accessible for the given presshell. + */ + DocAccessible* GetDocAccessible(const nsIPresShell* aPresShell) + { + if (!aPresShell) + return nullptr; + + DocAccessible* doc = aPresShell->GetDocAccessible(); + if (doc) + return doc; + + return GetDocAccessible(aPresShell->GetDocument()); + } + + /** + * Search through all document accessibles for an accessible with the given + * unique id. + */ + Accessible* FindAccessibleInCache(nsINode* aNode) const; + + /** + * Called by document accessible when it gets shutdown. + */ + void NotifyOfDocumentShutdown(DocAccessible* aDocument, + nsIDocument* aDOMDocument); + + /** + * Return XPCOM accessible document. + */ + xpcAccessibleDocument* GetXPCDocument(DocAccessible* aDocument); + xpcAccessibleDocument* GetCachedXPCDocument(DocAccessible* aDocument) const + { return mXPCDocumentCache.GetWeak(aDocument); } + + /* + * Notification that a top level document in a content process has gone away. + */ + static void RemoteDocShutdown(DocAccessibleParent* aDoc) + { + DebugOnly<bool> result = sRemoteDocuments->RemoveElement(aDoc); + MOZ_ASSERT(result, "Why didn't we find the document!"); + } + + /* + * Notify of a new top level document in a content process. + */ + static void RemoteDocAdded(DocAccessibleParent* aDoc); + + static const nsTArray<DocAccessibleParent*>* TopLevelRemoteDocs() + { return sRemoteDocuments; } + + /** + * Remove the xpc document for a remote document if there is one. + */ + static void NotifyOfRemoteDocShutdown(DocAccessibleParent* adoc); + + /** + * Get a XPC document for a remote document. + */ + static xpcAccessibleDocument* GetXPCDocument(DocAccessibleParent* aDoc); + static xpcAccessibleDocument* GetCachedXPCDocument(const DocAccessibleParent* aDoc) + { + return sRemoteXPCDocumentCache ? sRemoteXPCDocumentCache->GetWeak(aDoc) + : nullptr; + } + +#ifdef DEBUG + bool IsProcessingRefreshDriverNotification() const; +#endif + +protected: + DocManager(); + virtual ~DocManager() { } + + /** + * Initialize the manager. + */ + bool Init(); + + /** + * Shutdown the manager. + */ + void Shutdown(); + +private: + DocManager(const DocManager&); + DocManager& operator =(const DocManager&); + +private: + /** + * Create an accessible document if it was't created and fire accessibility + * events if needed. + * + * @param aDocument [in] loaded DOM document + * @param aLoadEventType [in] specifies the event type to fire load event, + * if 0 then no event is fired + */ + void HandleDOMDocumentLoad(nsIDocument* aDocument, + uint32_t aLoadEventType); + + /** + * Add/remove 'pagehide' and 'DOMContentLoaded' event listeners. + */ + void AddListeners(nsIDocument *aDocument, bool aAddPageShowListener); + void RemoveListeners(nsIDocument* aDocument); + + /** + * Create document or root accessible. + */ + DocAccessible* CreateDocOrRootAccessible(nsIDocument* aDocument); + + /** + * Clear the cache and shutdown the document accessibles. + */ + void ClearDocCache(); + + typedef nsRefPtrHashtable<nsPtrHashKey<const nsIDocument>, DocAccessible> + DocAccessibleHashtable; + DocAccessibleHashtable mDocAccessibleCache; + + typedef nsRefPtrHashtable<nsPtrHashKey<const DocAccessible>, xpcAccessibleDocument> + XPCDocumentHashtable; + XPCDocumentHashtable mXPCDocumentCache; + static nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, xpcAccessibleDocument>* + sRemoteXPCDocumentCache; + + /* + * The list of remote top level documents. + */ + static StaticAutoPtr<nsTArray<DocAccessibleParent*>> sRemoteDocuments; +}; + +/** + * Return the existing document accessible for the document if any. + * Note this returns the doc accessible for the primary pres shell if there is + * more than one. + */ +inline DocAccessible* +GetExistingDocAccessible(const nsIDocument* aDocument) +{ + nsIPresShell* ps = aDocument->GetShell(); + return ps ? ps->GetDocAccessible() : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11_DocManager_h_ diff --git a/accessible/base/EmbeddedObjCollector.cpp b/accessible/base/EmbeddedObjCollector.cpp new file mode 100644 index 000000000..226d1365f --- /dev/null +++ b/accessible/base/EmbeddedObjCollector.cpp @@ -0,0 +1,81 @@ +/* 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 "EmbeddedObjCollector.h" + +#include "Accessible.h" + +using namespace mozilla::a11y; + +uint32_t +EmbeddedObjCollector::Count() +{ + EnsureNGetIndex(nullptr); + return mObjects.Length(); +} + +Accessible* +EmbeddedObjCollector::GetAccessibleAt(uint32_t aIndex) +{ + Accessible* accessible = mObjects.SafeElementAt(aIndex, nullptr); + if (accessible) + return accessible; + + return EnsureNGetObject(aIndex); +} + +Accessible* +EmbeddedObjCollector::EnsureNGetObject(uint32_t aIndex) +{ + uint32_t childCount = mRoot->ChildCount(); + while (mRootChildIdx < childCount) { + Accessible* child = mRoot->GetChildAt(mRootChildIdx++); + if (child->IsText()) + continue; + + AppendObject(child); + if (mObjects.Length() - 1 == aIndex) + return mObjects[aIndex]; + } + + return nullptr; +} + +int32_t +EmbeddedObjCollector::EnsureNGetIndex(Accessible* aAccessible) +{ + uint32_t childCount = mRoot->ChildCount(); + while (mRootChildIdx < childCount) { + Accessible* child = mRoot->GetChildAt(mRootChildIdx++); + if (child->IsText()) + continue; + + AppendObject(child); + if (child == aAccessible) + return mObjects.Length() - 1; + } + + return -1; +} + +int32_t +EmbeddedObjCollector::GetIndexAt(Accessible* aAccessible) +{ + if (aAccessible->mParent != mRoot) + return -1; + + MOZ_ASSERT(!aAccessible->IsProxy()); + if (aAccessible->mInt.mIndexOfEmbeddedChild != -1) + return aAccessible->mInt.mIndexOfEmbeddedChild; + + return !aAccessible->IsText() ? EnsureNGetIndex(aAccessible) : -1; +} + +void +EmbeddedObjCollector::AppendObject(Accessible* aAccessible) +{ + MOZ_ASSERT(!aAccessible->IsProxy()); + aAccessible->mInt.mIndexOfEmbeddedChild = mObjects.Length(); + mObjects.AppendElement(aAccessible); +} diff --git a/accessible/base/EmbeddedObjCollector.h b/accessible/base/EmbeddedObjCollector.h new file mode 100644 index 000000000..b7a189922 --- /dev/null +++ b/accessible/base/EmbeddedObjCollector.h @@ -0,0 +1,69 @@ +/* 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_EmbeddedObjCollector_h__ +#define mozilla_a11y_EmbeddedObjCollector_h__ + +#include "nsTArray.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Collect embedded objects. Provide quick access to accessible by index and + * vice versa. + */ +class EmbeddedObjCollector final +{ +public: + ~EmbeddedObjCollector() { } + + /** + * Return index of the given accessible within the collection. + */ + int32_t GetIndexAt(Accessible* aAccessible); + + /** + * Return accessible count within the collection. + */ + uint32_t Count(); + + /** + * Return an accessible from the collection at the given index. + */ + Accessible* GetAccessibleAt(uint32_t aIndex); + +protected: + /** + * Ensure accessible at the given index is stored and return it. + */ + Accessible* EnsureNGetObject(uint32_t aIndex); + + /** + * Ensure index for the given accessible is stored and return it. + */ + int32_t EnsureNGetIndex(Accessible* aAccessible); + + // Make sure it's used by Accessible class only. + explicit EmbeddedObjCollector(Accessible* aRoot) : + mRoot(aRoot), mRootChildIdx(0) {} + + /** + * Append the object to collection. + */ + void AppendObject(Accessible* aAccessible); + + friend class Accessible; + + Accessible* mRoot; + uint32_t mRootChildIdx; + nsTArray<Accessible*> mObjects; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/EventQueue.cpp b/accessible/base/EventQueue.cpp new file mode 100644 index 000000000..c90f0aef8 --- /dev/null +++ b/accessible/base/EventQueue.cpp @@ -0,0 +1,344 @@ +/* -*- 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 "EventQueue.h" + +#include "Accessible-inl.h" +#include "nsEventShell.h" +#include "DocAccessible.h" +#include "DocAccessibleChild.h" +#include "nsAccessibilityService.h" +#include "nsTextEquivUtils.h" +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; + +// Defines the number of selection add/remove events in the queue when they +// aren't packed into single selection within event. +const unsigned int kSelChangeCountToPack = 5; + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue +//////////////////////////////////////////////////////////////////////////////// + +bool +EventQueue::PushEvent(AccEvent* aEvent) +{ + NS_ASSERTION((aEvent->mAccessible && aEvent->mAccessible->IsApplication()) || + aEvent->Document() == mDocument, + "Queued event belongs to another document!"); + + if (!mEvents.AppendElement(aEvent)) + return false; + + // Filter events. + CoalesceEvents(); + + if (aEvent->mEventRule != AccEvent::eDoNotEmit && + (aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE || + aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED || + aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) { + PushNameChange(aEvent->mAccessible); + } + return true; +} + +bool +EventQueue::PushNameChange(Accessible* aTarget) +{ + // Fire name change event on parent given that this event hasn't been + // coalesced, the parent's name was calculated from its subtree, and the + // subtree was changed. + if (aTarget->HasNameDependentParent()) { + // Only continue traversing up the tree if it's possible that the parent + // accessible's name can depend on this accessible's name. + Accessible* parent = aTarget->Parent(); + while (parent && + nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeIfReqRule)) { + // Test possible name dependent parent. + if (nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeRule)) { + nsAutoString name; + ENameValueFlag nameFlag = parent->Name(name); + // If name is obtained from subtree, fire name change event. + if (nameFlag == eNameFromSubtree) { + RefPtr<AccEvent> nameChangeEvent = + new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, parent); + return PushEvent(nameChangeEvent); + } + break; + } + parent = parent->Parent(); + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue: private + +void +EventQueue::CoalesceEvents() +{ + NS_ASSERTION(mEvents.Length(), "There should be at least one pending event!"); + uint32_t tail = mEvents.Length() - 1; + AccEvent* tailEvent = mEvents[tail]; + + switch(tailEvent->mEventRule) { + case AccEvent::eCoalesceReorder: + { + DebugOnly<Accessible*> target = tailEvent->mAccessible.get(); + MOZ_ASSERT(target->IsApplication() || + target->IsOuterDoc() || + target->IsXULTree(), + "Only app or outerdoc accessible reorder events are in the queue"); + MOZ_ASSERT(tailEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER, "only reorder events should be queued"); + break; // case eCoalesceReorder + } + + case AccEvent::eCoalesceOfSameType: + { + // Coalesce old events by newer event. + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* accEvent = mEvents[index]; + if (accEvent->mEventType == tailEvent->mEventType && + accEvent->mEventRule == tailEvent->mEventRule) { + accEvent->mEventRule = AccEvent::eDoNotEmit; + return; + } + } + } break; // case eCoalesceOfSameType + + case AccEvent::eCoalesceSelectionChange: + { + AccSelChangeEvent* tailSelChangeEvent = downcast_accEvent(tailEvent); + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule == tailEvent->mEventRule) { + AccSelChangeEvent* thisSelChangeEvent = + downcast_accEvent(thisEvent); + + // Coalesce selection change events within same control. + if (tailSelChangeEvent->mWidget == thisSelChangeEvent->mWidget) { + CoalesceSelChangeEvents(tailSelChangeEvent, thisSelChangeEvent, index); + return; + } + } + } + + } break; // eCoalesceSelectionChange + + case AccEvent::eCoalesceStateChange: + { + // If state change event is duped then ignore previous event. If state + // change event is opposite to previous event then no event is emitted + // (accessible state wasn't changed). + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule != AccEvent::eDoNotEmit && + thisEvent->mEventType == tailEvent->mEventType && + thisEvent->mAccessible == tailEvent->mAccessible) { + AccStateChangeEvent* thisSCEvent = downcast_accEvent(thisEvent); + AccStateChangeEvent* tailSCEvent = downcast_accEvent(tailEvent); + if (thisSCEvent->mState == tailSCEvent->mState) { + thisEvent->mEventRule = AccEvent::eDoNotEmit; + if (thisSCEvent->mIsEnabled != tailSCEvent->mIsEnabled) + tailEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + break; // eCoalesceStateChange + } + + case AccEvent::eCoalesceTextSelChange: + { + // Coalesce older event by newer event for the same selection or target. + // Events for same selection may have different targets and vice versa one + // target may be pointed by different selections (for latter see + // bug 927159). + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule != AccEvent::eDoNotEmit && + thisEvent->mEventType == tailEvent->mEventType) { + AccTextSelChangeEvent* thisTSCEvent = downcast_accEvent(thisEvent); + AccTextSelChangeEvent* tailTSCEvent = downcast_accEvent(tailEvent); + if (thisTSCEvent->mSel == tailTSCEvent->mSel || + thisEvent->mAccessible == tailEvent->mAccessible) + thisEvent->mEventRule = AccEvent::eDoNotEmit; + } + + } + } break; // eCoalesceTextSelChange + + case AccEvent::eRemoveDupes: + { + // Check for repeat events, coalesce newly appended event by more older + // event. + for (uint32_t index = tail - 1; index < tail; index--) { + AccEvent* accEvent = mEvents[index]; + if (accEvent->mEventType == tailEvent->mEventType && + accEvent->mEventRule == tailEvent->mEventRule && + accEvent->mAccessible == tailEvent->mAccessible) { + tailEvent->mEventRule = AccEvent::eDoNotEmit; + return; + } + } + } break; // case eRemoveDupes + + default: + break; // case eAllowDupes, eDoNotEmit + } // switch +} + +void +EventQueue::CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, + AccSelChangeEvent* aThisEvent, + uint32_t aThisIndex) +{ + aTailEvent->mPreceedingCount = aThisEvent->mPreceedingCount + 1; + + // Pack all preceding events into single selection within event + // when we receive too much selection add/remove events. + if (aTailEvent->mPreceedingCount >= kSelChangeCountToPack) { + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_WITHIN; + aTailEvent->mAccessible = aTailEvent->mWidget; + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + + // Do not emit any preceding selection events for same widget if they + // weren't coalesced yet. + if (aThisEvent->mEventType != nsIAccessibleEvent::EVENT_SELECTION_WITHIN) { + for (uint32_t jdx = aThisIndex - 1; jdx < aThisIndex; jdx--) { + AccEvent* prevEvent = mEvents[jdx]; + if (prevEvent->mEventRule == aTailEvent->mEventRule) { + AccSelChangeEvent* prevSelChangeEvent = + downcast_accEvent(prevEvent); + if (prevSelChangeEvent->mWidget == aTailEvent->mWidget) + prevSelChangeEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + return; + } + + // Pack sequential selection remove and selection add events into + // single selection change event. + if (aTailEvent->mPreceedingCount == 1 && + aTailEvent->mItem != aThisEvent->mItem) { + if (aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aTailEvent->mPackedEvent = aThisEvent; + return; + } + + if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aTailEvent->mEventRule = AccEvent::eDoNotEmit; + aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aThisEvent->mPackedEvent = aTailEvent; + return; + } + } + + // Unpack the packed selection change event because we've got one + // more selection add/remove. + if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + if (aThisEvent->mPackedEvent) { + aThisEvent->mPackedEvent->mEventType = + aThisEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd ? + nsIAccessibleEvent::EVENT_SELECTION_ADD : + nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + aThisEvent->mPackedEvent->mEventRule = + AccEvent::eCoalesceSelectionChange; + + aThisEvent->mPackedEvent = nullptr; + } + + aThisEvent->mEventType = + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd ? + nsIAccessibleEvent::EVENT_SELECTION_ADD : + nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + return; + } + + // Convert into selection add since control has single selection but other + // selection events for this control are queued. + if (aTailEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; +} + +//////////////////////////////////////////////////////////////////////////////// +// EventQueue: event queue + +void +EventQueue::ProcessEventQueue() +{ + // Process only currently queued events. + nsTArray<RefPtr<AccEvent> > events; + events.SwapElements(mEvents); + + uint32_t eventCount = events.Length(); +#ifdef A11Y_LOG + if (eventCount > 0 && logging::IsEnabled(logging::eEvents)) { + logging::MsgBegin("EVENTS", "events processing"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + for (uint32_t idx = 0; idx < eventCount; idx++) { + AccEvent* event = events[idx]; + if (event->mEventRule != AccEvent::eDoNotEmit) { + Accessible* target = event->GetAccessible(); + if (!target || target->IsDefunct()) + continue; + + // Dispatch the focus event if target is still focused. + if (event->mEventType == nsIAccessibleEvent::EVENT_FOCUS) { + FocusMgr()->ProcessFocusEvent(event); + continue; + } + + // Dispatch caret moved and text selection change events. + if (event->mEventType == nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED) { + SelectionMgr()->ProcessTextSelChangeEvent(event); + continue; + } + + // Fire selected state change events in support to selection events. + if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + true, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + false, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + event->mIsFromUserInput); + + if (selChangeEvent->mPackedEvent) { + nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible, + states::SELECTED, + (selChangeEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + selChangeEvent->mPackedEvent->mIsFromUserInput); + } + } + + nsEventShell::FireEvent(event); + } + + if (!mDocument) + return; + } +} diff --git a/accessible/base/EventQueue.h b/accessible/base/EventQueue.h new file mode 100644 index 000000000..57d1c236d --- /dev/null +++ b/accessible/base/EventQueue.h @@ -0,0 +1,77 @@ +/* -*- 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 mozilla_a11y_EventQueue_h_ +#define mozilla_a11y_EventQueue_h_ + +#include "AccEvent.h" + +namespace mozilla { +namespace a11y { + +class DocAccessible; + +/** + * Used to organize and coalesce pending events. + */ +class EventQueue +{ +protected: + explicit EventQueue(DocAccessible* aDocument) : mDocument(aDocument) { } + + /** + * Put an accessible event into the queue to process it later. + */ + bool PushEvent(AccEvent* aEvent); + + /** + * Puts a name change event into the queue, if needed. + */ + bool PushNameChange(Accessible* aTarget); + + /** + * Process events from the queue and fires events. + */ + void ProcessEventQueue(); + +private: + EventQueue(const EventQueue&) = delete; + EventQueue& operator = (const EventQueue&) = delete; + + // Event queue processing + /** + * Coalesce redundant events from the queue. + */ + void CoalesceEvents(); + + /** + * Coalesce events from the same subtree. + */ + void CoalesceReorderEvents(AccEvent* aTailEvent); + + /** + * Coalesce two selection change events within the same select control. + */ + void CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, + AccSelChangeEvent* aThisEvent, + uint32_t aThisIndex); + +protected: + /** + * The document accessible reference owning this queue. + */ + DocAccessible* mDocument; + + /** + * Pending events array. Don't make this an AutoTArray; we use + * SwapElements() on it. + */ + nsTArray<RefPtr<AccEvent>> mEvents; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_EventQueue_h_ diff --git a/accessible/base/EventTree.cpp b/accessible/base/EventTree.cpp new file mode 100644 index 000000000..cd4a07ba8 --- /dev/null +++ b/accessible/base/EventTree.cpp @@ -0,0 +1,618 @@ +/* -*- 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 "EventTree.h" + +#include "Accessible-inl.h" +#include "nsEventShell.h" +#include "DocAccessible.h" +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +#include "mozilla/UniquePtr.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TreeMutation class + +EventTree* const TreeMutation::kNoEventTree = reinterpret_cast<EventTree*>(-1); + +TreeMutation::TreeMutation(Accessible* aParent, bool aNoEvents) : + mParent(aParent), mStartIdx(UINT32_MAX), + mStateFlagsCopy(mParent->mStateFlags), + mQueueEvents(!aNoEvents) +{ +#ifdef DEBUG + mIsDone = false; +#endif + +#ifdef A11Y_LOG + if (mQueueEvents && logging::IsEnabled(logging::eEventTree)) { + logging::MsgBegin("EVENTS_TREE", "reordering tree before"); + logging::AccessibleInfo("reordering for", mParent); + Controller()->RootEventTree().Log(); + logging::MsgEnd(); + + if (logging::IsEnabled(logging::eVerbose)) { + logging::Tree("EVENTS_TREE", "Container tree", mParent->Document(), + PrefixLog, static_cast<void*>(this)); + } + } +#endif + + mParent->mStateFlags |= Accessible::eKidsMutating; +} + +TreeMutation::~TreeMutation() +{ + MOZ_ASSERT(mIsDone, "Done() must be called explicitly"); +} + +void +TreeMutation::AfterInsertion(Accessible* aChild) +{ + MOZ_ASSERT(aChild->Parent() == mParent); + + if (static_cast<uint32_t>(aChild->mIndexInParent) < mStartIdx) { + mStartIdx = aChild->mIndexInParent + 1; + } + + if (!mQueueEvents) { + return; + } + + RefPtr<AccShowEvent> ev = new AccShowEvent(aChild); + DebugOnly<bool> added = Controller()->QueueMutationEvent(ev); + MOZ_ASSERT(added); + aChild->SetShowEventTarget(true); +} + +void +TreeMutation::BeforeRemoval(Accessible* aChild, bool aNoShutdown) +{ + MOZ_ASSERT(aChild->Parent() == mParent); + + if (static_cast<uint32_t>(aChild->mIndexInParent) < mStartIdx) { + mStartIdx = aChild->mIndexInParent; + } + + if (!mQueueEvents) { + return; + } + + RefPtr<AccHideEvent> ev = new AccHideEvent(aChild, !aNoShutdown); + if (Controller()->QueueMutationEvent(ev)) { + aChild->SetHideEventTarget(true); + } +} + +void +TreeMutation::Done() +{ + MOZ_ASSERT(mParent->mStateFlags & Accessible::eKidsMutating); + mParent->mStateFlags &= ~Accessible::eKidsMutating; + + uint32_t length = mParent->mChildren.Length(); +#ifdef DEBUG + for (uint32_t idx = 0; idx < mStartIdx && idx < length; idx++) { + MOZ_ASSERT(mParent->mChildren[idx]->mIndexInParent == static_cast<int32_t>(idx), + "Wrong index detected"); + } +#endif + + for (uint32_t idx = mStartIdx; idx < length; idx++) { + mParent->mChildren[idx]->mInt.mIndexOfEmbeddedChild = -1; + mParent->mChildren[idx]->mStateFlags |= Accessible::eGroupInfoDirty; + } + + mParent->mEmbeddedObjCollector = nullptr; + mParent->mStateFlags |= mStateFlagsCopy & Accessible::eKidsMutating; + +#ifdef DEBUG + mIsDone = true; +#endif + +#ifdef A11Y_LOG + if (mQueueEvents && logging::IsEnabled(logging::eEventTree)) { + logging::MsgBegin("EVENTS_TREE", "reordering tree after"); + logging::AccessibleInfo("reordering for", mParent); + Controller()->RootEventTree().Log(); + logging::MsgEnd(); + } +#endif +} + +#ifdef A11Y_LOG +const char* +TreeMutation::PrefixLog(void* aData, Accessible* aAcc) +{ + TreeMutation* thisObj = reinterpret_cast<TreeMutation*>(aData); + if (thisObj->mParent == aAcc) { + return "_X_"; + } + const EventTree& ret = thisObj->Controller()->RootEventTree(); + if (ret.Find(aAcc)) { + return "_с_"; + } + return ""; +} +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// EventTree + +void +EventTree::Shown(Accessible* aChild) +{ + RefPtr<AccShowEvent> ev = new AccShowEvent(aChild); + Controller(aChild)->WithdrawPrecedingEvents(&ev->mPrecedingEvents); + Mutated(ev); +} + +void +EventTree::Hidden(Accessible* aChild, bool aNeedsShutdown) +{ + RefPtr<AccHideEvent> ev = new AccHideEvent(aChild, aNeedsShutdown); + if (!aNeedsShutdown) { + Controller(aChild)->StorePrecedingEvent(ev); + } + Mutated(ev); +} + +void +EventTree::Process(const RefPtr<DocAccessible>& aDeathGrip) +{ + while (mFirst) { + // Skip a node and its subtree if its container is not in the document. + if (mFirst->mContainer->IsInDocument()) { + mFirst->Process(aDeathGrip); + if (aDeathGrip->IsDefunct()) { + return; + } + } + mFirst = Move(mFirst->mNext); + } + + MOZ_ASSERT(mContainer || mDependentEvents.IsEmpty(), + "No container, no events"); + MOZ_ASSERT(!mContainer || !mContainer->IsDefunct(), + "Processing events for defunct container"); + MOZ_ASSERT(!mFireReorder || mContainer, "No target for reorder event"); + + // Fire mutation events. + uint32_t eventsCount = mDependentEvents.Length(); + for (uint32_t jdx = 0; jdx < eventsCount; jdx++) { + AccMutationEvent* mtEvent = mDependentEvents[jdx]; + MOZ_ASSERT(mtEvent->Document(), "No document for event target"); + + // Fire all hide events that has to be fired before this show event. + if (mtEvent->IsShow()) { + AccShowEvent* showEv = downcast_accEvent(mtEvent); + for (uint32_t i = 0; i < showEv->mPrecedingEvents.Length(); i++) { + nsEventShell::FireEvent(showEv->mPrecedingEvents[i]); + if (aDeathGrip->IsDefunct()) { + return; + } + } + } + + nsEventShell::FireEvent(mtEvent); + if (aDeathGrip->IsDefunct()) { + return; + } + + if (mtEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mtEvent->mTextChangeEvent); + if (aDeathGrip->IsDefunct()) { + return; + } + } + + if (mtEvent->IsHide()) { + // Fire menupopup end event before a hide event if a menu goes away. + + // XXX: We don't look into children of hidden subtree to find hiding + // menupopup (as we did prior bug 570275) because we don't do that when + // menu is showing (and that's impossible until bug 606924 is fixed). + // Nevertheless we should do this at least because layout coalesces + // the changes before our processing and we may miss some menupopup + // events. Now we just want to be consistent in content insertion/removal + // handling. + if (mtEvent->mAccessible->ARIARole() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + mtEvent->mAccessible); + if (aDeathGrip->IsDefunct()) { + return; + } + } + + AccHideEvent* hideEvent = downcast_accEvent(mtEvent); + if (hideEvent->NeedsShutdown()) { + aDeathGrip->ShutdownChildrenInSubtree(mtEvent->mAccessible); + } + } + } + + // Fire reorder event at last. + if (mFireReorder) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_REORDER, mContainer); + mContainer->Document()->MaybeNotifyOfValueChange(mContainer); + } + + mDependentEvents.Clear(); +} + +EventTree* +EventTree::FindOrInsert(Accessible* aContainer) +{ + if (!mFirst) { + mFirst.reset(new EventTree(aContainer, mDependentEvents.IsEmpty())); + return mFirst.get(); + } + + EventTree* prevNode = nullptr; + EventTree* node = mFirst.get(); + do { + MOZ_ASSERT(!node->mContainer->IsApplication(), + "No event for application accessible is expected here"); + MOZ_ASSERT(!node->mContainer->IsDefunct(), "An event target has to be alive"); + + // Case of same target. + if (node->mContainer == aContainer) { + return node; + } + + // Check if the given container is contained by a current node + Accessible* top = mContainer ? mContainer : aContainer->Document(); + Accessible* parent = aContainer; + while (parent) { + // Reached a top, no match for a current event. + if (parent == top) { + break; + } + + // We got a match. + if (parent->Parent() == node->mContainer) { + // Reject the node if it's contained by a show/hide event target + uint32_t evCount = node->mDependentEvents.Length(); + for (uint32_t idx = 0; idx < evCount; idx++) { + AccMutationEvent* ev = node->mDependentEvents[idx]; + if (ev->GetAccessible() == parent) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eEventTree)) { + logging::MsgBegin("EVENTS_TREE", + "Rejecting node contained by show/hide"); + logging::AccessibleInfo("Node", aContainer); + logging::MsgEnd(); + } +#endif + // If the node is rejected, then check if it has related hide event + // on stack, and if so, then connect it to the parent show event. + if (ev->IsShow()) { + AccShowEvent* showEv = downcast_accEvent(ev); + Controller(aContainer)-> + WithdrawPrecedingEvents(&showEv->mPrecedingEvents); + } + return nullptr; + } + } + + return node->FindOrInsert(aContainer); + } + + parent = parent->Parent(); + MOZ_ASSERT(parent, "Wrong tree"); + } + + // If the given container contains a current node + // then + // if show or hide of the given node contains a grand parent of the current node + // then ignore the current node and its show and hide events + // otherwise ignore the current node, but not its show and hide events + Accessible* curParent = node->mContainer; + while (curParent && !curParent->IsDoc()) { + if (curParent->Parent() != aContainer) { + curParent = curParent->Parent(); + continue; + } + + // Insert the tail node into the hierarchy between the current node and + // its parent. + node->mFireReorder = false; + UniquePtr<EventTree>& nodeOwnerRef = prevNode ? prevNode->mNext : mFirst; + UniquePtr<EventTree> newNode(new EventTree(aContainer, mDependentEvents.IsEmpty())); + newNode->mFirst = Move(nodeOwnerRef); + nodeOwnerRef = Move(newNode); + nodeOwnerRef->mNext = Move(node->mNext); + + // Check if a next node is contained by the given node too, and move them + // under the given node if so. + prevNode = nodeOwnerRef.get(); + node = nodeOwnerRef->mNext.get(); + UniquePtr<EventTree>* nodeRef = &nodeOwnerRef->mNext; + EventTree* insNode = nodeOwnerRef->mFirst.get(); + while (node) { + Accessible* curParent = node->mContainer; + while (curParent && !curParent->IsDoc()) { + if (curParent->Parent() != aContainer) { + curParent = curParent->Parent(); + continue; + } + + MOZ_ASSERT(!insNode->mNext); + + node->mFireReorder = false; + insNode->mNext = Move(*nodeRef); + insNode = insNode->mNext.get(); + + prevNode->mNext = Move(node->mNext); + node = prevNode; + break; + } + + prevNode = node; + nodeRef = &node->mNext; + node = node->mNext.get(); + } + + return nodeOwnerRef.get(); + } + + prevNode = node; + } while ((node = node->mNext.get())); + + MOZ_ASSERT(prevNode, "Nowhere to insert"); + MOZ_ASSERT(!prevNode->mNext, "Taken by another node"); + + // If 'this' node contains the given container accessible, then + // do not emit a reorder event for the container + // if a dependent show event target contains the given container then do not + // emit show / hide events (see Process() method) + + prevNode->mNext.reset(new EventTree(aContainer, mDependentEvents.IsEmpty())); + return prevNode->mNext.get(); +} + +void +EventTree::Clear() +{ + mFirst = nullptr; + mNext = nullptr; + mContainer = nullptr; + + uint32_t eventsCount = mDependentEvents.Length(); + for (uint32_t jdx = 0; jdx < eventsCount; jdx++) { + mDependentEvents[jdx]->mEventType = AccEvent::eDoNotEmit; + AccHideEvent* ev = downcast_accEvent(mDependentEvents[jdx]); + if (ev && ev->NeedsShutdown()) { + ev->Document()->ShutdownChildrenInSubtree(ev->mAccessible); + } + } + mDependentEvents.Clear(); +} + +const EventTree* +EventTree::Find(const Accessible* aContainer) const +{ + const EventTree* et = this; + while (et) { + if (et->mContainer == aContainer) { + return et; + } + + if (et->mFirst) { + et = et->mFirst.get(); + const EventTree* cet = et->Find(aContainer); + if (cet) { + return cet; + } + } + + et = et->mNext.get(); + const EventTree* cet = et->Find(aContainer); + if (cet) { + return cet; + } + } + + return nullptr; +} + +#ifdef A11Y_LOG +void +EventTree::Log(uint32_t aLevel) const +{ + if (aLevel == UINT32_MAX) { + if (mFirst) { + mFirst->Log(0); + } + return; + } + + for (uint32_t i = 0; i < aLevel; i++) { + printf(" "); + } + logging::AccessibleInfo("container", mContainer); + + for (uint32_t i = 0; i < mDependentEvents.Length(); i++) { + AccMutationEvent* ev = mDependentEvents[i]; + if (ev->IsShow()) { + for (uint32_t i = 0; i < aLevel + 1; i++) { + printf(" "); + } + logging::AccessibleInfo("shown", ev->mAccessible); + + AccShowEvent* showEv = downcast_accEvent(ev); + for (uint32_t i = 0; i < showEv->mPrecedingEvents.Length(); i++) { + for (uint32_t j = 0; j < aLevel + 1; j++) { + printf(" "); + } + logging::AccessibleInfo("preceding", + showEv->mPrecedingEvents[i]->mAccessible); + } + } + else { + for (uint32_t i = 0; i < aLevel + 1; i++) { + printf(" "); + } + logging::AccessibleInfo("hidden", ev->mAccessible); + } + } + + if (mFirst) { + mFirst->Log(aLevel + 1); + } + + if (mNext) { + mNext->Log(aLevel); + } +} +#endif + +void +EventTree::Mutated(AccMutationEvent* aEv) +{ + // If shown or hidden node is a root of previously mutated subtree, then + // discard those subtree mutations as we are no longer interested in them. + UniquePtr<EventTree>* node = &mFirst; + while (*node) { + Accessible* cntr = (*node)->mContainer; + while (cntr != mContainer) { + if (cntr == aEv->mAccessible) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eEventTree)) { + logging::MsgBegin("EVENTS_TREE", "Trim subtree"); + logging::AccessibleInfo("Show/hide container", aEv->mAccessible); + logging::AccessibleInfo("Trimmed subtree root", (*node)->mContainer); + logging::MsgEnd(); + } +#endif + + // If the new hide is part of a move and it contains existing child + // shows, then move preceding events from the child shows to the buffer, + // so the ongoing show event will pick them up. + if (aEv->IsHide()) { + AccHideEvent* hideEv = downcast_accEvent(aEv); + if (!hideEv->mNeedsShutdown) { + for (uint32_t i = 0; i < (*node)->mDependentEvents.Length(); i++) { + AccMutationEvent* childEv = (*node)->mDependentEvents[i]; + if (childEv->IsShow()) { + AccShowEvent* childShowEv = downcast_accEvent(childEv); + if (childShowEv->mPrecedingEvents.Length() > 0) { + Controller(mContainer)->StorePrecedingEvents( + mozilla::Move(childShowEv->mPrecedingEvents)); + } + } + } + } + } + // If the new show contains existing child shows, then move preceding + // events from the child shows to the new show. + else if (aEv->IsShow()) { + AccShowEvent* showEv = downcast_accEvent(aEv); + for (uint32_t i = 0; (*node)->mDependentEvents.Length(); i++) { + AccMutationEvent* childEv = (*node)->mDependentEvents[i]; + if (childEv->IsShow()) { + AccShowEvent* showChildEv = downcast_accEvent(childEv); + if (showChildEv->mPrecedingEvents.Length() > 0) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eEventTree)) { + logging::MsgBegin("EVENTS_TREE", "Adopt preceding events"); + logging::AccessibleInfo("Parent", aEv->mAccessible); + for (uint32_t j = 0; j < showChildEv->mPrecedingEvents.Length(); j++) { + logging::AccessibleInfo("Adoptee", + showChildEv->mPrecedingEvents[i]->mAccessible); + } + logging::MsgEnd(); + } +#endif + showEv->mPrecedingEvents.AppendElements(showChildEv->mPrecedingEvents); + } + } + } + } + + *node = Move((*node)->mNext); + break; + } + cntr = cntr->Parent(); + } + if (cntr == aEv->mAccessible) { + continue; + } + node = &(*node)->mNext; + } + + AccMutationEvent* prevEvent = mDependentEvents.SafeLastElement(nullptr); + mDependentEvents.AppendElement(aEv); + + // Coalesce text change events from this hide/show event and the previous one. + if (prevEvent && aEv->mEventType == prevEvent->mEventType) { + if (aEv->IsHide()) { + // XXX: we need a way to ignore SplitNode and JoinNode() when they do not + // affect the text within the hypertext. + AccTextChangeEvent* prevTextEvent = prevEvent->mTextChangeEvent; + if (prevTextEvent) { + AccHideEvent* hideEvent = downcast_accEvent(aEv); + AccHideEvent* prevHideEvent = downcast_accEvent(prevEvent); + + if (prevHideEvent->mNextSibling == hideEvent->mAccessible) { + hideEvent->mAccessible->AppendTextTo(prevTextEvent->mModifiedText); + } + else if (prevHideEvent->mPrevSibling == hideEvent->mAccessible) { + uint32_t oldLen = prevTextEvent->GetLength(); + hideEvent->mAccessible->AppendTextTo(prevTextEvent->mModifiedText); + prevTextEvent->mStart -= prevTextEvent->GetLength() - oldLen; + } + + hideEvent->mTextChangeEvent.swap(prevEvent->mTextChangeEvent); + } + } + else { + AccTextChangeEvent* prevTextEvent = prevEvent->mTextChangeEvent; + if (prevTextEvent) { + if (aEv->mAccessible->IndexInParent() == + prevEvent->mAccessible->IndexInParent() + 1) { + // If tail target was inserted after this target, i.e. tail target is next + // sibling of this target. + aEv->mAccessible->AppendTextTo(prevTextEvent->mModifiedText); + } + else if (aEv->mAccessible->IndexInParent() == + prevEvent->mAccessible->IndexInParent() - 1) { + // If tail target was inserted before this target, i.e. tail target is + // previous sibling of this target. + nsAutoString startText; + aEv->mAccessible->AppendTextTo(startText); + prevTextEvent->mModifiedText = startText + prevTextEvent->mModifiedText; + prevTextEvent->mStart -= startText.Length(); + } + + aEv->mTextChangeEvent.swap(prevEvent->mTextChangeEvent); + } + } + } + + // Create a text change event caused by this hide/show event. When a node is + // hidden/removed or shown/appended, the text in an ancestor hyper text will + // lose or get new characters. + if (aEv->mTextChangeEvent || !mContainer->IsHyperText()) { + return; + } + + nsAutoString text; + aEv->mAccessible->AppendTextTo(text); + if (text.IsEmpty()) { + return; + } + + int32_t offset = mContainer->AsHyperText()->GetChildOffset(aEv->mAccessible); + aEv->mTextChangeEvent = + new AccTextChangeEvent(mContainer, offset, text, aEv->IsShow(), + aEv->mIsFromUserInput ? eFromUserInput : eNoUserInput); +} diff --git a/accessible/base/EventTree.h b/accessible/base/EventTree.h new file mode 100644 index 000000000..932ceaf4e --- /dev/null +++ b/accessible/base/EventTree.h @@ -0,0 +1,121 @@ +/* -*- 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 mozilla_a11y_EventTree_h_ +#define mozilla_a11y_EventTree_h_ + +#include "AccEvent.h" +#include "Accessible.h" + +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace a11y { + +/** + * This class makes sure required tasks are done before and after tree + * mutations. Currently this only includes group info invalidation. You must + * have an object of this class on the stack when calling methods that mutate + * the accessible tree. + */ +class TreeMutation final +{ +public: + static const bool kNoEvents = true; + static const bool kNoShutdown = true; + + explicit TreeMutation(Accessible* aParent, bool aNoEvents = false); + ~TreeMutation(); + + void AfterInsertion(Accessible* aChild); + void BeforeRemoval(Accessible* aChild, bool aNoShutdown = false); + void Done(); + +private: + NotificationController* Controller() const + { return mParent->Document()->Controller(); } + + static EventTree* const kNoEventTree; + +#ifdef A11Y_LOG + static const char* PrefixLog(void* aData, Accessible*); +#endif + + Accessible* mParent; + uint32_t mStartIdx; + uint32_t mStateFlagsCopy; + + /* + * True if mutation events should be queued. + */ + bool mQueueEvents; + +#ifdef DEBUG + bool mIsDone; +#endif +}; + + +/** + * A mutation events coalescence structure. + */ +class EventTree final { +public: + EventTree() : + mFirst(nullptr), mNext(nullptr), mContainer(nullptr), mFireReorder(false) { } + explicit EventTree(Accessible* aContainer, bool aFireReorder) : + mFirst(nullptr), mNext(nullptr), mContainer(aContainer), + mFireReorder(aFireReorder) { } + ~EventTree() { Clear(); } + + void Shown(Accessible* aTarget); + void Hidden(Accessible*, bool); + + /** + * Return an event tree node for the given accessible. + */ + const EventTree* Find(const Accessible* aContainer) const; + + /** + * Add a mutation event to this event tree. + */ + void Mutated(AccMutationEvent* aEv); + +#ifdef A11Y_LOG + void Log(uint32_t aLevel = UINT32_MAX) const; +#endif + +private: + /** + * Processes the event queue and fires events. + */ + void Process(const RefPtr<DocAccessible>& aDeathGrip); + + /** + * Return an event subtree for the given accessible. + */ + EventTree* FindOrInsert(Accessible* aContainer); + + void Clear(); + + UniquePtr<EventTree> mFirst; + UniquePtr<EventTree> mNext; + + Accessible* mContainer; + nsTArray<RefPtr<AccMutationEvent>> mDependentEvents; + bool mFireReorder; + + static NotificationController* Controller(Accessible* aAcc) + { return aAcc->Document()->Controller(); } + + friend class NotificationController; +}; + + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_EventQueue_h_ diff --git a/accessible/base/Filters.cpp b/accessible/base/Filters.cpp new file mode 100644 index 000000000..ea74e3602 --- /dev/null +++ b/accessible/base/Filters.cpp @@ -0,0 +1,51 @@ +/* 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 "Filters.h" + +#include "Accessible-inl.h" +#include "nsAccUtils.h" +#include "Role.h" +#include "States.h" + +using namespace mozilla::a11y; +using namespace mozilla::a11y::filters; + +uint32_t +filters::GetSelected(Accessible* aAccessible) +{ + if (aAccessible->State() & states::SELECTED) + return eMatch | eSkipSubtree; + + return eSkip; +} + +uint32_t +filters::GetSelectable(Accessible* aAccessible) +{ + if (aAccessible->InteractiveState() & states::SELECTABLE) + return eMatch | eSkipSubtree; + + return eSkip; +} + +uint32_t +filters::GetRow(Accessible* aAccessible) +{ + a11y::role role = aAccessible->Role(); + if (role == roles::ROW) + return eMatch | eSkipSubtree; + + // Look for rows inside rowgroup. + if (role == roles::GROUPING) + return eSkip; + + return eSkipSubtree; +} + +uint32_t +filters::GetCell(Accessible* aAccessible) +{ + return aAccessible->IsTableCell() ? eMatch : eSkipSubtree; +} diff --git a/accessible/base/Filters.h b/accessible/base/Filters.h new file mode 100644 index 000000000..8ff2bc558 --- /dev/null +++ b/accessible/base/Filters.h @@ -0,0 +1,50 @@ +/* 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_Filters_h__ +#define mozilla_a11y_Filters_h__ + +#include <stdint.h> + +/** + * Predefined filters used for nsAccIterator and nsAccCollector. + */ +namespace mozilla { +namespace a11y { + +class Accessible; + +namespace filters { + +enum EResult { + eSkip = 0, + eMatch = 1, + eSkipSubtree = 2 +}; + +/** + * Return true if the traversed accessible complies with filter. + */ +typedef uint32_t (*FilterFuncPtr) (Accessible*); + +/** + * Matches selected/selectable accessibles in subtree. + */ +uint32_t GetSelected(Accessible* aAccessible); +uint32_t GetSelectable(Accessible* aAccessible); + +/** + * Matches row accessibles in subtree. + */ +uint32_t GetRow(Accessible* aAccessible); + +/** + * Matches cell accessibles in children. + */ +uint32_t GetCell(Accessible* aAccessible); +} // namespace filters +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/FocusManager.cpp b/accessible/base/FocusManager.cpp new file mode 100644 index 000000000..e3575415d --- /dev/null +++ b/accessible/base/FocusManager.cpp @@ -0,0 +1,404 @@ +/* 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 "FocusManager.h" + +#include "Accessible-inl.h" +#include "AccIterator.h" +#include "DocAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsEventShell.h" +#include "Role.h" + +#include "nsFocusManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/dom/Element.h" + +namespace mozilla { +namespace a11y { + +FocusManager::FocusManager() +{ +} + +FocusManager::~FocusManager() +{ +} + +Accessible* +FocusManager::FocusedAccessible() const +{ + if (mActiveItem) + return mActiveItem; + + nsINode* focusedNode = FocusedDOMNode(); + if (focusedNode) { + DocAccessible* doc = + GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); + return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) : nullptr; + } + + return nullptr; +} + +bool +FocusManager::IsFocused(const Accessible* aAccessible) const +{ + if (mActiveItem) + return mActiveItem == aAccessible; + + nsINode* focusedNode = FocusedDOMNode(); + if (focusedNode) { + // XXX: Before getting an accessible for node having a DOM focus make sure + // they belong to the same document because it can trigger unwanted document + // accessible creation for temporary about:blank document. Without this + // peculiarity we would end up with plain implementation based on + // FocusedAccessible() method call. Make sure this issue is fixed in + // bug 638465. + if (focusedNode->OwnerDoc() == aAccessible->GetNode()->OwnerDoc()) { + DocAccessible* doc = + GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); + return aAccessible == + (doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) : nullptr); + } + } + return false; +} + +bool +FocusManager::IsFocusWithin(const Accessible* aContainer) const +{ + Accessible* child = FocusedAccessible(); + while (child) { + if (child == aContainer) + return true; + + child = child->Parent(); + } + return false; +} + +FocusManager::FocusDisposition +FocusManager::IsInOrContainsFocus(const Accessible* aAccessible) const +{ + Accessible* focus = FocusedAccessible(); + if (!focus) + return eNone; + + // If focused. + if (focus == aAccessible) + return eFocused; + + // If contains the focus. + Accessible* child = focus->Parent(); + while (child) { + if (child == aAccessible) + return eContainsFocus; + + child = child->Parent(); + } + + // If contained by focus. + child = aAccessible->Parent(); + while (child) { + if (child == focus) + return eContainedByFocus; + + child = child->Parent(); + } + + return eNone; +} + +void +FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusNotificationTarget("DOM focus", "Target", aTarget); +#endif + + mActiveItem = nullptr; + + nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); + if (targetNode) { + DocAccessible* document = + GetAccService()->GetDocAccessible(targetNode->OwnerDoc()); + if (document) { + // Set selection listener for focused element. + if (targetNode->IsElement()) + SelectionMgr()->SetControlSelectionListener(targetNode->AsElement()); + + document->HandleNotification<FocusManager, nsINode> + (this, &FocusManager::ProcessDOMFocus, targetNode); + } + } +} + +void +FocusManager::NotifyOfDOMBlur(nsISupports* aTarget) +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusNotificationTarget("DOM blur", "Target", aTarget); +#endif + + mActiveItem = nullptr; + + // If DOM document stays focused then fire accessible focus event to process + // the case when no element within this DOM document will be focused. + nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget)); + if (targetNode && targetNode->OwnerDoc() == FocusedDOMDocument()) { + nsIDocument* DOMDoc = targetNode->OwnerDoc(); + DocAccessible* document = + GetAccService()->GetDocAccessible(DOMDoc); + if (document) { + // Clear selection listener for previously focused element. + if (targetNode->IsElement()) + SelectionMgr()->ClearControlSelectionListener(); + + document->HandleNotification<FocusManager, nsINode> + (this, &FocusManager::ProcessDOMFocus, DOMDoc); + } + } +} + +void +FocusManager::ActiveItemChanged(Accessible* aItem, bool aCheckIfActive) +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusNotificationTarget("active item changed", "Item", aItem); +#endif + + // Nothing changed, happens for XUL trees and HTML selects. + if (aItem && aItem == mActiveItem) + return; + + mActiveItem = nullptr; + + if (aItem && aCheckIfActive) { + Accessible* widget = aItem->ContainerWidget(); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::ActiveWidget(widget); +#endif + if (!widget || !widget->IsActiveWidget() || !widget->AreItemsOperable()) + return; + } + mActiveItem = aItem; + + // If active item is changed then fire accessible focus event on it, otherwise + // if there's no an active item then fire focus event to accessible having + // DOM focus. + Accessible* target = FocusedAccessible(); + if (target) + DispatchFocusEvent(target->Document(), target); +} + +void +FocusManager::ForceFocusEvent() +{ + nsINode* focusedNode = FocusedDOMNode(); + if (focusedNode) { + DocAccessible* document = + GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); + if (document) { + document->HandleNotification<FocusManager, nsINode> + (this, &FocusManager::ProcessDOMFocus, focusedNode); + } + } +} + +void +FocusManager::DispatchFocusEvent(DocAccessible* aDocument, + Accessible* aTarget) +{ + NS_PRECONDITION(aDocument, "No document for focused accessible!"); + if (aDocument) { + RefPtr<AccEvent> event = + new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, + eAutoDetect, AccEvent::eCoalesceOfSameType); + aDocument->FireDelayedEvent(event); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusDispatched(aTarget); +#endif + } +} + +void +FocusManager::ProcessDOMFocus(nsINode* aTarget) +{ +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusNotificationTarget("process DOM focus", "Target", aTarget); +#endif + + DocAccessible* document = + GetAccService()->GetDocAccessible(aTarget->OwnerDoc()); + if (!document) + return; + + Accessible* target = document->GetAccessibleEvenIfNotInMapOrContainer(aTarget); + if (target) { + // Check if still focused. Otherwise we can end up with storing the active + // item for control that isn't focused anymore. + nsINode* focusedNode = FocusedDOMNode(); + if (!focusedNode) + return; + + Accessible* DOMFocus = + document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); + if (target != DOMFocus) + return; + + Accessible* activeItem = target->CurrentItem(); + if (activeItem) { + mActiveItem = activeItem; + target = activeItem; + } + + DispatchFocusEvent(document, target); + } +} + +void +FocusManager::ProcessFocusEvent(AccEvent* aEvent) +{ + NS_PRECONDITION(aEvent->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS, + "Focus event is expected!"); + + // Emit focus event if event target is the active item. Otherwise then check + // if it's still focused and then update active item and emit focus event. + Accessible* target = aEvent->GetAccessible(); + if (target != mActiveItem) { + + // Check if still focused. Otherwise we can end up with storing the active + // item for control that isn't focused anymore. + DocAccessible* document = aEvent->Document(); + nsINode* focusedNode = FocusedDOMNode(); + if (!focusedNode) + return; + + Accessible* DOMFocus = + document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); + if (target != DOMFocus) + return; + + Accessible* activeItem = target->CurrentItem(); + if (activeItem) { + mActiveItem = activeItem; + target = activeItem; + } + } + + // Fire menu start/end events for ARIA menus. + if (target->IsARIARole(nsGkAtoms::menuitem)) { + // The focus was moved into menu. + Accessible* ARIAMenubar = nullptr; + for (Accessible* parent = target->Parent(); parent; parent = parent->Parent()) { + if (parent->IsARIARole(nsGkAtoms::menubar)) { + ARIAMenubar = parent; + break; + } + + // Go up in the parent chain of the menu hierarchy. + if (!parent->IsARIARole(nsGkAtoms::menuitem) && + !parent->IsARIARole(nsGkAtoms::menu)) { + break; + } + } + + if (ARIAMenubar != mActiveARIAMenubar) { + // Leaving ARIA menu. Fire menu_end event on current menubar. + if (mActiveARIAMenubar) { + RefPtr<AccEvent> menuEndEvent = + new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, + aEvent->FromUserInput()); + nsEventShell::FireEvent(menuEndEvent); + } + + mActiveARIAMenubar = ARIAMenubar; + + // Entering ARIA menu. Fire menu_start event. + if (mActiveARIAMenubar) { + RefPtr<AccEvent> menuStartEvent = + new AccEvent(nsIAccessibleEvent::EVENT_MENU_START, + mActiveARIAMenubar, aEvent->FromUserInput()); + nsEventShell::FireEvent(menuStartEvent); + } + } + } else if (mActiveARIAMenubar) { + // Focus left a menu. Fire menu_end event. + RefPtr<AccEvent> menuEndEvent = + new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar, + aEvent->FromUserInput()); + nsEventShell::FireEvent(menuEndEvent); + + mActiveARIAMenubar = nullptr; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) + logging::FocusNotificationTarget("fire focus event", "Target", target); +#endif + + // Reset cached caret value. The cache will be updated upon processing the + // next caret move event. This ensures that we will return the correct caret + // offset before the caret move event is handled. + SelectionMgr()->ResetCaretOffset(); + + RefPtr<AccEvent> focusEvent = + new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, target, aEvent->FromUserInput()); + nsEventShell::FireEvent(focusEvent); + + // Fire scrolling_start event when the document receives the focus if it has + // an anchor jump. If an accessible within the document receive the focus + // then null out the anchor jump because it no longer applies. + DocAccessible* targetDocument = target->Document(); + Accessible* anchorJump = targetDocument->AnchorJump(); + if (anchorJump) { + if (target == targetDocument) { + // XXX: bug 625699, note in some cases the node could go away before we + // we receive focus event, for example if the node is removed from DOM. + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START, + anchorJump, aEvent->FromUserInput()); + } + targetDocument->SetAnchorJump(nullptr); + } +} + +nsINode* +FocusManager::FocusedDOMNode() const +{ + nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager(); + nsIContent* focusedElm = DOMFocusManager->GetFocusedContent(); + + // No focus on remote target elements like xul:browser having DOM focus and + // residing in chrome process because it means an element in content process + // keeps the focus. + if (focusedElm) { + if (EventStateManager::IsRemoteTarget(focusedElm)) { + return nullptr; + } + return focusedElm; + } + + // Otherwise the focus can be on DOM document. + nsPIDOMWindowOuter* focusedWnd = DOMFocusManager->GetFocusedWindow(); + return focusedWnd ? focusedWnd->GetExtantDoc() : nullptr; +} + +nsIDocument* +FocusManager::FocusedDOMDocument() const +{ + nsINode* focusedNode = FocusedDOMNode(); + return focusedNode ? focusedNode->OwnerDoc() : nullptr; +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/FocusManager.h b/accessible/base/FocusManager.h new file mode 100644 index 000000000..633f9ccb2 --- /dev/null +++ b/accessible/base/FocusManager.h @@ -0,0 +1,132 @@ +/* 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_FocusManager_h_ +#define mozilla_a11y_FocusManager_h_ + +class nsINode; +class nsIDocument; +class nsISupports; + +namespace mozilla { +namespace a11y { + +class AccEvent; +class Accessible; +class DocAccessible; + +/** + * Manage the accessible focus. Used to fire and process accessible events. + */ +class FocusManager +{ +public: + virtual ~FocusManager(); + + /** + * Return a focused accessible. + */ + Accessible* FocusedAccessible() const; + + /** + * Return true if given accessible is focused. + */ + bool IsFocused(const Accessible* aAccessible) const; + + /** + * Return true if the given accessible is an active item, i.e. an item that + * is current within the active widget. + */ + inline bool IsActiveItem(const Accessible* aAccessible) + { return aAccessible == mActiveItem; } + + /** + * Return true if given DOM node has DOM focus. + */ + inline bool HasDOMFocus(const nsINode* aNode) const + { return aNode == FocusedDOMNode(); } + + /** + * Return true if focused accessible is within the given container. + */ + bool IsFocusWithin(const Accessible* aContainer) const; + + /** + * Return whether the given accessible is focused or contains the focus or + * contained by focused accessible. + */ + enum FocusDisposition { + eNone, + eFocused, + eContainsFocus, + eContainedByFocus + }; + FocusDisposition IsInOrContainsFocus(const Accessible* aAccessible) const; + + ////////////////////////////////////////////////////////////////////////////// + // Focus notifications and processing (don't use until you know what you do). + + /** + * Called when DOM focus event is fired. + */ + void NotifyOfDOMFocus(nsISupports* aTarget); + + /** + * Called when DOM blur event is fired. + */ + void NotifyOfDOMBlur(nsISupports* aTarget); + + /** + * Called when active item is changed. Note: must be called when accessible + * tree is up to date. + */ + void ActiveItemChanged(Accessible* aItem, bool aCheckIfActive = true); + + /** + * Dispatch delayed focus event for the current focus accessible. + */ + void ForceFocusEvent(); + + /** + * Dispatch delayed focus event for the given target. + */ + void DispatchFocusEvent(DocAccessible* aDocument, Accessible* aTarget); + + /** + * Process DOM focus notification. + */ + void ProcessDOMFocus(nsINode* aTarget); + + /** + * Process the delayed accessible event. + * do. + */ + void ProcessFocusEvent(AccEvent* aEvent); + +protected: + FocusManager(); + +private: + FocusManager(const FocusManager&); + FocusManager& operator =(const FocusManager&); + + /** + * Return DOM node having DOM focus. + */ + nsINode* FocusedDOMNode() const; + + /** + * Return DOM document having DOM focus. + */ + nsIDocument* FocusedDOMDocument() const; + +private: + RefPtr<Accessible> mActiveItem; + RefPtr<Accessible> mActiveARIAMenubar; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/Logging.cpp b/accessible/base/Logging.cpp new file mode 100644 index 000000000..afc37ef85 --- /dev/null +++ b/accessible/base/Logging.cpp @@ -0,0 +1,1039 @@ +/* -*- 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 "Logging.h" + +#include "Accessible-inl.h" +#include "AccEvent.h" +#include "DocAccessible.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" +#include "OuterDocAccessible.h" + +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsISelectionPrivate.h" +#include "nsTraceRefcnt.h" +#include "nsIWebProgress.h" +#include "prenv.h" +#include "nsIDocShellTreeItem.h" +#include "nsIURI.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Logging helpers + +static uint32_t sModules = 0; + +struct ModuleRep { + const char* mStr; + logging::EModules mModule; +}; + +static ModuleRep sModuleMap[] = { + { "docload", logging::eDocLoad }, + { "doccreate", logging::eDocCreate }, + { "docdestroy", logging::eDocDestroy }, + { "doclifecycle", logging::eDocLifeCycle }, + + { "events", logging::eEvents }, + { "eventTree", logging::eEventTree }, + { "platforms", logging::ePlatforms }, + { "text", logging::eText }, + { "tree", logging::eTree }, + + { "DOMEvents", logging::eDOMEvents }, + { "focus", logging::eFocus }, + { "selection", logging::eSelection }, + { "notifications", logging::eNotifications }, + + { "stack", logging::eStack }, + { "verbose", logging::eVerbose } +}; + +static void +EnableLogging(const char* aModulesStr) +{ + sModules = 0; + if (!aModulesStr) + return; + + const char* token = aModulesStr; + while (*token != '\0') { + size_t tokenLen = strcspn(token, ","); + for (unsigned int idx = 0; idx < ArrayLength(sModuleMap); idx++) { + if (strncmp(token, sModuleMap[idx].mStr, tokenLen) == 0) { +#if !defined(MOZ_PROFILING) && (!defined(DEBUG) || defined(MOZ_OPTIMIZE)) + // Stack tracing on profiling enabled or debug not optimized builds. + if (strncmp(token, "stack", tokenLen) == 0) + break; +#endif + sModules |= sModuleMap[idx].mModule; + printf("\n\nmodule enabled: %s\n", sModuleMap[idx].mStr); + break; + } + } + token += tokenLen; + + if (*token == ',') + token++; // skip ',' char + } +} + +static void +LogDocURI(nsIDocument* aDocumentNode) +{ + printf("uri: %s", aDocumentNode->GetDocumentURI()->GetSpecOrDefault().get()); +} + +static void +LogDocShellState(nsIDocument* aDocumentNode) +{ + printf("docshell busy: "); + + nsAutoCString docShellBusy; + nsCOMPtr<nsIDocShell> docShell = aDocumentNode->GetDocShell(); + uint32_t busyFlags = nsIDocShell::BUSY_FLAGS_NONE; + docShell->GetBusyFlags(&busyFlags); + if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { + printf("'none'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_BUSY) { + printf("'busy'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_BEFORE_PAGE_LOAD) { + printf(", 'before page load'"); + } + if (busyFlags & nsIDocShell::BUSY_FLAGS_PAGE_LOADING) { + printf(", 'page loading'"); + } +} + +static void +LogDocType(nsIDocument* aDocumentNode) +{ + if (aDocumentNode->IsActive()) { + bool isContent = nsCoreUtils::IsContentDocument(aDocumentNode); + printf("%s document", (isContent ? "content" : "chrome")); + } else { + printf("document type: [failed]");\ + } +} + +static void +LogDocShellTree(nsIDocument* aDocumentNode) +{ + if (aDocumentNode->IsActive()) { + nsCOMPtr<nsIDocShellTreeItem> treeItem(aDocumentNode->GetDocShell()); + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + treeItem->GetParent(getter_AddRefs(parentTreeItem)); + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetRootTreeItem(getter_AddRefs(rootTreeItem)); + printf("docshell hierarchy, parent: %p, root: %p, is tab document: %s;", + static_cast<void*>(parentTreeItem), static_cast<void*>(rootTreeItem), + (nsCoreUtils::IsTabDocument(aDocumentNode) ? "yes" : "no")); + } +} + +static void +LogDocState(nsIDocument* aDocumentNode) +{ + const char* docState = nullptr; + nsIDocument::ReadyState docStateFlag = aDocumentNode->GetReadyStateEnum(); + switch (docStateFlag) { + case nsIDocument::READYSTATE_UNINITIALIZED: + docState = "uninitialized"; + break; + case nsIDocument::READYSTATE_LOADING: + docState = "loading"; + break; + case nsIDocument::READYSTATE_INTERACTIVE: + docState = "interactive"; + break; + case nsIDocument::READYSTATE_COMPLETE: + docState = "complete"; + break; + } + + printf("doc state: %s", docState); + printf(", %sinitial", aDocumentNode->IsInitialDocument() ? "" : "not "); + printf(", %sshowing", aDocumentNode->IsShowing() ? "" : "not "); + printf(", %svisible", aDocumentNode->IsVisible() ? "" : "not "); + printf(", %svisible considering ancestors", aDocumentNode->IsVisibleConsideringAncestors() ? "" : "not "); + printf(", %sactive", aDocumentNode->IsActive() ? "" : "not "); + printf(", %sresource", aDocumentNode->IsResourceDoc() ? "" : "not "); + + dom::Element* rootEl = aDocumentNode->GetBodyElement(); + if (!rootEl) { + rootEl = aDocumentNode->GetRootElement(); + } + printf(", has %srole content", rootEl ? "" : "no "); +} + +static void +LogPresShell(nsIDocument* aDocumentNode) +{ + nsIPresShell* ps = aDocumentNode->GetShell(); + printf("presshell: %p", static_cast<void*>(ps)); + + nsIScrollableFrame* sf = nullptr; + if (ps) { + printf(", is %s destroying", (ps->IsDestroying() ? "" : "not")); + sf = ps->GetRootScrollFrameAsScrollable(); + } + printf(", root scroll frame: %p", static_cast<void*>(sf)); +} + +static void +LogDocLoadGroup(nsIDocument* aDocumentNode) +{ + nsCOMPtr<nsILoadGroup> loadGroup = aDocumentNode->GetDocumentLoadGroup(); + printf("load group: %p", static_cast<void*>(loadGroup)); +} + +static void +LogDocParent(nsIDocument* aDocumentNode) +{ + nsIDocument* parentDoc = aDocumentNode->GetParentDocument(); + printf("parent DOM document: %p", static_cast<void*>(parentDoc)); + if (parentDoc) { + printf(", parent acc document: %p", + static_cast<void*>(GetExistingDocAccessible(parentDoc))); + printf("\n parent "); + LogDocURI(parentDoc); + printf("\n"); + } +} + +static void +LogDocInfo(nsIDocument* aDocumentNode, DocAccessible* aDocument) +{ + printf(" DOM document: %p, acc document: %p\n ", + static_cast<void*>(aDocumentNode), static_cast<void*>(aDocument)); + + // log document info + if (aDocumentNode) { + LogDocURI(aDocumentNode); + printf("\n "); + LogDocShellState(aDocumentNode); + printf("; "); + LogDocType(aDocumentNode); + printf("\n "); + LogDocShellTree(aDocumentNode); + printf("\n "); + LogDocState(aDocumentNode); + printf("\n "); + LogPresShell(aDocumentNode); + printf("\n "); + LogDocLoadGroup(aDocumentNode); + printf(", "); + LogDocParent(aDocumentNode); + printf("\n"); + } +} + +static void +LogShellLoadType(nsIDocShell* aDocShell) +{ + printf("load type: "); + + uint32_t loadType = 0; + aDocShell->GetLoadType(&loadType); + switch (loadType) { + case LOAD_NORMAL: + printf("normal; "); + break; + case LOAD_NORMAL_REPLACE: + printf("normal replace; "); + break; + case LOAD_NORMAL_EXTERNAL: + printf("normal external; "); + break; + case LOAD_HISTORY: + printf("history; "); + break; + case LOAD_NORMAL_BYPASS_CACHE: + printf("normal bypass cache; "); + break; + case LOAD_NORMAL_BYPASS_PROXY: + printf("normal bypass proxy; "); + break; + case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE: + printf("normal bypass proxy and cache; "); + break; + case LOAD_NORMAL_ALLOW_MIXED_CONTENT: + printf("normal allow mixed content; "); + break; + case LOAD_RELOAD_NORMAL: + printf("reload normal; "); + break; + case LOAD_RELOAD_BYPASS_CACHE: + printf("reload bypass cache; "); + break; + case LOAD_RELOAD_BYPASS_PROXY: + printf("reload bypass proxy; "); + break; + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + printf("reload bypass proxy and cache; "); + break; + case LOAD_RELOAD_ALLOW_MIXED_CONTENT: + printf("reload allow mixed content; "); + break; + case LOAD_LINK: + printf("link; "); + break; + case LOAD_REFRESH: + printf("refresh; "); + break; + case LOAD_RELOAD_CHARSET_CHANGE: + printf("reload charset change; "); + break; + case LOAD_BYPASS_HISTORY: + printf("bypass history; "); + break; + case LOAD_STOP_CONTENT: + printf("stop content; "); + break; + case LOAD_STOP_CONTENT_AND_REPLACE: + printf("stop content and replace; "); + break; + case LOAD_PUSHSTATE: + printf("load pushstate; "); + break; + case LOAD_REPLACE_BYPASS_CACHE: + printf("replace bypass cache; "); + break; + case LOAD_ERROR_PAGE: + printf("error page;"); + break; + default: + printf("unknown"); + } +} + +static void +LogRequest(nsIRequest* aRequest) +{ + if (aRequest) { + nsAutoCString name; + aRequest->GetName(name); + printf(" request spec: %s\n", name.get()); + uint32_t loadFlags = 0; + aRequest->GetLoadFlags(&loadFlags); + printf(" request load flags: %x; ", loadFlags); + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) + printf("document uri; "); + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) + printf("retargeted document uri; "); + if (loadFlags & nsIChannel::LOAD_REPLACE) + printf("replace; "); + if (loadFlags & nsIChannel::LOAD_INITIAL_DOCUMENT_URI) + printf("initial document uri; "); + if (loadFlags & nsIChannel::LOAD_TARGETED) + printf("targeted; "); + if (loadFlags & nsIChannel::LOAD_CALL_CONTENT_SNIFFERS) + printf("call content sniffers; "); + if (loadFlags & nsIChannel::LOAD_CLASSIFY_URI) + printf("classify uri; "); + } else { + printf(" no request"); + } +} + +static void +LogDocAccState(DocAccessible* aDocument) +{ + printf("document acc state: "); + if (aDocument->HasLoadState(DocAccessible::eCompletelyLoaded)) + printf("completely loaded;"); + else if (aDocument->HasLoadState(DocAccessible::eReady)) + printf("ready;"); + else if (aDocument->HasLoadState(DocAccessible::eDOMLoaded)) + printf("DOM loaded;"); + else if (aDocument->HasLoadState(DocAccessible::eTreeConstructed)) + printf("tree constructed;"); +} + +static void +GetDocLoadEventType(AccEvent* aEvent, nsACString& aEventType) +{ + uint32_t type = aEvent->GetEventType(); + if (type == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED) { + aEventType.AssignLiteral("load stopped"); + } else if (type == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE) { + aEventType.AssignLiteral("load complete"); + } else if (type == nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD) { + aEventType.AssignLiteral("reload"); + } else if (type == nsIAccessibleEvent::EVENT_STATE_CHANGE) { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + if (event->GetState() == states::BUSY) { + aEventType.AssignLiteral("busy "); + if (event->IsStateEnabled()) + aEventType.AppendLiteral("true"); + else + aEventType.AppendLiteral("false"); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// namespace logging:: document life cycle logging methods + +static const char* sDocLoadTitle = "DOCLOAD"; +static const char* sDocCreateTitle = "DOCCREATE"; +static const char* sDocDestroyTitle = "DOCDESTROY"; +static const char* sDocEventTitle = "DOCEVENT"; +static const char* sFocusTitle = "FOCUS"; + +void +logging::DocLoad(const char* aMsg, nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags) +{ + MsgBegin(sDocLoadTitle, aMsg); + + nsCOMPtr<mozIDOMWindowProxy> DOMWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow)); + nsPIDOMWindowOuter* window = nsPIDOMWindowOuter::From(DOMWindow); + if (!window) { + MsgEnd(); + return; + } + + nsCOMPtr<nsIDocument> documentNode = window->GetDoc(); + if (!documentNode) { + MsgEnd(); + return; + } + + DocAccessible* document = GetExistingDocAccessible(documentNode); + + LogDocInfo(documentNode, document); + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + printf("\n "); + LogShellLoadType(docShell); + printf("\n"); + LogRequest(aRequest); + printf("\n"); + printf(" state flags: %x", aStateFlags); + bool isDocLoading; + aWebProgress->GetIsLoadingDocument(&isDocLoading); + printf(", document is %sloading\n", (isDocLoading ? "" : "not ")); + + MsgEnd(); +} + +void +logging::DocLoad(const char* aMsg, nsIDocument* aDocumentNode) +{ + MsgBegin(sDocLoadTitle, aMsg); + + DocAccessible* document = GetExistingDocAccessible(aDocumentNode); + LogDocInfo(aDocumentNode, document); + + MsgEnd(); +} + +void +logging::DocCompleteLoad(DocAccessible* aDocument, bool aIsLoadEventTarget) +{ + MsgBegin(sDocLoadTitle, "document loaded *completely*"); + + printf(" DOM document: %p, acc document: %p\n", + static_cast<void*>(aDocument->DocumentNode()), + static_cast<void*>(aDocument)); + + printf(" "); + LogDocURI(aDocument->DocumentNode()); + printf("\n"); + + printf(" "); + LogDocAccState(aDocument); + printf("\n"); + + printf(" document is load event target: %s\n", + (aIsLoadEventTarget ? "true" : "false")); + + MsgEnd(); +} + +void +logging::DocLoadEventFired(AccEvent* aEvent) +{ + nsAutoCString strEventType; + GetDocLoadEventType(aEvent, strEventType); + if (!strEventType.IsEmpty()) + printf(" fire: %s\n", strEventType.get()); +} + +void +logging::DocLoadEventHandled(AccEvent* aEvent) +{ + nsAutoCString strEventType; + GetDocLoadEventType(aEvent, strEventType); + if (strEventType.IsEmpty()) + return; + + MsgBegin(sDocEventTitle, "handled '%s' event", strEventType.get()); + + DocAccessible* document = aEvent->GetAccessible()->AsDoc(); + if (document) + LogDocInfo(document->DocumentNode(), document); + + MsgEnd(); +} + +void +logging::DocCreate(const char* aMsg, nsIDocument* aDocumentNode, + DocAccessible* aDocument) +{ + DocAccessible* document = aDocument ? + aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocCreateTitle, aMsg); + LogDocInfo(aDocumentNode, document); + MsgEnd(); +} + +void +logging::DocDestroy(const char* aMsg, nsIDocument* aDocumentNode, + DocAccessible* aDocument) +{ + DocAccessible* document = aDocument ? + aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocDestroyTitle, aMsg); + LogDocInfo(aDocumentNode, document); + MsgEnd(); +} + +void +logging::OuterDocDestroy(OuterDocAccessible* aOuterDoc) +{ + MsgBegin(sDocDestroyTitle, "outerdoc shutdown"); + logging::Address("outerdoc", aOuterDoc); + MsgEnd(); +} + +void +logging::FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + Accessible* aTarget) +{ + MsgBegin(sFocusTitle, aMsg); + AccessibleNNode(aTargetDescr, aTarget); + MsgEnd(); +} + +void +logging::FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsINode* aTargetNode) +{ + MsgBegin(sFocusTitle, aMsg); + Node(aTargetDescr, aTargetNode); + MsgEnd(); +} + +void +logging::FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsISupports* aTargetThing) +{ + MsgBegin(sFocusTitle, aMsg); + + if (aTargetThing) { + nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTargetThing)); + if (targetNode) + AccessibleNNode(aTargetDescr, targetNode); + else + printf(" %s: %p, window\n", aTargetDescr, + static_cast<void*>(aTargetThing)); + } + + MsgEnd(); +} + +void +logging::ActiveItemChangeCausedBy(const char* aCause, Accessible* aTarget) +{ + SubMsgBegin(); + printf(" Caused by: %s\n", aCause); + AccessibleNNode("Item", aTarget); + SubMsgEnd(); +} + +void +logging::ActiveWidget(Accessible* aWidget) +{ + SubMsgBegin(); + + AccessibleNNode("Widget", aWidget); + printf(" Widget is active: %s, has operable items: %s\n", + (aWidget && aWidget->IsActiveWidget() ? "true" : "false"), + (aWidget && aWidget->AreItemsOperable() ? "true" : "false")); + + SubMsgEnd(); +} + +void +logging::FocusDispatched(Accessible* aTarget) +{ + SubMsgBegin(); + AccessibleNNode("A11y target", aTarget); + SubMsgEnd(); +} + +void +logging::SelChange(nsISelection* aSelection, DocAccessible* aDocument, + int16_t aReason) +{ + nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection)); + + int16_t type = 0; + privSel->GetType(&type); + + const char* strType = 0; + if (type == nsISelectionController::SELECTION_NORMAL) + strType = "normal"; + else if (type == nsISelectionController::SELECTION_SPELLCHECK) + strType = "spellcheck"; + else + strType = "unknown"; + + bool isIgnored = !aDocument || !aDocument->IsContentLoaded(); + printf("\nSelection changed, selection type: %s, notification %s, reason: %d\n", + strType, (isIgnored ? "ignored" : "pending"), aReason); + + Stack(); +} + +void +logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, ...) +{ + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + va_list vl; + va_start(vl, aExtraFlags); + const char* descr = va_arg(vl, const char*); + if (descr) { + Accessible* acc = va_arg(vl, Accessible*); + MsgBegin("TREE", "%s; doc: %p", aMsg, acc ? acc->Document() : nullptr); + AccessibleInfo(descr, acc); + while ((descr = va_arg(vl, const char*))) { + AccessibleInfo(descr, va_arg(vl, Accessible*)); + } + } + else { + MsgBegin("TREE", aMsg); + } + va_end(vl); + MsgEnd(); + + if (aExtraFlags & eStack) { + Stack(); + } + } +} + +void +logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, + const char* aMsg1, Accessible* aAcc, + const char* aMsg2, nsINode* aNode) +{ + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + MsgBegin("TREE", "%s; doc: %p", aMsg, aAcc ? aAcc->Document() : nullptr); + AccessibleInfo(aMsg1, aAcc); + Accessible* acc = aAcc ? aAcc->Document()->GetAccessible(aNode) : nullptr; + if (acc) { + AccessibleInfo(aMsg2, acc); + } + else { + Node(aMsg2, aNode); + } + MsgEnd(); + } +} + + +void +logging::TreeInfo(const char* aMsg, uint32_t aExtraFlags, Accessible* aParent) +{ + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + MsgBegin("TREE", "%s; doc: %p", aMsg, aParent->Document()); + AccessibleInfo("container", aParent); + for (uint32_t idx = 0; idx < aParent->ChildCount(); idx++) { + AccessibleInfo("child", aParent->GetChildAt(idx)); + } + MsgEnd(); + } +} + +void +logging::Tree(const char* aTitle, const char* aMsgText, + Accessible* aRoot, GetTreePrefix aPrefixFunc, + void* aGetTreePrefixData) +{ + logging::MsgBegin(aTitle, aMsgText); + + nsAutoString level; + Accessible* root = aRoot; + do { + const char* prefix = aPrefixFunc ? aPrefixFunc(aGetTreePrefixData, root) : ""; + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::AccessibleInfo(prefix, root); + if (root->FirstChild() && !root->FirstChild()->IsDoc()) { + level.Append(NS_LITERAL_STRING(" ")); + root = root->FirstChild(); + continue; + } + int32_t idxInParent = root != aRoot && root->mParent ? + root->mParent->mChildren.IndexOf(root) : -1; + if (idxInParent != -1 && + idxInParent < static_cast<int32_t>(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + continue; + } + while (root != aRoot && (root = root->Parent())) { + level.Cut(0, 2); + int32_t idxInParent = !root->IsDoc() && root->mParent ? + root->mParent->mChildren.IndexOf(root) : -1; + if (idxInParent != -1 && + idxInParent < static_cast<int32_t>(root->mParent->mChildren.Length() - 1)) { + root = root->mParent->mChildren.ElementAt(idxInParent + 1); + break; + } + } + } + while (root && root != aRoot); + + logging::MsgEnd(); +} + +void +logging::DOMTree(const char* aTitle, const char* aMsgText, + DocAccessible* aDocument) +{ + logging::MsgBegin(aTitle, aMsgText); + nsAutoString level; + nsINode* root = aDocument->DocumentNode(); + do { + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::Node("", root); + if (root->GetFirstChild()) { + level.Append(NS_LITERAL_STRING(" ")); + root = root->GetFirstChild(); + continue; + } + if (root->GetNextSibling()) { + root = root->GetNextSibling(); + continue; + } + while ((root = root->GetParentNode())) { + level.Cut(0, 2); + if (root->GetNextSibling()) { + root = root->GetNextSibling(); + break; + } + } + } + while (root); + logging::MsgEnd(); +} + +void +logging::MsgBegin(const char* aTitle, const char* aMsgText, ...) +{ + printf("\nA11Y %s: ", aTitle); + + va_list argptr; + va_start(argptr, aMsgText); + vprintf(aMsgText, argptr); + va_end(argptr); + + PRIntervalTime time = PR_IntervalNow(); + uint32_t mins = (PR_IntervalToSeconds(time) / 60) % 60; + uint32_t secs = PR_IntervalToSeconds(time) % 60; + uint32_t msecs = PR_IntervalToMilliseconds(time) % 1000; + printf("; %02d:%02d.%03d", mins, secs, msecs); + + printf("\n {\n"); +} + +void +logging::MsgEnd() +{ + printf(" }\n"); +} + +void +logging::SubMsgBegin() +{ + printf(" {\n"); +} + +void +logging::SubMsgEnd() +{ + printf(" }\n"); +} + +void +logging::MsgEntry(const char* aEntryText, ...) +{ + printf(" "); + + va_list argptr; + va_start(argptr, aEntryText); + vprintf(aEntryText, argptr); + va_end(argptr); + + printf("\n"); +} + +void +logging::Text(const char* aText) +{ + printf(" %s\n", aText); +} + +void +logging::Address(const char* aDescr, Accessible* aAcc) +{ + if (!aAcc->IsDoc()) { + printf(" %s accessible: %p, node: %p\n", aDescr, + static_cast<void*>(aAcc), static_cast<void*>(aAcc->GetNode())); + } + + DocAccessible* doc = aAcc->Document(); + nsIDocument* docNode = doc->DocumentNode(); + printf(" document: %p, node: %p\n", + static_cast<void*>(doc), static_cast<void*>(docNode)); + + printf(" "); + LogDocURI(docNode); + printf("\n"); +} + +void +logging::Node(const char* aDescr, nsINode* aNode) +{ + printf(" "); + + if (!aNode) { + printf("%s: null\n", aDescr); + return; + } + + if (aNode->IsNodeOfType(nsINode::eDOCUMENT)) { + printf("%s: %p, document\n", aDescr, static_cast<void*>(aNode)); + return; + } + + nsINode* parentNode = aNode->GetParentNode(); + int32_t idxInParent = parentNode ? parentNode->IndexOf(aNode) : - 1; + + if (aNode->IsNodeOfType(nsINode::eTEXT)) { + printf("%s: %p, text node, idx in parent: %d\n", + aDescr, static_cast<void*>(aNode), idxInParent); + return; + } + + if (!aNode->IsElement()) { + printf("%s: %p, not accessible node type, idx in parent: %d\n", + aDescr, static_cast<void*>(aNode), idxInParent); + return; + } + + dom::Element* elm = aNode->AsElement(); + + nsAutoCString tag; + elm->NodeInfo()->NameAtom()->ToUTF8String(tag); + + nsIAtom* idAtom = elm->GetID(); + nsAutoCString id; + if (idAtom) + idAtom->ToUTF8String(id); + + printf("%s: %p, %s@id='%s', idx in parent: %d\n", + aDescr, static_cast<void*>(elm), tag.get(), id.get(), idxInParent); +} + +void +logging::Document(DocAccessible* aDocument) +{ + printf(" Document: %p, document node: %p\n", + static_cast<void*>(aDocument), + static_cast<void*>(aDocument->DocumentNode())); + + printf(" Document "); + LogDocURI(aDocument->DocumentNode()); + printf("\n"); +} + +void +logging::AccessibleInfo(const char* aDescr, Accessible* aAccessible) +{ + printf(" %s: %p; ", aDescr, static_cast<void*>(aAccessible)); + if (!aAccessible) { + printf("\n"); + return; + } + if (aAccessible->IsDefunct()) { + printf("defunct\n"); + return; + } + if (!aAccessible->Document() || aAccessible->Document()->IsDefunct()) { + printf("document is shutting down, no info\n"); + return; + } + + nsAutoString role; + GetAccService()->GetStringRole(aAccessible->Role(), role); + printf("role: %s", NS_ConvertUTF16toUTF8(role).get()); + + nsAutoString name; + aAccessible->Name(name); + if (!name.IsEmpty()) { + printf(", name: '%s'", NS_ConvertUTF16toUTF8(name).get()); + } + + printf(", idx: %d", aAccessible->IndexInParent()); + + nsINode* node = aAccessible->GetNode(); + if (!node) { + printf(", node: null\n"); + } + else if (node->IsNodeOfType(nsINode::eDOCUMENT)) { + printf(", document node: %p\n", static_cast<void*>(node)); + } + else if (node->IsNodeOfType(nsINode::eTEXT)) { + printf(", text node: %p\n", static_cast<void*>(node)); + } + else if (node->IsElement()) { + dom::Element* el = node->AsElement(); + + nsAutoCString tag; + el->NodeInfo()->NameAtom()->ToUTF8String(tag); + + nsIAtom* idAtom = el->GetID(); + nsAutoCString id; + if (idAtom) { + idAtom->ToUTF8String(id); + } + + printf(", element node: %p, %s@id='%s'\n", + static_cast<void*>(el), tag.get(), id.get()); + } +} + +void +logging::AccessibleNNode(const char* aDescr, Accessible* aAccessible) +{ + printf(" %s: %p; ", aDescr, static_cast<void*>(aAccessible)); + if (!aAccessible) + return; + + nsAutoString role; + GetAccService()->GetStringRole(aAccessible->Role(), role); + nsAutoString name; + aAccessible->Name(name); + + printf("role: %s, name: '%s';\n", NS_ConvertUTF16toUTF8(role).get(), + NS_ConvertUTF16toUTF8(name).get()); + + nsAutoCString nodeDescr(aDescr); + nodeDescr.AppendLiteral(" node"); + Node(nodeDescr.get(), aAccessible->GetNode()); + + Document(aAccessible->Document()); +} + +void +logging::AccessibleNNode(const char* aDescr, nsINode* aNode) +{ + DocAccessible* document = + GetAccService()->GetDocAccessible(aNode->OwnerDoc()); + + if (document) { + Accessible* accessible = document->GetAccessible(aNode); + if (accessible) { + AccessibleNNode(aDescr, accessible); + return; + } + } + + nsAutoCString nodeDescr("[not accessible] "); + nodeDescr.Append(aDescr); + Node(nodeDescr.get(), aNode); + + if (document) { + Document(document); + return; + } + + printf(" [contained by not accessible document]:\n"); + LogDocInfo(aNode->OwnerDoc(), document); + printf("\n"); +} + +void +logging::DOMEvent(const char* aDescr, nsINode* aOrigTarget, + const nsAString& aEventType) +{ + logging::MsgBegin("DOMEvents", "event '%s' %s", + NS_ConvertUTF16toUTF8(aEventType).get(), aDescr); + logging::AccessibleNNode("Target", aOrigTarget); + logging::MsgEnd(); +} + +void +logging::Stack() +{ + if (IsEnabled(eStack)) { + printf(" stack: \n"); + nsTraceRefcnt::WalkTheStack(stdout); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// namespace logging:: initialization + +bool +logging::IsEnabled(uint32_t aModules) +{ + return sModules & aModules; +} + +bool +logging::IsEnabledAll(uint32_t aModules) +{ + return (sModules & aModules) == aModules; +} + +bool +logging::IsEnabled(const nsAString& aModuleStr) +{ + for (unsigned int idx = 0; idx < ArrayLength(sModuleMap); idx++) { + if (aModuleStr.EqualsASCII(sModuleMap[idx].mStr)) + return sModules & sModuleMap[idx].mModule; + } + + return false; +} + +void +logging::Enable(const nsAFlatCString& aModules) +{ + EnableLogging(aModules.get()); +} + + +void +logging::CheckEnv() +{ + EnableLogging(PR_GetEnv("A11YLOG")); +} diff --git a/accessible/base/Logging.h b/accessible/base/Logging.h new file mode 100644 index 000000000..a5d2c16b2 --- /dev/null +++ b/accessible/base/Logging.h @@ -0,0 +1,225 @@ +/* -*- 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_logs_h__ +#define mozilla_a11y_logs_h__ + +#include "nscore.h" +#include "nsStringFwd.h" + +class nsIDocument; +class nsINode; +class nsIRequest; +class nsISelection; +class nsISupports; +class nsIWebProgress; + +namespace mozilla { +namespace a11y { + +class AccEvent; +class Accessible; +class DocAccessible; +class OuterDocAccessible; + +namespace logging { + +enum EModules { + eDocLoad = 1 << 0, + eDocCreate = 1 << 1, + eDocDestroy = 1 << 2, + eDocLifeCycle = eDocLoad | eDocCreate | eDocDestroy, + + eEvents = 1 << 3, + eEventTree = 1 << 4, + ePlatforms = 1 << 5, + eText = 1 << 6, + eTree = 1 << 7, + + eDOMEvents = 1 << 8, + eFocus = 1 << 9, + eSelection = 1 << 10, + eNotifications = eDOMEvents | eSelection | eFocus, + + // extras + eStack = 1 << 11, + eVerbose = 1 << 12 +}; + +/** + * Return true if any of the given modules is logged. + */ +bool IsEnabled(uint32_t aModules); + +/** + * Return true if all of the given modules are logged. + */ +bool IsEnabledAll(uint32_t aModules); + +/** + * Return true if the given module is logged. + */ +bool IsEnabled(const nsAString& aModules); + +/** + * Log the document loading progress. + */ +void DocLoad(const char* aMsg, nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags); +void DocLoad(const char* aMsg, nsIDocument* aDocumentNode); +void DocCompleteLoad(DocAccessible* aDocument, bool aIsLoadEventTarget); + +/** + * Log that document load event was fired. + */ +void DocLoadEventFired(AccEvent* aEvent); + +/** + * Log that document laod event was handled. + */ +void DocLoadEventHandled(AccEvent* aEvent); + +/** + * Log the document was created. + */ +void DocCreate(const char* aMsg, nsIDocument* aDocumentNode, + DocAccessible* aDocument = nullptr); + +/** + * Log the document was destroyed. + */ +void DocDestroy(const char* aMsg, nsIDocument* aDocumentNode, + DocAccessible* aDocument = nullptr); + +/** + * Log the outer document was destroyed. + */ +void OuterDocDestroy(OuterDocAccessible* OuterDoc); + +/** + * Log the focus notification target. + */ +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + Accessible* aTarget); +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsINode* aTargetNode); +void FocusNotificationTarget(const char* aMsg, const char* aTargetDescr, + nsISupports* aTargetThing); + +/** + * Log a cause of active item descendant change (submessage). + */ +void ActiveItemChangeCausedBy(const char* aMsg, Accessible* aTarget); + +/** + * Log the active widget (submessage). + */ +void ActiveWidget(Accessible* aWidget); + +/** + * Log the focus event was dispatched (submessage). + */ +void FocusDispatched(Accessible* aTarget); + +/** + * Log the selection change. + */ +void SelChange(nsISelection* aSelection, DocAccessible* aDocument, + int16_t aReason); + +/** + * Log the given accessible elements info. + */ +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, ...); +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, + const char* aMsg1, Accessible* aAcc, + const char* aMsg2, nsINode* aNode); +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, Accessible* aParent); + +/** + * Log the accessible/DOM tree. + */ +typedef const char* (*GetTreePrefix)(void* aData, Accessible*); +void Tree(const char* aTitle, const char* aMsgText, Accessible* aRoot, + GetTreePrefix aPrefixFunc = nullptr, void* aGetTreePrefixData = nullptr); +void DOMTree(const char* aTitle, const char* aMsgText, DocAccessible* aDoc); + +/** + * Log the message ('title: text' format) on new line. Print the start and end + * boundaries of the message body designated by '{' and '}' (2 spaces indent for + * body). + */ +void MsgBegin(const char* aTitle, const char* aMsgText, ...); +void MsgEnd(); + +/** + * Print start and end boundaries of the message body designated by '{' and '}' + * (2 spaces indent for body). + */ +void SubMsgBegin(); +void SubMsgEnd(); + +/** + * Log the entry into message body (4 spaces indent). + */ +void MsgEntry(const char* aEntryText, ...); + +/** + * Log the text, two spaces offset is used. + */ +void Text(const char* aText); + +/** + * Log the accessible object address as message entry (4 spaces indent). + */ +void Address(const char* aDescr, Accessible* aAcc); + +/** + * Log the DOM node info as message entry. + */ +void Node(const char* aDescr, nsINode* aNode); + +/** + * Log the document accessible info as message entry. + */ +void Document(DocAccessible* aDocument); + +/** + * Log the accessible and its DOM node as a message entry. + */ +void AccessibleInfo(const char* aDescr, Accessible* aAccessible); +void AccessibleNNode(const char* aDescr, Accessible* aAccessible); +void AccessibleNNode(const char* aDescr, nsINode* aNode); + +/** + * Log the DOM event. + */ +void DOMEvent(const char* aDescr, nsINode* aOrigTarget, + const nsAString& aEventType); + +/** + * Log the call stack, two spaces offset is used. + */ +void Stack(); + +/** + * Enable logging of the specified modules, all other modules aren't logged. + */ +void Enable(const nsAFlatCString& aModules); + +/** + * Enable logging of modules specified by A11YLOG environment variable, + * all other modules aren't logged. + */ +void CheckEnv(); + +} // namespace logging + +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/base/MarkupMap.h b/accessible/base/MarkupMap.h new file mode 100644 index 000000000..5ba1a06e6 --- /dev/null +++ b/accessible/base/MarkupMap.h @@ -0,0 +1,340 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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/. */ + +MARKUPMAP(a, + New_HTMLLink, + roles::LINK) + +MARKUPMAP(abbr, + New_HyperText, + 0) + +MARKUPMAP(acronym, + New_HyperText, + 0) + +MARKUPMAP(article, + New_HyperText, + roles::DOCUMENT, + Attr(xmlroles, article)) + +MARKUPMAP(aside, + New_HyperText, + roles::NOTE) + +MARKUPMAP(blockquote, + New_HyperText, + roles::SECTION) + +MARKUPMAP(dd, + New_HTMLDefinition, + roles::DEFINITION) + +MARKUPMAP(details, + New_HyperText, + roles::DETAILS) + +MARKUPMAP(div, + nullptr, + roles::SECTION) + +MARKUPMAP(dl, + New_HTMLList, + roles::DEFINITION_LIST) + +MARKUPMAP(dt, + New_HTMLListitem, + roles::TERM) + +MARKUPMAP(figcaption, + New_HTMLFigcaption, + roles::CAPTION) + +MARKUPMAP(figure, + New_HTMLFigure, + roles::FIGURE, + Attr(xmlroles, figure)) + +MARKUPMAP(form, + New_HyperText, + roles::FORM) + +MARKUPMAP(footer, + New_HyperText, + roles::FOOTER) + +MARKUPMAP(header, + New_HyperText, + roles::HEADER) + +MARKUPMAP(h1, + New_HyperText, + roles::HEADING) + +MARKUPMAP(h2, + New_HyperText, + roles::HEADING) + +MARKUPMAP(h3, + New_HyperText, + roles::HEADING) + +MARKUPMAP(h4, + New_HyperText, + roles::HEADING) + +MARKUPMAP(h5, + New_HyperText, + roles::HEADING) + +MARKUPMAP(h6, + New_HyperText, + roles::HEADING) + +MARKUPMAP(label, + New_HTMLLabel, + roles::LABEL) + +MARKUPMAP(legend, + New_HTMLLegend, + roles::LABEL) + +MARKUPMAP(li, + New_HTMLListitem, + 0) + +MARKUPMAP(map, + nullptr, + roles::TEXT_CONTAINER) + +MARKUPMAP(math, + New_HyperText, + roles::MATHML_MATH) + +MARKUPMAP(mi_, + New_HyperText, + roles::MATHML_IDENTIFIER) + +MARKUPMAP(mn_, + New_HyperText, + roles::MATHML_NUMBER) + +MARKUPMAP(mo_, + New_HyperText, + roles::MATHML_OPERATOR, + AttrFromDOM(accent_, accent_), + AttrFromDOM(fence_, fence_), + AttrFromDOM(separator_, separator_), + AttrFromDOM(largeop_, largeop_)) + +MARKUPMAP(mtext_, + New_HyperText, + roles::MATHML_TEXT) + +MARKUPMAP(ms_, + New_HyperText, + roles::MATHML_STRING_LITERAL) + +MARKUPMAP(mglyph_, + New_HyperText, + roles::MATHML_GLYPH) + +MARKUPMAP(mrow_, + New_HyperText, + roles::MATHML_ROW) + +MARKUPMAP(mfrac_, + New_HyperText, + roles::MATHML_FRACTION, + AttrFromDOM(bevelled_, bevelled_), + AttrFromDOM(linethickness_, linethickness_)) + +MARKUPMAP(msqrt_, + New_HyperText, + roles::MATHML_SQUARE_ROOT) + +MARKUPMAP(mroot_, + New_HyperText, + roles::MATHML_ROOT) + +MARKUPMAP(mfenced_, + New_HyperText, + roles::MATHML_FENCED, + AttrFromDOM(close, close), + AttrFromDOM(open, open), + AttrFromDOM(separators_, separators_)) + +MARKUPMAP(menclose_, + New_HyperText, + roles::MATHML_ENCLOSED, + AttrFromDOM(notation_, notation_)) + +MARKUPMAP(mstyle_, + New_HyperText, + roles::MATHML_STYLE) + +MARKUPMAP(msub_, + New_HyperText, + roles::MATHML_SUB) + +MARKUPMAP(msup_, + New_HyperText, + roles::MATHML_SUP) + +MARKUPMAP(msubsup_, + New_HyperText, + roles::MATHML_SUB_SUP) + +MARKUPMAP(munder_, + New_HyperText, + roles::MATHML_UNDER, + AttrFromDOM(accentunder_, accentunder_), + AttrFromDOM(align, align)) + +MARKUPMAP(mover_, + New_HyperText, + roles::MATHML_OVER, + AttrFromDOM(accent_, accent_), + AttrFromDOM(align, align)) + +MARKUPMAP(munderover_, + New_HyperText, + roles::MATHML_UNDER_OVER, + AttrFromDOM(accent_, accent_), + AttrFromDOM(accentunder_, accentunder_), + AttrFromDOM(align, align)) + +MARKUPMAP(mmultiscripts_, + New_HyperText, + roles::MATHML_MULTISCRIPTS) + +MARKUPMAP(mtable_, + New_HTMLTableAccessible, + roles::MATHML_TABLE, + AttrFromDOM(align, align), + AttrFromDOM(columnlines_, columnlines_), + AttrFromDOM(rowlines_, rowlines_)) + +MARKUPMAP(mlabeledtr_, + New_HTMLTableRowAccessible, + roles::MATHML_LABELED_ROW) + +MARKUPMAP(mtr_, + New_HTMLTableRowAccessible, + roles::MATHML_TABLE_ROW) + +MARKUPMAP(mtd_, + New_HTMLTableCellAccessible, + roles::MATHML_CELL) + +MARKUPMAP(maction_, + New_HyperText, + roles::MATHML_ACTION, + AttrFromDOM(actiontype_, actiontype_), + AttrFromDOM(selection_, selection_)) + +MARKUPMAP(merror_, + New_HyperText, + roles::MATHML_ERROR) + +MARKUPMAP(mstack_, + New_HyperText, + roles::MATHML_STACK, + AttrFromDOM(align, align), + AttrFromDOM(position, position)) + +MARKUPMAP(mlongdiv_, + New_HyperText, + roles::MATHML_LONG_DIVISION, + AttrFromDOM(longdivstyle_, longdivstyle_)) + +MARKUPMAP(msgroup_, + New_HyperText, + roles::MATHML_STACK_GROUP, + AttrFromDOM(position, position), + AttrFromDOM(shift_, shift_)) + +MARKUPMAP(msrow_, + New_HyperText, + roles::MATHML_STACK_ROW, + AttrFromDOM(position, position)) + +MARKUPMAP(mscarries_, + New_HyperText, + roles::MATHML_STACK_CARRIES, + AttrFromDOM(location_, location_), + AttrFromDOM(position, position)) + +MARKUPMAP(mscarry_, + New_HyperText, + roles::MATHML_STACK_CARRY, + AttrFromDOM(crossout_, crossout_)) + +MARKUPMAP(msline_, + New_HyperText, + roles::MATHML_STACK_LINE, + AttrFromDOM(position, position)) + +MARKUPMAP(nav, + New_HyperText, + roles::SECTION) + +MARKUPMAP(ol, + New_HTMLList, + roles::LIST) + +MARKUPMAP(option, + New_HTMLOption, + 0) + +MARKUPMAP(optgroup, + New_HTMLOptgroup, + 0) + +MARKUPMAP(output, + New_HTMLOutput, + roles::SECTION, + Attr(live, polite)) + +MARKUPMAP(p, + nullptr, + roles::PARAGRAPH) + +MARKUPMAP(progress, + New_HTMLProgress, + 0) + +MARKUPMAP(q, + New_HyperText, + 0) + +MARKUPMAP(section, + New_HyperText, + roles::SECTION, + Attr(xmlroles, region)) + +MARKUPMAP(summary, + New_HTMLSummary, + roles::SUMMARY) + +MARKUPMAP(time, + New_HyperText, + 0, + Attr(xmlroles, time), + AttrFromDOM(datetime, datetime)) + +MARKUPMAP(td, + New_HTMLTableHeaderCellIfScope, + 0) + +MARKUPMAP(th, + New_HTMLTableHeaderCell, + 0) + +MARKUPMAP(ul, + New_HTMLList, + roles::LIST) diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp new file mode 100644 index 000000000..73d364641 --- /dev/null +++ b/accessible/base/NotificationController.cpp @@ -0,0 +1,947 @@ +/* -*- 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 "NotificationController.h" + +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "TextLeafAccessible.h" +#include "TextUpdater.h" + +#include "mozilla/dom/TabChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector +//////////////////////////////////////////////////////////////////////////////// + +NotificationController::NotificationController(DocAccessible* aDocument, + nsIPresShell* aPresShell) : + EventQueue(aDocument), mObservingState(eNotObservingRefresh), + mPresShell(aPresShell), mEventGeneration(0) +{ +#ifdef DEBUG + mMoveGuardOnStack = false; +#endif + + // Schedule initial accessible tree construction. + ScheduleProcessing(); +} + +NotificationController::~NotificationController() +{ + NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); + if (mDocument) + Shutdown(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: AddRef/Release and cycle collection + +NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) +NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) + if (tmp->mDocument) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) + for (auto it = tmp->mContentInsertions.ConstIter(); !it.Done(); it.Next()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key"); + cb.NoteXPCOMChild(it.Key()); + nsTArray<nsCOMPtr<nsIContent>>* list = it.UserData(); + for (uint32_t i = 0; i < list->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, + "mContentInsertions value item"); + cb.NoteXPCOMChild(list->ElementAt(i)); + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(NotificationController, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(NotificationController, Release) + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: public + +void +NotificationController::Shutdown() +{ + if (mObservingState != eNotObservingRefresh && + mPresShell->RemoveRefreshObserver(this, Flush_Display)) { + mObservingState = eNotObservingRefresh; + } + + // Shutdown handling child documents. + int32_t childDocCount = mHangingChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + if (!mHangingChildDocuments[idx]->IsDefunct()) + mHangingChildDocuments[idx]->Shutdown(); + } + + mHangingChildDocuments.Clear(); + + mDocument = nullptr; + mPresShell = nullptr; + + mTextHash.Clear(); + mContentInsertions.Clear(); + mNotifications.Clear(); + mEvents.Clear(); + mRelocations.Clear(); + mEventTree.Clear(); +} + +EventTree* +NotificationController::QueueMutation(Accessible* aContainer) +{ + EventTree* tree = mEventTree.FindOrInsert(aContainer); + if (tree) { + ScheduleProcessing(); + } + return tree; +} + +bool +NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) +{ + // We have to allow there to be a hide and then a show event for a target + // because of targets getting moved. However we need to coalesce a show and + // then a hide for a target which means we need to check for that here. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE && + aEvent->GetAccessible()->ShowEventTarget()) { + AccTreeMutationEvent* showEvent = mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent); + DropMutationEvent(showEvent); + return false; + } + + AccMutationEvent* mutEvent = downcast_accEvent(aEvent); + mEventGeneration++; + mutEvent->SetEventGeneration(mEventGeneration); + + if (!mFirstMutationEvent) { + mFirstMutationEvent = aEvent; + ScheduleProcessing(); + } + + if (mLastMutationEvent) { + NS_ASSERTION(!mLastMutationEvent->NextEvent(), "why isn't the last event the end?"); + mLastMutationEvent->SetNextEvent(aEvent); + } + + aEvent->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent = aEvent; + mMutationMap.PutEvent(aEvent); + + // Because we could be hiding the target of a show event we need to get rid + // of any such events. It may be possible to do less than coallesce all + // events, however that is easiest. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + CoalesceMutationEvents(); + + // mLastMutationEvent will point to something other than aEvent if and only + // if aEvent was just coalesced away. In that case a parent accessible + // must already have the required reorder and text change events so we are + // done here. + if (mLastMutationEvent != aEvent) { + return false; + } + } + + // We need to fire a reorder event after all of the events targeted at shown or + // hidden children of a container. So either queue a new one, or move an + // existing one to the end of the queue if the container already has a + // reorder event. + Accessible* target = aEvent->GetAccessible(); + Accessible* container = aEvent->GetAccessible()->Parent(); + RefPtr<AccReorderEvent> reorder; + if (!container->ReorderEventTarget()) { + reorder = new AccReorderEvent(container); + container->SetReorderEventTarget(true); + mMutationMap.PutEvent(reorder); + + // Since this is the first child of container that is changing, the name of + // container may be changing. + QueueNameChange(target); + } else { + AccReorderEvent* event = downcast_accEvent(mMutationMap.GetEvent(container, EventMap::ReorderEvent)); + reorder = event; + if (mFirstMutationEvent == event) { + mFirstMutationEvent = event->NextEvent(); + } else { + event->PrevEvent()->SetNextEvent(event->NextEvent()); + } + + event->NextEvent()->SetPrevEvent(event->PrevEvent()); + event->SetNextEvent(nullptr); + } + + reorder->SetEventGeneration(mEventGeneration); + reorder->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent->SetNextEvent(reorder); + mLastMutationEvent = reorder; + + // It is not possible to have a text change event for something other than a + // hyper text accessible. + if (!container->IsHyperText()) { + return true; + } + + MOZ_ASSERT(mutEvent); + + nsString text; + aEvent->GetAccessible()->AppendTextTo(text); + if (text.IsEmpty()) { + return true; + } + + int32_t offset = container->AsHyperText()->GetChildOffset(target); + AccTreeMutationEvent* prevEvent = aEvent->PrevEvent(); + while (prevEvent && prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + prevEvent = prevEvent->PrevEvent(); + } + + if (prevEvent && prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE && + mutEvent->IsHide()) { + AccHideEvent* prevHide = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevHide->mTextChangeEvent; + if (prevTextChange && prevHide->Parent() == mutEvent->Parent()) { + if (prevHide->mNextSibling == target) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (prevHide->mPrevSibling == target) { + nsString temp; + target->AppendTextTo(temp); + + uint32_t extraLen = temp.Length(); + temp += prevTextChange->mModifiedText;; + prevTextChange->mModifiedText = temp; + prevTextChange->mStart -= extraLen; + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } else if (prevEvent && mutEvent->IsShow() && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + AccShowEvent* prevShow = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevShow->mTextChangeEvent; + if (prevTextChange && prevShow->Parent() == target->Parent()) { + int32_t index = target->IndexInParent(); + int32_t prevIndex = prevShow->GetAccessible()->IndexInParent(); + if (prevIndex + 1 == index) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (index + 1 == prevIndex) { + nsString temp; + target->AppendTextTo(temp); + prevTextChange->mStart -= temp.Length(); + temp += prevTextChange->mModifiedText; + prevTextChange->mModifiedText = temp; + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } + + if (!mutEvent->mTextChangeEvent) { + mutEvent->mTextChangeEvent = + new AccTextChangeEvent(container, offset, text, mutEvent->IsShow(), + aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput); + } + + return true; +} + +void +NotificationController::DropMutationEvent(AccTreeMutationEvent* aEvent) +{ + // unset the event bits since the event isn't being fired any more. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + aEvent->GetAccessible()->SetReorderEventTarget(false); + } else if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + aEvent->GetAccessible()->SetShowEventTarget(false); + } else { + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + MOZ_ASSERT(hideEvent); + + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible()); + } + } + + // Do the work to splice the event out of the list. + if (mFirstMutationEvent == aEvent) { + mFirstMutationEvent = aEvent->NextEvent(); + } else { + aEvent->PrevEvent()->SetNextEvent(aEvent->NextEvent()); + } + + if (mLastMutationEvent == aEvent) { + mLastMutationEvent = aEvent->PrevEvent(); + } else { + aEvent->NextEvent()->SetPrevEvent(aEvent->PrevEvent()); + } + + aEvent->SetPrevEvent(nullptr); + aEvent->SetNextEvent(nullptr); + mMutationMap.RemoveEvent(aEvent); +} + +void +NotificationController::CoalesceMutationEvents() +{ + AccTreeMutationEvent* event = mFirstMutationEvent; + while (event) { + AccTreeMutationEvent* nextEvent = event->NextEvent(); + uint32_t eventType = event->GetEventType(); + if (event->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + Accessible* acc = event->GetAccessible(); + while (acc) { + if (acc->IsDoc()) { + break; + } + + // if a parent of the reorder event's target is being hidden that + // hide event's target must have a parent that is also a reorder event + // target. That means we don't need this reorder event. + if (acc->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + Accessible* parent = acc->Parent(); + if (parent->ReorderEventTarget()) { + AccReorderEvent* reorder = downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ReorderEvent)); + + // We want to make sure that a reorder event comes after any show or + // hide events targeted at the children of its target. We keep the + // invariant that event generation goes up as you are farther in the + // queue, so we want to use the spot of the event with the higher + // generation number, and keep that generation number. + if (reorder && reorder->EventGeneration() < event->EventGeneration()) { + reorder->SetEventGeneration(event->EventGeneration()); + + // It may be true that reorder was before event, and we coalesced + // away all the show / hide events between them. In that case + // event is already immediately after reorder in the queue and we + // do not need to rearrange the list of events. + if (event != reorder->NextEvent()) { + // There really should be a show or hide event before the first + // reorder event. + if (reorder->PrevEvent()) { + reorder->PrevEvent()->SetNextEvent(reorder->NextEvent()); + } else { + mFirstMutationEvent = reorder->NextEvent(); + } + + reorder->NextEvent()->SetPrevEvent(reorder->PrevEvent()); + event->PrevEvent()->SetNextEvent(reorder); + reorder->SetPrevEvent(event->PrevEvent()); + event->SetPrevEvent(reorder); + reorder->SetNextEvent(event); + } + } + DropMutationEvent(event); + break; + } + + acc = parent; + } + } else if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + Accessible* parent = event->GetAccessible()->Parent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + // if the parent of a show event is being either shown or hidden then + // we don't need to fire a show event for a subtree of that change. + if (parent->ShowEventTarget() || parent->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + parent = parent->Parent(); + } + } else { + MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE, "mutation event list has an invalid event"); + + AccHideEvent* hideEvent = downcast_accEvent(event); + Accessible* parent = hideEvent->Parent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + if (parent->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + if (parent->ShowEventTarget()) { + AccShowEvent* showEvent = downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ShowEvent)); + if (showEvent->EventGeneration() < hideEvent->EventGeneration()) { + DropMutationEvent(hideEvent); + break; + } + } + + parent = parent->Parent(); + } + } + + event = nextEvent; + } +} + +void +NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) +{ + // Schedule child document binding to the tree. + mHangingChildDocuments.AppendElement(aDocument); + ScheduleProcessing(); +} + +void +NotificationController::ScheduleContentInsertion(Accessible* aContainer, + nsIContent* aStartChildNode, + nsIContent* aEndChildNode) +{ + nsTArray<nsCOMPtr<nsIContent>>* list = + mContentInsertions.LookupOrAdd(aContainer); + + bool needsProcessing = false; + nsIContent* node = aStartChildNode; + while (node != aEndChildNode) { + // Notification triggers for content insertion even if no content was + // actually inserted, check if the given content has a frame to discard + // this case early. + if (node->GetPrimaryFrame()) { + if (list->AppendElement(node)) + needsProcessing = true; + } + node = node->GetNextSibling(); + } + + if (needsProcessing) { + ScheduleProcessing(); + } +} + +void +NotificationController::ScheduleProcessing() +{ + // If notification flush isn't planed yet start notification flush + // asynchronously (after style and layout). + if (mObservingState == eNotObservingRefresh) { + if (mPresShell->AddRefreshObserver(this, Flush_Display)) + mObservingState = eRefreshObserving; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: protected + +bool +NotificationController::IsUpdatePending() +{ + return mPresShell->IsLayoutFlushObserver() || + mObservingState == eRefreshProcessingForUpdate || + mContentInsertions.Count() != 0 || mNotifications.Length() != 0 || + mTextHash.Count() != 0 || + !mDocument->HasLoadState(DocAccessible::eTreeConstructed); +} + +void +NotificationController::ProcessMutationEvents() +{ + // there is no reason to fire a hide event for a child of a show event + // target. That can happen if something is inserted into the tree and + // removed before the next refresh driver tick, but it should not be + // observable outside gecko so it should be safe to coalesce away any such + // events. This means that it should be fine to fire all of the hide events + // first, and then deal with any shown subtrees. + for (AccTreeMutationEvent* event = mFirstMutationEvent; + event; event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_HIDE) { + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + + // Fire menupopup end event before a hide event if a menu goes away. + + // XXX: We don't look into children of hidden subtree to find hiding + // menupopup (as we did prior bug 570275) because we don't do that when + // menu is showing (and that's impossible until bug 606924 is fixed). + // Nevertheless we should do this at least because layout coalesces + // the changes before our processing and we may miss some menupopup + // events. Now we just want to be consistent in content insertion/removal + // handling. + if (event->mAccessible->ARIARole() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + event->mAccessible); + if (!mDocument) { + return; + } + } + + AccHideEvent* hideEvent = downcast_accEvent(event); + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(event->mAccessible); + } + } + + // Group the show events by the parent of their target. + nsDataHashtable<nsPtrHashKey<Accessible>, nsTArray<AccTreeMutationEvent*>> showEvents; + for (AccTreeMutationEvent* event = mFirstMutationEvent; + event; event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) { + continue; + } + + Accessible* parent = event->GetAccessible()->Parent(); + showEvents.GetOrInsert(parent).AppendElement(event); + } + + // We need to fire show events for the children of an accessible in the order + // of their indices at this point. So sort each set of events for the same + // container by the index of their target. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + struct AccIdxComparator { + bool LessThan(const AccTreeMutationEvent* a, const AccTreeMutationEvent* b) const + { + int32_t aIdx = a->GetAccessible()->IndexInParent(); + int32_t bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx); + return aIdx < bIdx; + } + bool Equals(const AccTreeMutationEvent* a, const AccTreeMutationEvent* b) const + { + DebugOnly<int32_t> aIdx = a->GetAccessible()->IndexInParent(); + DebugOnly<int32_t> bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && aIdx != bIdx); + return false; + } + }; + + nsTArray<AccTreeMutationEvent*>& events = iter.Data(); + events.Sort(AccIdxComparator()); + for (AccTreeMutationEvent* event: events) { + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + } + } + + // Now we can fire the reorder events after all the show and hide events. + for (AccTreeMutationEvent* event = mFirstMutationEvent; + event; event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_REORDER) { + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + Accessible* target = event->GetAccessible(); + target->Document()->MaybeNotifyOfValueChange(target); + if (!mDocument) { + return; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: private + +void +NotificationController::WillRefresh(mozilla::TimeStamp aTime) +{ + PROFILER_LABEL_FUNC(js::ProfileEntry::Category::OTHER); + Telemetry::AutoTimer<Telemetry::A11Y_UPDATE_TIME> updateTimer; + + // If the document accessible that notification collector was created for is + // now shut down, don't process notifications anymore. + NS_ASSERTION(mDocument, + "The document was shut down while refresh observer is attached!"); + if (!mDocument) + return; + + if (mObservingState == eRefreshProcessing || + mObservingState == eRefreshProcessingForUpdate) + return; + + // Any generic notifications should be queued if we're processing content + // insertions or generic notifications. + mObservingState = eRefreshProcessingForUpdate; + + // Initial accessible tree construction. + if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { + // If document is not bound to parent at this point then the document is not + // ready yet (process notifications later). + if (!mDocument->IsBoundToParent()) { + mObservingState = eRefreshObserving; + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "initial tree created"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + mDocument->DoInitialUpdate(); + + NS_ASSERTION(mContentInsertions.Count() == 0, + "Pending content insertions while initial accessible tree isn't created!"); + } + + // Initialize scroll support if needed. + if (!(mDocument->mDocFlags & DocAccessible::eScrollInitialized)) + mDocument->AddScrollListener(); + + // Process rendered text change notifications. + for (auto iter = mTextHash.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtrHashKey<nsIContent>* entry = iter.Get(); + nsIContent* textNode = entry->GetKey(); + Accessible* textAcc = mDocument->GetAccessible(textNode); + + // If the text node is not in tree or doesn't have frame then this case should + // have been handled already by content removal notifications. + nsINode* containerNode = textNode->GetParentNode(); + if (!containerNode) { + NS_ASSERTION(!textAcc, + "Text node was removed but accessible is kept alive!"); + continue; + } + + nsIFrame* textFrame = textNode->GetPrimaryFrame(); + if (!textFrame) { + NS_ASSERTION(!textAcc, + "Text node isn't rendered but accessible is kept alive!"); + continue; + } + + nsIContent* containerElm = containerNode->IsElement() ? + containerNode->AsElement() : nullptr; + + nsIFrame::RenderedText text = textFrame->GetRenderedText(0, + UINT32_MAX, nsIFrame::TextOffsetType::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + + // Remove text accessible if rendered text is empty. + if (textAcc) { + if (text.mString.IsEmpty()) { + #ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node lost its content; doc: %p", mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } + #endif + + mDocument->ContentRemoved(containerElm, textNode); + continue; + } + + // Update text of the accessible and fire text change events. + #ifdef A11Y_LOG + if (logging::IsEnabled(logging::eText)) { + logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEntry("old text '%s'", + NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); + logging::MsgEntry("new text: '%s'", + NS_ConvertUTF16toUTF8(text.mString).get()); + logging::MsgEnd(); + } + #endif + + TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString); + continue; + } + + // Append an accessible if rendered text is not empty. + if (!text.mString.IsEmpty()) { + #ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node gains new content; doc: %p", mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } + #endif + + Accessible* container = mDocument->AccessibleOrTrueContainer(containerNode); + MOZ_ASSERT(container, + "Text node having rendered text hasn't accessible document!"); + if (container) { + nsTArray<nsCOMPtr<nsIContent>>* list = + mContentInsertions.LookupOrAdd(container); + list->AppendElement(textNode); + } + } + } + mTextHash.Clear(); + + // Process content inserted notifications to update the tree. + for (auto iter = mContentInsertions.ConstIter(); !iter.Done(); iter.Next()) { + mDocument->ProcessContentInserted(iter.Key(), iter.UserData()); + if (!mDocument) { + return; + } + } + mContentInsertions.Clear(); + + // Bind hanging child documents. + uint32_t hangingDocCnt = mHangingChildDocuments.Length(); + nsTArray<RefPtr<DocAccessible>> newChildDocs; + for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { + DocAccessible* childDoc = mHangingChildDocuments[idx]; + if (childDoc->IsDefunct()) + continue; + + nsIContent* ownerContent = mDocument->DocumentNode()-> + FindContentForSubDocument(childDoc->DocumentNode()); + if (ownerContent) { + Accessible* outerDocAcc = mDocument->GetAccessible(ownerContent); + if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { + if (mDocument->AppendChildDocument(childDoc)) { + newChildDocs.AppendElement(Move(mHangingChildDocuments[idx])); + continue; + } + + outerDocAcc->RemoveChild(childDoc); + } + + // Failed to bind the child document, destroy it. + childDoc->Shutdown(); + } + } + + mHangingChildDocuments.Clear(); + + // If the document is ready and all its subdocuments are completely loaded + // then process the document load. + if (mDocument->HasLoadState(DocAccessible::eReady) && + !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + hangingDocCnt == 0) { + uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; + for (; childDocIdx < childDocCnt; childDocIdx++) { + DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); + if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) + break; + } + + if (childDocIdx == childDocCnt) { + mDocument->ProcessLoad(); + if (!mDocument) + return; + } + } + + // Process only currently queued generic notifications. + nsTArray < RefPtr<Notification> > notifications; + notifications.SwapElements(mNotifications); + + uint32_t notificationCount = notifications.Length(); + for (uint32_t idx = 0; idx < notificationCount; idx++) { + notifications[idx]->Process(); + if (!mDocument) + return; + } + + // Process invalidation list of the document after all accessible tree + // modification are done. + mDocument->ProcessInvalidationList(); + + // We cannot rely on DOM tree to keep aria-owns relations updated. Make + // a validation to remove dead links. + mDocument->ValidateARIAOwned(); + + // Process relocation list. + for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) { + if (mRelocations[idx]->IsInDocument()) { + mDocument->DoARIAOwnsRelocation(mRelocations[idx]); + } + } + mRelocations.Clear(); + + // If a generic notification occurs after this point then we may be allowed to + // process it synchronously. However we do not want to reenter if fireing + // events causes script to run. + mObservingState = eRefreshProcessing; + + CoalesceMutationEvents(); + ProcessMutationEvents(); + mEventGeneration = 0; + + // Now that we are done with them get rid of the events we fired. + RefPtr<AccTreeMutationEvent> mutEvent = Move(mFirstMutationEvent); + mLastMutationEvent = nullptr; + mFirstMutationEvent = nullptr; + while (mutEvent) { + RefPtr<AccTreeMutationEvent> nextEvent = mutEvent->NextEvent(); + Accessible* target = mutEvent->GetAccessible(); + + // We need to be careful here, while it may seem that we can simply 0 all + // the pending event bits that is not true. Because accessibles may be + // reparented they may be the target of both a hide event and a show event + // at the same time. + if (mutEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + target->SetShowEventTarget(false); + } + + if (mutEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + target->SetHideEventTarget(false); + } + + // However it is not possible for a reorder event target to also be the + // target of a show or hide, so we can just zero that. + target->SetReorderEventTarget(false); + + mutEvent->SetPrevEvent(nullptr); + mutEvent->SetNextEvent(nullptr); + mMutationMap.RemoveEvent(mutEvent); + mutEvent = nextEvent; + } + + ProcessEventQueue(); + + if (IPCAccessibilityActive()) { + size_t newDocCount = newChildDocs.Length(); + for (size_t i = 0; i < newDocCount; i++) { + DocAccessible* childDoc = newChildDocs[i]; + if (childDoc->IsDefunct()) { + continue; + } + + Accessible* parent = childDoc->Parent(); + DocAccessibleChild* parentIPCDoc = mDocument->IPCDoc(); + uint64_t id = reinterpret_cast<uintptr_t>(parent->UniqueID()); + MOZ_ASSERT(id); + DocAccessibleChild* ipcDoc = childDoc->IPCDoc(); + if (ipcDoc) { + parentIPCDoc->SendBindChildDoc(ipcDoc, id); + continue; + } + + ipcDoc = new DocAccessibleChild(childDoc); + childDoc->SetIPCDoc(ipcDoc); + +#if defined(XP_WIN) + MOZ_ASSERT(parentIPCDoc); + parentIPCDoc->ConstructChildDocInParentProcess(ipcDoc, id, + AccessibleWrap::GetChildIDFor(childDoc)); +#else + nsCOMPtr<nsITabChild> tabChild = + do_GetInterface(mDocument->DocumentNode()->GetDocShell()); + if (tabChild) { + MOZ_ASSERT(parentIPCDoc); + static_cast<TabChild*>(tabChild.get())-> + SendPDocAccessibleConstructor(ipcDoc, parentIPCDoc, id, 0, 0); + } +#endif + } + } + + mObservingState = eRefreshObserving; + if (!mDocument) + return; + + // Stop further processing if there are no new notifications of any kind or + // events and document load is processed. + if (mContentInsertions.Count() == 0 && mNotifications.IsEmpty() && + mEvents.IsEmpty() && mTextHash.Count() == 0 && + mHangingChildDocuments.IsEmpty() && + mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + mPresShell->RemoveRefreshObserver(this, Flush_Display)) { + mObservingState = eNotObservingRefresh; + } +} + +void +NotificationController::EventMap::PutEvent(AccTreeMutationEvent* aEvent) +{ + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + mTable.Put(addr, aEvent); +} + +AccTreeMutationEvent* +NotificationController::EventMap::GetEvent(Accessible* aTarget, EventType aType) +{ + uint64_t addr = reinterpret_cast<uintptr_t>(aTarget); + MOZ_ASSERT((addr & 0x3) == 0, "target is not 4 byte aligned"); + + addr |= aType; + return mTable.GetWeak(addr); +} + +void +NotificationController::EventMap::RemoveEvent(AccTreeMutationEvent* aEvent) +{ + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + + MOZ_ASSERT(mTable.GetWeak(addr) == aEvent, "mTable has the wrong event"); + mTable.Remove(addr); +} + + NotificationController::EventMap::EventType +NotificationController::EventMap::GetEventType(AccTreeMutationEvent* aEvent) +{ + switch(aEvent->GetEventType()) + { + case nsIAccessibleEvent::EVENT_SHOW: + return ShowEvent; + case nsIAccessibleEvent::EVENT_HIDE: + return HideEvent; + case nsIAccessibleEvent::EVENT_REORDER: + return ReorderEvent; + default: + MOZ_ASSERT_UNREACHABLE("event has invalid type"); + return ShowEvent; + } +} diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h new file mode 100644 index 000000000..a909fc63d --- /dev/null +++ b/accessible/base/NotificationController.h @@ -0,0 +1,439 @@ +/* -*- 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 mozilla_a11y_NotificationController_h_ +#define mozilla_a11y_NotificationController_h_ + +#include "EventQueue.h" +#include "EventTree.h" + +#include "mozilla/IndexSequence.h" +#include "mozilla/Tuple.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefreshDriver.h" + +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +namespace mozilla { +namespace a11y { + +class DocAccessible; + +/** + * Notification interface. + */ +class Notification +{ +public: + NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::Notification) + + /** + * Process notification. + */ + virtual void Process() = 0; + +protected: + Notification() { } + + /** + * Protected destructor, to discourage deletion outside of Release(): + */ + virtual ~Notification() { } + +private: + Notification(const Notification&); + Notification& operator = (const Notification&); +}; + + +/** + * Template class for generic notification. + * + * @note Instance is kept as a weak ref, the caller must guarantee it exists + * longer than the document accessible owning the notification controller + * that this notification is processed by. + */ +template<class Class, class ... Args> +class TNotification : public Notification +{ +public: + typedef void (Class::*Callback)(Args* ...); + + TNotification(Class* aInstance, Callback aCallback, Args* ... aArgs) : + mInstance(aInstance), mCallback(aCallback), mArgs(aArgs...) { } + virtual ~TNotification() { mInstance = nullptr; } + + virtual void Process() override + { ProcessHelper(typename IndexSequenceFor<Args...>::Type()); } + +private: + TNotification(const TNotification&); + TNotification& operator = (const TNotification&); + + template <size_t... Indices> + void ProcessHelper(IndexSequence<Indices...>) + { + (mInstance->*mCallback)(Get<Indices>(mArgs)...); + } + + Class* mInstance; + Callback mCallback; + Tuple<RefPtr<Args> ...> mArgs; +}; + +/** + * Used to process notifications from core for the document accessible. + */ +class NotificationController final : public EventQueue, + public nsARefreshObserver +{ +public: + NotificationController(DocAccessible* aDocument, nsIPresShell* aPresShell); + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; + NS_IMETHOD_(MozExternalRefCountType) Release(void) override; + + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(NotificationController) + + /** + * Shutdown the notification controller. + */ + void Shutdown(); + + /** + * Add an accessible event into the queue to process it later. + */ + void QueueEvent(AccEvent* aEvent) + { + if (PushEvent(aEvent)) { + ScheduleProcessing(); + } + } + + /** + * Creates and adds a name change event into the queue for a container of + * the given accessible, if the accessible is a part of name computation of + * the container. + */ + void QueueNameChange(Accessible* aChangeTarget) + { + if (PushNameChange(aChangeTarget)) { + ScheduleProcessing(); + } + } + + /** + * Returns existing event tree for the given the accessible or creates one if + * it doesn't exists yet. + */ + EventTree* QueueMutation(Accessible* aContainer); + + class MoveGuard final { + public: + explicit MoveGuard(NotificationController* aController) : + mController(aController) + { +#ifdef DEBUG + MOZ_ASSERT(!mController->mMoveGuardOnStack, + "Move guard is on stack already!"); + mController->mMoveGuardOnStack = true; +#endif + } + ~MoveGuard() { +#ifdef DEBUG + MOZ_ASSERT(mController->mMoveGuardOnStack, "No move guard on stack!"); + mController->mMoveGuardOnStack = false; +#endif + mController->mPrecedingEvents.Clear(); + } + + private: + NotificationController* mController; + }; + +#ifdef A11Y_LOG + const EventTree& RootEventTree() const { return mEventTree; }; +#endif + + /** + * Queue a mutation event to emit if not coalesced away. Returns true if the + * event was queued and has not yet been coalesced. + */ + bool QueueMutationEvent(AccTreeMutationEvent* aEvent); + + /** + * Coalesce all queued mutation events. + */ + void CoalesceMutationEvents(); + + /** + * Schedule binding the child document to the tree of this document. + */ + void ScheduleChildDocBinding(DocAccessible* aDocument); + + /** + * Schedule the accessible tree update because of rendered text changes. + */ + inline void ScheduleTextUpdate(nsIContent* aTextNode) + { + // Make sure we are not called with a node that is not in the DOM tree or + // not visible. + MOZ_ASSERT(aTextNode->GetParentNode(), "A text node is not in DOM"); + MOZ_ASSERT(aTextNode->GetPrimaryFrame(), "A text node doesn't have a frame"); + MOZ_ASSERT(aTextNode->GetPrimaryFrame()->StyleVisibility()->IsVisible(), + "A text node is not visible"); + + mTextHash.PutEntry(aTextNode); + ScheduleProcessing(); + } + + /** + * Pend accessible tree update for content insertion. + */ + void ScheduleContentInsertion(Accessible* aContainer, + nsIContent* aStartChildNode, + nsIContent* aEndChildNode); + + /** + * Pend an accessible subtree relocation. + */ + void ScheduleRelocation(Accessible* aOwner) + { + if (!mRelocations.Contains(aOwner) && mRelocations.AppendElement(aOwner)) { + ScheduleProcessing(); + } + } + + /** + * Start to observe refresh to make notifications and events processing after + * layout. + */ + void ScheduleProcessing(); + + /** + * Process the generic notification synchronously if there are no pending + * layout changes and no notifications are pending or being processed right + * now. Otherwise, queue it up to process asynchronously. + * + * @note The caller must guarantee that the given instance still exists when + * the notification is processed. + */ + template<class Class, class Arg> + inline void HandleNotification(Class* aInstance, + typename TNotification<Class, Arg>::Callback aMethod, + Arg* aArg) + { + if (!IsUpdatePending()) { +#ifdef A11Y_LOG + if (mozilla::a11y::logging::IsEnabled(mozilla::a11y::logging::eNotifications)) + mozilla::a11y::logging::Text("sync notification processing"); +#endif + (aInstance->*aMethod)(aArg); + return; + } + + RefPtr<Notification> notification = + new TNotification<Class, Arg>(aInstance, aMethod, aArg); + if (notification && mNotifications.AppendElement(notification)) + ScheduleProcessing(); + } + + /** + * Schedule the generic notification to process asynchronously. + * + * @note The caller must guarantee that the given instance still exists when + * the notification is processed. + */ + template<class Class> + inline void ScheduleNotification(Class* aInstance, + typename TNotification<Class>::Callback aMethod) + { + RefPtr<Notification> notification = + new TNotification<Class>(aInstance, aMethod); + if (notification && mNotifications.AppendElement(notification)) + ScheduleProcessing(); + } + +#ifdef DEBUG + bool IsUpdating() const + { return mObservingState == eRefreshProcessingForUpdate; } +#endif + +protected: + virtual ~NotificationController(); + + nsCycleCollectingAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + /** + * Return true if the accessible tree state update is pending. + */ + bool IsUpdatePending(); + +private: + NotificationController(const NotificationController&); + NotificationController& operator = (const NotificationController&); + + // nsARefreshObserver + virtual void WillRefresh(mozilla::TimeStamp aTime) override; + + /** + * Set and returns a hide event, paired with a show event, for the move. + */ + void WithdrawPrecedingEvents(nsTArray<RefPtr<AccHideEvent>>* aEvs) + { + if (mPrecedingEvents.Length() > 0) { + aEvs->AppendElements(mozilla::Move(mPrecedingEvents)); + } + } + void StorePrecedingEvent(AccHideEvent* aEv) + { + MOZ_ASSERT(mMoveGuardOnStack, "No move guard on stack!"); + mPrecedingEvents.AppendElement(aEv); + } + void StorePrecedingEvents(nsTArray<RefPtr<AccHideEvent>>&& aEvs) + { + MOZ_ASSERT(mMoveGuardOnStack, "No move guard on stack!"); + mPrecedingEvents.InsertElementsAt(0, aEvs); + } + +private: + /** + * get rid of a mutation event that is no longer necessary. + */ + void DropMutationEvent(AccTreeMutationEvent* aEvent); + + /** + * Fire all necessary mutation events. + */ + void ProcessMutationEvents(); + + /** + * Indicates whether we're waiting on an event queue processing from our + * notification controller to flush events. + */ + enum eObservingState { + eNotObservingRefresh, + eRefreshObserving, + eRefreshProcessing, + eRefreshProcessingForUpdate + }; + eObservingState mObservingState; + + /** + * The presshell of the document accessible. + */ + nsIPresShell* mPresShell; + + /** + * Child documents that needs to be bound to the tree. + */ + nsTArray<RefPtr<DocAccessible> > mHangingChildDocuments; + + /** + * Pending accessible tree update notifications for content insertions. + */ + nsClassHashtable<nsRefPtrHashKey<Accessible>, + nsTArray<nsCOMPtr<nsIContent>>> mContentInsertions; + + template<class T> + class nsCOMPtrHashKey : public PLDHashEntryHdr + { + public: + typedef T* KeyType; + typedef const T* KeyTypePointer; + + explicit nsCOMPtrHashKey(const T* aKey) : mKey(const_cast<T*>(aKey)) {} + explicit nsCOMPtrHashKey(const nsPtrHashKey<T> &aToCopy) : mKey(aToCopy.mKey) {} + ~nsCOMPtrHashKey() { } + + KeyType GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const { return aKey == mKey; } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) + { return NS_PTR_TO_INT32(aKey) >> 2; } + + enum { ALLOW_MEMMOVE = true }; + + protected: + nsCOMPtr<T> mKey; + }; + + /** + * Pending accessible tree update notifications for rendered text changes. + */ + nsTHashtable<nsCOMPtrHashKey<nsIContent> > mTextHash; + + /** + * Other notifications like DOM events. Don't make this an AutoTArray; we + * use SwapElements() on it. + */ + nsTArray<RefPtr<Notification> > mNotifications; + + /** + * Holds all scheduled relocations. + */ + nsTArray<RefPtr<Accessible> > mRelocations; + + /** + * Holds all mutation events. + */ + EventTree mEventTree; + + /** + * A temporary collection of hide events that should be fired before related + * show event. Used by EventTree. + */ + nsTArray<RefPtr<AccHideEvent>> mPrecedingEvents; + +#ifdef DEBUG + bool mMoveGuardOnStack; +#endif + + friend class MoveGuard; + friend class EventTree; + + /** + * A list of all mutation events we may want to emit. Ordered from the first + * event that should be emitted to the last one to emit. + */ + RefPtr<AccTreeMutationEvent> mFirstMutationEvent; + RefPtr<AccTreeMutationEvent> mLastMutationEvent; + + /** + * A class to map an accessible and event type to an event. + */ + class EventMap + { + public: + enum EventType + { + ShowEvent = 0x0, + HideEvent = 0x1, + ReorderEvent = 0x2, + }; + + void PutEvent(AccTreeMutationEvent* aEvent); + AccTreeMutationEvent* GetEvent(Accessible* aTarget, EventType aType); + void RemoveEvent(AccTreeMutationEvent* aEvent); + void Clear() { mTable.Clear(); } + + private: + EventType GetEventType(AccTreeMutationEvent* aEvent); + + nsRefPtrHashtable<nsUint64HashKey, AccTreeMutationEvent> mTable; + }; + + EventMap mMutationMap; + uint32_t mEventGeneration; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_NotificationController_h_ diff --git a/accessible/base/Platform.h b/accessible/base/Platform.h new file mode 100644 index 000000000..25204565b --- /dev/null +++ b/accessible/base/Platform.h @@ -0,0 +1,88 @@ +/* -*- 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_Platform_h +#define mozilla_a11y_Platform_h + +#include <stdint.h> + +class nsString; + +namespace mozilla { +namespace a11y { + +class ProxyAccessible; + +enum EPlatformDisabledState { + ePlatformIsForceEnabled = -1, + ePlatformIsEnabled = 0, + ePlatformIsDisabled = 1 +}; + +/** + * Return the platform disabled state. + */ +EPlatformDisabledState PlatformDisabledState(); + +#ifdef MOZ_ACCESSIBILITY_ATK +/** + * Perform initialization that should be done as soon as possible, in order + * to minimize startup time. + * XXX: this function and the next defined in ApplicationAccessibleWrap.cpp + */ +void PreInit(); +#endif + +#if defined(MOZ_ACCESSIBILITY_ATK) || defined(XP_MACOSX) +/** + * Is platform accessibility enabled. + * Only used on linux with atk and MacOS for now. + */ +bool ShouldA11yBeEnabled(); +#endif + +/** + * Called to initialize platform specific accessibility support. + * Note this is called after internal accessibility support is initialized. + */ +void PlatformInit(); + +/** + * Shutdown platform accessibility. + * Note this is called before internal accessibility support is shutdown. + */ +void PlatformShutdown(); + +/** + * called when a new ProxyAccessible is created, so the platform may setup a + * wrapper for it, or take other action. + */ +void ProxyCreated(ProxyAccessible* aProxy, uint32_t aInterfaces); + +/** + * Called just before a ProxyAccessible is destroyed so its wrapper can be + * disposed of and other action taken. + */ +void ProxyDestroyed(ProxyAccessible*); + +/** + * Callied when an event is fired on a proxied accessible. + */ +void ProxyEvent(ProxyAccessible* aTarget, uint32_t aEventType); +void ProxyStateChangeEvent(ProxyAccessible* aTarget, uint64_t aState, + bool aEnabled); +void ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset); +void ProxyTextChangeEvent(ProxyAccessible* aTarget, const nsString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser); +void ProxyShowHideEvent(ProxyAccessible* aTarget, ProxyAccessible* aParent, + bool aInsert, bool aFromUser); +void ProxySelectionEvent(ProxyAccessible* aTarget, ProxyAccessible* aWidget, + uint32_t aType); +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_Platform_h diff --git a/accessible/base/Relation.h b/accessible/base/Relation.h new file mode 100644 index 000000000..49a2c692e --- /dev/null +++ b/accessible/base/Relation.h @@ -0,0 +1,108 @@ +/* -*- 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_relation_h_ +#define mozilla_a11y_relation_h_ + +#include "AccIterator.h" + +#include <memory> + +namespace mozilla { +namespace a11y { + +/** + * A collection of relation targets of a certain type. Targets are computed + * lazily while enumerating. + */ +class Relation +{ +public: + Relation() : mFirstIter(nullptr), mLastIter(nullptr) { } + + explicit Relation(AccIterable* aIter) : + mFirstIter(aIter), mLastIter(aIter) { } + + explicit Relation(Accessible* aAcc) : + mFirstIter(nullptr), mLastIter(nullptr) + { AppendTarget(aAcc); } + + Relation(DocAccessible* aDocument, nsIContent* aContent) : + mFirstIter(nullptr), mLastIter(nullptr) + { AppendTarget(aDocument, aContent); } + + Relation(Relation&& aOther) : + mFirstIter(Move(aOther.mFirstIter)), mLastIter(aOther.mLastIter) + { + aOther.mLastIter = nullptr; + } + + Relation& operator = (Relation&& aRH) + { + mFirstIter = Move(aRH.mFirstIter); + mLastIter = aRH.mLastIter; + aRH.mLastIter = nullptr; + return *this; + } + + inline void AppendIter(AccIterable* aIter) + { + if (mLastIter) + mLastIter->mNextIter.reset(aIter); + else + mFirstIter.reset(aIter); + + mLastIter = aIter; + } + + /** + * Append the given accessible to the set of related accessibles. + */ + inline void AppendTarget(Accessible* aAcc) + { + if (aAcc) + AppendIter(new SingleAccIterator(aAcc)); + } + + /** + * Append the one accessible for this content node to the set of related + * accessibles. + */ + void AppendTarget(DocAccessible* aDocument, nsIContent* aContent) + { + if (aContent) + AppendTarget(aDocument->GetAccessible(aContent)); + } + + /** + * compute and return the next related accessible. + */ + inline Accessible* Next() + { + Accessible* target = nullptr; + + while (mFirstIter && !(target = mFirstIter->Next())) + mFirstIter = std::move(mFirstIter->mNextIter); + + if (!mFirstIter) + mLastIter = nullptr; + + return target; + } + +private: + Relation& operator = (const Relation&) = delete; + Relation(const Relation&) = delete; + + std::unique_ptr<AccIterable> mFirstIter; + AccIterable* mLastIter; +}; + +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/base/RelationType.h b/accessible/base/RelationType.h new file mode 100644 index 000000000..ff2894d3b --- /dev/null +++ b/accessible/base/RelationType.h @@ -0,0 +1,163 @@ +/* -*- 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_relationtype_h_ +#define mozilla_a11y_relationtype_h_ + +namespace mozilla { +namespace a11y { + +enum class RelationType { + + /** + * This object is labelled by a target object. + */ + LABELLED_BY = 0x00, + + /** + * This object is label for a target object. + */ + LABEL_FOR = 0x01, + + /** + * This object is described by the target object. + */ + DESCRIBED_BY = 0x02, + + /** + * This object is describes the target object. + */ + DESCRIPTION_FOR = 0x3, + + /** + * This object is a child of a target object. + */ + NODE_CHILD_OF = 0x4, + + /** + * This object is a parent of a target object. A dual relation to + * NODE_CHILD_OF. + */ + NODE_PARENT_OF = 0x5, + + /** + * Some attribute of this object is affected by a target object. + */ + CONTROLLED_BY = 0x06, + + /** + * This object is interactive and controls some attribute of a target object. + */ + CONTROLLER_FOR = 0x07, + + /** + * Content flows from this object to a target object, i.e. has content that + * flows logically to another object in a sequential way, e.g. text flow. + */ + FLOWS_TO = 0x08, + + /** + * Content flows to this object from a target object, i.e. has content that + * flows logically from another object in a sequential way, e.g. text flow. + */ + FLOWS_FROM = 0x09, + + /** + * This object is a member of a group of one or more objects. When there is + * more than one object in the group each member may have one and the same + * target, e.g. a grouping object. It is also possible that each member has + * multiple additional targets, e.g. one for every other member in the group. + */ + MEMBER_OF = 0x0a, + + /** + * This object is a sub window of a target object. + */ + SUBWINDOW_OF = 0x0b, + + /** + * This object embeds a target object. This relation can be used on the + * OBJID_CLIENT accessible for a top level window to show where the content + * areas are. + */ + EMBEDS = 0x0c, + + /** + * This object is embedded by a target object. + */ + EMBEDDED_BY = 0x0d, + + /** + * This object is a transient component related to the target object. When + * this object is activated the target object doesn't lose focus. + */ + POPUP_FOR = 0x0e, + + /** + * This object is a parent window of the target object. + */ + PARENT_WINDOW_OF = 0x0f, + + /** + * Part of a form/dialog with a related default button. It is used for + * MSAA/XPCOM, it isn't for IA2 or ATK. + */ + DEFAULT_BUTTON = 0x10, + + /** + * The target object is the containing document object. + */ + CONTAINING_DOCUMENT = 0x11, + + /** + * The target object is the topmost containing document object in the tab pane. + */ + CONTAINING_TAB_PANE = 0x12, + + /** + * The target object is the containing window object. + */ + CONTAINING_WINDOW = 0x13, + + /** + * The target object is the containing application object. + */ + CONTAINING_APPLICATION = 0x14, + + + /** + * The target object provides the detailed, extended description for this + * object. It provides more detailed information than would normally be + * provided using the DESCRIBED_BY relation. A common use for this relation is + * in digital publishing where an extended description needs to be conveyed in + * a book that requires structural markup or the embedding of other technology + * to provide illustrative content. + */ + DETAILS = 0x15, + + /** + * This object provides the detailed, extended description for the target + * object. See DETAILS relation. + */ + DETAILS_FOR = 0x16, + + /** + * The target object is the error message for this object. + */ + ERRORMSG = 0x17, + + /** + * This object is the error message for the target object. + */ + ERRORMSG_FOR = 0x18, + + LAST = ERRORMSG_FOR +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/RelationTypeMap.h b/accessible/base/RelationTypeMap.h new file mode 100644 index 000000000..fb45e42c1 --- /dev/null +++ b/accessible/base/RelationTypeMap.h @@ -0,0 +1,154 @@ +/* -*- 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/. */ + +/** + * Usage: declare the macro RELATIONTYPE()with the following arguments: + * RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) + */ + +RELATIONTYPE(LABELLED_BY, + "labelled by", + ATK_RELATION_LABELLED_BY, + NAVRELATION_LABELLED_BY, + IA2_RELATION_LABELLED_BY) + +RELATIONTYPE(LABEL_FOR, + "label for", + ATK_RELATION_LABEL_FOR, + NAVRELATION_LABEL_FOR, + IA2_RELATION_LABEL_FOR) + +RELATIONTYPE(DESCRIBED_BY, + "described by", + ATK_RELATION_DESCRIBED_BY, + NAVRELATION_DESCRIBED_BY, + IA2_RELATION_DESCRIBED_BY) + +RELATIONTYPE(DESCRIPTION_FOR, + "description for", + ATK_RELATION_DESCRIPTION_FOR, + NAVRELATION_DESCRIPTION_FOR, + IA2_RELATION_DESCRIPTION_FOR) + +RELATIONTYPE(NODE_CHILD_OF, + "node child of", + ATK_RELATION_NODE_CHILD_OF, + NAVRELATION_NODE_CHILD_OF, + IA2_RELATION_NODE_CHILD_OF) + +RELATIONTYPE(NODE_PARENT_OF, + "node parent of", + ATK_RELATION_NODE_PARENT_OF, + NAVRELATION_NODE_PARENT_OF, + IA2_RELATION_NODE_PARENT_OF) + +RELATIONTYPE(CONTROLLED_BY, + "controlled by", + ATK_RELATION_CONTROLLED_BY, + NAVRELATION_CONTROLLED_BY, + IA2_RELATION_CONTROLLED_BY) + +RELATIONTYPE(CONTROLLER_FOR, + "controller for", + ATK_RELATION_CONTROLLER_FOR, + NAVRELATION_CONTROLLER_FOR, + IA2_RELATION_CONTROLLER_FOR) + +RELATIONTYPE(FLOWS_TO, + "flows to", + ATK_RELATION_FLOWS_TO, + NAVRELATION_FLOWS_TO, + IA2_RELATION_FLOWS_TO) + +RELATIONTYPE(FLOWS_FROM, + "flows from", + ATK_RELATION_FLOWS_FROM, + NAVRELATION_FLOWS_FROM, + IA2_RELATION_FLOWS_FROM) + +RELATIONTYPE(MEMBER_OF, + "member of", + ATK_RELATION_MEMBER_OF, + NAVRELATION_MEMBER_OF, + IA2_RELATION_MEMBER_OF) + +RELATIONTYPE(SUBWINDOW_OF, + "subwindow of", + ATK_RELATION_SUBWINDOW_OF, + NAVRELATION_SUBWINDOW_OF, + IA2_RELATION_SUBWINDOW_OF) + +RELATIONTYPE(EMBEDS, + "embeds", + ATK_RELATION_EMBEDS, + NAVRELATION_EMBEDS, + IA2_RELATION_EMBEDS) + +RELATIONTYPE(EMBEDDED_BY, + "embedded by", + ATK_RELATION_EMBEDDED_BY, + NAVRELATION_EMBEDDED_BY, + IA2_RELATION_EMBEDDED_BY) + +RELATIONTYPE(POPUP_FOR, + "popup for", + ATK_RELATION_POPUP_FOR, + NAVRELATION_POPUP_FOR, + IA2_RELATION_POPUP_FOR) + +RELATIONTYPE(PARENT_WINDOW_OF, + "parent window of", + ATK_RELATION_PARENT_WINDOW_OF, + NAVRELATION_PARENT_WINDOW_OF, + IA2_RELATION_PARENT_WINDOW_OF) + +RELATIONTYPE(DEFAULT_BUTTON, + "default button", + ATK_RELATION_NULL, + NAVRELATION_DEFAULT_BUTTON, + IA2_RELATION_NULL) + +RELATIONTYPE(CONTAINING_DOCUMENT, + "containing document", + ATK_RELATION_NULL, + NAVRELATION_CONTAINING_DOCUMENT, + IA2_RELATION_CONTAINING_DOCUMENT) + +RELATIONTYPE(CONTAINING_TAB_PANE, + "containing tab pane", + ATK_RELATION_NULL, + NAVRELATION_CONTAINING_TAB_PANE, + IA2_RELATION_CONTAINING_TAB_PANE) + +RELATIONTYPE(CONTAINING_APPLICATION, + "containing application", + ATK_RELATION_NULL, + NAVRELATION_CONTAINING_APPLICATION, + IA2_RELATION_CONTAINING_APPLICATION) + +RELATIONTYPE(DETAILS, + "details", + ATK_RELATION_NULL, + NAVRELATION_DETAILS, + IA2_RELATION_DETAILS) + +RELATIONTYPE(DETAILS_FOR, + "details for", + ATK_RELATION_NULL, + NAVRELATION_DETAILS_FOR, + IA2_RELATION_DETAILS_FOR) + +RELATIONTYPE(ERRORMSG, + "error", + ATK_RELATION_NULL, + NAVRELATION_ERROR, + IA2_RELATION_ERROR) + +RELATIONTYPE(ERRORMSG_FOR, + "error for", + ATK_RELATION_NULL, + NAVRELATION_ERROR_FOR, + IA2_RELATION_ERROR_FOR) diff --git a/accessible/base/Role.h b/accessible/base/Role.h new file mode 100644 index 000000000..6d76eebd7 --- /dev/null +++ b/accessible/base/Role.h @@ -0,0 +1,995 @@ +/* -*- 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 _role_h_ +#define _role_h_ + +/** + * @note Make sure to update the localized role names when changing the list. + * @note When adding a new role, be sure to also add it to base/RoleMap.h and + * update nsIAccessibleRole. + */ + +namespace mozilla { +namespace a11y { +namespace roles { + +enum Role { + /** + * Used when accessible hans't strong defined role. + */ + NOTHING = 0, + + /** + * Represents a title or caption bar for a window. It is used by MSAA only, + * supported automatically by MS Windows. + */ + TITLEBAR = 1, + + /** + * Represents the menu bar (positioned beneath the title bar of a window) + * from which menus are selected by the user. The role is used by + * xul:menubar or role="menubar". + */ + MENUBAR = 2, + + /** + * Represents a vertical or horizontal scroll bar, which is part of the client + * area or used in a control. + */ + SCROLLBAR = 3, + + /** + * Represents a special mouse pointer, which allows a user to manipulate user + * interface elements such as windows. For example, a user clicks and drags + * a sizing grip in the lower-right corner of a window to resize it. + */ + GRIP = 4, + + /** + * Represents a system sound, which is associated with various system events. + */ + SOUND = 5, + + /** + * Represents the system mouse pointer. + */ + CURSOR = 6, + + /** + * Represents the system caret. The role is supported for caret. + */ + CARET = 7, + + /** + * Represents an alert or a condition that a user should be notified about. + * Assistive Technologies typically respond to the role by reading the entire + * onscreen contents of containers advertising this role. Should be used for + * warning dialogs, etc. The role is used by xul:browsermessage, + * role="alert". + */ + ALERT = 8, + + /** + * Represents the window frame, which contains child objects such as + * a title bar, client, and other objects contained in a window. The role + * is supported automatically by MS Windows. + */ + WINDOW = 9, + + /** + * A sub-document (<frame> or <iframe>) + */ + INTERNAL_FRAME = 10, + + /** + * Represents a menu, which presents a list of options from which the user can + * make a selection to perform an action. It is used for role="menu". + */ + MENUPOPUP = 11, + + /** + * Represents a menu item, which is an entry in a menu that a user can choose + * to carry out a command, select an option. It is used for xul:menuitem, + * role="menuitem". + */ + MENUITEM = 12, + + /** + * Represents a ToolTip that provides helpful hints. + */ + TOOLTIP = 13, + + /** + * Represents a main window for an application. It is used for + * role="application". Also refer to APP_ROOT + */ + APPLICATION = 14, + + /** + * Represents a document window. A document window is always contained within + * an application window. It is used for role="document". + */ + DOCUMENT = 15, + + /** + * Represents a pane within a frame or document window. Users can navigate + * between panes and within the contents of the current pane, but cannot + * navigate between items in different panes. Thus, panes represent a level + * of grouping lower than frame windows or documents, but above individual + * controls. It is used for the first child of a <frame> or <iframe>. + */ + PANE = 16, + + /** + * Represents a graphical image used to represent data. + */ + CHART = 17, + + /** + * Represents a dialog box or message box. It is used for xul:dialog, + * role="dialog". + */ + DIALOG = 18, + + /** + * Represents a window border. + */ + BORDER = 19, + + /** + * Logically groups other objects. There is not always a parent-child + * relationship between the grouping object and the objects it contains. It + * is used for html:textfield, xul:groupbox, role="group". + */ + GROUPING = 20, + + /** + * Used to visually divide a space into two regions, such as a separator menu + * item or a bar that divides split panes within a window. It is used for + * xul:separator, html:hr, role="separator". + */ + SEPARATOR = 21, + + /** + * Represents a toolbar, which is a grouping of controls (push buttons or + * toggle buttons) that provides easy access to frequently used features. It + * is used for xul:toolbar, role="toolbar". + */ + TOOLBAR = 22, + + /** + * Represents a status bar, which is an area at the bottom of a window that + * displays information about the current operation, state of the application, + * or selected object. The status bar has multiple fields, which display + * different kinds of information. It is used for xul:statusbar. + */ + STATUSBAR = 23, + + /** + * Represents a table that contains rows and columns of cells, and optionally, + * row headers and column headers. It is used for html:table, + * role="grid". Also refer to the following role: COLUMNHEADER, + * ROWHEADER, COLUMN, ROW, CELL. + */ + TABLE = 24, + + /** + * Represents a column header, providing a visual label for a column in + * a table. It is used for XUL tree column headers, html:th, + * role="colheader". Also refer to TABLE. + */ + COLUMNHEADER = 25, + + /** + * Represents a row header, which provides a visual label for a table row. + * It is used for role="rowheader". Also, see TABLE. + */ + ROWHEADER = 26, + + /** + * Represents a column of cells within a table. Also, see TABLE. + */ + COLUMN = 27, + + /** + * Represents a row of cells within a table. Also, see TABLE. + */ + ROW = 28, + + /** + * Represents a cell within a table. It is used for html:td, + * xul:tree cell and xul:listcell. Also, see TABLE. + */ + CELL = 29, + + /** + * Represents a link to something else. This object might look like text or + * a graphic, but it acts like a button. It is used for + * xul:label@class="text-link", html:a, html:area. + */ + LINK = 30, + + /** + * Displays a Help topic in the form of a ToolTip or Help balloon. + */ + HELPBALLOON = 31, + + /** + * Represents a cartoon-like graphic object, such as Microsoft Office + * Assistant, which is displayed to provide help to users of an application. + */ + CHARACTER = 32, + + /** + * Represents a list box, allowing the user to select one or more items. It + * is used for xul:listbox, html:select@size, role="list". See also + * LIST_ITEM. + */ + LIST = 33, + + /** + * Represents an item in a list. See also LIST. + */ + LISTITEM = 34, + + /** + * Represents an outline or tree structure, such as a tree view control, + * that displays a hierarchical list and allows the user to expand and + * collapse branches. Is is used for role="tree". + */ + OUTLINE = 35, + + /** + * Represents an item in an outline or tree structure. It is used for + * role="treeitem". + */ + OUTLINEITEM = 36, + + /** + * Represents a page tab, it is a child of a page tab list. It is used for + * xul:tab, role="treeitem". Also refer to PAGETABLIST. + */ + PAGETAB = 37, + + /** + * Represents a property sheet. It is used for xul:tabpanel, + * role="tabpanel". + */ + PROPERTYPAGE = 38, + + /** + * Represents an indicator, such as a pointer graphic, that points to the + * current item. + */ + INDICATOR = 39, + + /** + * Represents a picture. Is is used for xul:image, html:img. + */ + GRAPHIC = 40, + + /** + * Represents read-only text, such as labels for other controls or + * instructions in a dialog box. Static text cannot be modified or selected. + * Is is used for xul:label, xul:description, html:label, role="label". + */ + STATICTEXT = 41, + + /** + * Represents selectable text that allows edits or is designated read-only. + */ + TEXT_LEAF = 42, + + /** + * Represents a push button control. It is used for xul:button, html:button, + * role="button". + */ + PUSHBUTTON = 43, + + /** + * Represents a check box control. It is used for xul:checkbox, + * html:input@type="checkbox", role="checkbox". + */ + CHECKBUTTON = 44, + + /** + * Represents an option button, also called a radio button. It is one of a + * group of mutually exclusive options. All objects sharing a single parent + * that have this attribute are assumed to be part of single mutually + * exclusive group. It is used for xul:radio, html:input@type="radio", + * role="radio". + */ + RADIOBUTTON = 45, + + /** + * Represents a combo box; an edit control with an associated list box that + * provides a set of predefined choices. It is used for html:select, + * xul:menulist, role="combobox". + */ + COMBOBOX = 46, + + /** + * Represents the calendar control. + */ + DROPLIST = 47, + + /** + * Represents a progress bar, dynamically showing the user the percent + * complete of an operation in progress. It is used for xul:progressmeter, + * role="progressbar". + */ + PROGRESSBAR = 48, + + /** + * Represents a dial or knob whose purpose is to allow a user to set a value. + */ + DIAL = 49, + + /** + * Represents a hot-key field that allows the user to enter a combination or + * sequence of keystrokes. + */ + HOTKEYFIELD = 50, + + /** + * Represents a slider, which allows the user to adjust a setting in given + * increments between minimum and maximum values. It is used by xul:scale, + * role="slider". + */ + SLIDER = 51, + + /** + * Represents a spin box, which is a control that allows the user to increment + * or decrement the value displayed in a separate "buddy" control associated + * with the spin box. It is used for xul:spinbuttons. + */ + SPINBUTTON = 52, + + /** + * Represents a graphical image used to diagram data. It is used for svg:svg. + */ + DIAGRAM = 53, + + /** + * Represents an animation control, which contains content that changes over + * time, such as a control that displays a series of bitmap frames. + */ + ANIMATION = 54, + + /** + * Represents a mathematical equation. It is used by MATHML, where there is a + * rich DOM subtree for an equation. Use FLAT_EQUATION for <img role="math" alt="[TeX]"/> + */ + EQUATION = 55, + + /** + * Represents a button that drops down a list of items. + */ + BUTTONDROPDOWN = 56, + + /** + * Represents a button that drops down a menu. + */ + BUTTONMENU = 57, + + /** + * Represents a button that drops down a grid. It is used for xul:colorpicker. + */ + BUTTONDROPDOWNGRID = 58, + + /** + * Represents blank space between other objects. + */ + WHITESPACE = 59, + + /** + * Represents a container of page tab controls. Is it used for xul:tabs, + * DHTML: role="tabs". Also refer to PAGETAB. + */ + PAGETABLIST = 60, + + /** + * Represents a control that displays time. + */ + CLOCK = 61, + + /** + * Represents a button on a toolbar that has a drop-down list icon directly + * adjacent to the button. + */ + SPLITBUTTON = 62, + + /** + * Represents an edit control designed for an Internet Protocol (IP) address. + * The edit control is divided into sections for the different parts of the + * IP address. + */ + IPADDRESS = 63, + + /** + * Represents a label control that has an accelerator. + */ + ACCEL_LABEL = 64, + + /** + * Represents an arrow in one of the four cardinal directions. + */ + ARROW = 65, + + /** + * Represents a control that can be drawn into and is used to trap events. + * It is used for html:canvas. + */ + CANVAS = 66, + + /** + * Represents a menu item with a check box. + */ + CHECK_MENU_ITEM = 67, + + /** + * Represents a specialized dialog that lets the user choose a color. + */ + COLOR_CHOOSER = 68, + + /** + * Represents control whose purpose is to allow a user to edit a date. + */ + DATE_EDITOR = 69, + + /** + * An iconified internal frame in an DESKTOP_PANE. Also refer to + * INTERNAL_FRAME. + */ + DESKTOP_ICON = 70, + + /** + * A desktop pane. A pane that supports internal frames and iconified + * versions of those internal frames. + */ + DESKTOP_FRAME = 71, + + /** + * A directory pane. A pane that allows the user to navigate through + * and select the contents of a directory. May be used by a file chooser. + * Also refer to FILE_CHOOSER. + */ + DIRECTORY_PANE = 72, + + /** + * A file chooser. A specialized dialog that displays the files in the + * directory and lets the user select a file, browse a different directory, + * or specify a filename. May use the directory pane to show the contents of + * a directory. Also refer to DIRECTORY_PANE. + */ + FILE_CHOOSER = 73, + + /** + * A font chooser. A font chooser is a component that lets the user pick + * various attributes for fonts. + */ + FONT_CHOOSER = 74, + + /** + * Frame role. A top level window with a title bar, border, menu bar, etc. + * It is often used as the primary window for an application. + */ + CHROME_WINDOW = 75, + + /** + * A glass pane. A pane that is guaranteed to be painted on top of all + * panes beneath it. Also refer to ROOT_PANE. + */ + GLASS_PANE = 76, + + /** + * A document container for HTML, whose children represent the document + * content. + */ + HTML_CONTAINER = 77, + + /** + * A small fixed size picture, typically used to decorate components. + */ + ICON = 78, + + /** + * Presents an icon or short string in an interface. + */ + LABEL = 79, + + /** + * A layered pane. A specialized pane that allows its children to be drawn + * in layers, providing a form of stacking order. This is usually the pane + * that holds the menu bar as well as the pane that contains most of the + * visual components in a window. Also refer to GLASS_PANE and + * ROOT_PANE. + */ + LAYERED_PANE = 80, + + /** + * A specialized pane whose primary use is inside a dialog. + */ + OPTION_PANE = 81, + + /** + * A text object uses for passwords, or other places where the text content + * is not shown visibly to the user. + */ + PASSWORD_TEXT = 82, + + /** + * A temporary window that is usually used to offer the user a list of + * choices, and then hides when the user selects one of those choices. + */ + POPUP_MENU = 83, + + /** + * A radio button that is a menu item. + */ + RADIO_MENU_ITEM = 84, + + /** + * A root pane. A specialized pane that has a glass pane and a layered pane + * as its children. Also refer to GLASS_PANE and LAYERED_PANE. + */ + ROOT_PANE = 85, + + /** + * A scroll pane. An object that allows a user to incrementally view a large + * amount of information. Its children can include scroll bars and a + * viewport. Also refer to VIEW_PORT. + */ + SCROLL_PANE = 86, + + /** + * A split pane. A specialized panel that presents two other panels at the + * same time. Between the two panels is a divider the user can manipulate to + * make one panel larger and the other panel smaller. + */ + SPLIT_PANE = 87, + + /** + * The header for a column of a table. + * XXX: it looks this role is dupe of COLUMNHEADER. + */ + TABLE_COLUMN_HEADER = 88, + + /** + * The header for a row of a table. + * XXX: it looks this role is dupe of ROWHEADER + */ + TABLE_ROW_HEADER = 89, + + /** + * A menu item used to tear off and reattach its menu. + */ + TEAR_OFF_MENU_ITEM = 90, + + /** + * Represents an accessible terminal. + */ + TERMINAL = 91, + + /** + * Collection of objects that constitute a logical text entity. + */ + TEXT_CONTAINER = 92, + + /** + * A toggle button. A specialized push button that can be checked or + * unchecked, but does not provide a separate indicator for the current state. + */ + TOGGLE_BUTTON = 93, + + /** + * Represent a control that is capable of expanding and collapsing rows as + * well as showing multiple columns of data. + */ + TREE_TABLE = 94, + + /** + * A viewport. An object usually used in a scroll pane. It represents the + * portion of the entire data that the user can see. As the user manipulates + * the scroll bars, the contents of the viewport can change. Also refer to + * SCROLL_PANE. + */ + VIEWPORT = 95, + + /** + * Header of a document page. Also refer to FOOTER. + */ + HEADER = 96, + + /** + * Footer of a document page. Also refer to HEADER. + */ + FOOTER = 97, + + /** + * A paragraph of text. + */ + PARAGRAPH = 98, + + /** + * A ruler such as those used in word processors. + */ + RULER = 99, + + /** + * A text entry having dialog or list containing items for insertion into + * an entry widget, for instance a list of words for completion of a + * text entry. It is used for xul:textbox@autocomplete + */ + AUTOCOMPLETE = 100, + + /** + * An editable text object in a toolbar. + */ + EDITBAR = 101, + + /** + * An control whose textual content may be entered or modified by the user. + */ + ENTRY = 102, + + /** + * A caption describing another object. + */ + CAPTION = 103, + + /** + * A visual frame or container which contains a view of document content. + * Document frames may occur within another Document instance, in which case + * the second document may be said to be embedded in the containing instance. + * HTML frames are often DOCUMENT_FRAME. Either this object, or a + * singleton descendant, should implement the Document interface. + */ + DOCUMENT_FRAME = 104, + + /** + * Heading. + */ + HEADING = 105, + + /** + * An object representing a page of document content. It is used in documents + * which are accessed by the user on a page by page basis. + */ + PAGE = 106, + + /** + * A container of document content. An example of the use of this role is to + * represent an html:div. + */ + SECTION = 107, + + /** + * An object which is redundant with another object in the accessible + * hierarchy. ATs typically ignore objects with this role. + */ + REDUNDANT_OBJECT = 108, + + /** + * A container of form controls. An example of the use of this role is to + * represent an html:form. + */ + FORM = 109, + + /** + * An object which is used to allow input of characters not found on a + * keyboard, such as the input of Chinese characters on a Western keyboard. + */ + IME = 110, + + /** + * XXX: document this. + */ + APP_ROOT = 111, + + /** + * Represents a menu item, which is an entry in a menu that a user can choose + * to display another menu. + */ + PARENT_MENUITEM = 112, + + /** + * A calendar that allows the user to select a date. + */ + CALENDAR = 113, + + /** + * A list of items that is shown by combobox. + */ + COMBOBOX_LIST = 114, + + /** + * A item of list that is shown by combobox. + */ + COMBOBOX_OPTION = 115, + + /** + * An image map -- has child links representing the areas + */ + IMAGE_MAP = 116, + + /** + * An option in a listbox + */ + OPTION = 117, + + /** + * A rich option in a listbox, it can have other widgets as children + */ + RICH_OPTION = 118, + + /** + * A list of options + */ + LISTBOX = 119, + + /** + * Represents a mathematical equation in the accessible name + */ + FLAT_EQUATION = 120, + + /** + * Represents a cell within a grid. It is used for role="gridcell". Unlike + * CELL, it allows the calculation of the accessible name from subtree. + * Also, see TABLE. + */ + GRID_CELL = 121, + + /** + * Represents an embedded object. It is used for html:object or html:embed. + */ + EMBEDDED_OBJECT = 122, + + /** + * A note. Originally intended to be hidden until activated, but now also used + * for things like html 'aside'. + */ + NOTE = 123, + + /** + * A figure. Used for things like HTML5 figure element. + */ + FIGURE = 124, + + /** + * Represents a rich item with a check box. + */ + CHECK_RICH_OPTION = 125, + + /** + * Represent a definition list (dl in HTML). + */ + DEFINITION_LIST = 126, + + /** + * Represent a term in a definition list (dt in HTML). + */ + TERM = 127, + + /** + * Represent a definition in a definition list (dd in HTML) + */ + DEFINITION = 128, + + /** + * Represent a keyboard or keypad key (ARIA role "key"). + */ + KEY = 129, + + /** + * Represent a switch control widget (ARIA role "switch"). + */ + SWITCH = 130, + + /** + * A block of MathML code (math). + */ + MATHML_MATH = 131, + + /** + * A MathML identifier (mi in MathML). + */ + MATHML_IDENTIFIER = 132, + + /** + * A MathML number (mn in MathML). + */ + MATHML_NUMBER = 133, + + /** + * A MathML operator (mo in MathML). + */ + MATHML_OPERATOR = 134, + + /** + * A MathML text (mtext in MathML). + */ + MATHML_TEXT = 135, + + /** + * A MathML string literal (ms in MathML). + */ + MATHML_STRING_LITERAL = 136, + + /** + * A MathML glyph (mglyph in MathML). + */ + MATHML_GLYPH = 137, + + /** + * A MathML row (mrow in MathML). + */ + MATHML_ROW = 138, + + /** + * A MathML fraction (mfrac in MathML). + */ + MATHML_FRACTION = 139, + + /** + * A MathML square root (msqrt in MathML). + */ + MATHML_SQUARE_ROOT = 140, + + /** + * A MathML root (mroot in MathML). + */ + MATHML_ROOT = 141, + + /** + * A MathML fenced element (mfenced in MathML). + */ + MATHML_FENCED = 142, + + /** + * A MathML enclosed element (menclose in MathML). + */ + MATHML_ENCLOSED = 143, + + /** + * A MathML styling element (mstyle in MathML). + */ + MATHML_STYLE = 144, + + /** + * A MathML subscript (msub in MathML). + */ + MATHML_SUB = 145, + + /** + * A MathML superscript (msup in MathML). + */ + MATHML_SUP = 146, + + /** + * A MathML subscript and superscript (msubsup in MathML). + */ + MATHML_SUB_SUP = 147, + + /** + * A MathML underscript (munder in MathML). + */ + MATHML_UNDER = 148, + + /** + * A MathML overscript (mover in MathML). + */ + MATHML_OVER = 149, + + /** + * A MathML underscript and overscript (munderover in MathML). + */ + MATHML_UNDER_OVER = 150, + + /** + * A MathML multiple subscript and superscript element (mmultiscripts in + * MathML). + */ + MATHML_MULTISCRIPTS = 151, + + /** + * A MathML table (mtable in MathML). + */ + MATHML_TABLE = 152, + + /** + * A MathML labelled table row (mlabeledtr in MathML). + */ + MATHML_LABELED_ROW = 153, + + /** + * A MathML table row (mtr in MathML). + */ + MATHML_TABLE_ROW = 154, + + /** + * A MathML table entry or cell (mtd in MathML). + */ + MATHML_CELL = 155, + + /** + * A MathML interactive element (maction in MathML). + */ + MATHML_ACTION = 156, + + /** + * A MathML error message (merror in MathML). + */ + MATHML_ERROR = 157, + + /** + * A MathML stacked (rows of numbers) element (mstack in MathML). + */ + MATHML_STACK = 158, + + /** + * A MathML long division element (mlongdiv in MathML). + */ + MATHML_LONG_DIVISION = 159, + + /** + * A MathML stack group (msgroup in MathML). + */ + MATHML_STACK_GROUP = 160, + + /** + * A MathML stack row (msrow in MathML). + */ + MATHML_STACK_ROW = 161, + + /** + * MathML carries, borrows, or crossouts for a row (mscarries in MathML). + */ + MATHML_STACK_CARRIES = 162, + + /** + * A MathML carry, borrow, or crossout for a column (mscarry in MathML). + */ + MATHML_STACK_CARRY = 163, + + /** + * A MathML line in a stack (msline in MathML). + */ + MATHML_STACK_LINE = 164, + + /** + * A group containing radio buttons + */ + RADIO_GROUP = 165, + + /** + * A text container exposing brief amount of information. See related + * TEXT_CONTAINER role. + */ + TEXT = 166, + + /** + * The html:details element. + */ + DETAILS = 167, + + /** + * The html:summary element. + */ + SUMMARY = 168, + + LAST_ROLE = SUMMARY +}; + +} // namespace role + +typedef enum mozilla::a11y::roles::Role role; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/RoleMap.h b/accessible/base/RoleMap.h new file mode 100644 index 000000000..c931355ae --- /dev/null +++ b/accessible/base/RoleMap.h @@ -0,0 +1,1370 @@ +/* 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/. */ + +/** + * Usage: declare the macro ROLE()with the following arguments: + * ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, nameRule) + */ + +ROLE(NOTHING, + "nothing", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeIfReqRule) + +ROLE(TITLEBAR, + "titlebar", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Irrelevant on OS X; windows are always native. + ROLE_SYSTEM_TITLEBAR, + ROLE_SYSTEM_TITLEBAR, + eNoNameRule) + +ROLE(MENUBAR, + "menubar", + ATK_ROLE_MENU_BAR, + NSAccessibilityMenuBarRole, //Irrelevant on OS X; the menubar will always be native and on the top of the screen. + ROLE_SYSTEM_MENUBAR, + ROLE_SYSTEM_MENUBAR, + eNoNameRule) + +ROLE(SCROLLBAR, + "scrollbar", + ATK_ROLE_SCROLL_BAR, + NSAccessibilityScrollBarRole, //We might need to make this its own mozAccessible, to support the children objects (valueindicator, down/up buttons). + ROLE_SYSTEM_SCROLLBAR, + ROLE_SYSTEM_SCROLLBAR, + eNameFromValueRule) + +ROLE(GRIP, + "grip", + ATK_ROLE_UNKNOWN, + NSAccessibilitySplitterRole, + ROLE_SYSTEM_GRIP, + ROLE_SYSTEM_GRIP, + eNoNameRule) + +ROLE(SOUND, + "sound", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + ROLE_SYSTEM_SOUND, + ROLE_SYSTEM_SOUND, + eNoNameRule) + +ROLE(CURSOR, + "cursor", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + ROLE_SYSTEM_CURSOR, + ROLE_SYSTEM_CURSOR, + eNoNameRule) + +ROLE(CARET, + "caret", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + ROLE_SYSTEM_CARET, + ROLE_SYSTEM_CARET, + eNoNameRule) + +ROLE(ALERT, + "alert", + ATK_ROLE_ALERT, + NSAccessibilityGroupRole, + ROLE_SYSTEM_ALERT, + ROLE_SYSTEM_ALERT, + eNoNameRule) + +ROLE(WINDOW, + "window", + ATK_ROLE_WINDOW, + NSAccessibilityWindowRole, //Irrelevant on OS X; all window a11y is handled by the system. + ROLE_SYSTEM_WINDOW, + ROLE_SYSTEM_WINDOW, + eNoNameRule) + +ROLE(INTERNAL_FRAME, + "internal frame", + ATK_ROLE_INTERNAL_FRAME, + NSAccessibilityScrollAreaRole, + USE_ROLE_STRING, + IA2_ROLE_INTERNAL_FRAME, + eNoNameRule) + +ROLE(MENUPOPUP, + "menupopup", + ATK_ROLE_MENU, + NSAccessibilityMenuRole, //The parent of menuitems. + ROLE_SYSTEM_MENUPOPUP, + ROLE_SYSTEM_MENUPOPUP, + eNoNameRule) + +ROLE(MENUITEM, + "menuitem", + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + eNameFromSubtreeRule) + +ROLE(TOOLTIP, + "tooltip", + ATK_ROLE_TOOL_TIP, + @"AXHelpTag", //10.4+ only, so we re-define the constant. + ROLE_SYSTEM_TOOLTIP, + ROLE_SYSTEM_TOOLTIP, + eNameFromSubtreeRule) + +ROLE(APPLICATION, + "application", + ATK_ROLE_EMBEDDED, + NSAccessibilityGroupRole, //Unused on OS X. the system will take care of this. + ROLE_SYSTEM_APPLICATION, + ROLE_SYSTEM_APPLICATION, + eNoNameRule) + +ROLE(DOCUMENT, + "document", + ATK_ROLE_DOCUMENT_FRAME, + @"AXWebArea", + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + eNoNameRule) + +/** + * msaa comment: + * We used to map to ROLE_SYSTEM_PANE, but JAWS would + * not read the accessible name for the contaning pane. + * However, JAWS will read the accessible name for a groupbox. + * By mapping a PANE to a GROUPING, we get no undesirable effects, + * but fortunately JAWS will then read the group's label, + * when an inner control gets focused. + */ +ROLE(PANE, + "pane", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + eNoNameRule) + +ROLE(CHART, + "chart", + ATK_ROLE_CHART, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_CHART, + ROLE_SYSTEM_CHART, + eNoNameRule) + +ROLE(DIALOG, + "dialog", + ATK_ROLE_DIALOG, + NSAccessibilityWindowRole, //There's a dialog subrole. + ROLE_SYSTEM_DIALOG, + ROLE_SYSTEM_DIALOG, + eNoNameRule) + +ROLE(BORDER, + "border", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + ROLE_SYSTEM_BORDER, + ROLE_SYSTEM_BORDER, + eNoNameRule) + +ROLE(GROUPING, + "grouping", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + eNoNameRule) + +ROLE(SEPARATOR, + "separator", + ATK_ROLE_SEPARATOR, + NSAccessibilitySplitterRole, + ROLE_SYSTEM_SEPARATOR, + ROLE_SYSTEM_SEPARATOR, + eNoNameRule) + +ROLE(TOOLBAR, + "toolbar", + ATK_ROLE_TOOL_BAR, + NSAccessibilityToolbarRole, + ROLE_SYSTEM_TOOLBAR, + ROLE_SYSTEM_TOOLBAR, + eNoNameRule) + +ROLE(STATUSBAR, + "statusbar", + ATK_ROLE_STATUSBAR, + NSAccessibilityUnknownRole, //Doesn't exist on OS X (a status bar is its parts; a progressbar, a label, etc.) + ROLE_SYSTEM_STATUSBAR, + ROLE_SYSTEM_STATUSBAR, + eNoNameRule) + +ROLE(TABLE, + "table", + ATK_ROLE_TABLE, + NSAccessibilityTableRole, + ROLE_SYSTEM_TABLE, + ROLE_SYSTEM_TABLE, + eNoNameRule) + +ROLE(COLUMNHEADER, + "columnheader", + ATK_ROLE_COLUMN_HEADER, + NSAccessibilityCellRole, + ROLE_SYSTEM_COLUMNHEADER, + ROLE_SYSTEM_COLUMNHEADER, + eNameFromSubtreeRule) + +ROLE(ROWHEADER, + "rowheader", + ATK_ROLE_ROW_HEADER, + NSAccessibilityCellRole, + ROLE_SYSTEM_ROWHEADER, + ROLE_SYSTEM_ROWHEADER, + eNameFromSubtreeRule) + +ROLE(COLUMN, + "column", + ATK_ROLE_UNKNOWN, + NSAccessibilityColumnRole, + ROLE_SYSTEM_COLUMN, + ROLE_SYSTEM_COLUMN, + eNameFromSubtreeRule) + +ROLE(ROW, + "row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityRowRole, + ROLE_SYSTEM_ROW, + ROLE_SYSTEM_ROW, + eNameFromSubtreeRule) + +ROLE(CELL, + "cell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityCellRole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + eNameFromSubtreeIfReqRule) + +ROLE(LINK, + "link", + ATK_ROLE_LINK, + @"AXLink", //10.4+ the attr first define in SDK 10.4, so we define it here too. ROLE_LINK + ROLE_SYSTEM_LINK, + ROLE_SYSTEM_LINK, + eNameFromSubtreeRule) + +ROLE(HELPBALLOON, + "helpballoon", + ATK_ROLE_UNKNOWN, + @"AXHelpTag", + ROLE_SYSTEM_HELPBALLOON, + ROLE_SYSTEM_HELPBALLOON, + eNameFromSubtreeRule) + +ROLE(CHARACTER, + "character", + ATK_ROLE_IMAGE, + NSAccessibilityUnknownRole, //Unused on OS X. + ROLE_SYSTEM_CHARACTER, + ROLE_SYSTEM_CHARACTER, + eNoNameRule) + +ROLE(LIST, + "list", + ATK_ROLE_LIST, + NSAccessibilityListRole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + eNameFromSubtreeIfReqRule) + +ROLE(LISTITEM, + "listitem", + ATK_ROLE_LIST_ITEM, + NSAccessibilityGroupRole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + eNameFromSubtreeRule) + +ROLE(OUTLINE, + "outline", + ATK_ROLE_TREE, + NSAccessibilityOutlineRole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + eNoNameRule) + +ROLE(OUTLINEITEM, + "outlineitem", + ATK_ROLE_LIST_ITEM, + NSAccessibilityRowRole, //XXX: use OutlineRow as subrole. + ROLE_SYSTEM_OUTLINEITEM, + ROLE_SYSTEM_OUTLINEITEM, + eNameFromSubtreeRule) + +ROLE(PAGETAB, + "pagetab", + ATK_ROLE_PAGE_TAB, + NSAccessibilityRadioButtonRole, + ROLE_SYSTEM_PAGETAB, + ROLE_SYSTEM_PAGETAB, + eNameFromSubtreeRule) + +ROLE(PROPERTYPAGE, + "propertypage", + ATK_ROLE_SCROLL_PANE, + NSAccessibilityGroupRole, + ROLE_SYSTEM_PROPERTYPAGE, + ROLE_SYSTEM_PROPERTYPAGE, + eNoNameRule) + +ROLE(INDICATOR, + "indicator", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_INDICATOR, + ROLE_SYSTEM_INDICATOR, + eNoNameRule) + +ROLE(GRAPHIC, + "graphic", + ATK_ROLE_IMAGE, + NSAccessibilityImageRole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + eNoNameRule) + +ROLE(STATICTEXT, + "statictext", + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + eNoNameRule) + +ROLE(TEXT_LEAF, + "text leaf", + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + eNoNameRule) + +ROLE(PUSHBUTTON, + "pushbutton", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + eNameFromSubtreeRule) + +ROLE(CHECKBUTTON, + "checkbutton", + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + eNameFromSubtreeRule) + +ROLE(RADIOBUTTON, + "radiobutton", + ATK_ROLE_RADIO_BUTTON, + NSAccessibilityRadioButtonRole, + ROLE_SYSTEM_RADIOBUTTON, + ROLE_SYSTEM_RADIOBUTTON, + eNameFromSubtreeRule) + +ROLE(COMBOBOX, + "combobox", + ATK_ROLE_COMBO_BOX, + NSAccessibilityPopUpButtonRole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + eNameFromValueRule) + +ROLE(DROPLIST, + "droplist", + ATK_ROLE_COMBO_BOX, + NSAccessibilityPopUpButtonRole, + ROLE_SYSTEM_DROPLIST, + ROLE_SYSTEM_DROPLIST, + eNoNameRule) + +ROLE(PROGRESSBAR, + "progressbar", + ATK_ROLE_PROGRESS_BAR, + NSAccessibilityProgressIndicatorRole, + ROLE_SYSTEM_PROGRESSBAR, + ROLE_SYSTEM_PROGRESSBAR, + eNameFromValueRule) + +ROLE(DIAL, + "dial", + ATK_ROLE_DIAL, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_DIAL, + ROLE_SYSTEM_DIAL, + eNoNameRule) + +ROLE(HOTKEYFIELD, + "hotkeyfield", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_HOTKEYFIELD, + ROLE_SYSTEM_HOTKEYFIELD, + eNoNameRule) + +ROLE(SLIDER, + "slider", + ATK_ROLE_SLIDER, + NSAccessibilitySliderRole, + ROLE_SYSTEM_SLIDER, + ROLE_SYSTEM_SLIDER, + eNameFromValueRule) + +ROLE(SPINBUTTON, + "spinbutton", + ATK_ROLE_SPIN_BUTTON, + NSAccessibilityIncrementorRole, //Subroles: Increment/Decrement. + ROLE_SYSTEM_SPINBUTTON, + ROLE_SYSTEM_SPINBUTTON, + eNameFromValueRule) + +ROLE(DIAGRAM, + "diagram", + ATK_ROLE_IMAGE, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_DIAGRAM, + ROLE_SYSTEM_DIAGRAM, + eNoNameRule) + +ROLE(ANIMATION, + "animation", + ATK_ROLE_ANIMATION, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_ANIMATION, + ROLE_SYSTEM_ANIMATION, + eNoNameRule) + +ROLE(EQUATION, + "equation", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + eNoNameRule) + +ROLE(BUTTONDROPDOWN, + "buttondropdown", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityPopUpButtonRole, + ROLE_SYSTEM_BUTTONDROPDOWN, + ROLE_SYSTEM_BUTTONDROPDOWN, + eNameFromSubtreeRule) + +ROLE(BUTTONMENU, + "buttonmenu", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityMenuButtonRole, + ROLE_SYSTEM_BUTTONMENU, + ROLE_SYSTEM_BUTTONMENU, + eNameFromSubtreeRule) + +ROLE(BUTTONDROPDOWNGRID, + "buttondropdowngrid", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + ROLE_SYSTEM_BUTTONDROPDOWNGRID, + ROLE_SYSTEM_BUTTONDROPDOWNGRID, + eNameFromSubtreeRule) + +ROLE(WHITESPACE, + "whitespace", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_WHITESPACE, + ROLE_SYSTEM_WHITESPACE, + eNoNameRule) + +ROLE(PAGETABLIST, + "pagetablist", + ATK_ROLE_PAGE_TAB_LIST, + NSAccessibilityTabGroupRole, + ROLE_SYSTEM_PAGETABLIST, + ROLE_SYSTEM_PAGETABLIST, + eNoNameRule) + +ROLE(CLOCK, + "clock", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X + ROLE_SYSTEM_CLOCK, + ROLE_SYSTEM_CLOCK, + eNoNameRule) + +ROLE(SPLITBUTTON, + "splitbutton", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + ROLE_SYSTEM_SPLITBUTTON, + ROLE_SYSTEM_SPLITBUTTON, + eNoNameRule) + +ROLE(IPADDRESS, + "ipaddress", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_IPADDRESS, + ROLE_SYSTEM_IPADDRESS, + eNoNameRule) + +ROLE(ACCEL_LABEL, + "accel label", + ATK_ROLE_ACCEL_LABEL, + NSAccessibilityStaticTextRole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + eNoNameRule) + +ROLE(ARROW, + "arrow", + ATK_ROLE_ARROW, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_INDICATOR, + ROLE_SYSTEM_INDICATOR, + eNoNameRule) + +ROLE(CANVAS, + "canvas", + ATK_ROLE_CANVAS, + NSAccessibilityImageRole, + USE_ROLE_STRING, + IA2_ROLE_CANVAS, + eNoNameRule) + +ROLE(CHECK_MENU_ITEM, + "check menu item", + ATK_ROLE_CHECK_MENU_ITEM, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_CHECK_MENU_ITEM, + eNameFromSubtreeRule) + +ROLE(COLOR_CHOOSER, + "color chooser", + ATK_ROLE_COLOR_CHOOSER, + NSAccessibilityColorWellRole, + ROLE_SYSTEM_DIALOG, + IA2_ROLE_COLOR_CHOOSER, + eNoNameRule) + +ROLE(DATE_EDITOR, + "date editor", + ATK_ROLE_DATE_EDITOR, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_DATE_EDITOR, + eNoNameRule) + +ROLE(DESKTOP_ICON, + "desktop icon", + ATK_ROLE_DESKTOP_ICON, + NSAccessibilityImageRole, + USE_ROLE_STRING, + IA2_ROLE_DESKTOP_ICON, + eNoNameRule) + +ROLE(DESKTOP_FRAME, + "desktop frame", + ATK_ROLE_DESKTOP_FRAME, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_DESKTOP_PANE, + eNoNameRule) + +ROLE(DIRECTORY_PANE, + "directory pane", + ATK_ROLE_DIRECTORY_PANE, + NSAccessibilityBrowserRole, + USE_ROLE_STRING, + IA2_ROLE_DIRECTORY_PANE, + eNoNameRule) + +ROLE(FILE_CHOOSER, + "file chooser", + ATK_ROLE_FILE_CHOOSER, + NSAccessibilityUnknownRole, //Unused on OS X + USE_ROLE_STRING, + IA2_ROLE_FILE_CHOOSER, + eNoNameRule) + +ROLE(FONT_CHOOSER, + "font chooser", + ATK_ROLE_FONT_CHOOSER, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_FONT_CHOOSER, + eNoNameRule) + +ROLE(CHROME_WINDOW, + "chrome window", + ATK_ROLE_FRAME, + NSAccessibilityGroupRole, //Contains the main Firefox UI + ROLE_SYSTEM_APPLICATION, + IA2_ROLE_FRAME, + eNoNameRule) + +ROLE(GLASS_PANE, + "glass pane", + ATK_ROLE_GLASS_PANE, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_GLASS_PANE, + eNoNameRule) + +ROLE(HTML_CONTAINER, + "html container", + ATK_ROLE_HTML_CONTAINER, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeIfReqRule) + +ROLE(ICON, + "icon", + ATK_ROLE_ICON, + NSAccessibilityImageRole, + ROLE_SYSTEM_PUSHBUTTON, + IA2_ROLE_ICON, + eNoNameRule) + +ROLE(LABEL, + "label", + ATK_ROLE_LABEL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_STATICTEXT, + IA2_ROLE_LABEL, + eNameFromSubtreeRule) + +ROLE(LAYERED_PANE, + "layered pane", + ATK_ROLE_LAYERED_PANE, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_LAYERED_PANE, + eNoNameRule) + +ROLE(OPTION_PANE, + "option pane", + ATK_ROLE_OPTION_PANE, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_OPTION_PANE, + eNoNameRule) + +ROLE(PASSWORD_TEXT, + "password text", + ATK_ROLE_PASSWORD_TEXT, + NSAccessibilityTextFieldRole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + eNoNameRule) + +ROLE(POPUP_MENU, + "popup menu", + ATK_ROLE_POPUP_MENU, + NSAccessibilityUnknownRole, //Unused + ROLE_SYSTEM_MENUPOPUP, + ROLE_SYSTEM_MENUPOPUP, + eNoNameRule) + +ROLE(RADIO_MENU_ITEM, + "radio menu item", + ATK_ROLE_RADIO_MENU_ITEM, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_RADIO_MENU_ITEM, + eNameFromSubtreeRule) + +ROLE(ROOT_PANE, + "root pane", + ATK_ROLE_ROOT_PANE, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_ROOT_PANE, + eNoNameRule) + +ROLE(SCROLL_PANE, + "scroll pane", + ATK_ROLE_SCROLL_PANE, + NSAccessibilityScrollAreaRole, + USE_ROLE_STRING, + IA2_ROLE_SCROLL_PANE, + eNoNameRule) + +ROLE(SPLIT_PANE, + "split pane", + ATK_ROLE_SPLIT_PANE, + NSAccessibilitySplitGroupRole, + USE_ROLE_STRING, + IA2_ROLE_SPLIT_PANE, + eNoNameRule) + +ROLE(TABLE_COLUMN_HEADER, + "table column header", + ATK_ROLE_TABLE_COLUMN_HEADER, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_COLUMNHEADER, + ROLE_SYSTEM_COLUMNHEADER, + eNameFromSubtreeRule) + +ROLE(TABLE_ROW_HEADER, + "table row header", + ATK_ROLE_TABLE_ROW_HEADER, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_ROWHEADER, + ROLE_SYSTEM_ROWHEADER, + eNameFromSubtreeRule) + +ROLE(TEAR_OFF_MENU_ITEM, + "tear off menu item", + ATK_ROLE_TEAR_OFF_MENU_ITEM, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_TEAR_OFF_MENU, + eNameFromSubtreeRule) + +ROLE(TERMINAL, + "terminal", + ATK_ROLE_TERMINAL, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_TERMINAL, + eNoNameRule) + +ROLE(TEXT_CONTAINER, + "text container", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_TEXT_FRAME, + eNameFromSubtreeIfReqRule) + +ROLE(TOGGLE_BUTTON, + "toggle button", + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityButtonRole, + ROLE_SYSTEM_PUSHBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + eNameFromSubtreeRule) + +ROLE(TREE_TABLE, + "tree table", + ATK_ROLE_TREE_TABLE, + NSAccessibilityTableRole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + eNoNameRule) + +ROLE(VIEWPORT, + "viewport", + ATK_ROLE_VIEWPORT, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_PANE, + IA2_ROLE_VIEW_PORT, + eNoNameRule) + +ROLE(HEADER, + "header", + ATK_ROLE_HEADER, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_HEADER, + eNoNameRule) + +ROLE(FOOTER, + "footer", + ATK_ROLE_FOOTER, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_FOOTER, + eNoNameRule) + +ROLE(PARAGRAPH, + "paragraph", + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_PARAGRAPH, + eNameFromSubtreeIfReqRule) + +ROLE(RULER, + "ruler", + ATK_ROLE_RULER, + @"AXRuler", //10.4+ only, so we re-define the constant. + USE_ROLE_STRING, + IA2_ROLE_RULER, + eNoNameRule) + +ROLE(AUTOCOMPLETE, + "autocomplete", + ATK_ROLE_AUTOCOMPLETE, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + eNoNameRule) + +ROLE(EDITBAR, + "editbar", + ATK_ROLE_EDITBAR, + NSAccessibilityTextFieldRole, + ROLE_SYSTEM_TEXT, + IA2_ROLE_EDITBAR, + eNoNameRule) + +ROLE(ENTRY, + "entry", + ATK_ROLE_ENTRY, + NSAccessibilityTextFieldRole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + eNameFromValueRule) + +ROLE(CAPTION, + "caption", + ATK_ROLE_CAPTION, + NSAccessibilityStaticTextRole, + USE_ROLE_STRING, + IA2_ROLE_CAPTION, + eNameFromSubtreeIfReqRule) + +ROLE(DOCUMENT_FRAME, + "document frame", + ATK_ROLE_DOCUMENT_FRAME, + NSAccessibilityScrollAreaRole, + USE_ROLE_STRING, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(HEADING, + "heading", + ATK_ROLE_HEADING, + @"AXHeading", + USE_ROLE_STRING, + IA2_ROLE_HEADING, + eNameFromSubtreeIfReqRule) + +ROLE(PAGE, + "page", + ATK_ROLE_PAGE, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_PAGE, + eNoNameRule) + +ROLE(SECTION, + "section", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_SECTION, + eNameFromSubtreeIfReqRule) + +ROLE(REDUNDANT_OBJECT, + "redundant object", + ATK_ROLE_REDUNDANT_OBJECT, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_REDUNDANT_OBJECT, + eNoNameRule) + +ROLE(FORM, + "form", + ATK_ROLE_FORM, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_FORM, + eNoNameRule) + +ROLE(IME, + "ime", + ATK_ROLE_INPUT_METHOD_WINDOW, + NSAccessibilityUnknownRole, + USE_ROLE_STRING, + IA2_ROLE_INPUT_METHOD_WINDOW, + eNoNameRule) + +ROLE(APP_ROOT, + "app root", + ATK_ROLE_APPLICATION, + NSAccessibilityUnknownRole, //Unused on OS X + ROLE_SYSTEM_APPLICATION, + ROLE_SYSTEM_APPLICATION, + eNoNameRule) + +ROLE(PARENT_MENUITEM, + "parent menuitem", + ATK_ROLE_MENU, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + eNameFromSubtreeRule) + +ROLE(CALENDAR, + "calendar", + ATK_ROLE_CALENDAR, + NSAccessibilityGroupRole, + ROLE_SYSTEM_CLIENT, + ROLE_SYSTEM_CLIENT, + eNoNameRule) + +ROLE(COMBOBOX_LIST, + "combobox list", + ATK_ROLE_MENU, + NSAccessibilityMenuRole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + eNoNameRule) + +ROLE(COMBOBOX_OPTION, + "combobox option", + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + eNameFromSubtreeRule) + +ROLE(IMAGE_MAP, + "image map", + ATK_ROLE_IMAGE, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + eNoNameRule) + +ROLE(OPTION, + "listbox option", + ATK_ROLE_LIST_ITEM, + NSAccessibilityStaticTextRole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + eNameFromSubtreeRule) + +ROLE(RICH_OPTION, + "listbox rich option", + ATK_ROLE_LIST_ITEM, + NSAccessibilityRowRole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + eNameFromSubtreeRule) + +ROLE(LISTBOX, + "listbox", + ATK_ROLE_LIST_BOX, + NSAccessibilityListRole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + eNoNameRule) + +ROLE(FLAT_EQUATION, + "flat equation", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + eNoNameRule) + +ROLE(GRID_CELL, + "gridcell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + eNameFromSubtreeRule) + +ROLE(EMBEDDED_OBJECT, + "embedded object", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_EMBEDDED_OBJECT, + eNoNameRule) + +ROLE(NOTE, + "note", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_NOTE, + eNameFromSubtreeIfReqRule) + +ROLE(FIGURE, + "figure", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + eNoNameRule) + +ROLE(CHECK_RICH_OPTION, + "check rich option", + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + eNameFromSubtreeRule) + +ROLE(DEFINITION_LIST, + "definitionlist", + ATK_ROLE_LIST, + NSAccessibilityListRole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + eNameFromSubtreeIfReqRule) + +ROLE(TERM, + "term", + ATK_ROLE_LIST_ITEM, + NSAccessibilityGroupRole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + eNameFromSubtreeRule) + +ROLE(DEFINITION, + "definition", + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_PARAGRAPH, + eNameFromSubtreeRule) + +ROLE(KEY, + "key", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + eNameFromSubtreeRule) + +ROLE(SWITCH, + "switch", + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + ROLE_SYSTEM_CHECKBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + eNameFromSubtreeRule) + +ROLE(MATHML_MATH, + "math", + ATK_ROLE_MATH, + NSAccessibilityGroupRole, + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + eNoNameRule) + +ROLE(MATHML_IDENTIFIER, + "mathml identifier", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_NUMBER, + "mathml number", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_OPERATOR, + "mathml operator", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_TEXT, + "mathml text", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_STRING_LITERAL, + "mathml string literal", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_GLYPH, + "mathml glyph", + ATK_ROLE_IMAGE, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNameFromSubtreeRule) + +ROLE(MATHML_ROW, + "mathml row", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_FRACTION, + "mathml fraction", + ATK_ROLE_MATH_FRACTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_SQUARE_ROOT, + "mathml square root", + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_ROOT, + "mathml root", + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_FENCED, + "mathml fenced", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_ENCLOSED, + "mathml enclosed", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STYLE, + "mathml style", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_SUB, + "mathml sub", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_SUP, + "mathml sup", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_SUB_SUP, + "mathml sub sup", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_UNDER, + "mathml under", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_OVER, + "mathml over", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_UNDER_OVER, + "mathml under over", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_MULTISCRIPTS, + "mathml multiscripts", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_TABLE, + "mathml table", + ATK_ROLE_TABLE, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_LABELED_ROW, + "mathml labeled row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_TABLE_ROW, + "mathml table row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_CELL, + "mathml cell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_ACTION, + "mathml action", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_ERROR, + "mathml error", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK, + "mathml stack", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_LONG_DIVISION, + "mathml long division", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK_GROUP, + "mathml stack group", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK_ROW, + "mathml stack row", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK_CARRIES, + "mathml stack carries", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK_CARRY, + "mathml stack carry", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(MATHML_STACK_LINE, + "mathml stack line", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + 0, + IA2_ROLE_UNKNOWN, + eNoNameRule) + +ROLE(RADIO_GROUP, + "grouping", + ATK_ROLE_PANEL, + NSAccessibilityRadioGroupRole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + eNoNameRule) + +ROLE(TEXT, + "text", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + USE_ROLE_STRING, + IA2_ROLE_TEXT_FRAME, + eNameFromSubtreeIfReqRule) + +ROLE(DETAILS, + "details", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + eNoNameRule) + +ROLE(SUMMARY, + "summary", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityGroupRole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + eNameFromSubtreeRule) + diff --git a/accessible/base/SelectionManager.cpp b/accessible/base/SelectionManager.cpp new file mode 100644 index 000000000..30e01cbfc --- /dev/null +++ b/accessible/base/SelectionManager.cpp @@ -0,0 +1,232 @@ +/* -*- Mode: C++; tab-width: 4; 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 "mozilla/a11y/SelectionManager.h" + +#include "DocAccessible-inl.h" +#include "HyperTextAccessible.h" +#include "HyperTextAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "nsEventShell.h" +#include "nsFrameSelection.h" + +#include "nsIAccessibleTypes.h" +#include "nsIDOMDocument.h" +#include "nsIPresShell.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using mozilla::dom::Selection; + +struct mozilla::a11y::SelData final +{ + SelData(Selection* aSel, int32_t aReason) : + mSel(aSel), mReason(aReason) {} + + RefPtr<Selection> mSel; + int16_t mReason; + + NS_INLINE_DECL_REFCOUNTING(SelData) + +private: + // Private destructor, to discourage deletion outside of Release(): + ~SelData() {} +}; + +SelectionManager::SelectionManager() : + mCaretOffset(-1), mAccWithCaret(nullptr) +{ + +} + +void +SelectionManager::ClearControlSelectionListener() +{ + + // Remove 'this' registered as selection listener for the normal selection. + nsCOMPtr<nsISelection> normalSel = do_QueryReferent(mCurrCtrlNormalSel); + if (normalSel) { + normalSel->AsSelection()->RemoveSelectionListener(this); + mCurrCtrlNormalSel = nullptr; + } + + // Remove 'this' registered as selection listener for the spellcheck + // selection. + nsCOMPtr<nsISelection> spellSel = do_QueryReferent(mCurrCtrlSpellSel); + if (spellSel) { + spellSel->AsSelection()->RemoveSelectionListener(this); + mCurrCtrlSpellSel = nullptr; + } +} + +void +SelectionManager::SetControlSelectionListener(dom::Element* aFocusedElm) +{ + // When focus moves such that the caret is part of a new frame selection + // this removes the old selection listener and attaches a new one for + // the current focus. + ClearControlSelectionListener(); + + nsIFrame* controlFrame = aFocusedElm->GetPrimaryFrame(); + if (!controlFrame) + return; + + const nsFrameSelection* frameSel = controlFrame->GetConstFrameSelection(); + NS_ASSERTION(frameSel, "No frame selection for focused element!"); + if (!frameSel) + return; + + // Register 'this' as selection listener for the normal selection. + nsCOMPtr<nsISelection> normalSel = frameSel->GetSelection(SelectionType::eNormal); + normalSel->AsSelection()->AddSelectionListener(this); + mCurrCtrlNormalSel = do_GetWeakReference(normalSel); + + // Register 'this' as selection listener for the spell check selection. + nsCOMPtr<nsISelection> spellSel = frameSel->GetSelection(SelectionType::eSpellCheck); + spellSel->AsSelection()->AddSelectionListener(this); + mCurrCtrlSpellSel = do_GetWeakReference(spellSel); +} + +void +SelectionManager::AddDocSelectionListener(nsIPresShell* aPresShell) +{ + const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection(); + + // Register 'this' as selection listener for the normal selection. + Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal); + normalSel->AddSelectionListener(this); + + // Register 'this' as selection listener for the spell check selection. + Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck); + spellSel->AddSelectionListener(this); +} + +void +SelectionManager::RemoveDocSelectionListener(nsIPresShell* aPresShell) +{ + const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection(); + + // Remove 'this' registered as selection listener for the normal selection. + Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal); + normalSel->RemoveSelectionListener(this); + + // Remove 'this' registered as selection listener for the spellcheck + // selection. + Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck); + spellSel->RemoveSelectionListener(this); +} + +void +SelectionManager::ProcessTextSelChangeEvent(AccEvent* aEvent) +{ + // Fire selection change event if it's not pure caret-move selection change, + // i.e. the accessible has or had not collapsed selection. + AccTextSelChangeEvent* event = downcast_accEvent(aEvent); + if (!event->IsCaretMoveOnly()) + nsEventShell::FireEvent(aEvent); + + // Fire caret move event if there's a caret in the selection. + nsINode* caretCntrNode = + nsCoreUtils::GetDOMNodeFromDOMPoint(event->mSel->GetFocusNode(), + event->mSel->FocusOffset()); + if (!caretCntrNode) + return; + + HyperTextAccessible* caretCntr = nsAccUtils::GetTextContainer(caretCntrNode); + NS_ASSERTION(caretCntr, + "No text container for focus while there's one for common ancestor?!"); + if (!caretCntr) + return; + + Selection* selection = caretCntr->DOMSelection(); + + // XXX Sometimes we can't get a selection for caretCntr, in that case assume + // event->mSel is correct. + if (!selection) + selection = event->mSel; + + mCaretOffset = caretCntr->DOMPointToOffset(selection->GetFocusNode(), + selection->FocusOffset()); + mAccWithCaret = caretCntr; + if (mCaretOffset != -1) { + RefPtr<AccCaretMoveEvent> caretMoveEvent = + new AccCaretMoveEvent(caretCntr, mCaretOffset, aEvent->FromUserInput()); + nsEventShell::FireEvent(caretMoveEvent); + } +} + +NS_IMETHODIMP +SelectionManager::NotifySelectionChanged(nsIDOMDocument* aDOMDocument, + nsISelection* aSelection, + int16_t aReason) +{ + if (NS_WARN_IF(!aDOMDocument) || NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<nsIDocument> documentNode(do_QueryInterface(aDOMDocument)); + DocAccessible* document = GetAccService()->GetDocAccessible(documentNode); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eSelection)) + logging::SelChange(aSelection, document, aReason); +#endif + + if (document) { + // Selection manager has longer lifetime than any document accessible, + // so that we are guaranteed that the notification is processed before + // the selection manager is destroyed. + RefPtr<SelData> selData = + new SelData(aSelection->AsSelection(), aReason); + document->HandleNotification<SelectionManager, SelData> + (this, &SelectionManager::ProcessSelectionChanged, selData); + } + + return NS_OK; +} + +void +SelectionManager::ProcessSelectionChanged(SelData* aSelData) +{ + Selection* selection = aSelData->mSel; + if (!selection->GetPresShell()) + return; + + const nsRange* range = selection->GetAnchorFocusRange(); + nsINode* cntrNode = nullptr; + if (range) + cntrNode = range->GetCommonAncestor(); + + if (!cntrNode) { + cntrNode = selection->GetFrameSelection()->GetAncestorLimiter(); + if (!cntrNode) { + cntrNode = selection->GetPresShell()->GetDocument(); + NS_ASSERTION(aSelData->mSel->GetPresShell()->ConstFrameSelection() == selection->GetFrameSelection(), + "Wrong selection container was used!"); + } + } + + HyperTextAccessible* text = nsAccUtils::GetTextContainer(cntrNode); + if (!text) { + NS_NOTREACHED("We must reach document accessible implementing text interface!"); + return; + } + + if (selection->GetType() == SelectionType::eNormal) { + RefPtr<AccEvent> event = + new AccTextSelChangeEvent(text, selection, aSelData->mReason); + text->Document()->FireDelayedEvent(event); + + } else if (selection->GetType() == SelectionType::eSpellCheck) { + // XXX: fire an event for container accessible of the focus/anchor range + // of the spelcheck selection. + text->Document()->FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED, + text); + } +} diff --git a/accessible/base/SelectionManager.h b/accessible/base/SelectionManager.h new file mode 100644 index 000000000..d8e6dabbd --- /dev/null +++ b/accessible/base/SelectionManager.h @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 4; 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 mozilla_a11y_SelectionManager_h__ +#define mozilla_a11y_SelectionManager_h__ + +#include "nsIFrame.h" +#include "nsISelectionListener.h" + +class nsIPresShell; + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { + +class AccEvent; +class HyperTextAccessible; + +/** + * This special accessibility class is for the caret and selection management. + * There is only 1 visible caret per top level window. However, there may be + * several visible selections. + * + * The important selections are the one owned by each document, and the one in + * the currently focused control. + * + * On Windows this class is used to move an invisible system caret that + * shadows the Mozilla caret. Windows will also automatically map this to + * the MSAA caret accessible object (via OBJID_CARET) (as opposed to the root + * accessible tree for a window which is retrieved with OBJID_CLIENT). + * + * For ATK and IAccessible2, this class is used to fire caret move and + * selection change events. + */ + +struct SelData; + +class SelectionManager : public nsISelectionListener +{ +public: + // nsISupports + // implemented by derived nsAccessibilityService + + // nsISelectionListener + NS_DECL_NSISELECTIONLISTENER + + // SelectionManager + void Shutdown() { ClearControlSelectionListener(); } + + /** + * Listen to selection events on the focused control. + * + * Note: only one control's selection events are listened to at a time. This + * will remove the previous control's selection listener. + */ + void SetControlSelectionListener(dom::Element* aFocusedElm); + + /** + * Stop listening to selection events on the control. + */ + void ClearControlSelectionListener(); + + /** + * Listen to selection events on the document. + */ + void AddDocSelectionListener(nsIPresShell* aPresShell); + + /** + * Stop listening to selection events for a given document + */ + void RemoveDocSelectionListener(nsIPresShell* aShell); + + /** + * Process delayed event, results in caret move and text selection change + * events. + */ + void ProcessTextSelChangeEvent(AccEvent* aEvent); + + /** + * Gets the current caret offset/hypertext accessible pair. If there is no + * current pair, then returns -1 for the offset and a nullptr for the + * accessible. + */ + inline HyperTextAccessible* AccessibleWithCaret(int32_t* aCaret) + { + if (aCaret) + *aCaret = mCaretOffset; + + return mAccWithCaret; + } + + /** + * Update caret offset when it doesn't go through a caret move event. + */ + inline void UpdateCaretOffset(HyperTextAccessible* aItem, int32_t aOffset) + { + mAccWithCaret = aItem; + mCaretOffset = aOffset; + } + + inline void ResetCaretOffset() + { + mCaretOffset = -1; + mAccWithCaret = nullptr; + } + +protected: + + SelectionManager(); + + /** + * Process DOM selection change. Fire selection and caret move events. + */ + void ProcessSelectionChanged(SelData* aSelData); + +private: + int32_t mCaretOffset; + HyperTextAccessible* mAccWithCaret; + // Currently focused controls. + nsWeakPtr mCurrCtrlNormalSel; + nsWeakPtr mCurrCtrlSpellSel; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/States.h b/accessible/base/States.h new file mode 100644 index 000000000..3f9f486f6 --- /dev/null +++ b/accessible/base/States.h @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set expandtab shiftwidth=2 tabstop=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 _states_h_ +#define _states_h_ + +#include <stdint.h> + +namespace mozilla { +namespace a11y { +namespace states { + + /** + * The object is disabled, opposite to enabled and sensitive. + */ + const uint64_t UNAVAILABLE = ((uint64_t) 0x1) << 0; + + /** + * The object is selected. + */ + const uint64_t SELECTED = ((uint64_t) 0x1) << 1; + + /** + * The object has the keyboard focus. + */ + const uint64_t FOCUSED = ((uint64_t) 0x1) << 2; + + /** + * The object is pressed. + */ + const uint64_t PRESSED = ((uint64_t) 0x1) << 3; + + /** + * The checkable object is checked, applied to check box controls, + * @see CHECKABLE and MIXED states. + */ + const uint64_t CHECKED = ((uint64_t) 0x1) << 4; + + /** + * Indicates that the state of a three-state check box or tool bar button is + * undetermined. The check box is neither checked or unchecked, and is + * in the third or mixed state. + */ + const uint64_t MIXED = ((uint64_t) 0x1) << 5; + + /** + * The object is designated read-only, so it can't be edited. + */ + const uint64_t READONLY = ((uint64_t) 0x1) << 6; + + /** + * The object is hot-tracked by the mouse, which means that its appearance + * has changed to indicate that the mouse pointer is located over it. + * + * This is currently unused. + */ + const uint64_t HOTTRACKED = ((uint64_t) 0x1) << 7; + + /** + * This object is the default button in a window. + */ + const uint64_t DEFAULT = ((uint64_t) 0x1) << 8; + + /** + * The expandable object's children are displayed, the opposite of collapsed, + * applied to trees, list and other controls. + * @see COLLAPSED state + */ + const uint64_t EXPANDED = ((uint64_t) 0x1) << 9; + + /** + * The expandable object's children are not displayed, the opposite of + * expanded, applied to tree lists and other controls, + * @see EXPANDED state. + */ + const uint64_t COLLAPSED = ((uint64_t) 0x1) << 10; + + /** + * The control or document can not accept input at this time. + */ + const uint64_t BUSY = ((uint64_t) 0x1) << 11; + + /** + * The object is out of normal flow, may be outside of boundaries of its + * parent. + */ + const uint64_t FLOATING = ((uint64_t) 0x1) << 12; + + /** + * The object can be checked. + */ + const uint64_t CHECKABLE = ((uint64_t) 0x1) << 13; + + /** + * This object is a graphic which is rapidly changing appearance. + */ + const uint64_t ANIMATED = ((uint64_t) 0x1) << 14; + + /** + * The object is programmatically hidden. + * So user action like scrolling or switching tabs won't make this visible. + */ + const uint64_t INVISIBLE = ((uint64_t) 0x1) << 15; + + /** + * The object is scrolled off screen. + * User action such as scrolling or changing tab may make the object + * visible. + */ + const uint64_t OFFSCREEN = ((uint64_t) 0x1) << 16; + + /** + * The object can be resized. + */ + const uint64_t SIZEABLE = ((uint64_t) 0x1) << 17; + + /** + * The object can be moved to a different position. + */ + const uint64_t MOVEABLE = ((uint64_t) 0x1) << 18; + + /** + * The object describes itself with speech. + * Other speech related assistive technology may want to avoid speaking + * information about this object, because the object is already doing this. + */ + const uint64_t SELFVOICING = ((uint64_t) 0x1) << 19; + + /** + * The object can have the focus and become focused. + */ + const uint64_t FOCUSABLE = ((uint64_t) 0x1) << 20; + + /** + * The object can be selected. + */ + const uint64_t SELECTABLE = ((uint64_t) 0x1) << 21; + + /** + * This object is a link. + */ + const uint64_t LINKED = ((uint64_t) 0x1) << 22; + + /** + * This is used for links that have been traversed + * i.e. the linked page has been visited. + */ + const uint64_t TRAVERSED = ((uint64_t) 0x1) << 23; + + /** + * Supports multiple selection. + */ + const uint64_t MULTISELECTABLE = ((uint64_t) 0x1) << 24; + + /** + * Supports extended selection. + * All objects supporting this are also multipselectable. + * This only makes sense for msaa see bug 635690. + */ + const uint64_t EXTSELECTABLE = ((uint64_t) 0x1) << 25; + + /** + * The user is required to interact with this object. + */ + const uint64_t REQUIRED = ((uint64_t) 0x1) << 26; + + /** + * The object is an alert, notifying the user of something important. + */ + const uint64_t ALERT = ((uint64_t) 0x1) << 27; + + /** + * Used for text fields containing invalid values. + */ + const uint64_t INVALID = ((uint64_t) 0x1) << 28; + + /** + * The controls value can not be obtained, and is returned as a set of "*"s. + */ + const uint64_t PROTECTED = ((uint64_t) 0x1) << 29; + + /** + * The object can be invoked to show a pop up menu or window. + */ + const uint64_t HASPOPUP = ((uint64_t) 0x1) << 30; + + /** + * The editable area has some kind of autocompletion. + */ + const uint64_t SUPPORTS_AUTOCOMPLETION = ((uint64_t) 0x1) << 31; + + /** + * The object is no longer available to be queried. + */ + const uint64_t DEFUNCT = ((uint64_t) 0x1) << 32; + + /** + * The text is selectable, the object must implement the text interface. + */ + const uint64_t SELECTABLE_TEXT = ((uint64_t) 0x1) << 33; + + /** + * The text in this object can be edited. + */ + const uint64_t EDITABLE = ((uint64_t) 0x1) << 34; + + /** + * This window is currently the active window. + */ + const uint64_t ACTIVE = ((uint64_t) 0x1) << 35; + + /** + * Indicates that the object is modal. Modal objects have the behavior + * that something must be done with the object before the user can + * interact with an object in a different window. + */ + const uint64_t MODAL = ((uint64_t) 0x1) << 36; + + /** + * Edit control that can take multiple lines. + */ + const uint64_t MULTI_LINE = ((uint64_t) 0x1) << 37; + + /** + * Uses horizontal layout. + */ + const uint64_t HORIZONTAL = ((uint64_t) 0x1) << 38; + + /** + * Indicates this object paints every pixel within its rectangular region. + */ + const uint64_t OPAQUE1 = ((uint64_t) 0x1) << 39; + + /** + * This text object can only contain 1 line of text. + */ + const uint64_t SINGLE_LINE = ((uint64_t) 0x1) << 40; + + /** + * The parent object manages descendants, and this object may only exist + * while it is visible or has focus. + * For example the focused cell of a table or the current element of a list box may have this state. + */ + const uint64_t TRANSIENT = ((uint64_t) 0x1) << 41; + + /** + * Uses vertical layout. + * Especially used for sliders and scrollbars. + */ + const uint64_t VERTICAL = ((uint64_t) 0x1) << 42; + + /** + * Object not dead, but not up-to-date either. + */ + const uint64_t STALE = ((uint64_t) 0x1) << 43; + + /** + * A widget that is not unavailable. + */ + const uint64_t ENABLED = ((uint64_t) 0x1) << 44; + + /** + * Same as ENABLED state for now see bug 636158 + */ + const uint64_t SENSITIVE = ((uint64_t) 0x1) << 45; + + /** + * The object is expandable, provides a UI to expand/collapse its children + * @see EXPANDED and COLLAPSED states. + */ + const uint64_t EXPANDABLE = ((uint64_t) 0x1) << 46; + + /** + * The object is pinned, usually indicating it is fixed in place and has permanence. + */ + const uint64_t PINNED = ((uint64_t) 0x1) << 47; +} // namespace states +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/base/Statistics.h b/accessible/base/Statistics.h new file mode 100644 index 000000000..c9f1832b2 --- /dev/null +++ b/accessible/base/Statistics.h @@ -0,0 +1,39 @@ +/* -*- 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 A11Y_STATISTICS_H_ +#define A11Y_STATISTICS_H_ + +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace a11y { +namespace statistics { + + inline void A11yInitialized() + { Telemetry::Accumulate(Telemetry::A11Y_INSTANTIATED_FLAG, true); } + + inline void A11yConsumers(uint32_t aConsumer) + { Telemetry::Accumulate(Telemetry::A11Y_CONSUMERS, aConsumer); } + + /** + * Report that ISimpleDOM* has been used. + */ + inline void ISimpleDOMUsed() + { Telemetry::Accumulate(Telemetry::A11Y_ISIMPLEDOM_USAGE_FLAG, true); } + + /** + * Report that IAccessibleTable has been used. + */ + inline void IAccessibleTableUsed() + { Telemetry::Accumulate(Telemetry::A11Y_IATABLE_USAGE_FLAG, true); } + +} // namespace statistics +} // namespace a11y +} // namespace mozilla + +#endif + diff --git a/accessible/base/StyleInfo.cpp b/accessible/base/StyleInfo.cpp new file mode 100644 index 000000000..d49152cba --- /dev/null +++ b/accessible/base/StyleInfo.cpp @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set expandtab shiftwidth=2 tabstop=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 "StyleInfo.h" + +#include "mozilla/dom/Element.h" +#include "nsComputedDOMStyle.h" +#include "nsCSSProps.h" +#include "nsIFrame.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +StyleInfo::StyleInfo(dom::Element* aElement, nsIPresShell* aPresShell) : + mElement(aElement) +{ + mStyleContext = + nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, + nullptr, + aPresShell); +} + +void +StyleInfo::Display(nsAString& aValue) +{ + aValue.Truncate(); + AppendASCIItoUTF16( + nsCSSProps::ValueToKeyword(mStyleContext->StyleDisplay()->mDisplay, + nsCSSProps::kDisplayKTable), aValue); +} + +void +StyleInfo::TextAlign(nsAString& aValue) +{ + aValue.Truncate(); + AppendASCIItoUTF16( + nsCSSProps::ValueToKeyword(mStyleContext->StyleText()->mTextAlign, + nsCSSProps::kTextAlignKTable), aValue); +} + +void +StyleInfo::TextIndent(nsAString& aValue) +{ + aValue.Truncate(); + + const nsStyleCoord& styleCoord = + mStyleContext->StyleText()->mTextIndent; + + nscoord coordVal = 0; + switch (styleCoord.GetUnit()) { + case eStyleUnit_Coord: + coordVal = styleCoord.GetCoordValue(); + aValue.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(coordVal)); + aValue.AppendLiteral("px"); + break; + + case eStyleUnit_Percent: + aValue.AppendFloat(styleCoord.GetPercentValue() * 100); + aValue.AppendLiteral("%"); + break; + + case eStyleUnit_Null: + case eStyleUnit_Normal: + case eStyleUnit_Auto: + case eStyleUnit_None: + case eStyleUnit_Factor: + case eStyleUnit_Degree: + case eStyleUnit_Grad: + case eStyleUnit_Radian: + case eStyleUnit_Turn: + case eStyleUnit_FlexFraction: + case eStyleUnit_Integer: + case eStyleUnit_Enumerated: + case eStyleUnit_Calc: + aValue.AppendLiteral("0px"); + break; + } +} + +void +StyleInfo::Margin(css::Side aSide, nsAString& aValue) +{ + MOZ_ASSERT(mElement->GetPrimaryFrame(), " mElement->GetPrimaryFrame() needs to be valid pointer"); + aValue.Truncate(); + + nscoord coordVal = mElement->GetPrimaryFrame()->GetUsedMargin().Side(aSide); + aValue.AppendFloat(nsPresContext::AppUnitsToFloatCSSPixels(coordVal)); + aValue.AppendLiteral("px"); +} + +void +StyleInfo::FormatColor(const nscolor& aValue, nsString& aFormattedValue) +{ + // Combine the string like rgb(R, G, B) from nscolor. + aFormattedValue.AppendLiteral("rgb("); + aFormattedValue.AppendInt(NS_GET_R(aValue)); + aFormattedValue.AppendLiteral(", "); + aFormattedValue.AppendInt(NS_GET_G(aValue)); + aFormattedValue.AppendLiteral(", "); + aFormattedValue.AppendInt(NS_GET_B(aValue)); + aFormattedValue.Append(')'); +} + +void +StyleInfo::FormatFontStyle(const nscoord& aValue, nsAString& aFormattedValue) +{ + nsCSSKeyword keyword = + nsCSSProps::ValueToKeywordEnum(aValue, nsCSSProps::kFontStyleKTable); + AppendUTF8toUTF16(nsCSSKeywords::GetStringValue(keyword), aFormattedValue); +} + +void +StyleInfo::FormatTextDecorationStyle(uint8_t aValue, nsAString& aFormattedValue) +{ + nsCSSKeyword keyword = + nsCSSProps::ValueToKeywordEnum(aValue, + nsCSSProps::kTextDecorationStyleKTable); + AppendUTF8toUTF16(nsCSSKeywords::GetStringValue(keyword), aFormattedValue); +} diff --git a/accessible/base/StyleInfo.h b/accessible/base/StyleInfo.h new file mode 100644 index 000000000..c8cc03388 --- /dev/null +++ b/accessible/base/StyleInfo.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set expandtab shiftwidth=2 tabstop=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 _mozilla_a11y_style_h_ +#define _mozilla_a11y_style_h_ + +#include "mozilla/gfx/Types.h" +#include "nsStyleContext.h" + +namespace mozilla { +namespace a11y { + +class StyleInfo +{ +public: + StyleInfo(dom::Element* aElement, nsIPresShell* aPresShell); + ~StyleInfo() { } + + void Display(nsAString& aValue); + void TextAlign(nsAString& aValue); + void TextIndent(nsAString& aValue); + void MarginLeft(nsAString& aValue) { Margin(eSideLeft, aValue); } + void MarginRight(nsAString& aValue) { Margin(eSideRight, aValue); } + void MarginTop(nsAString& aValue) { Margin(eSideTop, aValue); } + void MarginBottom(nsAString& aValue) { Margin(eSideBottom, aValue); } + + static void FormatColor(const nscolor& aValue, nsString& aFormattedValue); + static void FormatFontStyle(const nscoord& aValue, nsAString& aFormattedValue); + static void FormatTextDecorationStyle(uint8_t aValue, nsAString& aFormattedValue); + +private: + StyleInfo() = delete; + StyleInfo(const StyleInfo&) = delete; + StyleInfo& operator = (const StyleInfo&) = delete; + + void Margin(Side aSide, nsAString& aValue); + + dom::Element* mElement; + RefPtr<nsStyleContext> mStyleContext; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextAttrs.cpp b/accessible/base/TextAttrs.cpp new file mode 100644 index 000000000..0f9e4f6fe --- /dev/null +++ b/accessible/base/TextAttrs.cpp @@ -0,0 +1,913 @@ +/* -*- 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 "TextAttrs.h" + +#include "Accessible-inl.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "StyleInfo.h" + +#include "gfxFont.h" +#include "nsFontMetrics.h" +#include "nsLayoutUtils.h" +#include "nsContainerFrame.h" +#include "HyperTextAccessible.h" +#include "mozilla/AppUnits.h" +#include "mozilla/gfx/2D.h" + +#if defined(MOZ_WIDGET_GTK) +#include "gfxPlatformGtk.h" // xxx - for UseFcFontList +#endif + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TextAttrsMgr +//////////////////////////////////////////////////////////////////////////////// + +void +TextAttrsMgr::GetAttributes(nsIPersistentProperties* aAttributes, + uint32_t* aStartOffset, + uint32_t* aEndOffset) +{ + // 1. Hyper text accessible must be specified always. + // 2. Offset accessible and result hyper text offsets must be specified in + // the case of text attributes. + // 3. Offset accessible and result hyper text offsets must not be specified + // but include default text attributes flag and attributes list must be + // specified in the case of default text attributes. + NS_PRECONDITION(mHyperTextAcc && + ((mOffsetAcc && mOffsetAccIdx != -1 && + aStartOffset && aEndOffset) || + (!mOffsetAcc && mOffsetAccIdx == -1 && + !aStartOffset && !aEndOffset && + mIncludeDefAttrs && aAttributes)), + "Wrong usage of TextAttrsMgr!"); + + // Embedded objects are combined into own range with empty attributes set. + if (mOffsetAcc && !mOffsetAcc->IsText()) { + for (int32_t childIdx = mOffsetAccIdx - 1; childIdx >= 0; childIdx--) { + Accessible* currAcc = mHyperTextAcc->GetChildAt(childIdx); + if (currAcc->IsText()) + break; + + (*aStartOffset)--; + } + + uint32_t childCount = mHyperTextAcc->ChildCount(); + for (uint32_t childIdx = mOffsetAccIdx + 1; childIdx < childCount; + childIdx++) { + Accessible* currAcc = mHyperTextAcc->GetChildAt(childIdx); + if (currAcc->IsText()) + break; + + (*aEndOffset)++; + } + + return; + } + + // Get the content and frame of the accessible. In the case of document + // accessible it's role content and root frame. + nsIContent* hyperTextElm = mHyperTextAcc->GetContent(); + if (!hyperTextElm) + return; // XXX: we don't support text attrs on document with no body + + nsIFrame* rootFrame = mHyperTextAcc->GetFrame(); + MOZ_ASSERT(rootFrame, "No frame for accessible!"); + if (!rootFrame) + return; + + nsIContent *offsetNode = nullptr, *offsetElm = nullptr; + nsIFrame *frame = nullptr; + if (mOffsetAcc) { + offsetNode = mOffsetAcc->GetContent(); + offsetElm = nsCoreUtils::GetDOMElementFor(offsetNode); + MOZ_ASSERT(offsetElm, "No element for offset accessible!"); + if (!offsetElm) + return; + + frame = offsetElm->GetPrimaryFrame(); + } + + // "language" text attribute + LangTextAttr langTextAttr(mHyperTextAcc, hyperTextElm, offsetNode); + + // "aria-invalid" text attribute + InvalidTextAttr invalidTextAttr(hyperTextElm, offsetNode); + + // "background-color" text attribute + BGColorTextAttr bgColorTextAttr(rootFrame, frame); + + // "color" text attribute + ColorTextAttr colorTextAttr(rootFrame, frame); + + // "font-family" text attribute + FontFamilyTextAttr fontFamilyTextAttr(rootFrame, frame); + + // "font-size" text attribute + FontSizeTextAttr fontSizeTextAttr(rootFrame, frame); + + // "font-style" text attribute + FontStyleTextAttr fontStyleTextAttr(rootFrame, frame); + + // "font-weight" text attribute + FontWeightTextAttr fontWeightTextAttr(rootFrame, frame); + + // "auto-generated" text attribute + AutoGeneratedTextAttr autoGenTextAttr(mHyperTextAcc, mOffsetAcc); + + // "text-underline(line-through)-style(color)" text attributes + TextDecorTextAttr textDecorTextAttr(rootFrame, frame); + + // "text-position" text attribute + TextPosTextAttr textPosTextAttr(rootFrame, frame); + + TextAttr* attrArray[] = + { + &langTextAttr, + &invalidTextAttr, + &bgColorTextAttr, + &colorTextAttr, + &fontFamilyTextAttr, + &fontSizeTextAttr, + &fontStyleTextAttr, + &fontWeightTextAttr, + &autoGenTextAttr, + &textDecorTextAttr, + &textPosTextAttr + }; + + // Expose text attributes if applicable. + if (aAttributes) { + for (uint32_t idx = 0; idx < ArrayLength(attrArray); idx++) + attrArray[idx]->Expose(aAttributes, mIncludeDefAttrs); + } + + // Expose text attributes range where they are applied if applicable. + if (mOffsetAcc) + GetRange(attrArray, ArrayLength(attrArray), aStartOffset, aEndOffset); +} + +void +TextAttrsMgr::GetRange(TextAttr* aAttrArray[], uint32_t aAttrArrayLen, + uint32_t* aStartOffset, uint32_t* aEndOffset) +{ + // Navigate backward from anchor accessible to find start offset. + for (int32_t childIdx = mOffsetAccIdx - 1; childIdx >= 0; childIdx--) { + Accessible* currAcc = mHyperTextAcc->GetChildAt(childIdx); + + // Stop on embedded accessible since embedded accessibles are combined into + // own range. + if (!currAcc->IsText()) + break; + + MOZ_ASSERT(nsCoreUtils::GetDOMElementFor(currAcc->GetContent()), + "Text accessible has to have an associated DOM element"); + + bool offsetFound = false; + for (uint32_t attrIdx = 0; attrIdx < aAttrArrayLen; attrIdx++) { + TextAttr* textAttr = aAttrArray[attrIdx]; + if (!textAttr->Equal(currAcc)) { + offsetFound = true; + break; + } + } + + if (offsetFound) + break; + + *(aStartOffset) -= nsAccUtils::TextLength(currAcc); + } + + // Navigate forward from anchor accessible to find end offset. + uint32_t childLen = mHyperTextAcc->ChildCount(); + for (uint32_t childIdx = mOffsetAccIdx + 1; childIdx < childLen; childIdx++) { + Accessible* currAcc = mHyperTextAcc->GetChildAt(childIdx); + if (!currAcc->IsText()) + break; + + MOZ_ASSERT(nsCoreUtils::GetDOMElementFor(currAcc->GetContent()), + "Text accessible has to have an associated DOM element"); + + bool offsetFound = false; + for (uint32_t attrIdx = 0; attrIdx < aAttrArrayLen; attrIdx++) { + TextAttr* textAttr = aAttrArray[attrIdx]; + + // Alter the end offset when text attribute changes its value and stop + // the search. + if (!textAttr->Equal(currAcc)) { + offsetFound = true; + break; + } + } + + if (offsetFound) + break; + + (*aEndOffset) += nsAccUtils::TextLength(currAcc); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// LangTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::LangTextAttr:: + LangTextAttr(HyperTextAccessible* aRoot, + nsIContent* aRootElm, nsIContent* aElm) : + TTextAttr<nsString>(!aElm), mRootContent(aRootElm) +{ + aRoot->Language(mRootNativeValue); + mIsRootDefined = !mRootNativeValue.IsEmpty(); + + if (aElm) { + nsCoreUtils::GetLanguageFor(aElm, mRootContent, mNativeValue); + mIsDefined = !mNativeValue.IsEmpty(); + } +} + +TextAttrsMgr::LangTextAttr:: + ~LangTextAttr() {} + +bool +TextAttrsMgr::LangTextAttr:: + GetValueFor(Accessible* aAccessible, nsString* aValue) +{ + nsCoreUtils::GetLanguageFor(aAccessible->GetContent(), mRootContent, *aValue); + return !aValue->IsEmpty(); +} + +void +TextAttrsMgr::LangTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nsString& aValue) +{ + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::language, aValue); +} + +//////////////////////////////////////////////////////////////////////////////// +// InvalidTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::InvalidTextAttr:: + InvalidTextAttr(nsIContent* aRootElm, nsIContent* aElm) : + TTextAttr<uint32_t>(!aElm), mRootElm(aRootElm) +{ + mIsRootDefined = GetValue(mRootElm, &mRootNativeValue); + if (aElm) + mIsDefined = GetValue(aElm, &mNativeValue); +} + +bool +TextAttrsMgr::InvalidTextAttr:: + GetValueFor(Accessible* aAccessible, uint32_t* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + return elm ? GetValue(elm, aValue) : false; +} + +void +TextAttrsMgr::InvalidTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const uint32_t& aValue) +{ + switch (aValue) { + case eFalse: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, + NS_LITERAL_STRING("false")); + break; + + case eGrammar: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, + NS_LITERAL_STRING("grammar")); + break; + + case eSpelling: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, + NS_LITERAL_STRING("spelling")); + break; + + case eTrue: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, + NS_LITERAL_STRING("true")); + break; + } +} + +bool +TextAttrsMgr::InvalidTextAttr:: + GetValue(nsIContent* aElm, uint32_t* aValue) +{ + nsIContent* elm = aElm; + do { + if (nsAccUtils::HasDefinedARIAToken(elm, nsGkAtoms::aria_invalid)) { + static nsIContent::AttrValuesArray tokens[] = + { &nsGkAtoms::_false, &nsGkAtoms::grammar, &nsGkAtoms::spelling, + nullptr }; + + int32_t idx = elm->FindAttrValueIn(kNameSpaceID_None, + nsGkAtoms::aria_invalid, tokens, + eCaseMatters); + switch (idx) { + case 0: + *aValue = eFalse; + return true; + case 1: + *aValue = eGrammar; + return true; + case 2: + *aValue = eSpelling; + return true; + default: + *aValue = eTrue; + return true; + } + } + } while ((elm = elm->GetParent()) && elm != mRootElm); + + return false; +} + + +//////////////////////////////////////////////////////////////////////////////// +// BGColorTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::BGColorTextAttr:: + BGColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<nscolor>(!aFrame), mRootFrame(aRootFrame) +{ + mIsRootDefined = GetColor(mRootFrame, &mRootNativeValue); + if (aFrame) + mIsDefined = GetColor(aFrame, &mNativeValue); +} + +bool +TextAttrsMgr::BGColorTextAttr:: + GetValueFor(Accessible* aAccessible, nscolor* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + return GetColor(frame, aValue); + } + } + return false; +} + +void +TextAttrsMgr::BGColorTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nscolor& aValue) +{ + nsAutoString formattedValue; + StyleInfo::FormatColor(aValue, formattedValue); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::backgroundColor, + formattedValue); +} + +bool +TextAttrsMgr::BGColorTextAttr:: + GetColor(nsIFrame* aFrame, nscolor* aColor) +{ + const nsStyleBackground* styleBackground = aFrame->StyleBackground(); + + if (NS_GET_A(styleBackground->mBackgroundColor) > 0) { + *aColor = styleBackground->mBackgroundColor; + return true; + } + + nsContainerFrame *parentFrame = aFrame->GetParent(); + if (!parentFrame) { + *aColor = aFrame->PresContext()->DefaultBackgroundColor(); + return true; + } + + // Each frame of parents chain for the initially passed 'aFrame' has + // transparent background color. So background color isn't changed from + // 'mRootFrame' to initially passed 'aFrame'. + if (parentFrame == mRootFrame) + return false; + + return GetColor(parentFrame, aColor); +} + + +//////////////////////////////////////////////////////////////////////////////// +// ColorTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::ColorTextAttr:: + ColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<nscolor>(!aFrame) +{ + mRootNativeValue = aRootFrame->StyleColor()->mColor; + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleColor()->mColor; + mIsDefined = true; + } +} + +bool +TextAttrsMgr::ColorTextAttr:: + GetValueFor(Accessible* aAccessible, nscolor* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + *aValue = frame->StyleColor()->mColor; + return true; + } + } + return false; +} + +void +TextAttrsMgr::ColorTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nscolor& aValue) +{ + nsAutoString formattedValue; + StyleInfo::FormatColor(aValue, formattedValue); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::color, formattedValue); +} + + +//////////////////////////////////////////////////////////////////////////////// +// FontFamilyTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontFamilyTextAttr:: + FontFamilyTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<nsString>(!aFrame) +{ + mIsRootDefined = GetFontFamily(aRootFrame, mRootNativeValue); + + if (aFrame) + mIsDefined = GetFontFamily(aFrame, mNativeValue); +} + +bool +TextAttrsMgr::FontFamilyTextAttr:: + GetValueFor(Accessible* aAccessible, nsString* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + return GetFontFamily(frame, *aValue); + } + } + return false; +} + +void +TextAttrsMgr::FontFamilyTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nsString& aValue) +{ + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::font_family, aValue); +} + +bool +TextAttrsMgr::FontFamilyTextAttr:: + GetFontFamily(nsIFrame* aFrame, nsString& aFamily) +{ + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f); + + gfxFontGroup* fontGroup = fm->GetThebesFontGroup(); + gfxFont* font = fontGroup->GetFirstValidFont(); + gfxFontEntry* fontEntry = font->GetFontEntry(); + aFamily = fontEntry->FamilyName(); + return true; +} + + +//////////////////////////////////////////////////////////////////////////////// +// FontSizeTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontSizeTextAttr:: + FontSizeTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<nscoord>(!aFrame) +{ + mDC = aRootFrame->PresContext()->DeviceContext(); + + mRootNativeValue = aRootFrame->StyleFont()->mSize; + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleFont()->mSize; + mIsDefined = true; + } +} + +bool +TextAttrsMgr::FontSizeTextAttr:: + GetValueFor(Accessible* aAccessible, nscoord* aValue) +{ + nsIContent* el = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (el) { + nsIFrame* frame = el->GetPrimaryFrame(); + if (frame) { + *aValue = frame->StyleFont()->mSize; + return true; + } + } + return false; +} + +void +TextAttrsMgr::FontSizeTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nscoord& aValue) +{ + // Convert from nscoord to pt. + // + // Note: according to IA2, "The conversion doesn't have to be exact. + // The intent is to give the user a feel for the size of the text." + // + // ATK does not specify a unit and will likely follow IA2 here. + // + // XXX todo: consider sharing this code with layout module? (bug 474621) + float px = + NSAppUnitsToFloatPixels(aValue, mozilla::AppUnitsPerCSSPixel()); + // Each pt is 4/3 of a CSS pixel. + int pts = NS_lround(px*3/4); + + nsAutoString value; + value.AppendInt(pts); + value.AppendLiteral("pt"); + + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::font_size, value); +} + + +//////////////////////////////////////////////////////////////////////////////// +// FontStyleTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontStyleTextAttr:: + FontStyleTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<nscoord>(!aFrame) +{ + mRootNativeValue = aRootFrame->StyleFont()->mFont.style; + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleFont()->mFont.style; + mIsDefined = true; + } +} + +bool +TextAttrsMgr::FontStyleTextAttr:: + GetValueFor(Accessible* aAccessible, nscoord* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + *aValue = frame->StyleFont()->mFont.style; + return true; + } + } + return false; +} + +void +TextAttrsMgr::FontStyleTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const nscoord& aValue) +{ + nsAutoString formattedValue; + StyleInfo::FormatFontStyle(aValue, formattedValue); + + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::font_style, formattedValue); +} + + +//////////////////////////////////////////////////////////////////////////////// +// FontWeightTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontWeightTextAttr:: + FontWeightTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<int32_t>(!aFrame) +{ + mRootNativeValue = GetFontWeight(aRootFrame); + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = GetFontWeight(aFrame); + mIsDefined = true; + } +} + +bool +TextAttrsMgr::FontWeightTextAttr:: + GetValueFor(Accessible* aAccessible, int32_t* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + *aValue = GetFontWeight(frame); + return true; + } + } + return false; +} + +void +TextAttrsMgr::FontWeightTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const int32_t& aValue) +{ + nsAutoString formattedValue; + formattedValue.AppendInt(aValue); + + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::fontWeight, formattedValue); +} + +int32_t +TextAttrsMgr::FontWeightTextAttr:: + GetFontWeight(nsIFrame* aFrame) +{ + // nsFont::width isn't suitable here because it's necessary to expose real + // value of font weight (used font might not have some font weight values). + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f); + + gfxFontGroup *fontGroup = fm->GetThebesFontGroup(); + gfxFont *font = fontGroup->GetFirstValidFont(); + + // When there doesn't exist a bold font in the family and so the rendering of + // a non-bold font face is changed so that the user sees what looks like a + // bold font, i.e. synthetic bolding is used. IsSyntheticBold method is only + // needed on Mac, but it is "safe" to use on all platforms. (For non-Mac + // platforms it always return false.) + if (font->IsSyntheticBold()) + return 700; + + bool useFontEntryWeight = true; + + // Under Linux, when gfxPangoFontGroup code is used, + // font->GetStyle()->weight will give the absolute weight requested of the + // font face. The gfxPangoFontGroup code uses the gfxFontEntry constructor + // which doesn't initialize the weight field. +#if defined(MOZ_WIDGET_GTK) + useFontEntryWeight = gfxPlatformGtk::UseFcFontList(); +#endif + + if (useFontEntryWeight) { + // On Windows, font->GetStyle()->weight will give the same weight as + // fontEntry->Weight(), the weight of the first font in the font group, + // which may not be the weight of the font face used to render the + // characters. On Mac, font->GetStyle()->weight will just give the same + // number as getComputedStyle(). fontEntry->Weight() will give the weight + // of the font face used. + gfxFontEntry *fontEntry = font->GetFontEntry(); + return fontEntry->Weight(); + } else { + return font->GetStyle()->weight; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// AutoGeneratedTextAttr +//////////////////////////////////////////////////////////////////////////////// +TextAttrsMgr::AutoGeneratedTextAttr:: + AutoGeneratedTextAttr(HyperTextAccessible* aHyperTextAcc, + Accessible* aAccessible) : + TTextAttr<bool>(!aAccessible) +{ + mRootNativeValue = false; + mIsRootDefined = false; + + if (aAccessible) + mIsDefined = mNativeValue = (aAccessible->NativeRole() == roles::STATICTEXT); +} + +bool +TextAttrsMgr::AutoGeneratedTextAttr:: + GetValueFor(Accessible* aAccessible, bool* aValue) +{ + return *aValue = (aAccessible->NativeRole() == roles::STATICTEXT); +} + +void +TextAttrsMgr::AutoGeneratedTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const bool& aValue) +{ + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::auto_generated, + aValue ? NS_LITERAL_STRING("true") : NS_LITERAL_STRING("false")); +} + + +//////////////////////////////////////////////////////////////////////////////// +// TextDecorTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::TextDecorValue:: + TextDecorValue(nsIFrame* aFrame) +{ + const nsStyleTextReset* textReset = aFrame->StyleTextReset(); + mStyle = textReset->mTextDecorationStyle; + mColor = aFrame->StyleColor()-> + CalcComplexColor(textReset->mTextDecorationColor); + mLine = textReset->mTextDecorationLine & + (NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE | + NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH); +} + +TextAttrsMgr::TextDecorTextAttr:: + TextDecorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<TextDecorValue>(!aFrame) +{ + mRootNativeValue = TextDecorValue(aRootFrame); + mIsRootDefined = mRootNativeValue.IsDefined(); + + if (aFrame) { + mNativeValue = TextDecorValue(aFrame); + mIsDefined = mNativeValue.IsDefined(); + } +} + +bool +TextAttrsMgr::TextDecorTextAttr:: + GetValueFor(Accessible* aAccessible, TextDecorValue* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + *aValue = TextDecorValue(frame); + return aValue->IsDefined(); + } + } + return false; +} + +void +TextAttrsMgr::TextDecorTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const TextDecorValue& aValue) +{ + if (aValue.IsUnderline()) { + nsAutoString formattedStyle; + StyleInfo::FormatTextDecorationStyle(aValue.Style(), formattedStyle); + nsAccUtils::SetAccAttr(aAttributes, + nsGkAtoms::textUnderlineStyle, + formattedStyle); + + nsAutoString formattedColor; + StyleInfo::FormatColor(aValue.Color(), formattedColor); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textUnderlineColor, + formattedColor); + return; + } + + if (aValue.IsLineThrough()) { + nsAutoString formattedStyle; + StyleInfo::FormatTextDecorationStyle(aValue.Style(), formattedStyle); + nsAccUtils::SetAccAttr(aAttributes, + nsGkAtoms::textLineThroughStyle, + formattedStyle); + + nsAutoString formattedColor; + StyleInfo::FormatColor(aValue.Color(), formattedColor); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textLineThroughColor, + formattedColor); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TextPosTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::TextPosTextAttr:: + TextPosTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame) : + TTextAttr<TextPosValue>(!aFrame) +{ + mRootNativeValue = GetTextPosValue(aRootFrame); + mIsRootDefined = mRootNativeValue != eTextPosNone; + + if (aFrame) { + mNativeValue = GetTextPosValue(aFrame); + mIsDefined = mNativeValue != eTextPosNone; + } +} + +bool +TextAttrsMgr::TextPosTextAttr:: + GetValueFor(Accessible* aAccessible, TextPosValue* aValue) +{ + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + *aValue = GetTextPosValue(frame); + return *aValue != eTextPosNone; + } + } + return false; +} + +void +TextAttrsMgr::TextPosTextAttr:: + ExposeValue(nsIPersistentProperties* aAttributes, const TextPosValue& aValue) +{ + switch (aValue) { + case eTextPosBaseline: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textPosition, + NS_LITERAL_STRING("baseline")); + break; + + case eTextPosSub: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textPosition, + NS_LITERAL_STRING("sub")); + break; + + case eTextPosSuper: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textPosition, + NS_LITERAL_STRING("super")); + break; + + case eTextPosNone: + break; + } +} + +TextAttrsMgr::TextPosValue +TextAttrsMgr::TextPosTextAttr:: + GetTextPosValue(nsIFrame* aFrame) const +{ + const nsStyleCoord& styleCoord = aFrame->StyleDisplay()->mVerticalAlign; + switch (styleCoord.GetUnit()) { + case eStyleUnit_Enumerated: + switch (styleCoord.GetIntValue()) { + case NS_STYLE_VERTICAL_ALIGN_BASELINE: + return eTextPosBaseline; + case NS_STYLE_VERTICAL_ALIGN_SUB: + return eTextPosSub; + case NS_STYLE_VERTICAL_ALIGN_SUPER: + return eTextPosSuper; + + // No good guess for these: + // NS_STYLE_VERTICAL_ALIGN_TOP + // NS_STYLE_VERTICAL_ALIGN_TEXT_TOP + // NS_STYLE_VERTICAL_ALIGN_MIDDLE + // NS_STYLE_VERTICAL_ALIGN_TEXT_BOTTOM + // NS_STYLE_VERTICAL_ALIGN_BOTTOM + // NS_STYLE_VERTICAL_ALIGN_MIDDLE_WITH_BASELINE + // Do not expose value of text-position attribute. + + default: + break; + } + return eTextPosNone; + + case eStyleUnit_Percent: + { + float percentValue = styleCoord.GetPercentValue(); + return percentValue > 0 ? + eTextPosSuper : + (percentValue < 0 ? eTextPosSub : eTextPosBaseline); + } + + case eStyleUnit_Coord: + { + nscoord coordValue = styleCoord.GetCoordValue(); + return coordValue > 0 ? + eTextPosSuper : + (coordValue < 0 ? eTextPosSub : eTextPosBaseline); + } + + case eStyleUnit_Null: + case eStyleUnit_Normal: + case eStyleUnit_Auto: + case eStyleUnit_None: + case eStyleUnit_Factor: + case eStyleUnit_Degree: + case eStyleUnit_Grad: + case eStyleUnit_Radian: + case eStyleUnit_Turn: + case eStyleUnit_FlexFraction: + case eStyleUnit_Integer: + case eStyleUnit_Calc: + break; + } + + const nsIContent* content = aFrame->GetContent(); + if (content) { + if (content->IsHTMLElement(nsGkAtoms::sup)) + return eTextPosSuper; + if (content->IsHTMLElement(nsGkAtoms::sub)) + return eTextPosSub; + } + + return eTextPosNone; +} diff --git a/accessible/base/TextAttrs.h b/accessible/base/TextAttrs.h new file mode 100644 index 000000000..a8db5a1d1 --- /dev/null +++ b/accessible/base/TextAttrs.h @@ -0,0 +1,478 @@ +/* -*- 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 nsTextAttrs_h_ +#define nsTextAttrs_h_ + +#include "nsCOMPtr.h" +#include "nsColor.h" +#include "nsStyleConsts.h" + +class nsIFrame; +class nsIPersistentProperties; +class nsIContent; +class nsDeviceContext; + +namespace mozilla { +namespace a11y { + +class Accessible; +class HyperTextAccessible; + +/** + * Used to expose text attributes for the hyper text accessible (see + * HyperTextAccessible class). + * + * @note "invalid: spelling" text attribute is implemented entirely in + * HyperTextAccessible class. + */ +class TextAttrsMgr +{ +public: + /** + * Constructor. Used to expose default text attributes. + */ + explicit TextAttrsMgr(HyperTextAccessible* aHyperTextAcc) : + mOffsetAcc(nullptr), mHyperTextAcc(aHyperTextAcc), + mOffsetAccIdx(-1), mIncludeDefAttrs(true) { } + + /** + * Constructor. Used to expose text attributes at the given offset. + * + * @param aHyperTextAcc [in] hyper text accessible text attributes are + * calculated for + * @param aIncludeDefAttrs [optional] indicates whether default text + * attributes should be included into list of exposed + * text attributes + * @param oOffsetAcc [optional] offset an accessible the text attributes + * should be calculated for + * @param oOffsetAccIdx [optional] index in parent of offset accessible + */ + TextAttrsMgr(HyperTextAccessible* aHyperTextAcc, + bool aIncludeDefAttrs, + Accessible* aOffsetAcc, + int32_t aOffsetAccIdx) : + mOffsetAcc(aOffsetAcc), mHyperTextAcc(aHyperTextAcc), + mOffsetAccIdx(aOffsetAccIdx), mIncludeDefAttrs(aIncludeDefAttrs) { } + + /* + * Return text attributes and hyper text offsets where these attributes are + * applied. Offsets are calculated in the case of non default attributes. + * + * @note In the case of default attributes pointers on hyper text offsets + * must be skipped. + * + * @param aAttributes [in, out] text attributes list + * @param aStartHTOffset [out, optional] start hyper text offset + * @param aEndHTOffset [out, optional] end hyper text offset + */ + void GetAttributes(nsIPersistentProperties* aAttributes, + uint32_t* aStartHTOffset = nullptr, + uint32_t* aEndHTOffset = nullptr); + +protected: + /** + * Calculates range (start and end offsets) of text where the text attributes + * are stretched. New offsets may be smaller if one of text attributes changes + * its value before or after the given offsets. + * + * @param aTextAttrArray [in] text attributes array + * @param aAttrArrayLen [in] text attributes array length + * @param aStartHTOffset [in, out] the start offset + * @param aEndHTOffset [in, out] the end offset + */ + class TextAttr; + void GetRange(TextAttr* aAttrArray[], uint32_t aAttrArrayLen, + uint32_t* aStartOffset, uint32_t* aEndOffset); + +private: + Accessible* mOffsetAcc; + HyperTextAccessible* mHyperTextAcc; + int32_t mOffsetAccIdx; + bool mIncludeDefAttrs; + +protected: + + /** + * Interface class of text attribute class implementations. + */ + class TextAttr + { + public: + /** + * Expose the text attribute to the given attribute set. + * + * @param aAttributes [in] the given attribute set + * @param aIncludeDefAttrValue [in] if true then attribute is exposed even + * if its value is the same as default one + */ + virtual void Expose(nsIPersistentProperties* aAttributes, + bool aIncludeDefAttrValue) = 0; + + /** + * Return true if the text attribute value on the given element equals with + * predefined attribute value. + */ + virtual bool Equal(Accessible* aAccessible) = 0; + }; + + + /** + * Base class to work with text attributes. See derived classes below. + */ + template<class T> + class TTextAttr : public TextAttr + { + public: + explicit TTextAttr(bool aGetRootValue) : mGetRootValue(aGetRootValue) {} + + // TextAttr + virtual void Expose(nsIPersistentProperties* aAttributes, + bool aIncludeDefAttrValue) override + { + if (mGetRootValue) { + if (mIsRootDefined) + ExposeValue(aAttributes, mRootNativeValue); + return; + } + + if (mIsDefined) { + if (aIncludeDefAttrValue || mRootNativeValue != mNativeValue) + ExposeValue(aAttributes, mNativeValue); + return; + } + + if (aIncludeDefAttrValue && mIsRootDefined) + ExposeValue(aAttributes, mRootNativeValue); + } + + virtual bool Equal(Accessible* aAccessible) override + { + T nativeValue; + bool isDefined = GetValueFor(aAccessible, &nativeValue); + + if (!mIsDefined && !isDefined) + return true; + + if (mIsDefined && isDefined) + return nativeValue == mNativeValue; + + if (mIsDefined) + return mNativeValue == mRootNativeValue; + + return nativeValue == mRootNativeValue; + } + + protected: + + // Expose the text attribute with the given value to attribute set. + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const T& aValue) = 0; + + // Return native value for the given DOM element. + virtual bool GetValueFor(Accessible* aAccessible, T* aValue) = 0; + + // Indicates if root value should be exposed. + bool mGetRootValue; + + // Native value and flag indicating if the value is defined (initialized in + // derived classes). Note, undefined native value means it is inherited + // from root. + MOZ_INIT_OUTSIDE_CTOR T mNativeValue; + MOZ_INIT_OUTSIDE_CTOR bool mIsDefined; + + // Native root value and flag indicating if the value is defined (initialized + // in derived classes). + MOZ_INIT_OUTSIDE_CTOR T mRootNativeValue; + MOZ_INIT_OUTSIDE_CTOR bool mIsRootDefined; + }; + + + /** + * Class is used for the work with 'language' text attribute. + */ + class LangTextAttr : public TTextAttr<nsString> + { + public: + LangTextAttr(HyperTextAccessible* aRoot, nsIContent* aRootElm, + nsIContent* aElm); + virtual ~LangTextAttr(); + + protected: + + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, nsString* aValue) override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nsString& aValue) override; + + private: + nsCOMPtr<nsIContent> mRootContent; + }; + + + /** + * Class is used for the 'invalid' text attribute. Note, it calculated + * the attribute from aria-invalid attribute only; invalid:spelling attribute + * calculated from misspelled text in the editor is managed by + * HyperTextAccessible and applied on top of the value from aria-invalid. + */ + class InvalidTextAttr : public TTextAttr<uint32_t> + { + public: + InvalidTextAttr(nsIContent* aRootElm, nsIContent* aElm); + virtual ~InvalidTextAttr() { }; + + protected: + + enum { + eFalse, + eGrammar, + eSpelling, + eTrue + }; + + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, uint32_t* aValue) override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const uint32_t& aValue) override; + + private: + bool GetValue(nsIContent* aElm, uint32_t* aValue); + nsIContent* mRootElm; + }; + + + /** + * Class is used for the work with 'background-color' text attribute. + */ + class BGColorTextAttr : public TTextAttr<nscolor> + { + public: + BGColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~BGColorTextAttr() { } + + protected: + + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, nscolor* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nscolor& aValue) override; + + private: + bool GetColor(nsIFrame* aFrame, nscolor* aColor); + nsIFrame* mRootFrame; + }; + + + /** + * Class is used for the work with 'color' text attribute. + */ + class ColorTextAttr : public TTextAttr<nscolor> + { + public: + ColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~ColorTextAttr() { } + + protected: + + // TTextAttr + virtual bool GetValueFor(Accessible* aAccessible, nscolor* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nscolor& aValue) override; + }; + + + /** + * Class is used for the work with "font-family" text attribute. + */ + class FontFamilyTextAttr : public TTextAttr<nsString> + { + public: + FontFamilyTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontFamilyTextAttr() { } + + protected: + + // TTextAttr + virtual bool GetValueFor(Accessible* aAccessible, nsString* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nsString& aValue) override; + + private: + + bool GetFontFamily(nsIFrame* aFrame, nsString& aFamily); + }; + + + /** + * Class is used for the work with "font-size" text attribute. + */ + class FontSizeTextAttr : public TTextAttr<nscoord> + { + public: + FontSizeTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontSizeTextAttr() { } + + protected: + + // TTextAttr + virtual bool GetValueFor(Accessible* aAccessible, nscoord* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nscoord& aValue) override; + + private: + nsDeviceContext* mDC; + }; + + + /** + * Class is used for the work with "font-style" text attribute. + */ + class FontStyleTextAttr : public TTextAttr<nscoord> + { + public: + FontStyleTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontStyleTextAttr() { } + + protected: + + // TTextAttr + virtual bool GetValueFor(Accessible* aContent, nscoord* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const nscoord& aValue) override; + }; + + + /** + * Class is used for the work with "font-weight" text attribute. + */ + class FontWeightTextAttr : public TTextAttr<int32_t> + { + public: + FontWeightTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontWeightTextAttr() { } + + protected: + + // TTextAttr + virtual bool GetValueFor(Accessible* aAccessible, int32_t* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const int32_t& aValue) override; + + private: + int32_t GetFontWeight(nsIFrame* aFrame); + }; + + /** + * Class is used for the work with 'auto-generated' text attribute. + */ + class AutoGeneratedTextAttr : public TTextAttr<bool> + { + public: + AutoGeneratedTextAttr(HyperTextAccessible* aHyperTextAcc, + Accessible* aAccessible); + virtual ~AutoGeneratedTextAttr() { } + + protected: + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, bool* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const bool& aValue) override; + }; + + + /** + * TextDecorTextAttr class is used for the work with + * "text-line-through-style", "text-line-through-color", + * "text-underline-style" and "text-underline-color" text attributes. + */ + + class TextDecorValue + { + public: + TextDecorValue() { } + explicit TextDecorValue(nsIFrame* aFrame); + + nscolor Color() const { return mColor; } + uint8_t Style() const { return mStyle; } + + bool IsDefined() const + { return IsUnderline() || IsLineThrough(); } + bool IsUnderline() const + { return mLine & NS_STYLE_TEXT_DECORATION_LINE_UNDERLINE; } + bool IsLineThrough() const + { return mLine & NS_STYLE_TEXT_DECORATION_LINE_LINE_THROUGH; } + + bool operator ==(const TextDecorValue& aValue) + { + return mColor == aValue.mColor && mLine == aValue.mLine && + mStyle == aValue.mStyle; + } + bool operator !=(const TextDecorValue& aValue) + { return !(*this == aValue); } + + private: + nscolor mColor; + uint8_t mLine; + uint8_t mStyle; + }; + + class TextDecorTextAttr : public TTextAttr<TextDecorValue> + { + public: + TextDecorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~TextDecorTextAttr() { } + + protected: + + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, TextDecorValue* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const TextDecorValue& aValue) override; + }; + + /** + * Class is used for the work with "text-position" text attribute. + */ + + enum TextPosValue { + eTextPosNone = 0, + eTextPosBaseline, + eTextPosSub, + eTextPosSuper + }; + + class TextPosTextAttr : public TTextAttr<TextPosValue> + { + public: + TextPosTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~TextPosTextAttr() { } + + protected: + + // TextAttr + virtual bool GetValueFor(Accessible* aAccessible, TextPosValue* aValue) + override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const TextPosValue& aValue) override; + + private: + TextPosValue GetTextPosValue(nsIFrame* aFrame) const; + }; + +}; // TextAttrMgr + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextRange-inl.h b/accessible/base/TextRange-inl.h new file mode 100644 index 000000000..15bbaa235 --- /dev/null +++ b/accessible/base/TextRange-inl.h @@ -0,0 +1,29 @@ +/* -*- 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_TextRange_inl_h__ +#define mozilla_a11y_TextRange_inl_h__ + +#include "TextRange.h" +#include "HyperTextAccessible.h" + +namespace mozilla { +namespace a11y { + +inline Accessible* +TextRange::Container() const +{ + uint32_t pos1 = 0, pos2 = 0; + AutoTArray<Accessible*, 30> parents1, parents2; + return CommonParent(mStartContainer, mEndContainer, + &parents1, &pos1, &parents2, &pos2); +} + + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextRange.cpp b/accessible/base/TextRange.cpp new file mode 100644 index 000000000..30474d2fa --- /dev/null +++ b/accessible/base/TextRange.cpp @@ -0,0 +1,380 @@ +/* -*- 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 "TextRange-inl.h" + +#include "Accessible-inl.h" +#include "nsAccUtils.h" + +namespace mozilla { +namespace a11y { + +//////////////////////////////////////////////////////////////////////////////// +// TextPoint + +bool +TextPoint::operator <(const TextPoint& aPoint) const +{ + if (mContainer == aPoint.mContainer) + return mOffset < aPoint.mOffset; + + // Build the chain of parents + Accessible* p1 = mContainer; + Accessible* p2 = aPoint.mContainer; + AutoTArray<Accessible*, 30> parents1, parents2; + do { + parents1.AppendElement(p1); + p1 = p1->Parent(); + } while (p1); + do { + parents2.AppendElement(p2); + p2 = p2->Parent(); + } while (p2); + + // Find where the parent chain differs + uint32_t pos1 = parents1.Length(), pos2 = parents2.Length(); + for (uint32_t len = std::min(pos1, pos2); len > 0; --len) { + Accessible* child1 = parents1.ElementAt(--pos1); + Accessible* child2 = parents2.ElementAt(--pos2); + if (child1 != child2) + return child1->IndexInParent() < child2->IndexInParent(); + } + + NS_ERROR("Broken tree?!"); + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// TextRange + +TextRange::TextRange(HyperTextAccessible* aRoot, + HyperTextAccessible* aStartContainer, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset) : + mRoot(aRoot), mStartContainer(aStartContainer), mEndContainer(aEndContainer), + mStartOffset(aStartOffset), mEndOffset(aEndOffset) +{ +} + +void +TextRange::EmbeddedChildren(nsTArray<Accessible*>* aChildren) const +{ + if (mStartContainer == mEndContainer) { + int32_t startIdx = mStartContainer->GetChildIndexAtOffset(mStartOffset); + int32_t endIdx = mStartContainer->GetChildIndexAtOffset(mEndOffset); + for (int32_t idx = startIdx; idx <= endIdx; idx++) { + Accessible* child = mStartContainer->GetChildAt(idx); + if (!child->IsText()) { + aChildren->AppendElement(child); + } + } + return; + } + + Accessible* p1 = mStartContainer->GetChildAtOffset(mStartOffset); + Accessible* p2 = mEndContainer->GetChildAtOffset(mEndOffset); + + uint32_t pos1 = 0, pos2 = 0; + AutoTArray<Accessible*, 30> parents1, parents2; + Accessible* container = + CommonParent(p1, p2, &parents1, &pos1, &parents2, &pos2); + + // Traverse the tree up to the container and collect embedded objects. + for (uint32_t idx = 0; idx < pos1 - 1; idx++) { + Accessible* parent = parents1[idx + 1]; + Accessible* child = parents1[idx]; + uint32_t childCount = parent->ChildCount(); + for (uint32_t childIdx = child->IndexInParent(); childIdx < childCount; childIdx++) { + Accessible* next = parent->GetChildAt(childIdx); + if (!next->IsText()) { + aChildren->AppendElement(next); + } + } + } + + // Traverse through direct children in the container. + int32_t endIdx = parents2[pos2 - 1]->IndexInParent(); + int32_t childIdx = parents1[pos1 - 1]->IndexInParent() + 1; + for (; childIdx < endIdx; childIdx++) { + Accessible* next = container->GetChildAt(childIdx); + if (!next->IsText()) { + aChildren->AppendElement(next); + } + } + + // Traverse down from the container to end point. + for (int32_t idx = pos2 - 2; idx > 0; idx--) { + Accessible* parent = parents2[idx]; + Accessible* child = parents2[idx - 1]; + int32_t endIdx = child->IndexInParent(); + for (int32_t childIdx = 0; childIdx < endIdx; childIdx++) { + Accessible* next = parent->GetChildAt(childIdx); + if (!next->IsText()) { + aChildren->AppendElement(next); + } + } + } +} + +void +TextRange::Text(nsAString& aText) const +{ + Accessible* current = mStartContainer->GetChildAtOffset(mStartOffset); + uint32_t startIntlOffset = + mStartOffset - mStartContainer->GetChildOffset(current); + + while (current && TextInternal(aText, current, startIntlOffset)) { + current = current->Parent(); + if (!current) + break; + + current = current->NextSibling(); + } +} + +void +TextRange::Bounds(nsTArray<nsIntRect> aRects) const +{ + +} + +void +TextRange::Normalize(ETextUnit aUnit) +{ + +} + +bool +TextRange::Crop(Accessible* aContainer) +{ + uint32_t boundaryPos = 0, containerPos = 0; + AutoTArray<Accessible*, 30> boundaryParents, containerParents; + + // Crop the start boundary. + Accessible* container = nullptr; + Accessible* boundary = mStartContainer->GetChildAtOffset(mStartOffset); + if (boundary != aContainer) { + CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos, + &containerParents, &containerPos); + + if (boundaryPos == 0) { + if (containerPos != 0) { + // The container is contained by the start boundary, reduce the range to + // the point starting at the container. + aContainer->ToTextPoint(mStartContainer.StartAssignment(), &mStartOffset); + static_cast<Accessible*>(mStartContainer)->AddRef(); + } + else { + // The start boundary and the container are siblings. + container = aContainer; + } + } + else if (containerPos != 0) { + // The container does not contain the start boundary. + boundary = boundaryParents[boundaryPos]; + container = containerParents[containerPos]; + } + + if (container) { + // If the range start is after the container, then make the range invalid. + if (boundary->IndexInParent() > container->IndexInParent()) { + return !!(mRoot = nullptr); + } + + // If the range starts before the container, then reduce the range to + // the point starting at the container. + if (boundary->IndexInParent() < container->IndexInParent()) { + container->ToTextPoint(mStartContainer.StartAssignment(), &mStartOffset); + mStartContainer.get()->AddRef(); + } + } + + boundaryParents.SetLengthAndRetainStorage(0); + containerParents.SetLengthAndRetainStorage(0); + } + + boundary = mEndContainer->GetChildAtOffset(mEndOffset); + if (boundary == aContainer) { + return true; + } + + // Crop the end boundary. + container = nullptr; + CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos, + &containerParents, &containerPos); + + if (boundaryPos == 0) { + if (containerPos != 0) { + aContainer->ToTextPoint(mEndContainer.StartAssignment(), &mEndOffset, false); + static_cast<Accessible*>(mEndContainer)->AddRef(); + } + else { + container = aContainer; + } + } + else if (containerPos != 0) { + boundary = boundaryParents[boundaryPos]; + container = containerParents[containerPos]; + } + + if (!container) { + return true; + } + + if (boundary->IndexInParent() < container->IndexInParent()) { + return !!(mRoot = nullptr); + } + + if (boundary->IndexInParent() > container->IndexInParent()) { + container->ToTextPoint(mEndContainer.StartAssignment(), &mEndOffset, false); + static_cast<Accessible*>(mEndContainer)->AddRef(); + } + + return true; +} + +void +TextRange::FindText(const nsAString& aText, EDirection aDirection, + nsCaseTreatment aCaseSensitive, TextRange* aFoundRange) const +{ + +} + +void +TextRange::FindAttr(EAttr aAttr, nsIVariant* aValue, EDirection aDirection, + TextRange* aFoundRange) const +{ + +} + +void +TextRange::AddToSelection() const +{ + +} + +void +TextRange::RemoveFromSelection() const +{ + +} + +void +TextRange::Select() const +{ +} + +void +TextRange::ScrollIntoView(EHowToAlign aHow) const +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// pivate + +void +TextRange::Set(HyperTextAccessible* aRoot, + HyperTextAccessible* aStartContainer, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset) +{ + mRoot = aRoot; + mStartContainer = aStartContainer; + mEndContainer = aEndContainer; + mStartOffset = aStartOffset; + mEndOffset = aEndOffset; +} + +bool +TextRange::TextInternal(nsAString& aText, Accessible* aCurrent, + uint32_t aStartIntlOffset) const +{ + bool moveNext = true; + int32_t endIntlOffset = -1; + if (aCurrent->Parent() == mEndContainer && + mEndContainer->GetChildAtOffset(mEndOffset) == aCurrent) { + + uint32_t currentStartOffset = mEndContainer->GetChildOffset(aCurrent); + endIntlOffset = mEndOffset - currentStartOffset; + if (endIntlOffset == 0) + return false; + + moveNext = false; + } + + if (aCurrent->IsTextLeaf()) { + aCurrent->AppendTextTo(aText, aStartIntlOffset, + endIntlOffset - aStartIntlOffset); + if (!moveNext) + return false; + } + + Accessible* next = aCurrent->FirstChild(); + if (next) { + if (!TextInternal(aText, next, 0)) + return false; + } + + next = aCurrent->NextSibling(); + if (next) { + if (!TextInternal(aText, next, 0)) + return false; + } + + return moveNext; +} + + +void +TextRange::MoveInternal(ETextUnit aUnit, int32_t aCount, + HyperTextAccessible& aContainer, int32_t aOffset, + HyperTextAccessible* aStopContainer, int32_t aStopOffset) +{ + +} + +Accessible* +TextRange::CommonParent(Accessible* aAcc1, Accessible* aAcc2, + nsTArray<Accessible*>* aParents1, uint32_t* aPos1, + nsTArray<Accessible*>* aParents2, uint32_t* aPos2) const +{ + if (aAcc1 == aAcc2) { + return aAcc1; + } + + MOZ_ASSERT(aParents1->Length() == 0 || aParents2->Length() == 0, + "Wrong arguments"); + + // Build the chain of parents. + Accessible* p1 = aAcc1; + Accessible* p2 = aAcc2; + do { + aParents1->AppendElement(p1); + p1 = p1->Parent(); + } while (p1); + do { + aParents2->AppendElement(p2); + p2 = p2->Parent(); + } while (p2); + + // Find where the parent chain differs + *aPos1 = aParents1->Length(); + *aPos2 = aParents2->Length(); + Accessible* parent = nullptr; + uint32_t len = 0; + for (len = std::min(*aPos1, *aPos2); len > 0; --len) { + Accessible* child1 = aParents1->ElementAt(--(*aPos1)); + Accessible* child2 = aParents2->ElementAt(--(*aPos2)); + if (child1 != child2) + break; + + parent = child1; + } + + return parent; +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/TextRange.h b/accessible/base/TextRange.h new file mode 100644 index 000000000..662b67e42 --- /dev/null +++ b/accessible/base/TextRange.h @@ -0,0 +1,270 @@ +/* -*- 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_TextRange_h__ +#define mozilla_a11y_TextRange_h__ + +#include "mozilla/Move.h" +#include "nsCaseTreatment.h" +#include "nsRect.h" +#include "nsTArray.h" + + class nsIVariant; + +namespace mozilla { +namespace a11y { + +class Accessible; +class HyperTextAccessible; + +/** + * A text point (hyper text + offset), represents a boundary of text range. + */ +struct TextPoint final +{ + TextPoint(HyperTextAccessible* aContainer, int32_t aOffset) : + mContainer(aContainer), mOffset(aOffset) { } + TextPoint(const TextPoint& aPoint) : + mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) { } + + HyperTextAccessible* mContainer; + int32_t mOffset; + + bool operator ==(const TextPoint& aPoint) const + { return mContainer == aPoint.mContainer && mOffset == aPoint.mOffset; } + bool operator <(const TextPoint& aPoint) const; +}; + +/** + * Represents a text range within the text control or document. + */ +class TextRange final +{ +public: + TextRange(HyperTextAccessible* aRoot, + HyperTextAccessible* aStartContainer, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset); + TextRange() {} + TextRange(TextRange&& aRange) : + mRoot(mozilla::Move(aRange.mRoot)), + mStartContainer(mozilla::Move(aRange.mStartContainer)), + mEndContainer(mozilla::Move(aRange.mEndContainer)), + mStartOffset(aRange.mStartOffset), mEndOffset(aRange.mEndOffset) {} + + TextRange& operator= (TextRange&& aRange) + { + mRoot = mozilla::Move(aRange.mRoot); + mStartContainer = mozilla::Move(aRange.mStartContainer); + mEndContainer = mozilla::Move(aRange.mEndContainer); + mStartOffset = aRange.mStartOffset; + mEndOffset = aRange.mEndOffset; + return *this; + } + + HyperTextAccessible* StartContainer() const { return mStartContainer; } + int32_t StartOffset() const { return mStartOffset; } + HyperTextAccessible* EndContainer() const { return mEndContainer; } + int32_t EndOffset() const { return mEndOffset; } + + bool operator ==(const TextRange& aRange) const + { + return mStartContainer == aRange.mStartContainer && + mStartOffset == aRange.mStartOffset && + mEndContainer == aRange.mEndContainer && mEndOffset == aRange.mEndOffset; + } + + TextPoint StartPoint() const { return TextPoint(mStartContainer, mStartOffset); } + TextPoint EndPoint() const { return TextPoint(mEndContainer, mEndOffset); } + + /** + * Return a container containing both start and end points. + */ + Accessible* Container() const; + + /** + * Return a list of embedded objects enclosed by the text range (includes + * partially overlapped objects). + */ + void EmbeddedChildren(nsTArray<Accessible*>* aChildren) const; + + /** + * Return text enclosed by the range. + */ + void Text(nsAString& aText) const; + + /** + * Return list of bounding rects of the text range by lines. + */ + void Bounds(nsTArray<nsIntRect> aRects) const; + + enum ETextUnit { + eFormat, + eWord, + eLine, + eParagraph, + ePage, + eDocument + }; + + /** + * Move the range or its points on specified amount of given units. + */ + void Move(ETextUnit aUnit, int32_t aCount) + { + MoveEnd(aUnit, aCount); + MoveStart(aUnit, aCount); + } + void MoveStart(ETextUnit aUnit, int32_t aCount) + { + MoveInternal(aUnit, aCount, *mStartContainer, mStartOffset, + mEndContainer, mEndOffset); + } + void MoveEnd(ETextUnit aUnit, int32_t aCount) + { MoveInternal(aUnit, aCount, *mEndContainer, mEndOffset); } + + /** + * Move the range points to the closest unit boundaries. + */ + void Normalize(ETextUnit aUnit); + + /** + * Crops the range if it overlaps the given accessible element boundaries, + * returns true if the range was cropped successfully. + */ + bool Crop(Accessible* aContainer); + + enum EDirection { + eBackward, + eForward + }; + + /** + * Return range enclosing the found text. + */ + void FindText(const nsAString& aText, EDirection aDirection, + nsCaseTreatment aCaseSensitive, TextRange* aFoundRange) const; + + enum EAttr { + eAnimationStyleAttr, + eAnnotationObjectsAttr, + eAnnotationTypesAttr, + eBackgroundColorAttr, + eBulletStyleAttr, + eCapStyleAttr, + eCaretBidiModeAttr, + eCaretPositionAttr, + eCultureAttr, + eFontNameAttr, + eFontSizeAttr, + eFontWeightAttr, + eForegroundColorAttr, + eHorizontalTextAlignmentAttr, + eIndentationFirstLineAttr, + eIndentationLeadingAttr, + eIndentationTrailingAttr, + eIsActiveAttr, + eIsHiddenAttr, + eIsItalicAttr, + eIsReadOnlyAttr, + eIsSubscriptAttr, + eIsSuperscriptAttr, + eLinkAttr, + eMarginBottomAttr, + eMarginLeadingAttr, + eMarginTopAttr, + eMarginTrailingAttr, + eOutlineStylesAttr, + eOverlineColorAttr, + eOverlineStyleAttr, + eSelectionActiveEndAttr, + eStrikethroughColorAttr, + eStrikethroughStyleAttr, + eStyleIdAttr, + eStyleNameAttr, + eTabsAttr, + eTextFlowDirectionsAttr, + eUnderlineColorAttr, + eUnderlineStyleAttr + }; + + /** + * Return range enclosing text having requested attribute. + */ + void FindAttr(EAttr aAttr, nsIVariant* aValue, EDirection aDirection, + TextRange* aFoundRange) const; + + /** + * Add/remove the text range from selection. + */ + void AddToSelection() const; + void RemoveFromSelection() const; + void Select() const; + + /** + * Scroll the text range into view. + */ + enum EHowToAlign { + eAlignToTop, + eAlignToBottom + }; + void ScrollIntoView(EHowToAlign aHow) const; + + /** + * Return true if this TextRange object represents an actual range of text. + */ + bool IsValid() const { return mRoot; } + + void SetStartPoint(HyperTextAccessible* aContainer, int32_t aOffset) + { mStartContainer = aContainer; mStartOffset = aOffset; } + void SetEndPoint(HyperTextAccessible* aContainer, int32_t aOffset) + { mStartContainer = aContainer; mStartOffset = aOffset; } + +private: + TextRange(const TextRange& aRange) = delete; + TextRange& operator=(const TextRange& aRange) = delete; + + friend class HyperTextAccessible; + friend class xpcAccessibleTextRange; + + void Set(HyperTextAccessible* aRoot, + HyperTextAccessible* aStartContainer, int32_t aStartOffset, + HyperTextAccessible* aEndContainer, int32_t aEndOffset); + + /** + * Text() method helper. + * @param aText [in,out] calculated text + * @param aCurrent [in] currently traversed node + * @param aStartIntlOffset [in] start offset if current node is a text node + * @return true if calculation is not finished yet + */ + bool TextInternal(nsAString& aText, Accessible* aCurrent, + uint32_t aStartIntlOffset) const; + + void MoveInternal(ETextUnit aUnit, int32_t aCount, + HyperTextAccessible& aContainer, int32_t aOffset, + HyperTextAccessible* aStopContainer = nullptr, + int32_t aStopOffset = 0); + + /** + * A helper method returning a common parent for two given accessible + * elements. + */ + Accessible* CommonParent(Accessible* aAcc1, Accessible* aAcc2, + nsTArray<Accessible*>* aParents1, uint32_t* aPos1, + nsTArray<Accessible*>* aParents2, uint32_t* aPos2) const; + + RefPtr<HyperTextAccessible> mRoot; + RefPtr<HyperTextAccessible> mStartContainer; + RefPtr<HyperTextAccessible> mEndContainer; + int32_t mStartOffset; + int32_t mEndOffset; +}; + + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextUpdater.cpp b/accessible/base/TextUpdater.cpp new file mode 100644 index 000000000..75cbbe7e1 --- /dev/null +++ b/accessible/base/TextUpdater.cpp @@ -0,0 +1,202 @@ +/* -*- 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 "TextUpdater.h" + +#include "Accessible-inl.h" +#include "DocAccessible-inl.h" +#include "TextLeafAccessible.h" +#include <algorithm> + +using namespace mozilla::a11y; + +void +TextUpdater::Run(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf, + const nsAString& aNewText) +{ + NS_ASSERTION(aTextLeaf, "No text leaf accessible?"); + + const nsString& oldText = aTextLeaf->Text(); + uint32_t oldLen = oldText.Length(), newLen = aNewText.Length(); + uint32_t minLen = std::min(oldLen, newLen); + + // Skip coinciding begin substrings. + uint32_t skipStart = 0; + for (; skipStart < minLen; skipStart++) { + if (aNewText[skipStart] != oldText[skipStart]) + break; + } + + // The text was changed. Do update. + if (skipStart != minLen || oldLen != newLen) { + TextUpdater updater(aDocument, aTextLeaf); + updater.DoUpdate(aNewText, oldText, skipStart); + } +} + +void +TextUpdater::DoUpdate(const nsAString& aNewText, const nsAString& aOldText, + uint32_t aSkipStart) +{ + Accessible* parent = mTextLeaf->Parent(); + if (!parent) + return; + + mHyperText = parent->AsHyperText(); + if (!mHyperText) { + NS_ERROR("Text leaf parent is not hypertext!"); + return; + } + + // Get the text leaf accessible offset and invalidate cached offsets after it. + mTextOffset = mHyperText->GetChildOffset(mTextLeaf, true); + NS_ASSERTION(mTextOffset != -1, + "Text leaf hasn't offset within hyper text!"); + + uint32_t oldLen = aOldText.Length(), newLen = aNewText.Length(); + uint32_t minLen = std::min(oldLen, newLen); + + // Trim coinciding substrings from the end. + uint32_t skipEnd = 0; + while (minLen - skipEnd > aSkipStart && + aNewText[newLen - skipEnd - 1] == aOldText[oldLen - skipEnd - 1]) { + skipEnd++; + } + + uint32_t strLen1 = oldLen - aSkipStart - skipEnd; + uint32_t strLen2 = newLen - aSkipStart - skipEnd; + + const nsAString& str1 = Substring(aOldText, aSkipStart, strLen1); + const nsAString& str2 = Substring(aNewText, aSkipStart, strLen2); + + // Increase offset of the text leaf on skipped characters amount. + mTextOffset += aSkipStart; + + // It could be single insertion or removal or the case of long strings. Do not + // calculate the difference between long strings and prefer to fire pair of + // insert/remove events as the old string was replaced on the new one. + if (strLen1 == 0 || strLen2 == 0 || + strLen1 > kMaxStrLen || strLen2 > kMaxStrLen) { + if (strLen1 > 0) { + // Fire text change event for removal. + RefPtr<AccEvent> textRemoveEvent = + new AccTextChangeEvent(mHyperText, mTextOffset, str1, false); + mDocument->FireDelayedEvent(textRemoveEvent); + } + + if (strLen2 > 0) { + // Fire text change event for insertion. + RefPtr<AccEvent> textInsertEvent = + new AccTextChangeEvent(mHyperText, mTextOffset, str2, true); + mDocument->FireDelayedEvent(textInsertEvent); + } + + mDocument->MaybeNotifyOfValueChange(mHyperText); + + // Update the text. + mTextLeaf->SetText(aNewText); + return; + } + + // Otherwise find the difference between strings and fire events. + // Note: we can skip initial and final coinciding characters since they don't + // affect the Levenshtein distance. + + // Compute the flat structured matrix need to compute the difference. + uint32_t len1 = strLen1 + 1, len2 = strLen2 + 1; + uint32_t* entries = new uint32_t[len1 * len2]; + + for (uint32_t colIdx = 0; colIdx < len1; colIdx++) + entries[colIdx] = colIdx; + + uint32_t* row = entries; + for (uint32_t rowIdx = 1; rowIdx < len2; rowIdx++) { + uint32_t* prevRow = row; + row += len1; + row[0] = rowIdx; + for (uint32_t colIdx = 1; colIdx < len1; colIdx++) { + if (str1[colIdx - 1] != str2[rowIdx - 1]) { + uint32_t left = row[colIdx - 1]; + uint32_t up = prevRow[colIdx]; + uint32_t upleft = prevRow[colIdx - 1]; + row[colIdx] = std::min(upleft, std::min(left, up)) + 1; + } else { + row[colIdx] = prevRow[colIdx - 1]; + } + } + } + + // Compute events based on the difference. + nsTArray<RefPtr<AccEvent> > events; + ComputeTextChangeEvents(str1, str2, entries, events); + + delete [] entries; + + // Fire events. + for (int32_t idx = events.Length() - 1; idx >= 0; idx--) + mDocument->FireDelayedEvent(events[idx]); + + mDocument->MaybeNotifyOfValueChange(mHyperText); + + // Update the text. + mTextLeaf->SetText(aNewText); +} + +void +TextUpdater::ComputeTextChangeEvents(const nsAString& aStr1, + const nsAString& aStr2, + uint32_t* aEntries, + nsTArray<RefPtr<AccEvent> >& aEvents) +{ + int32_t colIdx = aStr1.Length(), rowIdx = aStr2.Length(); + + // Point at which strings last matched. + int32_t colEnd = colIdx; + int32_t rowEnd = rowIdx; + + int32_t colLen = colEnd + 1; + uint32_t* row = aEntries + rowIdx * colLen; + uint32_t dist = row[colIdx]; // current Levenshtein distance + while (rowIdx && colIdx) { // stop when we can't move diagonally + if (aStr1[colIdx - 1] == aStr2[rowIdx - 1]) { // match + if (rowIdx < rowEnd) { // deal with any pending insertion + FireInsertEvent(Substring(aStr2, rowIdx, rowEnd - rowIdx), + rowIdx, aEvents); + } + if (colIdx < colEnd) { // deal with any pending deletion + FireDeleteEvent(Substring(aStr1, colIdx, colEnd - colIdx), + rowIdx, aEvents); + } + + colEnd = --colIdx; // reset the match point + rowEnd = --rowIdx; + row -= colLen; + continue; + } + --dist; + if (dist == row[colIdx - 1 - colLen]) { // substitution + --colIdx; + --rowIdx; + row -= colLen; + continue; + } + if (dist == row[colIdx - colLen]) { // insertion + --rowIdx; + row -= colLen; + continue; + } + if (dist == row[colIdx - 1]) { // deletion + --colIdx; + continue; + } + NS_NOTREACHED("huh?"); + return; + } + + if (rowEnd) + FireInsertEvent(Substring(aStr2, 0, rowEnd), 0, aEvents); + if (colEnd) + FireDeleteEvent(Substring(aStr1, 0, colEnd), 0, aEvents); +} diff --git a/accessible/base/TextUpdater.h b/accessible/base/TextUpdater.h new file mode 100644 index 000000000..06df3890d --- /dev/null +++ b/accessible/base/TextUpdater.h @@ -0,0 +1,96 @@ +/* -*- 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 mozilla_a11y_TextUpdater_h__ +#define mozilla_a11y_TextUpdater_h__ + +#include "AccEvent.h" +#include "HyperTextAccessible.h" + +namespace mozilla { +namespace a11y { + +/** + * Used to find a difference between old and new text and fire text change + * events. + */ +class TextUpdater +{ +public: + /** + * Start text of the text leaf update. + */ + static void Run(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf, + const nsAString& aNewText); + +private: + TextUpdater(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf) : + mDocument(aDocument), mTextLeaf(aTextLeaf), mHyperText(nullptr), + mTextOffset(-1) { } + + ~TextUpdater() + { mDocument = nullptr; mTextLeaf = nullptr; mHyperText = nullptr; } + + /** + * Update text of the text leaf accessible, fire text change and value change + * (if applicable) events for its container hypertext accessible. + */ + void DoUpdate(const nsAString& aNewText, const nsAString& aOldText, + uint32_t aSkipStart); + +private: + TextUpdater(); + TextUpdater(const TextUpdater&); + TextUpdater& operator=(const TextUpdater&); + + /** + * Fire text change events based on difference between strings. + */ + void ComputeTextChangeEvents(const nsAString& aStr1, + const nsAString& aStr2, + uint32_t* aEntries, + nsTArray<RefPtr<AccEvent> >& aEvents); + + /** + * Helper to create text change events for inserted text. + */ + inline void FireInsertEvent(const nsAString& aText, uint32_t aAddlOffset, + nsTArray<RefPtr<AccEvent> >& aEvents) + { + RefPtr<AccEvent> event = + new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset, + aText, true); + aEvents.AppendElement(event); + } + + /** + * Helper to create text change events for removed text. + */ + inline void FireDeleteEvent(const nsAString& aText, uint32_t aAddlOffset, + nsTArray<RefPtr<AccEvent> >& aEvents) + { + RefPtr<AccEvent> event = + new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset, + aText, false); + aEvents.AppendElement(event); + } + + /** + * The constant used to skip string difference calculation in case of long + * strings. + */ + const static uint32_t kMaxStrLen = 1 << 6; + +private: + DocAccessible* mDocument; + TextLeafAccessible* mTextLeaf; + HyperTextAccessible* mHyperText; + int32_t mTextOffset; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TreeWalker.cpp b/accessible/base/TreeWalker.cpp new file mode 100644 index 000000000..8c04b5d6f --- /dev/null +++ b/accessible/base/TreeWalker.cpp @@ -0,0 +1,325 @@ +/* -*- 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 "TreeWalker.h" + +#include "Accessible.h" +#include "AccIterator.h" +#include "nsAccessibilityService.h" +#include "DocAccessible.h" + +#include "mozilla/dom/ChildIterator.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TreeWalker +//////////////////////////////////////////////////////////////////////////////// + +TreeWalker:: + TreeWalker(Accessible* aContext) : + mDoc(aContext->Document()), mContext(aContext), mAnchorNode(nullptr), + mARIAOwnsIdx(0), + mChildFilter(nsIContent::eSkipPlaceholderContent), mFlags(0), + mPhase(eAtStart) +{ + mChildFilter |= mContext->NoXBLKids() ? + nsIContent::eAllButXBL : nsIContent::eAllChildren; + + mAnchorNode = mContext->IsDoc() ? + mDoc->DocumentNode()->GetRootElement() : mContext->GetContent(); + + MOZ_COUNT_CTOR(TreeWalker); +} + +TreeWalker:: + TreeWalker(Accessible* aContext, nsIContent* aAnchorNode, uint32_t aFlags) : + mDoc(aContext->Document()), mContext(aContext), mAnchorNode(aAnchorNode), + mARIAOwnsIdx(0), + mChildFilter(nsIContent::eSkipPlaceholderContent), mFlags(aFlags), + mPhase(eAtStart) +{ + MOZ_ASSERT(mFlags & eWalkCache, "This constructor cannot be used for tree creation"); + MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker"); + + mChildFilter |= mContext->NoXBLKids() ? + nsIContent::eAllButXBL : nsIContent::eAllChildren; + + MOZ_COUNT_CTOR(TreeWalker); +} + +TreeWalker::~TreeWalker() +{ + MOZ_COUNT_DTOR(TreeWalker); +} + +Accessible* +TreeWalker::Scope(nsIContent* aAnchorNode) +{ + Reset(); + + mAnchorNode = aAnchorNode; + + bool skipSubtree = false; + Accessible* acc = AccessibleFor(aAnchorNode, 0, &skipSubtree); + if (acc) { + mPhase = eAtEnd; + return acc; + } + + return skipSubtree ? nullptr : Next(); +} + +bool +TreeWalker::Seek(nsIContent* aChildNode) +{ + MOZ_ASSERT(aChildNode, "Child cannot be null"); + + Reset(); + + if (mAnchorNode == aChildNode) { + return true; + } + + nsIContent* childNode = nullptr; + nsINode* parentNode = aChildNode; + do { + childNode = parentNode->AsContent(); + parentNode = childNode->HasFlag(NODE_MAY_BE_IN_BINDING_MNGR) && + (mChildFilter & nsIContent::eAllButXBL) ? + childNode->GetParentNode() : childNode->GetFlattenedTreeParent(); + + if (!parentNode || !parentNode->IsElement()) { + return false; + } + + // If ARIA owned child. + Accessible* child = mDoc->GetAccessible(childNode); + if (child && child->IsRelocated()) { + if (child->Parent() != mContext) { + return false; + } + + Accessible* ownedChild = nullptr; + while ((ownedChild = mDoc->ARIAOwnedAt(mContext, mARIAOwnsIdx++)) && + ownedChild != child); + + MOZ_ASSERT(ownedChild, "A child has to be in ARIA owned elements"); + mPhase = eAtARIAOwns; + return true; + } + + // Look in DOM. + dom::AllChildrenIterator* iter = PrependState(parentNode->AsElement(), true); + if (!iter->Seek(childNode)) { + return false; + } + + if (parentNode == mAnchorNode) { + mPhase = eAtDOM; + return true; + } + } while (true); + + return false; +} + +Accessible* +TreeWalker::Next() +{ + if (mStateStack.IsEmpty()) { + if (mPhase == eAtEnd) { + return nullptr; + } + + if (mPhase == eAtDOM || mPhase == eAtARIAOwns) { + mPhase = eAtARIAOwns; + Accessible* child = mDoc->ARIAOwnedAt(mContext, mARIAOwnsIdx); + if (child) { + mARIAOwnsIdx++; + return child; + } + mPhase = eAtEnd; + return nullptr; + } + + if (!mAnchorNode) { + mPhase = eAtEnd; + return nullptr; + } + + mPhase = eAtDOM; + PushState(mAnchorNode, true); + } + + dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1]; + while (top) { + while (nsIContent* childNode = top->GetNextChild()) { + bool skipSubtree = false; + Accessible* child = AccessibleFor(childNode, mFlags, &skipSubtree); + if (child) { + return child; + } + + // Walk down the subtree if allowed. + if (!skipSubtree && childNode->IsElement()) { + top = PushState(childNode, true); + } + } + top = PopState(); + } + + // If we traversed the whole subtree of the anchor node. Move to next node + // relative anchor node within the context subtree if asked. + if (mFlags != eWalkContextTree) { + // eWalkCache flag presence indicates that the search is scoped to the + // anchor (no ARIA owns stuff). + if (mFlags & eWalkCache) { + mPhase = eAtEnd; + return nullptr; + } + return Next(); + } + + nsINode* contextNode = mContext->GetNode(); + while (mAnchorNode != contextNode) { + nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent(); + if (!parentNode || !parentNode->IsElement()) + return nullptr; + + nsIContent* parent = parentNode->AsElement(); + top = PushState(parent, true); + if (top->Seek(mAnchorNode)) { + mAnchorNode = parent; + return Next(); + } + + // XXX We really should never get here, it means we're trying to find an + // accessible for a dom node where iterating over its parent's children + // doesn't return it. However this sometimes happens when we're asked for + // the nearest accessible to place holder content which we ignore. + mAnchorNode = parent; + } + + return Next(); +} + +Accessible* +TreeWalker::Prev() +{ + if (mStateStack.IsEmpty()) { + if (mPhase == eAtStart || mPhase == eAtDOM) { + mPhase = eAtStart; + return nullptr; + } + + if (mPhase == eAtEnd) { + mARIAOwnsIdx = mDoc->ARIAOwnedCount(mContext); + mPhase = eAtARIAOwns; + } + + if (mPhase == eAtARIAOwns) { + if (mARIAOwnsIdx > 0) { + return mDoc->ARIAOwnedAt(mContext, --mARIAOwnsIdx); + } + + if (!mAnchorNode) { + mPhase = eAtStart; + return nullptr; + } + + mPhase = eAtDOM; + PushState(mAnchorNode, false); + } + } + + dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1]; + while (top) { + while (nsIContent* childNode = top->GetPreviousChild()) { + // No accessible creation on the way back. + bool skipSubtree = false; + Accessible* child = AccessibleFor(childNode, eWalkCache, &skipSubtree); + if (child) { + return child; + } + + // Walk down into subtree to find accessibles. + if (!skipSubtree && childNode->IsElement()) { + top = PushState(childNode, false); + } + } + top = PopState(); + } + + // Move to a previous node relative the anchor node within the context + // subtree if asked. + if (mFlags != eWalkContextTree) { + mPhase = eAtStart; + return nullptr; + } + + nsINode* contextNode = mContext->GetNode(); + while (mAnchorNode != contextNode) { + nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent(); + if (!parentNode || !parentNode->IsElement()) { + return nullptr; + } + + nsIContent* parent = parentNode->AsElement(); + top = PushState(parent, true); + if (top->Seek(mAnchorNode)) { + mAnchorNode = parent; + return Prev(); + } + + mAnchorNode = parent; + } + + mPhase = eAtStart; + return nullptr; +} + +Accessible* +TreeWalker::AccessibleFor(nsIContent* aNode, uint32_t aFlags, bool* aSkipSubtree) +{ + // Ignore the accessible and its subtree if it was repositioned by means + // of aria-owns. + Accessible* child = mDoc->GetAccessible(aNode); + if (child) { + if (child->IsRelocated()) { + *aSkipSubtree = true; + return nullptr; + } + return child; + } + + // Create an accessible if allowed. + if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode)) { + // We may have ARIA owned element in the dependent attributes map, but the + // element may be not allowed for this ARIA owns relation, if the relation + // crosses out XBL anonymous content boundaries. In this case we won't + // create an accessible object for it, when aria-owns is processed, which + // may make the element subtree inaccessible. To avoid that let's create + // an accessible object now, and later, if allowed, move it in the tree, + // when aria-owns relation is processed. + if (mDoc->RelocateARIAOwnedIfNeeded(aNode) && !aNode->IsXULElement()) { + *aSkipSubtree = true; + return nullptr; + } + return GetAccService()->CreateAccessible(aNode, mContext, aSkipSubtree); + } + + return nullptr; +} + +dom::AllChildrenIterator* +TreeWalker::PopState() +{ + size_t length = mStateStack.Length(); + mStateStack.RemoveElementAt(length - 1); + return mStateStack.IsEmpty() ? nullptr : &mStateStack.LastElement(); +} diff --git a/accessible/base/TreeWalker.h b/accessible/base/TreeWalker.h new file mode 100644 index 000000000..377a5e3b8 --- /dev/null +++ b/accessible/base/TreeWalker.h @@ -0,0 +1,144 @@ +/* -*- 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 mozilla_a11y_TreeWalker_h_ +#define mozilla_a11y_TreeWalker_h_ + +#include "mozilla/Attributes.h" +#include <stdint.h> +#include "mozilla/dom/ChildIterator.h" +#include "nsCOMPtr.h" + +class nsIContent; + +namespace mozilla { +namespace a11y { + +class Accessible; +class DocAccessible; + +/** + * This class is used to walk the DOM tree to create accessible tree. + */ +class TreeWalker final +{ +public: + enum { + // used to walk the existing tree of the given node + eWalkCache = 1, + // used to walk the context tree starting from given node + eWalkContextTree = 2 | eWalkCache + }; + + /** + * Used to navigate and create if needed the accessible children. + */ + explicit TreeWalker(Accessible* aContext); + + /** + * Used to navigate the accessible children relative to the anchor. + * + * @param aContext [in] container accessible for the given node, used to + * define accessible context + * @param aAnchorNode [in] the node the search will be prepared relative to + * @param aFlags [in] flags (see enum above) + */ + TreeWalker(Accessible* aContext, nsIContent* aAnchorNode, uint32_t aFlags = eWalkCache); + + ~TreeWalker(); + + /** + * Resets the walker state, and sets the given node as an anchor. Returns a + * first accessible element within the node including the node itself. + */ + Accessible* Scope(nsIContent* aAnchorNode); + + /** + * Resets the walker state. + */ + void Reset() + { + mPhase = eAtStart; + mStateStack.Clear(); + mARIAOwnsIdx = 0; + } + + /** + * Sets the walker state to the given child node if it's within the anchor. + */ + bool Seek(nsIContent* aChildNode); + + /** + * Return the next/prev accessible. + * + * @note Returned accessible is bound to the document, if the accessible is + * rejected during tree creation then the caller should be unbind it + * from the document. + */ + Accessible* Next(); + Accessible* Prev(); + + Accessible* Context() const { return mContext; } + DocAccessible* Document() const { return mDoc; } + +private: + TreeWalker(); + TreeWalker(const TreeWalker&); + TreeWalker& operator =(const TreeWalker&); + + /** + * Return an accessible for the given node if any. + */ + Accessible* AccessibleFor(nsIContent* aNode, uint32_t aFlags, + bool* aSkipSubtree); + + /** + * Create new state for the given node and push it on top of stack / at bottom + * of stack. + * + * @note State stack is used to navigate up/down the DOM subtree during + * accessible children search. + */ + dom::AllChildrenIterator* PushState(nsIContent* aContent, + bool aStartAtBeginning) + { + return mStateStack.AppendElement( + dom::AllChildrenIterator(aContent, mChildFilter, aStartAtBeginning)); + } + dom::AllChildrenIterator* PrependState(nsIContent* aContent, + bool aStartAtBeginning) + { + return mStateStack.InsertElementAt(0, + dom::AllChildrenIterator(aContent, mChildFilter, aStartAtBeginning)); + } + + /** + * Pop state from stack. + */ + dom::AllChildrenIterator* PopState(); + + DocAccessible* mDoc; + Accessible* mContext; + nsIContent* mAnchorNode; + + AutoTArray<dom::AllChildrenIterator, 20> mStateStack; + uint32_t mARIAOwnsIdx; + + int32_t mChildFilter; + uint32_t mFlags; + + enum Phase { + eAtStart, + eAtDOM, + eAtARIAOwns, + eAtEnd + }; + Phase mPhase; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_TreeWalker_h_ diff --git a/accessible/base/moz.build b/accessible/base/moz.build new file mode 100644 index 000000000..dcccc4b54 --- /dev/null +++ b/accessible/base/moz.build @@ -0,0 +1,114 @@ +# -*- 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 += [ + 'AccEvent.h', + 'nsAccessibilityService.h' +] + +EXPORTS.mozilla.a11y += [ + 'AccTypes.h', + 'DocManager.h', + 'FocusManager.h', + 'Platform.h', + 'RelationType.h', + 'Role.h', + 'SelectionManager.h', + 'States.h', +] + +if CONFIG['MOZ_DEBUG']: + EXPORTS.mozilla.a11y += [ + 'Logging.h', + ] + +UNIFIED_SOURCES += [ + 'AccessibleOrProxy.cpp', + 'AccEvent.cpp', + 'AccGroupInfo.cpp', + 'AccIterator.cpp', + 'ARIAMap.cpp', + 'ARIAStateMap.cpp', + 'Asserts.cpp', + 'DocManager.cpp', + 'EmbeddedObjCollector.cpp', + 'EventQueue.cpp', + 'EventTree.cpp', + 'Filters.cpp', + 'FocusManager.cpp', + 'NotificationController.cpp', + 'nsAccessibilityService.cpp', + 'nsAccessiblePivot.cpp', + 'nsAccUtils.cpp', + 'nsCoreUtils.cpp', + 'nsEventShell.cpp', + 'nsTextEquivUtils.cpp', + 'SelectionManager.cpp', + 'StyleInfo.cpp', + 'TextAttrs.cpp', + 'TextRange.cpp', + 'TextUpdater.cpp', + 'TreeWalker.cpp', +] + +if CONFIG['A11Y_LOG']: + UNIFIED_SOURCES += [ + 'Logging.cpp', + ] + +LOCAL_INCLUDES += [ + '/accessible/generic', + '/accessible/html', + '/accessible/ipc', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + LOCAL_INCLUDES += [ + '/accessible/ipc/win', + ] +else: + LOCAL_INCLUDES += [ + '/accessible/ipc/other', + ] + +LOCAL_INCLUDES += [ + '/accessible/xpcom', + '/accessible/xul', + '/dom/base', + '/dom/xbl', + '/ipc/chromium/src', + '/layout/generic', + '/layout/style', + '/layout/svg', + '/layout/xul', + '/layout/xul/tree/', +] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + LOCAL_INCLUDES += [ + '/accessible/atk', + ] + CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS'] +elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + LOCAL_INCLUDES += [ + '/accessible/windows/ia2', + '/accessible/windows/msaa', + ] +elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa': + LOCAL_INCLUDES += [ + '/accessible/mac', + ] +else: + LOCAL_INCLUDES += [ + '/accessible/other', + ] + +FINAL_LIBRARY = 'xul' + +include('/ipc/chromium/chromium-config.mozbuild') + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/accessible/base/nsAccCache.h b/accessible/base/nsAccCache.h new file mode 100644 index 000000000..cb39accde --- /dev/null +++ b/accessible/base/nsAccCache.h @@ -0,0 +1,28 @@ +/* -*- 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 _nsAccCache_H_ +#define _nsAccCache_H_ + +#include "xpcAccessibleDocument.h" + +//////////////////////////////////////////////////////////////////////////////// +// Accessible cache utils +//////////////////////////////////////////////////////////////////////////////// + +template <class T> +void +UnbindCacheEntriesFromDocument( + nsRefPtrHashtable<nsPtrHashKey<const void>, T>& aCache) +{ + for (auto iter = aCache.Iter(); !iter.Done(); iter.Next()) { + T* accessible = iter.Data(); + MOZ_ASSERT(accessible && !accessible->IsDefunct()); + accessible->Document()->UnbindFromDocument(accessible); + iter.Remove(); + } +} + +#endif diff --git a/accessible/base/nsAccUtils.cpp b/accessible/base/nsAccUtils.cpp new file mode 100644 index 000000000..59db53c4b --- /dev/null +++ b/accessible/base/nsAccUtils.cpp @@ -0,0 +1,447 @@ +/* -*- 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 "nsAccUtils.h" + +#include "Accessible-inl.h" +#include "ARIAMap.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" +#include "DocAccessible.h" +#include "HyperTextAccessible.h" +#include "nsIAccessibleTypes.h" +#include "Role.h" +#include "States.h" +#include "TextLeafAccessible.h" + +#include "nsIDOMXULContainerElement.h" +#include "nsIPersistentProperties2.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +void +nsAccUtils::GetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom *aAttrName, nsAString& aAttrValue) +{ + aAttrValue.Truncate(); + + aAttributes->GetStringProperty(nsAtomCString(aAttrName), aAttrValue); +} + +void +nsAccUtils::SetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom *aAttrName, const nsAString& aAttrValue) +{ + nsAutoString oldValue; + aAttributes->SetStringProperty(nsAtomCString(aAttrName), aAttrValue, oldValue); +} + +void +nsAccUtils::SetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom* aAttrName, nsIAtom* aAttrValue) +{ + nsAutoString oldValue; + aAttributes->SetStringProperty(nsAtomCString(aAttrName), + nsAtomString(aAttrValue), oldValue); +} + +void +nsAccUtils::SetAccGroupAttrs(nsIPersistentProperties *aAttributes, + int32_t aLevel, int32_t aSetSize, + int32_t aPosInSet) +{ + nsAutoString value; + + if (aLevel) { + value.AppendInt(aLevel); + SetAccAttr(aAttributes, nsGkAtoms::level, value); + } + + if (aSetSize && aPosInSet) { + value.Truncate(); + value.AppendInt(aPosInSet); + SetAccAttr(aAttributes, nsGkAtoms::posinset, value); + + value.Truncate(); + value.AppendInt(aSetSize); + SetAccAttr(aAttributes, nsGkAtoms::setsize, value); + } +} + +int32_t +nsAccUtils::GetDefaultLevel(Accessible* aAccessible) +{ + roles::Role role = aAccessible->Role(); + + if (role == roles::OUTLINEITEM) + return 1; + + if (role == roles::ROW) { + Accessible* parent = aAccessible->Parent(); + // It is a row inside flatten treegrid. Group level is always 1 until it + // is overriden by aria-level attribute. + if (parent && parent->Role() == roles::TREE_TABLE) + return 1; + } + + return 0; +} + +int32_t +nsAccUtils::GetARIAOrDefaultLevel(Accessible* aAccessible) +{ + int32_t level = 0; + nsCoreUtils::GetUIntAttr(aAccessible->GetContent(), + nsGkAtoms::aria_level, &level); + + if (level != 0) + return level; + + return GetDefaultLevel(aAccessible); +} + +int32_t +nsAccUtils::GetLevelForXULContainerItem(nsIContent *aContent) +{ + nsCOMPtr<nsIDOMXULContainerItemElement> item(do_QueryInterface(aContent)); + if (!item) + return 0; + + nsCOMPtr<nsIDOMXULContainerElement> container; + item->GetParentContainer(getter_AddRefs(container)); + if (!container) + return 0; + + // Get level of the item. + int32_t level = -1; + while (container) { + level++; + + nsCOMPtr<nsIDOMXULContainerElement> parentContainer; + container->GetParentContainer(getter_AddRefs(parentContainer)); + parentContainer.swap(container); + } + + return level; +} + +void +nsAccUtils::SetLiveContainerAttributes(nsIPersistentProperties *aAttributes, + nsIContent* aStartContent, + dom::Element* aTopEl) +{ + nsAutoString live, relevant, busy; + nsIContent* ancestor = aStartContent; + while (ancestor) { + + // container-relevant attribute + if (relevant.IsEmpty() && + HasDefinedARIAToken(ancestor, nsGkAtoms::aria_relevant) && + ancestor->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_relevant, relevant)) + SetAccAttr(aAttributes, nsGkAtoms::containerRelevant, relevant); + + // container-live, and container-live-role attributes + if (live.IsEmpty()) { + const nsRoleMapEntry* role = nullptr; + if (ancestor->IsElement()) { + role = aria::GetRoleMap(ancestor->AsElement()); + } + if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) { + ancestor->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_live, live); + } else if (role) { + GetLiveAttrValue(role->liveAttRule, live); + } + if (!live.IsEmpty()) { + SetAccAttr(aAttributes, nsGkAtoms::containerLive, live); + if (role) { + SetAccAttr(aAttributes, nsGkAtoms::containerLiveRole, + role->ARIARoleString()); + } + } + } + + // container-atomic attribute + if (ancestor->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_atomic, + nsGkAtoms::_true, eCaseMatters)) { + SetAccAttr(aAttributes, nsGkAtoms::containerAtomic, + NS_LITERAL_STRING("true")); + } + + // container-busy attribute + if (busy.IsEmpty() && + HasDefinedARIAToken(ancestor, nsGkAtoms::aria_busy) && + ancestor->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_busy, busy)) + SetAccAttr(aAttributes, nsGkAtoms::containerBusy, busy); + + if (ancestor == aTopEl) + break; + + ancestor = ancestor->GetParent(); + if (!ancestor) + ancestor = aTopEl; // Use <body>/<frameset> + } +} + +bool +nsAccUtils::HasDefinedARIAToken(nsIContent *aContent, nsIAtom *aAtom) +{ + NS_ASSERTION(aContent, "aContent is null in call to HasDefinedARIAToken!"); + + if (!aContent->HasAttr(kNameSpaceID_None, aAtom) || + aContent->AttrValueIs(kNameSpaceID_None, aAtom, + nsGkAtoms::_empty, eCaseMatters) || + aContent->AttrValueIs(kNameSpaceID_None, aAtom, + nsGkAtoms::_undefined, eCaseMatters)) { + return false; + } + return true; +} + +nsIAtom* +nsAccUtils::GetARIAToken(dom::Element* aElement, nsIAtom* aAttr) +{ + if (!HasDefinedARIAToken(aElement, aAttr)) + return nsGkAtoms::_empty; + + static nsIContent::AttrValuesArray tokens[] = + { &nsGkAtoms::_false, &nsGkAtoms::_true, + &nsGkAtoms::mixed, nullptr}; + + int32_t idx = aElement->FindAttrValueIn(kNameSpaceID_None, + aAttr, tokens, eCaseMatters); + if (idx >= 0) + return *(tokens[idx]); + + return nullptr; +} + +Accessible* +nsAccUtils::GetSelectableContainer(Accessible* aAccessible, uint64_t aState) +{ + if (!aAccessible) + return nullptr; + + if (!(aState & states::SELECTABLE)) + return nullptr; + + Accessible* parent = aAccessible; + while ((parent = parent->Parent()) && !parent->IsSelect()) { + if (parent->Role() == roles::PANE) + return nullptr; + } + return parent; +} + +bool +nsAccUtils::IsARIASelected(Accessible* aAccessible) +{ + return aAccessible->GetContent()-> + AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_selected, + nsGkAtoms::_true, eCaseMatters); +} + +Accessible* +nsAccUtils::TableFor(Accessible* aRow) +{ + if (aRow) { + Accessible* table = aRow->Parent(); + if (table) { + roles::Role tableRole = table->Role(); + if (tableRole == roles::GROUPING) { // if there's a rowgroup. + table = table->Parent(); + if (table) + tableRole = table->Role(); + } + + return (tableRole == roles::TABLE || tableRole == roles::TREE_TABLE || + tableRole == roles::MATHML_TABLE) ? table : nullptr; + } + } + + return nullptr; +} + +HyperTextAccessible* +nsAccUtils::GetTextContainer(nsINode* aNode) +{ + // Get text accessible containing the result node. + DocAccessible* doc = + GetAccService()->GetDocAccessible(aNode->OwnerDoc()); + Accessible* accessible = + doc ? doc->GetAccessibleOrContainer(aNode) : nullptr; + if (!accessible) + return nullptr; + + do { + HyperTextAccessible* textAcc = accessible->AsHyperText(); + if (textAcc) + return textAcc; + + accessible = accessible->Parent(); + } while (accessible); + + return nullptr; +} + +nsIntPoint +nsAccUtils::ConvertToScreenCoords(int32_t aX, int32_t aY, + uint32_t aCoordinateType, + Accessible* aAccessible) +{ + nsIntPoint coords(aX, aY); + + switch (aCoordinateType) { + case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE: + break; + + case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: + { + coords += nsCoreUtils::GetScreenCoordsForWindow(aAccessible->GetNode()); + break; + } + + case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: + { + coords += GetScreenCoordsForParent(aAccessible); + break; + } + + default: + NS_NOTREACHED("invalid coord type!"); + } + + return coords; +} + +void +nsAccUtils::ConvertScreenCoordsTo(int32_t *aX, int32_t *aY, + uint32_t aCoordinateType, + Accessible* aAccessible) +{ + switch (aCoordinateType) { + case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE: + break; + + case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: + { + nsIntPoint coords = nsCoreUtils::GetScreenCoordsForWindow(aAccessible->GetNode()); + *aX -= coords.x; + *aY -= coords.y; + break; + } + + case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: + { + nsIntPoint coords = GetScreenCoordsForParent(aAccessible); + *aX -= coords.x; + *aY -= coords.y; + break; + } + + default: + NS_NOTREACHED("invalid coord type!"); + } +} + +nsIntPoint +nsAccUtils::GetScreenCoordsForParent(Accessible* aAccessible) +{ + Accessible* parent = aAccessible->Parent(); + if (!parent) + return nsIntPoint(0, 0); + + nsIFrame *parentFrame = parent->GetFrame(); + if (!parentFrame) + return nsIntPoint(0, 0); + + nsRect rect = parentFrame->GetScreenRectInAppUnits(); + return nsPoint(rect.x, rect.y). + ToNearestPixels(parentFrame->PresContext()->AppUnitsPerDevPixel()); +} + +bool +nsAccUtils::GetLiveAttrValue(uint32_t aRule, nsAString& aValue) +{ + switch (aRule) { + case eOffLiveAttr: + aValue = NS_LITERAL_STRING("off"); + return true; + case ePoliteLiveAttr: + aValue = NS_LITERAL_STRING("polite"); + return true; + } + + return false; +} + +#ifdef DEBUG + +bool +nsAccUtils::IsTextInterfaceSupportCorrect(Accessible* aAccessible) +{ + // Don't test for accessible docs, it makes us create accessibles too + // early and fire mutation events before we need to + if (aAccessible->IsDoc()) + return true; + + bool foundText = false; + uint32_t childCount = aAccessible->ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + Accessible* child = aAccessible->GetChildAt(childIdx); + if (child->IsText()) { + foundText = true; + break; + } + } + + return !foundText || aAccessible->IsHyperText(); +} +#endif + +uint32_t +nsAccUtils::TextLength(Accessible* aAccessible) +{ + if (!aAccessible->IsText()) { + return 1; + } + + TextLeafAccessible* textLeaf = aAccessible->AsTextLeaf(); + if (textLeaf) + return textLeaf->Text().Length(); + + // For list bullets (or anything other accessible which would compute its own + // text. They don't have their own frame. + // XXX In the future, list bullets may have frame and anon content, so + // we should be able to remove this at that point + nsAutoString text; + aAccessible->AppendTextTo(text); // Get all the text + return text.Length(); +} + +bool +nsAccUtils::MustPrune(Accessible* aAccessible) +{ + roles::Role role = aAccessible->Role(); + + // Don't prune the tree for certain roles if the tree is more complex than + // a single text leaf. + return + (role == roles::MENUITEM || + role == roles::COMBOBOX_OPTION || + role == roles::OPTION || + role == roles::ENTRY || + role == roles::FLAT_EQUATION || + role == roles::PASSWORD_TEXT || + role == roles::PUSHBUTTON || + role == roles::TOGGLE_BUTTON || + role == roles::GRAPHIC || + role == roles::SLIDER || + role == roles::PROGRESSBAR || + role == roles::SEPARATOR) && + aAccessible->ContentChildCount() == 1 && + aAccessible->ContentChildAt(0)->IsTextLeaf(); +} diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h new file mode 100644 index 000000000..9a9bc2590 --- /dev/null +++ b/accessible/base/nsAccUtils.h @@ -0,0 +1,243 @@ +/* -*- 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 nsAccUtils_h_ +#define nsAccUtils_h_ + +#include "mozilla/a11y/Accessible.h" + +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" + +#include "nsIDocShell.h" +#include "nsPoint.h" + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { + +class HyperTextAccessible; +class DocAccessible; + +class nsAccUtils +{ +public: + /** + * Returns value of attribute from the given attributes container. + * + * @param aAttributes - attributes container + * @param aAttrName - the name of requested attribute + * @param aAttrValue - value of attribute + */ + static void GetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom *aAttrName, + nsAString& aAttrValue); + + /** + * Set value of attribute for the given attributes container. + * + * @param aAttributes - attributes container + * @param aAttrName - the name of requested attribute + * @param aAttrValue - new value of attribute + */ + static void SetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom *aAttrName, + const nsAString& aAttrValue); + + static void SetAccAttr(nsIPersistentProperties *aAttributes, + nsIAtom* aAttrName, + nsIAtom* aAttrValue); + + /** + * Set group attributes ('level', 'setsize', 'posinset'). + */ + static void SetAccGroupAttrs(nsIPersistentProperties *aAttributes, + int32_t aLevel, int32_t aSetSize, + int32_t aPosInSet); + + /** + * Get default value of the level for the given accessible. + */ + static int32_t GetDefaultLevel(Accessible* aAcc); + + /** + * Return ARIA level value or the default one if ARIA is missed for the + * given accessible. + */ + static int32_t GetARIAOrDefaultLevel(Accessible* aAccessible); + + /** + * Compute group level for nsIDOMXULContainerItemElement node. + */ + static int32_t GetLevelForXULContainerItem(nsIContent *aContent); + + /** + * Set container-foo live region attributes for the given node. + * + * @param aAttributes where to store the attributes + * @param aStartContent node to start from + * @param aTopContent node to end at + */ + static void SetLiveContainerAttributes(nsIPersistentProperties *aAttributes, + nsIContent* aStartContent, + mozilla::dom::Element* aTopEl); + + /** + * Any ARIA property of type boolean or NMTOKEN is undefined if the ARIA + * property is not present, or is "" or "undefined". Do not call + * this method for properties of type string, decimal, IDREF or IDREFS. + * + * Return true if the ARIA property is defined, otherwise false + */ + static bool HasDefinedARIAToken(nsIContent *aContent, nsIAtom *aAtom); + + /** + * Return atomic value of ARIA attribute of boolean or NMTOKEN type. + */ + static nsIAtom* GetARIAToken(mozilla::dom::Element* aElement, nsIAtom* aAttr); + + /** + * Return document accessible for the given DOM node. + */ + static DocAccessible* GetDocAccessibleFor(nsINode* aNode) + { + nsIPresShell *presShell = nsCoreUtils::GetPresShellFor(aNode); + return GetAccService()->GetDocAccessible(presShell); + } + + /** + * Return document accessible for the given docshell. + */ + static DocAccessible* GetDocAccessibleFor(nsIDocShellTreeItem* aContainer) + { + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer)); + return GetAccService()->GetDocAccessible(docShell->GetPresShell()); + } + + /** + * Return single or multi selectable container for the given item. + * + * @param aAccessible [in] the item accessible + * @param aState [in] the state of the item accessible + */ + static Accessible* GetSelectableContainer(Accessible* aAccessible, + uint64_t aState); + + /** + * Return a text container accessible for the given node. + */ + static HyperTextAccessible* GetTextContainer(nsINode* aNode); + + static Accessible* TableFor(Accessible* aRow); + + /** + * Return true if the DOM node of given accessible has aria-selected="true" + * attribute. + */ + static bool IsARIASelected(Accessible* aAccessible); + + /** + * Converts the given coordinates to coordinates relative screen. + * + * @param aX [in] the given x coord + * @param aY [in] the given y coord + * @param aCoordinateType [in] specifies coordinates origin (refer to + * nsIAccessibleCoordinateType) + * @param aAccessible [in] the accessible if coordinates are given + * relative it. + * @return converted coordinates + */ + static nsIntPoint ConvertToScreenCoords(int32_t aX, int32_t aY, + uint32_t aCoordinateType, + Accessible* aAccessible); + + /** + * Converts the given coordinates relative screen to another coordinate + * system. + * + * @param aX [in, out] the given x coord + * @param aY [in, out] the given y coord + * @param aCoordinateType [in] specifies coordinates origin (refer to + * nsIAccessibleCoordinateType) + * @param aAccessible [in] the accessible if coordinates are given + * relative it + */ + static void ConvertScreenCoordsTo(int32_t* aX, int32_t* aY, + uint32_t aCoordinateType, + Accessible* aAccessible); + + /** + * Returns coordinates relative screen for the parent of the given accessible. + * + * @param [in] aAccessible the accessible + */ + static nsIntPoint GetScreenCoordsForParent(Accessible* aAccessible); + + /** + * Get the 'live' or 'container-live' object attribute value from the given + * ELiveAttrRule constant. + * + * @param aRule [in] rule constant (see ELiveAttrRule in nsAccMap.h) + * @param aValue [out] object attribute value + * + * @return true if object attribute should be exposed + */ + static bool GetLiveAttrValue(uint32_t aRule, nsAString& aValue); + +#ifdef DEBUG + /** + * Detect whether the given accessible object implements nsIAccessibleText, + * when it is text or has text child node. + */ + static bool IsTextInterfaceSupportCorrect(Accessible* aAccessible); +#endif + + /** + * Return text length of the given accessible, return 0 on failure. + */ + static uint32_t TextLength(Accessible* aAccessible); + + /** + * Transform nsIAccessibleStates constants to internal state constant. + */ + static inline uint64_t To64State(uint32_t aState1, uint32_t aState2) + { + return static_cast<uint64_t>(aState1) + + (static_cast<uint64_t>(aState2) << 31); + } + + /** + * Transform internal state constant to nsIAccessibleStates constants. + */ + static inline void To32States(uint64_t aState64, + uint32_t* aState1, uint32_t* aState2) + { + *aState1 = aState64 & 0x7fffffff; + if (aState2) + *aState2 = static_cast<uint32_t>(aState64 >> 31); + } + + static uint32_t To32States(uint64_t aState, bool* aIsExtra) + { + uint32_t extraState = aState >> 31; + *aIsExtra = !!extraState; + return aState | extraState; + } + + /** + * Return true if the given accessible can't have children. Used when exposing + * to platform accessibility APIs, should the children be pruned off? + */ + static bool MustPrune(Accessible* aAccessible); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp new file mode 100644 index 000000000..2590969a0 --- /dev/null +++ b/accessible/base/nsAccessibilityService.cpp @@ -0,0 +1,1885 @@ +/* -*- 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 "nsAccessibilityService.h" + +// NOTE: alphabetically ordered +#include "ApplicationAccessibleWrap.h" +#include "ARIAGridAccessibleWrap.h" +#include "ARIAMap.h" +#include "DocAccessible-inl.h" +#include "FocusManager.h" +#include "HTMLCanvasAccessible.h" +#include "HTMLElementAccessibles.h" +#include "HTMLImageMapAccessible.h" +#include "HTMLLinkAccessible.h" +#include "HTMLListAccessible.h" +#include "HTMLSelectAccessible.h" +#include "HTMLTableAccessibleWrap.h" +#include "HyperTextAccessibleWrap.h" +#include "RootAccessible.h" +#include "nsAccUtils.h" +#include "nsArrayUtils.h" +#include "nsAttrName.h" +#include "nsEventShell.h" +#include "nsIURI.h" +#include "OuterDocAccessible.h" +#include "Platform.h" +#include "Role.h" +#ifdef MOZ_ACCESSIBILITY_ATK +#include "RootAccessibleWrap.h" +#endif +#include "States.h" +#include "Statistics.h" +#include "TextLeafAccessibleWrap.h" +#include "TreeWalker.h" +#include "xpcAccessibleApplication.h" +#include "xpcAccessibleDocument.h" + +#ifdef MOZ_ACCESSIBILITY_ATK +#include "AtkSocketAccessible.h" +#endif + +#ifdef XP_WIN +#include "mozilla/a11y/Compatibility.h" +#include "mozilla/dom/ContentChild.h" +#include "HTMLWin32ObjectAccessible.h" +#include "mozilla/StaticPtr.h" +#endif + +#ifdef A11Y_LOG +#include "Logging.h" +#endif + +#ifdef MOZ_CRASHREPORTER +#include "nsExceptionHandler.h" +#endif + +#include "nsImageFrame.h" +#include "nsIObserverService.h" +#include "nsLayoutUtils.h" +#include "nsPluginFrame.h" +#include "nsSVGPathGeometryFrame.h" +#include "nsTreeBodyFrame.h" +#include "nsTreeColumns.h" +#include "nsTreeUtils.h" +#include "nsXBLPrototypeBinding.h" +#include "nsXBLBinding.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "nsDeckFrame.h" + +#ifdef MOZ_XUL +#include "XULAlertAccessible.h" +#include "XULColorPickerAccessible.h" +#include "XULComboboxAccessible.h" +#include "XULElementAccessibles.h" +#include "XULFormControlAccessible.h" +#include "XULListboxAccessibleWrap.h" +#include "XULMenuAccessibleWrap.h" +#include "XULSliderAccessible.h" +#include "XULTabAccessible.h" +#include "XULTreeGridAccessibleWrap.h" +#endif + +#if defined(XP_WIN) || defined(MOZ_ACCESSIBILITY_ATK) +#include "nsNPAPIPluginInstance.h" +#endif + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// Statics +//////////////////////////////////////////////////////////////////////////////// + +/** + * Return true if the element must be accessible. + */ +static bool +MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) +{ + if (aContent->GetPrimaryFrame()->IsFocusable()) + return true; + + uint32_t attrCount = aContent->GetAttrCount(); + for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) { + const nsAttrName* attr = aContent->GetAttrNameAt(attrIdx); + if (attr->NamespaceEquals(kNameSpaceID_None)) { + nsIAtom* attrAtom = attr->Atom(); + nsDependentAtomString attrStr(attrAtom); + if (!StringBeginsWith(attrStr, NS_LITERAL_STRING("aria-"))) + continue; // not ARIA + + // A global state or a property and in case of token defined. + uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom); + if ((attrFlags & ATTR_GLOBAL) && (!(attrFlags & ATTR_VALTOKEN) || + nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) { + return true; + } + } + } + + // If the given ID is referred by relation attribute then create an accessible + // for it. + nsAutoString id; + if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) + return aDocument->IsDependentID(id); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// Accessible constructors + +static Accessible* +New_HTMLLink(nsIContent* aContent, Accessible* aContext) +{ + // Only some roles truly enjoy life as HTMLLinkAccessibles, for details + // see closed bug 494807. + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(aContent->AsElement()); + if (roleMapEntry && roleMapEntry->role != roles::NOTHING && + roleMapEntry->role != roles::LINK) { + return new HyperTextAccessibleWrap(aContent, aContext->Document()); + } + + return new HTMLLinkAccessible(aContent, aContext->Document()); +} + +static Accessible* New_HyperText(nsIContent* aContent, Accessible* aContext) + { return new HyperTextAccessibleWrap(aContent, aContext->Document()); } + +static Accessible* New_HTMLFigcaption(nsIContent* aContent, Accessible* aContext) + { return new HTMLFigcaptionAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLFigure(nsIContent* aContent, Accessible* aContext) + { return new HTMLFigureAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLLegend(nsIContent* aContent, Accessible* aContext) + { return new HTMLLegendAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLOption(nsIContent* aContent, Accessible* aContext) + { return new HTMLSelectOptionAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLOptgroup(nsIContent* aContent, Accessible* aContext) + { return new HTMLSelectOptGroupAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLList(nsIContent* aContent, Accessible* aContext) + { return new HTMLListAccessible(aContent, aContext->Document()); } + +static Accessible* +New_HTMLListitem(nsIContent* aContent, Accessible* aContext) +{ + // If list item is a child of accessible list then create an accessible for + // it unconditionally by tag name. nsBlockFrame creates the list item + // accessible for other elements styled as list items. + if (aContext->IsList() && aContext->GetContent() == aContent->GetParent()) + return new HTMLLIAccessible(aContent, aContext->Document()); + + return nullptr; +} + +static Accessible* +New_HTMLDefinition(nsIContent* aContent, Accessible* aContext) +{ + if (aContext->IsList()) + return new HyperTextAccessibleWrap(aContent, aContext->Document()); + return nullptr; +} + +static Accessible* New_HTMLLabel(nsIContent* aContent, Accessible* aContext) + { return new HTMLLabelAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLOutput(nsIContent* aContent, Accessible* aContext) + { return new HTMLOutputAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLProgress(nsIContent* aContent, Accessible* aContext) + { return new HTMLProgressMeterAccessible(aContent, aContext->Document()); } + +static Accessible* New_HTMLSummary(nsIContent* aContent, Accessible* aContext) + { return new HTMLSummaryAccessible(aContent, aContext->Document()); } + +static Accessible* +New_HTMLTableAccessible(nsIContent* aContent, Accessible* aContext) + { return new HTMLTableAccessible(aContent, aContext->Document()); } + +static Accessible* +New_HTMLTableRowAccessible(nsIContent* aContent, Accessible* aContext) + { return new HTMLTableRowAccessible(aContent, aContext->Document()); } + +static Accessible* +New_HTMLTableCellAccessible(nsIContent* aContent, Accessible* aContext) + { return new HTMLTableCellAccessible(aContent, aContext->Document()); } + +static Accessible* +New_HTMLTableHeaderCell(nsIContent* aContent, Accessible* aContext) +{ + if (aContext->IsTableRow() && aContext->GetContent() == aContent->GetParent()) + return new HTMLTableHeaderCellAccessibleWrap(aContent, aContext->Document()); + return nullptr; +} + +static Accessible* +New_HTMLTableHeaderCellIfScope(nsIContent* aContent, Accessible* aContext) +{ + if (aContext->IsTableRow() && aContext->GetContent() == aContent->GetParent() && + aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::scope)) + return new HTMLTableHeaderCellAccessibleWrap(aContent, aContext->Document()); + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// Markup maps array. + +#define Attr(name, value) \ + { &nsGkAtoms::name, &nsGkAtoms::value } + +#define AttrFromDOM(name, DOMAttrName) \ + { &nsGkAtoms::name, nullptr, &nsGkAtoms::DOMAttrName } + +#define AttrFromDOMIf(name, DOMAttrName, DOMAttrValue) \ + { &nsGkAtoms::name, nullptr, &nsGkAtoms::DOMAttrName, &nsGkAtoms::DOMAttrValue } + +#define MARKUPMAP(atom, new_func, r, ... ) \ + { &nsGkAtoms::atom, new_func, static_cast<a11y::role>(r), { __VA_ARGS__ } }, + +static const MarkupMapInfo sMarkupMapList[] = { + #include "MarkupMap.h" +}; + +#undef Attr +#undef AttrFromDOM +#undef AttrFromDOMIf +#undef MARKUPMAP + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService +//////////////////////////////////////////////////////////////////////////////// + +nsAccessibilityService *nsAccessibilityService::gAccessibilityService = nullptr; +ApplicationAccessible* nsAccessibilityService::gApplicationAccessible = nullptr; +xpcAccessibleApplication* nsAccessibilityService::gXPCApplicationAccessible = nullptr; +uint32_t nsAccessibilityService::gConsumers = 0; + +nsAccessibilityService::nsAccessibilityService() : + DocManager(), FocusManager(), mMarkupMaps(ArrayLength(sMarkupMapList)) +{ +} + +nsAccessibilityService::~nsAccessibilityService() +{ + NS_ASSERTION(IsShutdown(), "Accessibility wasn't shutdown!"); + gAccessibilityService = nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIListenerChangeListener + +NS_IMETHODIMP +nsAccessibilityService::ListenersChanged(nsIArray* aEventChanges) +{ + uint32_t targetCount; + nsresult rv = aEventChanges->GetLength(&targetCount); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0 ; i < targetCount ; i++) { + nsCOMPtr<nsIEventListenerChange> change = do_QueryElementAt(aEventChanges, i); + + nsCOMPtr<nsIDOMEventTarget> target; + change->GetTarget(getter_AddRefs(target)); + nsCOMPtr<nsIContent> node(do_QueryInterface(target)); + if (!node || !node->IsHTMLElement()) { + continue; + } + nsCOMPtr<nsIArray> listenerNames; + change->GetChangedListenerNames(getter_AddRefs(listenerNames)); + + uint32_t changeCount; + rv = listenerNames->GetLength(&changeCount); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0 ; i < changeCount ; i++) { + nsCOMPtr<nsIAtom> listenerName = do_QueryElementAt(listenerNames, i); + + // We are only interested in event listener changes which may + // make an element accessible or inaccessible. + if (listenerName != nsGkAtoms::onclick && + listenerName != nsGkAtoms::onmousedown && + listenerName != nsGkAtoms::onmouseup) { + continue; + } + + nsIDocument* ownerDoc = node->OwnerDoc(); + DocAccessible* document = GetExistingDocAccessible(ownerDoc); + + // Create an accessible for a inaccessible element having click event + // handler. + if (document && !document->HasAccessible(node) && + nsCoreUtils::HasClickListener(node)) { + nsIContent* parentEl = node->GetFlattenedTreeParent(); + if (parentEl) { + document->ContentInserted(parentEl, node, node->GetNextSibling()); + } + break; + } + } + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS_INHERITED(nsAccessibilityService, + DocManager, + nsIObserver, + nsIListenerChangeListener, + nsISelectionListener) // from SelectionManager + +//////////////////////////////////////////////////////////////////////////////// +// nsIObserver + +NS_IMETHODIMP +nsAccessibilityService::Observe(nsISupports *aSubject, const char *aTopic, + const char16_t *aData) +{ + if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) + Shutdown(); + + return NS_OK; +} + +void +nsAccessibilityService::NotifyOfAnchorJumpTo(nsIContent* aTargetNode) +{ + nsIDocument* documentNode = aTargetNode->GetUncomposedDoc(); + if (documentNode) { + DocAccessible* document = GetDocAccessible(documentNode); + if (document) + document->SetAnchorJump(aTargetNode); + } +} + +void +nsAccessibilityService::FireAccessibleEvent(uint32_t aEvent, + Accessible* aTarget) +{ + nsEventShell::FireEvent(aEvent, aTarget); +} + +Accessible* +nsAccessibilityService::GetRootDocumentAccessible(nsIPresShell* aPresShell, + bool aCanCreate) +{ + nsIPresShell* ps = aPresShell; + nsIDocument* documentNode = aPresShell->GetDocument(); + if (documentNode) { + nsCOMPtr<nsIDocShellTreeItem> treeItem(documentNode->GetDocShell()); + if (treeItem) { + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetRootTreeItem(getter_AddRefs(rootTreeItem)); + if (treeItem != rootTreeItem) { + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootTreeItem)); + ps = docShell->GetPresShell(); + } + + return aCanCreate ? GetDocAccessible(ps) : ps->GetDocAccessible(); + } + } + return nullptr; +} + +#ifdef XP_WIN +static StaticAutoPtr<nsTArray<nsCOMPtr<nsIContent> > > sPendingPlugins; +static StaticAutoPtr<nsTArray<nsCOMPtr<nsITimer> > > sPluginTimers; + +class PluginTimerCallBack final : public nsITimerCallback +{ + ~PluginTimerCallBack() {} + +public: + PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD Notify(nsITimer* aTimer) final + { + if (!mContent->IsInUncomposedDoc()) + return NS_OK; + + nsIPresShell* ps = mContent->OwnerDoc()->GetShell(); + if (ps) { + DocAccessible* doc = ps->GetDocAccessible(); + if (doc) { + // Make sure that if we created an accessible for the plugin that wasn't + // a plugin accessible we remove it before creating the right accessible. + doc->RecreateAccessible(mContent); + sPluginTimers->RemoveElement(aTimer); + return NS_OK; + } + } + + // We couldn't get a doc accessible so presumably the document went away. + // In this case don't leak our ref to the content or timer. + sPendingPlugins->RemoveElement(mContent); + sPluginTimers->RemoveElement(aTimer); + return NS_OK; + } + +private: + nsCOMPtr<nsIContent> mContent; +}; + +NS_IMPL_ISUPPORTS(PluginTimerCallBack, nsITimerCallback) +#endif + +already_AddRefed<Accessible> +nsAccessibilityService::CreatePluginAccessible(nsPluginFrame* aFrame, + nsIContent* aContent, + Accessible* aContext) +{ + // nsPluginFrame means a plugin, so we need to use the accessibility support + // of the plugin. + if (aFrame->GetRect().IsEmpty()) + return nullptr; + +#if defined(XP_WIN) || defined(MOZ_ACCESSIBILITY_ATK) + RefPtr<nsNPAPIPluginInstance> pluginInstance; + if (NS_SUCCEEDED(aFrame->GetPluginInstance(getter_AddRefs(pluginInstance))) && + pluginInstance) { +#ifdef XP_WIN + if (!sPendingPlugins->Contains(aContent) && + (Preferences::GetBool("accessibility.delay_plugins") || + Compatibility::IsJAWS() || Compatibility::IsWE())) { + nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID); + RefPtr<PluginTimerCallBack> cb = new PluginTimerCallBack(aContent); + timer->InitWithCallback(cb, Preferences::GetUint("accessibility.delay_plugin_time"), + nsITimer::TYPE_ONE_SHOT); + sPluginTimers->AppendElement(timer); + sPendingPlugins->AppendElement(aContent); + return nullptr; + } + + // We need to remove aContent from the pending plugins here to avoid + // reentrancy. When the timer fires it calls + // DocAccessible::ContentInserted() which does the work async. + sPendingPlugins->RemoveElement(aContent); + + // Note: pluginPort will be null if windowless. + HWND pluginPort = nullptr; + aFrame->GetPluginPort(&pluginPort); + + RefPtr<Accessible> accessible = + new HTMLWin32ObjectOwnerAccessible(aContent, aContext->Document(), + pluginPort); + return accessible.forget(); + +#elif MOZ_ACCESSIBILITY_ATK + if (!AtkSocketAccessible::gCanEmbed) + return nullptr; + + // Note this calls into the plugin, so crazy things may happen and aFrame + // may go away. + nsCString plugId; + nsresult rv = pluginInstance->GetValueFromPlugin( + NPPVpluginNativeAccessibleAtkPlugId, &plugId); + if (NS_SUCCEEDED(rv) && !plugId.IsEmpty()) { + RefPtr<AtkSocketAccessible> socketAccessible = + new AtkSocketAccessible(aContent, aContext->Document(), plugId); + + return socketAccessible.forget(); + } +#endif + } +#endif + + return nullptr; +} + +void +nsAccessibilityService::DeckPanelSwitched(nsIPresShell* aPresShell, + nsIContent* aDeckNode, + nsIFrame* aPrevBoxFrame, + nsIFrame* aCurrentBoxFrame) +{ + // Ignore tabpanels elements (a deck having an accessible) since their + // children are accessible not depending on selected tab. + DocAccessible* document = GetDocAccessible(aPresShell); + if (!document || document->HasAccessible(aDeckNode)) + return; + + if (aPrevBoxFrame) { + nsIContent* panelNode = aPrevBoxFrame->GetContent(); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "deck panel unselected"); + logging::Node("container", panelNode); + logging::Node("content", aDeckNode); + logging::MsgEnd(); + } +#endif + + document->ContentRemoved(aDeckNode, panelNode); + } + + if (aCurrentBoxFrame) { + nsIContent* panelNode = aCurrentBoxFrame->GetContent(); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "deck panel selected"); + logging::Node("container", panelNode); + logging::Node("content", aDeckNode); + logging::MsgEnd(); + } +#endif + + document->ContentInserted(aDeckNode, panelNode, panelNode->GetNextSibling()); + } +} + +void +nsAccessibilityService::ContentRangeInserted(nsIPresShell* aPresShell, + nsIContent* aContainer, + nsIContent* aStartChild, + nsIContent* aEndChild) +{ + DocAccessible* document = GetDocAccessible(aPresShell); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "content inserted; doc: %p", document); + logging::Node("container", aContainer); + for (nsIContent* child = aStartChild; child != aEndChild; + child = child->GetNextSibling()) { + logging::Node("content", child); + } + logging::MsgEnd(); + logging::Stack(); + } +#endif + + if (document) { + document->ContentInserted(aContainer, aStartChild, aEndChild); + } +} + +void +nsAccessibilityService::ContentRemoved(nsIPresShell* aPresShell, + nsIContent* aChildNode) +{ + DocAccessible* document = GetDocAccessible(aPresShell); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "content removed; doc: %p", document); + logging::Node("container node", aChildNode->GetFlattenedTreeParent()); + logging::Node("content node", aChildNode); + logging::MsgEnd(); + } +#endif + + if (document) { + // Flatten hierarchy may be broken at this point so we cannot get a true + // container by traversing up the DOM tree. Find a parent of first accessible + // from the subtree of the given DOM node, that'll be a container. If no + // accessibles in subtree then we don't care about the change. + Accessible* child = document->GetAccessible(aChildNode); + if (!child) { + Accessible* container = document->GetContainerAccessible(aChildNode); + a11y::TreeWalker walker(container ? container : document, aChildNode, + a11y::TreeWalker::eWalkCache); + child = walker.Next(); + } + + if (child) { + document->ContentRemoved(child->Parent(), aChildNode); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) + logging::AccessibleNNode("real container", child->Parent()); +#endif + } + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgEnd(); + logging::Stack(); + } +#endif +} + +void +nsAccessibilityService::UpdateText(nsIPresShell* aPresShell, + nsIContent* aContent) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) + document->UpdateText(aContent); +} + +void +nsAccessibilityService::TreeViewChanged(nsIPresShell* aPresShell, + nsIContent* aContent, + nsITreeView* aView) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) { + Accessible* accessible = document->GetAccessible(aContent); + if (accessible) { + XULTreeAccessible* treeAcc = accessible->AsXULTree(); + if (treeAcc) + treeAcc->TreeViewChanged(aView); + } + } +} + +void +nsAccessibilityService::RangeValueChanged(nsIPresShell* aPresShell, + nsIContent* aContent) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) { + Accessible* accessible = document->GetAccessible(aContent); + if (accessible) { + document->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, + accessible); + } + } +} + +void +nsAccessibilityService::UpdateListBullet(nsIPresShell* aPresShell, + nsIContent* aHTMLListItemContent, + bool aHasBullet) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) { + Accessible* accessible = document->GetAccessible(aHTMLListItemContent); + if (accessible) { + HTMLLIAccessible* listItem = accessible->AsHTMLListItem(); + if (listItem) + listItem->UpdateBullet(aHasBullet); + } + } +} + +void +nsAccessibilityService::UpdateImageMap(nsImageFrame* aImageFrame) +{ + nsIPresShell* presShell = aImageFrame->PresContext()->PresShell(); + DocAccessible* document = GetDocAccessible(presShell); + if (document) { + Accessible* accessible = + document->GetAccessible(aImageFrame->GetContent()); + if (accessible) { + HTMLImageMapAccessible* imageMap = accessible->AsImageMap(); + if (imageMap) { + imageMap->UpdateChildAreas(); + return; + } + + // If image map was initialized after we created an accessible (that'll + // be an image accessible) then recreate it. + RecreateAccessible(presShell, aImageFrame->GetContent()); + } + } +} + +void +nsAccessibilityService::UpdateLabelValue(nsIPresShell* aPresShell, + nsIContent* aLabelElm, + const nsString& aNewValue) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) { + Accessible* accessible = document->GetAccessible(aLabelElm); + if (accessible) { + XULLabelAccessible* xulLabel = accessible->AsXULLabel(); + NS_ASSERTION(xulLabel, + "UpdateLabelValue was called for wrong accessible!"); + if (xulLabel) + xulLabel->UpdateLabelValue(aNewValue); + } + } +} + +void +nsAccessibilityService::PresShellActivated(nsIPresShell* aPresShell) +{ + DocAccessible* document = aPresShell->GetDocAccessible(); + if (document) { + RootAccessible* rootDocument = document->RootAccessible(); + NS_ASSERTION(rootDocument, "Entirely broken tree: no root document!"); + if (rootDocument) + rootDocument->DocumentActivated(document); + } +} + +void +nsAccessibilityService::RecreateAccessible(nsIPresShell* aPresShell, + nsIContent* aContent) +{ + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) + document->RecreateAccessible(aContent); +} + +void +nsAccessibilityService::GetStringRole(uint32_t aRole, nsAString& aString) +{ +#define ROLE(geckoRole, stringRole, atkRole, \ + macRole, msaaRole, ia2Role, nameRule) \ + case roles::geckoRole: \ + CopyUTF8toUTF16(stringRole, aString); \ + return; + + switch (aRole) { +#include "RoleMap.h" + default: + aString.AssignLiteral("unknown"); + return; + } + +#undef ROLE +} + +void +nsAccessibilityService::GetStringStates(uint32_t aState, uint32_t aExtraState, + nsISupports **aStringStates) +{ + RefPtr<DOMStringList> stringStates = new DOMStringList(); + + uint64_t state = nsAccUtils::To64State(aState, aExtraState); + + // states + if (state & states::UNAVAILABLE) { + stringStates->Add(NS_LITERAL_STRING("unavailable")); + } + if (state & states::SELECTED) { + stringStates->Add(NS_LITERAL_STRING("selected")); + } + if (state & states::FOCUSED) { + stringStates->Add(NS_LITERAL_STRING("focused")); + } + if (state & states::PRESSED) { + stringStates->Add(NS_LITERAL_STRING("pressed")); + } + if (state & states::CHECKED) { + stringStates->Add(NS_LITERAL_STRING("checked")); + } + if (state & states::MIXED) { + stringStates->Add(NS_LITERAL_STRING("mixed")); + } + if (state & states::READONLY) { + stringStates->Add(NS_LITERAL_STRING("readonly")); + } + if (state & states::HOTTRACKED) { + stringStates->Add(NS_LITERAL_STRING("hottracked")); + } + if (state & states::DEFAULT) { + stringStates->Add(NS_LITERAL_STRING("default")); + } + if (state & states::EXPANDED) { + stringStates->Add(NS_LITERAL_STRING("expanded")); + } + if (state & states::COLLAPSED) { + stringStates->Add(NS_LITERAL_STRING("collapsed")); + } + if (state & states::BUSY) { + stringStates->Add(NS_LITERAL_STRING("busy")); + } + if (state & states::FLOATING) { + stringStates->Add(NS_LITERAL_STRING("floating")); + } + if (state & states::ANIMATED) { + stringStates->Add(NS_LITERAL_STRING("animated")); + } + if (state & states::INVISIBLE) { + stringStates->Add(NS_LITERAL_STRING("invisible")); + } + if (state & states::OFFSCREEN) { + stringStates->Add(NS_LITERAL_STRING("offscreen")); + } + if (state & states::SIZEABLE) { + stringStates->Add(NS_LITERAL_STRING("sizeable")); + } + if (state & states::MOVEABLE) { + stringStates->Add(NS_LITERAL_STRING("moveable")); + } + if (state & states::SELFVOICING) { + stringStates->Add(NS_LITERAL_STRING("selfvoicing")); + } + if (state & states::FOCUSABLE) { + stringStates->Add(NS_LITERAL_STRING("focusable")); + } + if (state & states::SELECTABLE) { + stringStates->Add(NS_LITERAL_STRING("selectable")); + } + if (state & states::LINKED) { + stringStates->Add(NS_LITERAL_STRING("linked")); + } + if (state & states::TRAVERSED) { + stringStates->Add(NS_LITERAL_STRING("traversed")); + } + if (state & states::MULTISELECTABLE) { + stringStates->Add(NS_LITERAL_STRING("multiselectable")); + } + if (state & states::EXTSELECTABLE) { + stringStates->Add(NS_LITERAL_STRING("extselectable")); + } + if (state & states::PROTECTED) { + stringStates->Add(NS_LITERAL_STRING("protected")); + } + if (state & states::HASPOPUP) { + stringStates->Add(NS_LITERAL_STRING("haspopup")); + } + if (state & states::REQUIRED) { + stringStates->Add(NS_LITERAL_STRING("required")); + } + if (state & states::ALERT) { + stringStates->Add(NS_LITERAL_STRING("alert")); + } + if (state & states::INVALID) { + stringStates->Add(NS_LITERAL_STRING("invalid")); + } + if (state & states::CHECKABLE) { + stringStates->Add(NS_LITERAL_STRING("checkable")); + } + + // extraStates + if (state & states::SUPPORTS_AUTOCOMPLETION) { + stringStates->Add(NS_LITERAL_STRING("autocompletion")); + } + if (state & states::DEFUNCT) { + stringStates->Add(NS_LITERAL_STRING("defunct")); + } + if (state & states::SELECTABLE_TEXT) { + stringStates->Add(NS_LITERAL_STRING("selectable text")); + } + if (state & states::EDITABLE) { + stringStates->Add(NS_LITERAL_STRING("editable")); + } + if (state & states::ACTIVE) { + stringStates->Add(NS_LITERAL_STRING("active")); + } + if (state & states::MODAL) { + stringStates->Add(NS_LITERAL_STRING("modal")); + } + if (state & states::MULTI_LINE) { + stringStates->Add(NS_LITERAL_STRING("multi line")); + } + if (state & states::HORIZONTAL) { + stringStates->Add(NS_LITERAL_STRING("horizontal")); + } + if (state & states::OPAQUE1) { + stringStates->Add(NS_LITERAL_STRING("opaque")); + } + if (state & states::SINGLE_LINE) { + stringStates->Add(NS_LITERAL_STRING("single line")); + } + if (state & states::TRANSIENT) { + stringStates->Add(NS_LITERAL_STRING("transient")); + } + if (state & states::VERTICAL) { + stringStates->Add(NS_LITERAL_STRING("vertical")); + } + if (state & states::STALE) { + stringStates->Add(NS_LITERAL_STRING("stale")); + } + if (state & states::ENABLED) { + stringStates->Add(NS_LITERAL_STRING("enabled")); + } + if (state & states::SENSITIVE) { + stringStates->Add(NS_LITERAL_STRING("sensitive")); + } + if (state & states::EXPANDABLE) { + stringStates->Add(NS_LITERAL_STRING("expandable")); + } + + //unknown states + if (!stringStates->Length()) { + stringStates->Add(NS_LITERAL_STRING("unknown")); + } + + stringStates.forget(aStringStates); +} + +void +nsAccessibilityService::GetStringEventType(uint32_t aEventType, + nsAString& aString) +{ + NS_ASSERTION(nsIAccessibleEvent::EVENT_LAST_ENTRY == ArrayLength(kEventTypeNames), + "nsIAccessibleEvent constants are out of sync to kEventTypeNames"); + + if (aEventType >= ArrayLength(kEventTypeNames)) { + aString.AssignLiteral("unknown"); + return; + } + + CopyUTF8toUTF16(kEventTypeNames[aEventType], aString); +} + +void +nsAccessibilityService::GetStringRelationType(uint32_t aRelationType, + nsAString& aString) +{ + NS_ENSURE_TRUE_VOID(aRelationType <= static_cast<uint32_t>(RelationType::LAST)); + +#define RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) \ + case RelationType::geckoType: \ + aString.AssignLiteral(geckoTypeName); \ + return; + + RelationType relationType = static_cast<RelationType>(aRelationType); + switch (relationType) { +#include "RelationTypeMap.h" + default: + aString.AssignLiteral("unknown"); + return; + } + +#undef RELATIONTYPE +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService public + +Accessible* +nsAccessibilityService::CreateAccessible(nsINode* aNode, + Accessible* aContext, + bool* aIsSubtreeHidden) +{ + MOZ_ASSERT(aContext, "No context provided"); + MOZ_ASSERT(aNode, "No node to create an accessible for"); + MOZ_ASSERT(gConsumers, "No creation after shutdown"); + + if (aIsSubtreeHidden) + *aIsSubtreeHidden = false; + + DocAccessible* document = aContext->Document(); + MOZ_ASSERT(!document->GetAccessible(aNode), + "We already have an accessible for this node."); + + if (aNode->IsNodeOfType(nsINode::eDOCUMENT)) { + // If it's document node then ask accessible document loader for + // document accessible, otherwise return null. + nsCOMPtr<nsIDocument> document(do_QueryInterface(aNode)); + return GetDocAccessible(document); + } + + // We have a content node. + if (!aNode->GetComposedDoc()) { + NS_WARNING("Creating accessible for node with no document"); + return nullptr; + } + + if (aNode->OwnerDoc() != document->DocumentNode()) { + NS_ERROR("Creating accessible for wrong document"); + return nullptr; + } + + if (!aNode->IsContent()) + return nullptr; + + nsIContent* content = aNode->AsContent(); + nsIFrame* frame = content->GetPrimaryFrame(); + + // Check frame and its visibility. Note, hidden frame allows visible + // elements in subtree. + if (!frame || !frame->StyleVisibility()->IsVisible()) { + if (aIsSubtreeHidden && !frame) + *aIsSubtreeHidden = true; + + return nullptr; + } + + if (frame->GetContent() != content) { + // Not the main content for this frame. This happens because <area> + // elements return the image frame as their primary frame. The main content + // for the image frame is the image content. If the frame is not an image + // frame or the node is not an area element then null is returned. + // This setup will change when bug 135040 is fixed. Make sure we don't + // create area accessible here. Hopefully assertion below will handle that. + +#ifdef DEBUG + nsImageFrame* imageFrame = do_QueryFrame(frame); + NS_ASSERTION(imageFrame && content->IsHTMLElement(nsGkAtoms::area), + "Unknown case of not main content for the frame!"); +#endif + return nullptr; + } + +#ifdef DEBUG + nsImageFrame* imageFrame = do_QueryFrame(frame); + NS_ASSERTION(!imageFrame || !content->IsHTMLElement(nsGkAtoms::area), + "Image map manages the area accessible creation!"); +#endif + + // Attempt to create an accessible based on what we know. + RefPtr<Accessible> newAcc; + + // Create accessible for visible text frames. + if (content->IsNodeOfType(nsINode::eTEXT)) { + nsIFrame::RenderedText text = frame->GetRenderedText(0, + UINT32_MAX, nsIFrame::TextOffsetType::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + // Ignore not rendered text nodes and whitespace text nodes between table + // cells. + if (text.mString.IsEmpty() || + (aContext->IsTableRow() && nsCoreUtils::IsWhitespaceString(text.mString))) { + if (aIsSubtreeHidden) + *aIsSubtreeHidden = true; + + return nullptr; + } + + newAcc = CreateAccessibleByFrameType(frame, content, aContext); + document->BindToDocument(newAcc, nullptr); + newAcc->AsTextLeaf()->SetText(text.mString); + return newAcc; + } + + if (content->IsHTMLElement(nsGkAtoms::map)) { + // Create hyper text accessible for HTML map if it is used to group links + // (see http://www.w3.org/TR/WCAG10-HTML-TECHS/#group-bypass). If the HTML + // map rect is empty then it is used for links grouping. Otherwise it should + // be used in conjunction with HTML image element and in this case we don't + // create any accessible for it and don't walk into it. The accessibles for + // HTML area (HTMLAreaAccessible) the map contains are attached as + // children of the appropriate accessible for HTML image + // (ImageAccessible). + if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, + frame->GetParent()).IsEmpty()) { + if (aIsSubtreeHidden) + *aIsSubtreeHidden = true; + + return nullptr; + } + + newAcc = new HyperTextAccessibleWrap(content, document); + document->BindToDocument(newAcc, aria::GetRoleMap(content->AsElement())); + return newAcc; + } + + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(content->AsElement()); + + // If the element is focusable or global ARIA attribute is applied to it or + // it is referenced by ARIA relationship then treat role="presentation" on + // the element as the role is not there. + if (roleMapEntry && + (roleMapEntry->Is(nsGkAtoms::presentation) || + roleMapEntry->Is(nsGkAtoms::none))) { + if (!MustBeAccessible(content, document)) + return nullptr; + + roleMapEntry = nullptr; + } + + if (!newAcc && content->IsHTMLElement()) { // HTML accessibles + bool isARIATablePart = roleMapEntry && + (roleMapEntry->accTypes & (eTableCell | eTableRow | eTable)); + + if (!isARIATablePart || + frame->AccessibleType() == eHTMLTableCellType || + frame->AccessibleType() == eHTMLTableRowType || + frame->AccessibleType() == eHTMLTableType) { + // Prefer to use markup to decide if and what kind of accessible to create, + const MarkupMapInfo* markupMap = + mMarkupMaps.Get(content->NodeInfo()->NameAtom()); + if (markupMap && markupMap->new_func) + newAcc = markupMap->new_func(content, aContext); + + if (!newAcc) // try by frame accessible type. + newAcc = CreateAccessibleByFrameType(frame, content, aContext); + } + + // In case of ARIA grid or table use table-specific classes if it's not + // native table based. + if (isARIATablePart && (!newAcc || newAcc->IsGenericHyperText())) { + if ((roleMapEntry->accTypes & eTableCell)) { + if (aContext->IsTableRow()) + newAcc = new ARIAGridCellAccessibleWrap(content, document); + + } else if (roleMapEntry->IsOfType(eTableRow)) { + if (aContext->IsTable()) + newAcc = new ARIARowAccessible(content, document); + + } else if (roleMapEntry->IsOfType(eTable)) { + newAcc = new ARIAGridAccessibleWrap(content, document); + } + } + + // If table has strong ARIA role then all table descendants shouldn't + // expose their native roles. + if (!roleMapEntry && newAcc && aContext->HasStrongARIARole()) { + if (frame->AccessibleType() == eHTMLTableRowType) { + const nsRoleMapEntry* contextRoleMap = aContext->ARIARoleMap(); + if (!contextRoleMap->IsOfType(eTable)) + roleMapEntry = &aria::gEmptyRoleMap; + + } else if (frame->AccessibleType() == eHTMLTableCellType && + aContext->ARIARoleMap() == &aria::gEmptyRoleMap) { + roleMapEntry = &aria::gEmptyRoleMap; + + } else if (content->IsAnyOfHTMLElements(nsGkAtoms::dt, + nsGkAtoms::li, + nsGkAtoms::dd) || + frame->AccessibleType() == eHTMLLiType) { + const nsRoleMapEntry* contextRoleMap = aContext->ARIARoleMap(); + if (!contextRoleMap->IsOfType(eList)) + roleMapEntry = &aria::gEmptyRoleMap; + } + } + } + + // Accessible XBL types and deck stuff are used in XUL only currently. + if (!newAcc && content->IsXULElement()) { + // No accessible for not selected deck panel and its children. + if (!aContext->IsXULTabpanels()) { + nsDeckFrame* deckFrame = do_QueryFrame(frame->GetParent()); + if (deckFrame && deckFrame->GetSelectedBox() != frame) { + if (aIsSubtreeHidden) + *aIsSubtreeHidden = true; + + return nullptr; + } + } + + // XBL bindings may use @role attribute to point the accessible type + // they belong to. + newAcc = CreateAccessibleByType(content, document); + + // Any XUL box can be used as tabpanel, make sure we create a proper + // accessible for it. + if (!newAcc && aContext->IsXULTabpanels() && + content->GetParent() == aContext->GetContent()) { + nsIAtom* frameType = frame->GetType(); + if (frameType == nsGkAtoms::boxFrame || + frameType == nsGkAtoms::scrollFrame) { + newAcc = new XULTabpanelAccessible(content, document); + } + } + } + + if (!newAcc) { + if (content->IsSVGElement()) { + nsSVGPathGeometryFrame* pathGeometryFrame = do_QueryFrame(frame); + if (pathGeometryFrame) { + // A graphic elements: rect, circle, ellipse, line, path, polygon, + // polyline and image. A 'use' and 'text' graphic elements require + // special support. + newAcc = new EnumRoleAccessible<roles::GRAPHIC>(content, document); + } else if (content->IsSVGElement(nsGkAtoms::svg)) { + newAcc = new EnumRoleAccessible<roles::DIAGRAM>(content, document); + } + + } else if (content->IsMathMLElement()) { + const MarkupMapInfo* markupMap = + mMarkupMaps.Get(content->NodeInfo()->NameAtom()); + if (markupMap && markupMap->new_func) + newAcc = markupMap->new_func(content, aContext); + + // Fall back to text when encountering Content MathML. + if (!newAcc && !content->IsAnyOfMathMLElements(nsGkAtoms::annotation_, + nsGkAtoms::annotation_xml_, + nsGkAtoms::mpadded_, + nsGkAtoms::mphantom_, + nsGkAtoms::maligngroup_, + nsGkAtoms::malignmark_, + nsGkAtoms::mspace_, + nsGkAtoms::semantics_)) { + newAcc = new HyperTextAccessible(content, document); + } + } + } + + // If no accessible, see if we need to create a generic accessible because + // of some property that makes this object interesting + // We don't do this for <body>, <html>, <window>, <dialog> etc. which + // correspond to the doc accessible and will be created in any case + if (!newAcc && !content->IsHTMLElement(nsGkAtoms::body) && + content->GetParent() && + (roleMapEntry || MustBeAccessible(content, document) || + (content->IsHTMLElement() && + nsCoreUtils::HasClickListener(content)))) { + // This content is focusable or has an interesting dynamic content accessibility property. + // If it's interesting we need it in the accessibility hierarchy so that events or + // other accessibles can point to it, or so that it can hold a state, etc. + if (content->IsHTMLElement()) { + // Interesting HTML container which may have selectable text and/or embedded objects + newAcc = new HyperTextAccessibleWrap(content, document); + } else { // XUL, SVG, MathML etc. + // Interesting generic non-HTML container + newAcc = new AccessibleWrap(content, document); + } + } + + if (newAcc) { + document->BindToDocument(newAcc, roleMapEntry); + } + return newAcc; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService private + +bool +nsAccessibilityService::Init() +{ + // Initialize accessible document manager. + if (!DocManager::Init()) + return false; + + // Add observers. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return false; + + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + + static const char16_t kInitIndicator[] = { '1', 0 }; + observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", kInitIndicator); + + // Subscribe to EventListenerService. + nsCOMPtr<nsIEventListenerService> eventListenerService = + do_GetService("@mozilla.org/eventlistenerservice;1"); + if (!eventListenerService) + return false; + + eventListenerService->AddListenerChangeListener(this); + + for (uint32_t i = 0; i < ArrayLength(sMarkupMapList); i++) + mMarkupMaps.Put(*sMarkupMapList[i].tag, &sMarkupMapList[i]); + +#ifdef A11Y_LOG + logging::CheckEnv(); +#endif + + gAccessibilityService = this; + NS_ADDREF(gAccessibilityService); // will release in Shutdown() + + if (XRE_IsParentProcess()) { + gApplicationAccessible = new ApplicationAccessibleWrap(); + } else { +#if defined(XP_WIN) + dom::ContentChild* contentChild = dom::ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + // If we were instantiated by the chrome process, GetMsaaID() will return + // a non-zero value and we may safely continue with initialization. + if (!contentChild->GetMsaaID()) { + // Since we were not instantiated by chrome, we need to synchronously + // obtain a MSAA content process id. + contentChild->SendGetA11yContentId(); + } +#endif // defined(XP_WIN) + + gApplicationAccessible = new ApplicationAccessible(); + } + + NS_ADDREF(gApplicationAccessible); // will release in Shutdown() + gApplicationAccessible->Init(); + +#ifdef MOZ_CRASHREPORTER + CrashReporter:: + AnnotateCrashReport(NS_LITERAL_CSTRING("Accessibility"), + NS_LITERAL_CSTRING("Active")); +#endif + +#ifdef XP_WIN + sPendingPlugins = new nsTArray<nsCOMPtr<nsIContent> >; + sPluginTimers = new nsTArray<nsCOMPtr<nsITimer> >; +#endif + + // Now its safe to start platform accessibility. + if (XRE_IsParentProcess()) + PlatformInit(); + + statistics::A11yInitialized(); + + return true; +} + +void +nsAccessibilityService::Shutdown() +{ + // Application is going to be closed, shutdown accessibility and mark + // accessibility service as shutdown to prevent calls of its methods. + // Don't null accessibility service static member at this point to be safe + // if someone will try to operate with it. + + MOZ_ASSERT(gConsumers, "Accessibility was shutdown already"); + + gConsumers = 0; + + // Remove observers. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + + static const char16_t kShutdownIndicator[] = { '0', 0 }; + observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", kShutdownIndicator); + } + + // Stop accessible document loader. + DocManager::Shutdown(); + + SelectionManager::Shutdown(); + +#ifdef XP_WIN + sPendingPlugins = nullptr; + + uint32_t timerCount = sPluginTimers->Length(); + for (uint32_t i = 0; i < timerCount; i++) + sPluginTimers->ElementAt(i)->Cancel(); + + sPluginTimers = nullptr; +#endif + + if (XRE_IsParentProcess()) + PlatformShutdown(); + + gApplicationAccessible->Shutdown(); + NS_RELEASE(gApplicationAccessible); + gApplicationAccessible = nullptr; + + NS_IF_RELEASE(gXPCApplicationAccessible); + gXPCApplicationAccessible = nullptr; + + NS_RELEASE(gAccessibilityService); + gAccessibilityService = nullptr; +} + +already_AddRefed<Accessible> +nsAccessibilityService::CreateAccessibleByType(nsIContent* aContent, + DocAccessible* aDoc) +{ + nsAutoString role; + nsCoreUtils::XBLBindingRole(aContent, role); + if (role.IsEmpty() || role.EqualsLiteral("none")) + return nullptr; + + if (role.EqualsLiteral("outerdoc")) { + RefPtr<Accessible> accessible = new OuterDocAccessible(aContent, aDoc); + return accessible.forget(); + } + + RefPtr<Accessible> accessible; +#ifdef MOZ_XUL + // XUL controls + if (role.EqualsLiteral("xul:alert")) { + accessible = new XULAlertAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:button")) { + accessible = new XULButtonAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:checkbox")) { + accessible = new XULCheckboxAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:colorpicker")) { + accessible = new XULColorPickerAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:colorpickertile")) { + accessible = new XULColorPickerTileAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:combobox")) { + accessible = new XULComboboxAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:tabpanels")) { + accessible = new XULTabpanelsAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:dropmarker")) { + accessible = new XULDropmarkerAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:groupbox")) { + accessible = new XULGroupboxAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:image")) { + if (aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::onclick)) { + accessible = new XULToolbarButtonAccessible(aContent, aDoc); + + } else { + // Don't include nameless images in accessible tree. + if (!aContent->HasAttr(kNameSpaceID_None, + nsGkAtoms::tooltiptext)) + return nullptr; + + accessible = new ImageAccessibleWrap(aContent, aDoc); + } + + } else if (role.EqualsLiteral("xul:link")) { + accessible = new XULLinkAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:listbox")) { + accessible = new XULListboxAccessibleWrap(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:listcell")) { + // Only create cells if there's more than one per row. + nsIContent* listItem = aContent->GetParent(); + if (!listItem) + return nullptr; + + for (nsIContent* child = listItem->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsXULElement(nsGkAtoms::listcell) && child != aContent) { + accessible = new XULListCellAccessibleWrap(aContent, aDoc); + break; + } + } + + } else if (role.EqualsLiteral("xul:listhead")) { + accessible = new XULColumAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:listheader")) { + accessible = new XULColumnItemAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:listitem")) { + accessible = new XULListitemAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:menubar")) { + accessible = new XULMenubarAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:menulist")) { + accessible = new XULComboboxAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:menuitem")) { + accessible = new XULMenuitemAccessibleWrap(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:menupopup")) { +#ifdef MOZ_ACCESSIBILITY_ATK + // ATK considers this node to be redundant when within menubars, and it makes menu + // navigation with assistive technologies more difficult + // XXX In the future we will should this for consistency across the nsIAccessible + // implementations on each platform for a consistent scripting environment, but + // then strip out redundant accessibles in the AccessibleWrap class for each platform. + nsIContent *parent = aContent->GetParent(); + if (parent && parent->IsXULElement(nsGkAtoms::menu)) + return nullptr; +#endif + + accessible = new XULMenupopupAccessible(aContent, aDoc); + + } else if(role.EqualsLiteral("xul:menuseparator")) { + accessible = new XULMenuSeparatorAccessible(aContent, aDoc); + + } else if(role.EqualsLiteral("xul:pane")) { + accessible = new EnumRoleAccessible<roles::PANE>(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:panel")) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::noautofocus, + nsGkAtoms::_true, eCaseMatters)) + accessible = new XULAlertAccessible(aContent, aDoc); + else + accessible = new EnumRoleAccessible<roles::PANE>(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:progressmeter")) { + accessible = new XULProgressMeterAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:statusbar")) { + accessible = new XULStatusBarAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:scale")) { + accessible = new XULSliderAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:radiobutton")) { + accessible = new XULRadioButtonAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:radiogroup")) { + accessible = new XULRadioGroupAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:tab")) { + accessible = new XULTabAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:tabs")) { + accessible = new XULTabsAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:text")) { + accessible = new XULLabelAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:textbox")) { + accessible = new EnumRoleAccessible<roles::SECTION>(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:thumb")) { + accessible = new XULThumbAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:tree")) { + accessible = CreateAccessibleForXULTree(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:treecolumns")) { + accessible = new XULTreeColumAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:treecolumnitem")) { + accessible = new XULColumnItemAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:toolbar")) { + accessible = new XULToolbarAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:toolbarseparator")) { + accessible = new XULToolbarSeparatorAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:tooltip")) { + accessible = new XULTooltipAccessible(aContent, aDoc); + + } else if (role.EqualsLiteral("xul:toolbarbutton")) { + accessible = new XULToolbarButtonAccessible(aContent, aDoc); + + } +#endif // MOZ_XUL + + return accessible.forget(); +} + +already_AddRefed<Accessible> +nsAccessibilityService::CreateAccessibleByFrameType(nsIFrame* aFrame, + nsIContent* aContent, + Accessible* aContext) +{ + DocAccessible* document = aContext->Document(); + + RefPtr<Accessible> newAcc; + switch (aFrame->AccessibleType()) { + case eNoType: + return nullptr; + case eHTMLBRType: + newAcc = new HTMLBRAccessible(aContent, document); + break; + case eHTMLButtonType: + newAcc = new HTMLButtonAccessible(aContent, document); + break; + case eHTMLCanvasType: + newAcc = new HTMLCanvasAccessible(aContent, document); + break; + case eHTMLCaptionType: + if (aContext->IsTable() && + aContext->GetContent() == aContent->GetParent()) { + newAcc = new HTMLCaptionAccessible(aContent, document); + } + break; + case eHTMLCheckboxType: + newAcc = new HTMLCheckboxAccessible(aContent, document); + break; + case eHTMLComboboxType: + newAcc = new HTMLComboboxAccessible(aContent, document); + break; + case eHTMLFileInputType: + newAcc = new HTMLFileInputAccessible(aContent, document); + break; + case eHTMLGroupboxType: + newAcc = new HTMLGroupboxAccessible(aContent, document); + break; + case eHTMLHRType: + newAcc = new HTMLHRAccessible(aContent, document); + break; + case eHTMLImageMapType: + newAcc = new HTMLImageMapAccessible(aContent, document); + break; + case eHTMLLiType: + if (aContext->IsList() && + aContext->GetContent() == aContent->GetParent()) { + newAcc = new HTMLLIAccessible(aContent, document); + } else { + // Otherwise create a generic text accessible to avoid text jamming. + newAcc = new HyperTextAccessibleWrap(aContent, document); + } + break; + case eHTMLSelectListType: + newAcc = new HTMLSelectListAccessible(aContent, document); + break; + case eHTMLMediaType: + newAcc = new EnumRoleAccessible<roles::GROUPING>(aContent, document); + break; + case eHTMLRadioButtonType: + newAcc = new HTMLRadioButtonAccessible(aContent, document); + break; + case eHTMLRangeType: + newAcc = new HTMLRangeAccessible(aContent, document); + break; + case eHTMLSpinnerType: + newAcc = new HTMLSpinnerAccessible(aContent, document); + break; + case eHTMLTableType: + if (aContent->IsHTMLElement(nsGkAtoms::table)) + newAcc = new HTMLTableAccessibleWrap(aContent, document); + else + newAcc = new HyperTextAccessibleWrap(aContent, document); + break; + case eHTMLTableCellType: + // Accessible HTML table cell should be a child of accessible HTML table + // or its row (CSS HTML tables are polite to the used markup at + // certain degree). + // Otherwise create a generic text accessible to avoid text jamming + // when reading by AT. + if (aContext->IsHTMLTableRow() || aContext->IsHTMLTable()) + newAcc = new HTMLTableCellAccessibleWrap(aContent, document); + else + newAcc = new HyperTextAccessibleWrap(aContent, document); + break; + + case eHTMLTableRowType: { + // Accessible HTML table row may be a child of tbody/tfoot/thead of + // accessible HTML table or a direct child of accessible of HTML table. + Accessible* table = aContext->IsTable() ? aContext : nullptr; + if (!table && aContext->Parent() && aContext->Parent()->IsTable()) + table = aContext->Parent(); + + if (table) { + nsIContent* parentContent = aContent->GetParent(); + nsIFrame* parentFrame = parentContent->GetPrimaryFrame(); + if (parentFrame->GetType() != nsGkAtoms::tableWrapperFrame) { + parentContent = parentContent->GetParent(); + parentFrame = parentContent->GetPrimaryFrame(); + } + + if (parentFrame->GetType() == nsGkAtoms::tableWrapperFrame && + table->GetContent() == parentContent) { + newAcc = new HTMLTableRowAccessible(aContent, document); + } + } + break; + } + case eHTMLTextFieldType: + newAcc = new HTMLTextFieldAccessible(aContent, document); + break; + case eHyperTextType: + if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) + newAcc = new HyperTextAccessibleWrap(aContent, document); + break; + + case eImageType: + newAcc = new ImageAccessibleWrap(aContent, document); + break; + case eOuterDocType: + newAcc = new OuterDocAccessible(aContent, document); + break; + case ePluginType: { + nsPluginFrame* pluginFrame = do_QueryFrame(aFrame); + newAcc = CreatePluginAccessible(pluginFrame, aContent, aContext); + break; + } + case eTextLeafType: + newAcc = new TextLeafAccessibleWrap(aContent, document); + break; + default: + MOZ_ASSERT(false); + break; + } + + return newAcc.forget(); +} + +void +nsAccessibilityService::MarkupAttributes(const nsIContent* aContent, + nsIPersistentProperties* aAttributes) const +{ + const mozilla::a11y::MarkupMapInfo* markupMap = + mMarkupMaps.Get(aContent->NodeInfo()->NameAtom()); + if (!markupMap) + return; + + for (uint32_t i = 0; i < ArrayLength(markupMap->attrs); i++) { + const MarkupAttrInfo* info = markupMap->attrs + i; + if (!info->name) + break; + + if (info->DOMAttrName) { + if (info->DOMAttrValue) { + if (aContent->AttrValueIs(kNameSpaceID_None, *info->DOMAttrName, + *info->DOMAttrValue, eCaseMatters)) { + nsAccUtils::SetAccAttr(aAttributes, *info->name, *info->DOMAttrValue); + } + continue; + } + + nsAutoString value; + aContent->GetAttr(kNameSpaceID_None, *info->DOMAttrName, value); + if (!value.IsEmpty()) + nsAccUtils::SetAccAttr(aAttributes, *info->name, value); + + continue; + } + + nsAccUtils::SetAccAttr(aAttributes, *info->name, *info->value); + } +} + +Accessible* +nsAccessibilityService::AddNativeRootAccessible(void* aAtkAccessible) +{ +#ifdef MOZ_ACCESSIBILITY_ATK + ApplicationAccessible* applicationAcc = ApplicationAcc(); + if (!applicationAcc) + return nullptr; + + GtkWindowAccessible* nativeWnd = + new GtkWindowAccessible(static_cast<AtkObject*>(aAtkAccessible)); + + if (applicationAcc->AppendChild(nativeWnd)) + return nativeWnd; +#endif + + return nullptr; +} + +void +nsAccessibilityService::RemoveNativeRootAccessible(Accessible* aAccessible) +{ +#ifdef MOZ_ACCESSIBILITY_ATK + ApplicationAccessible* applicationAcc = ApplicationAcc(); + + if (applicationAcc) + applicationAcc->RemoveChild(aAccessible); +#endif +} + +bool +nsAccessibilityService::HasAccessible(nsIDOMNode* aDOMNode) +{ + nsCOMPtr<nsINode> node(do_QueryInterface(aDOMNode)); + if (!node) + return false; + + DocAccessible* document = GetDocAccessible(node->OwnerDoc()); + if (!document) + return false; + + return document->HasAccessible(node); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService private (DON'T put methods here) + +#ifdef MOZ_XUL +already_AddRefed<Accessible> +nsAccessibilityService::CreateAccessibleForXULTree(nsIContent* aContent, + DocAccessible* aDoc) +{ + nsIContent* child = nsTreeUtils::GetDescendantChild(aContent, + nsGkAtoms::treechildren); + if (!child) + return nullptr; + + nsTreeBodyFrame* treeFrame = do_QueryFrame(child->GetPrimaryFrame()); + if (!treeFrame) + return nullptr; + + RefPtr<nsTreeColumns> treeCols = treeFrame->Columns(); + int32_t count = 0; + treeCols->GetCount(&count); + + // Outline of list accessible. + if (count == 1) { + RefPtr<Accessible> accessible = + new XULTreeAccessible(aContent, aDoc, treeFrame); + return accessible.forget(); + } + + // Table or tree table accessible. + RefPtr<Accessible> accessible = + new XULTreeGridAccessibleWrap(aContent, aDoc, treeFrame); + return accessible.forget(); +} +#endif + +nsAccessibilityService* +GetOrCreateAccService(uint32_t aNewConsumer) +{ + if (!nsAccessibilityService::gAccessibilityService) { + RefPtr<nsAccessibilityService> service = new nsAccessibilityService(); + if (!service->Init()) { + service->Shutdown(); + return nullptr; + } + } + + MOZ_ASSERT(nsAccessibilityService::gAccessibilityService, + "Accessible service is not initialized."); + nsAccessibilityService::gConsumers |= aNewConsumer; + return nsAccessibilityService::gAccessibilityService; +} + +void +MaybeShutdownAccService(uint32_t aFormerConsumer) +{ + nsAccessibilityService* accService = + nsAccessibilityService::gAccessibilityService; + + if (!accService || accService->IsShutdown()) { + return; + } + + if (nsCoreUtils::AccEventObserversExist() || + xpcAccessibilityService::IsInUse()) { + // Still used by XPCOM + nsAccessibilityService::gConsumers = + (nsAccessibilityService::gConsumers & ~aFormerConsumer) | + nsAccessibilityService::eXPCOM; + return; + } + + if (nsAccessibilityService::gConsumers & ~aFormerConsumer) { + nsAccessibilityService::gConsumers &= ~aFormerConsumer; + } else { + accService->Shutdown(); // Will unset all nsAccessibilityService::gConsumers + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Services +//////////////////////////////////////////////////////////////////////////////// + +namespace mozilla { +namespace a11y { + +FocusManager* +FocusMgr() +{ + return nsAccessibilityService::gAccessibilityService; +} + +SelectionManager* +SelectionMgr() +{ + return nsAccessibilityService::gAccessibilityService; +} + +ApplicationAccessible* +ApplicationAcc() +{ + return nsAccessibilityService::gApplicationAccessible; +} + +xpcAccessibleApplication* +XPCApplicationAcc() +{ + if (!nsAccessibilityService::gXPCApplicationAccessible && + nsAccessibilityService::gApplicationAccessible) { + nsAccessibilityService::gXPCApplicationAccessible = + new xpcAccessibleApplication(nsAccessibilityService::gApplicationAccessible); + NS_ADDREF(nsAccessibilityService::gXPCApplicationAccessible); + } + + return nsAccessibilityService::gXPCApplicationAccessible; +} + +EPlatformDisabledState +PlatformDisabledState() +{ + static int disabledState = 0xff; + + if (disabledState == 0xff) { + disabledState = Preferences::GetInt("accessibility.force_disabled", 0); + if (disabledState < ePlatformIsForceEnabled) + disabledState = ePlatformIsForceEnabled; + else if (disabledState > ePlatformIsDisabled) + disabledState = ePlatformIsDisabled; + } + + return (EPlatformDisabledState)disabledState; +} + +} +} diff --git a/accessible/base/nsAccessibilityService.h b/accessible/base/nsAccessibilityService.h new file mode 100644 index 000000000..562150592 --- /dev/null +++ b/accessible/base/nsAccessibilityService.h @@ -0,0 +1,442 @@ +/* -*- 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 __nsAccessibilityService_h__ +#define __nsAccessibilityService_h__ + +#include "mozilla/a11y/DocManager.h" +#include "mozilla/a11y/FocusManager.h" +#include "mozilla/a11y/Role.h" +#include "mozilla/a11y/SelectionManager.h" +#include "mozilla/Preferences.h" + +#include "nsIObserver.h" +#include "nsIAccessibleEvent.h" +#include "nsIEventListenerService.h" +#include "xpcAccessibilityService.h" + +class nsImageFrame; +class nsIArray; +class nsIPersistentProperties; +class nsPluginFrame; +class nsITreeView; + +namespace mozilla { +namespace a11y { + +class ApplicationAccessible; +class xpcAccessibleApplication; + +/** + * Return focus manager. + */ +FocusManager* FocusMgr(); + +/** + * Return selection manager. + */ +SelectionManager* SelectionMgr(); + +/** + * Returns the application accessible. + */ +ApplicationAccessible* ApplicationAcc(); +xpcAccessibleApplication* XPCApplicationAcc(); + +typedef Accessible* (New_Accessible)(nsIContent* aContent, Accessible* aContext); + +struct MarkupAttrInfo { + nsIAtom** name; + nsIAtom** value; + + nsIAtom** DOMAttrName; + nsIAtom** DOMAttrValue; +}; + +struct MarkupMapInfo { + nsIAtom** tag; + New_Accessible* new_func; + a11y::role role; + MarkupAttrInfo attrs[4]; +}; + +} // namespace a11y +} // namespace mozilla + +class nsAccessibilityService final : public mozilla::a11y::DocManager, + public mozilla::a11y::FocusManager, + public mozilla::a11y::SelectionManager, + public nsIListenerChangeListener, + public nsIObserver +{ +public: + typedef mozilla::a11y::Accessible Accessible; + typedef mozilla::a11y::DocAccessible DocAccessible; + + // nsIListenerChangeListener + NS_IMETHOD ListenersChanged(nsIArray* aEventChanges) override; + +protected: + ~nsAccessibilityService(); + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOBSERVER + + Accessible* GetRootDocumentAccessible(nsIPresShell* aPresShell, + bool aCanCreate); + already_AddRefed<Accessible> + CreatePluginAccessible(nsPluginFrame* aFrame, nsIContent* aContent, + Accessible* aContext); + + /** + * Adds/remove ATK root accessible for gtk+ native window to/from children + * of the application accessible. + */ + Accessible* AddNativeRootAccessible(void* aAtkAccessible); + void RemoveNativeRootAccessible(Accessible* aRootAccessible); + + bool HasAccessible(nsIDOMNode* aDOMNode); + + /** + * Get a string equivalent for an accessilbe role value. + */ + void GetStringRole(uint32_t aRole, nsAString& aString); + + /** + * Get a string equivalent for an accessible state/extra state. + */ + void GetStringStates(uint32_t aState, uint32_t aExtraState, + nsISupports **aStringStates); + + /** + * Get a string equivalent for an accessible event value. + */ + void GetStringEventType(uint32_t aEventType, nsAString& aString); + + /** + * Get a string equivalent for an accessible relation type. + */ + void GetStringRelationType(uint32_t aRelationType, nsAString& aString); + + // nsAccesibilityService + /** + * Notification used to update the accessible tree when deck panel is + * switched. + */ + void DeckPanelSwitched(nsIPresShell* aPresShell, nsIContent* aDeckNode, + nsIFrame* aPrevBoxFrame, nsIFrame* aCurrentBoxFrame); + + /** + * Notification used to update the accessible tree when new content is + * inserted. + */ + void ContentRangeInserted(nsIPresShell* aPresShell, nsIContent* aContainer, + nsIContent* aStartChild, nsIContent* aEndChild); + + /** + * Notification used to update the accessible tree when content is removed. + */ + void ContentRemoved(nsIPresShell* aPresShell, nsIContent* aChild); + + void UpdateText(nsIPresShell* aPresShell, nsIContent* aContent); + + /** + * Update XUL:tree accessible tree when treeview is changed. + */ + void TreeViewChanged(nsIPresShell* aPresShell, nsIContent* aContent, + nsITreeView* aView); + + /** + * Notify of input@type="element" value change. + */ + void RangeValueChanged(nsIPresShell* aPresShell, nsIContent* aContent); + + /** + * Update list bullet accessible. + */ + void UpdateListBullet(nsIPresShell* aPresShell, + nsIContent* aHTMLListItemContent, + bool aHasBullet); + + /** + * Update the image map. + */ + void UpdateImageMap(nsImageFrame* aImageFrame); + + /** + * Update the label accessible tree when rendered @value is changed. + */ + void UpdateLabelValue(nsIPresShell* aPresShell, nsIContent* aLabelElm, + const nsString& aNewValue); + + /** + * Notify accessibility that anchor jump has been accomplished to the given + * target. Used by layout. + */ + void NotifyOfAnchorJumpTo(nsIContent *aTarget); + + /** + * Notify that presshell is activated. + */ + void PresShellActivated(nsIPresShell* aPresShell); + + /** + * Recreate an accessible for the given content node in the presshell. + */ + void RecreateAccessible(nsIPresShell* aPresShell, nsIContent* aContent); + + void FireAccessibleEvent(uint32_t aEvent, Accessible* aTarget); + + // nsAccessibiltiyService + + /** + * Return true if accessibility service has been shutdown. + */ + static bool IsShutdown() + { + return gConsumers == 0; + }; + + /** + * Creates an accessible for the given DOM node. + * + * @param aNode [in] the given node + * @param aContext [in] context the accessible is created in + * @param aIsSubtreeHidden [out, optional] indicates whether the node's + * frame and its subtree is hidden + */ + Accessible* CreateAccessible(nsINode* aNode, Accessible* aContext, + bool* aIsSubtreeHidden = nullptr); + + mozilla::a11y::role MarkupRole(const nsIContent* aContent) const + { + const mozilla::a11y::MarkupMapInfo* markupMap = + mMarkupMaps.Get(aContent->NodeInfo()->NameAtom()); + return markupMap ? markupMap->role : mozilla::a11y::roles::NOTHING; + } + + /** + * Set the object attribute defined by markup for the given element. + */ + void MarkupAttributes(const nsIContent* aContent, + nsIPersistentProperties* aAttributes) const; + + /** + * A list of possible accessibility service consumers. Accessibility service + * can only be shut down when there are no remaining consumers. + * + * eXPCOM - accessibility service is used by XPCOM. + * + * eMainProcess - accessibility service was started by main process in the + * content process. + * + * ePlatformAPI - accessibility service is used by the platform api in the + * main process. + */ + enum ServiceConsumer + { + eXPCOM = 1 << 0, + eMainProcess = 1 << 1, + ePlatformAPI = 1 << 2, + }; + +private: + // nsAccessibilityService creation is controlled by friend + // GetOrCreateAccService, keep constructors private. + nsAccessibilityService(); + nsAccessibilityService(const nsAccessibilityService&); + nsAccessibilityService& operator =(const nsAccessibilityService&); + +private: + /** + * Initialize accessibility service. + */ + bool Init(); + + /** + * Shutdowns accessibility service. + */ + void Shutdown(); + + /** + * Create accessible for the element having XBL bindings. + */ + already_AddRefed<Accessible> + CreateAccessibleByType(nsIContent* aContent, DocAccessible* aDoc); + + /** + * Create an accessible whose type depends on the given frame. + */ + already_AddRefed<Accessible> + CreateAccessibleByFrameType(nsIFrame* aFrame, nsIContent* aContent, + Accessible* aContext); + +#ifdef MOZ_XUL + /** + * Create accessible for XUL tree element. + */ + already_AddRefed<Accessible> + CreateAccessibleForXULTree(nsIContent* aContent, DocAccessible* aDoc); +#endif + + /** + * Reference for accessibility service instance. + */ + static nsAccessibilityService* gAccessibilityService; + + /** + * Reference for application accessible instance. + */ + static mozilla::a11y::ApplicationAccessible* gApplicationAccessible; + static mozilla::a11y::xpcAccessibleApplication* gXPCApplicationAccessible; + + /** + * Contains a set of accessibility service consumers. + */ + static uint32_t gConsumers; + + nsDataHashtable<nsPtrHashKey<const nsIAtom>, const mozilla::a11y::MarkupMapInfo*> mMarkupMaps; + + friend nsAccessibilityService* GetAccService(); + friend nsAccessibilityService* GetOrCreateAccService(uint32_t); + friend void MaybeShutdownAccService(uint32_t); + friend mozilla::a11y::FocusManager* mozilla::a11y::FocusMgr(); + friend mozilla::a11y::SelectionManager* mozilla::a11y::SelectionMgr(); + friend mozilla::a11y::ApplicationAccessible* mozilla::a11y::ApplicationAcc(); + friend mozilla::a11y::xpcAccessibleApplication* mozilla::a11y::XPCApplicationAcc(); + friend class xpcAccessibilityService; +}; + +/** + * Return the accessibility service instance. (Handy global function) + */ +inline nsAccessibilityService* +GetAccService() +{ + return nsAccessibilityService::gAccessibilityService; +} + +/** + * Return accessibility service instance; creating one if necessary. + */ +nsAccessibilityService* GetOrCreateAccService( + uint32_t aNewConsumer = nsAccessibilityService::ePlatformAPI); + +/** + * Shutdown accessibility service if needed. + */ +void MaybeShutdownAccService(uint32_t aFormerConsumer); + +/** + * Return true if we're in a content process and not B2G. + */ +inline bool +IPCAccessibilityActive() +{ +#ifdef MOZ_B2G + return false; +#else + return XRE_IsContentProcess() && + mozilla::Preferences::GetBool("accessibility.ipc_architecture.enabled", true); +#endif +} + +/** + * Map nsIAccessibleEvents constants to strings. Used by + * nsAccessibilityService::GetStringEventType() method. + */ +static const char kEventTypeNames[][40] = { + "unknown", // + "show", // EVENT_SHOW + "hide", // EVENT_HIDE + "reorder", // EVENT_REORDER + "active decendent change", // EVENT_ACTIVE_DECENDENT_CHANGED + "focus", // EVENT_FOCUS + "state change", // EVENT_STATE_CHANGE + "location change", // EVENT_LOCATION_CHANGE + "name changed", // EVENT_NAME_CHANGE + "description change", // EVENT_DESCRIPTION_CHANGE + "value change", // EVENT_VALUE_CHANGE + "help change", // EVENT_HELP_CHANGE + "default action change", // EVENT_DEFACTION_CHANGE + "action change", // EVENT_ACTION_CHANGE + "accelerator change", // EVENT_ACCELERATOR_CHANGE + "selection", // EVENT_SELECTION + "selection add", // EVENT_SELECTION_ADD + "selection remove", // EVENT_SELECTION_REMOVE + "selection within", // EVENT_SELECTION_WITHIN + "alert", // EVENT_ALERT + "foreground", // EVENT_FOREGROUND + "menu start", // EVENT_MENU_START + "menu end", // EVENT_MENU_END + "menupopup start", // EVENT_MENUPOPUP_START + "menupopup end", // EVENT_MENUPOPUP_END + "capture start", // EVENT_CAPTURE_START + "capture end", // EVENT_CAPTURE_END + "movesize start", // EVENT_MOVESIZE_START + "movesize end", // EVENT_MOVESIZE_END + "contexthelp start", // EVENT_CONTEXTHELP_START + "contexthelp end", // EVENT_CONTEXTHELP_END + "dragdrop start", // EVENT_DRAGDROP_START + "dragdrop end", // EVENT_DRAGDROP_END + "dialog start", // EVENT_DIALOG_START + "dialog end", // EVENT_DIALOG_END + "scrolling start", // EVENT_SCROLLING_START + "scrolling end", // EVENT_SCROLLING_END + "minimize start", // EVENT_MINIMIZE_START + "minimize end", // EVENT_MINIMIZE_END + "document load complete", // EVENT_DOCUMENT_LOAD_COMPLETE + "document reload", // EVENT_DOCUMENT_RELOAD + "document load stopped", // EVENT_DOCUMENT_LOAD_STOPPED + "document attributes changed", // EVENT_DOCUMENT_ATTRIBUTES_CHANGED + "document content changed", // EVENT_DOCUMENT_CONTENT_CHANGED + "property changed", // EVENT_PROPERTY_CHANGED + "page changed", // EVENT_PAGE_CHANGED + "text attribute changed", // EVENT_TEXT_ATTRIBUTE_CHANGED + "text caret moved", // EVENT_TEXT_CARET_MOVED + "text changed", // EVENT_TEXT_CHANGED + "text inserted", // EVENT_TEXT_INSERTED + "text removed", // EVENT_TEXT_REMOVED + "text updated", // EVENT_TEXT_UPDATED + "text selection changed", // EVENT_TEXT_SELECTION_CHANGED + "visible data changed", // EVENT_VISIBLE_DATA_CHANGED + "text column changed", // EVENT_TEXT_COLUMN_CHANGED + "section changed", // EVENT_SECTION_CHANGED + "table caption changed", // EVENT_TABLE_CAPTION_CHANGED + "table model changed", // EVENT_TABLE_MODEL_CHANGED + "table summary changed", // EVENT_TABLE_SUMMARY_CHANGED + "table row description changed", // EVENT_TABLE_ROW_DESCRIPTION_CHANGED + "table row header changed", // EVENT_TABLE_ROW_HEADER_CHANGED + "table row insert", // EVENT_TABLE_ROW_INSERT + "table row delete", // EVENT_TABLE_ROW_DELETE + "table row reorder", // EVENT_TABLE_ROW_REORDER + "table column description changed", // EVENT_TABLE_COLUMN_DESCRIPTION_CHANGED + "table column header changed", // EVENT_TABLE_COLUMN_HEADER_CHANGED + "table column insert", // EVENT_TABLE_COLUMN_INSERT + "table column delete", // EVENT_TABLE_COLUMN_DELETE + "table column reorder", // EVENT_TABLE_COLUMN_REORDER + "window activate", // EVENT_WINDOW_ACTIVATE + "window create", // EVENT_WINDOW_CREATE + "window deactivate", // EVENT_WINDOW_DEACTIVATE + "window destroy", // EVENT_WINDOW_DESTROY + "window maximize", // EVENT_WINDOW_MAXIMIZE + "window minimize", // EVENT_WINDOW_MINIMIZE + "window resize", // EVENT_WINDOW_RESIZE + "window restore", // EVENT_WINDOW_RESTORE + "hyperlink end index changed", // EVENT_HYPERLINK_END_INDEX_CHANGED + "hyperlink number of anchors changed", // EVENT_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED + "hyperlink selected link changed", // EVENT_HYPERLINK_SELECTED_LINK_CHANGED + "hypertext link activated", // EVENT_HYPERTEXT_LINK_ACTIVATED + "hypertext link selected", // EVENT_HYPERTEXT_LINK_SELECTED + "hyperlink start index changed", // EVENT_HYPERLINK_START_INDEX_CHANGED + "hypertext changed", // EVENT_HYPERTEXT_CHANGED + "hypertext links count changed", // EVENT_HYPERTEXT_NLINKS_CHANGED + "object attribute changed", // EVENT_OBJECT_ATTRIBUTE_CHANGED + "virtual cursor changed", // EVENT_VIRTUALCURSOR_CHANGED + "text value change", // EVENT_TEXT_VALUE_CHANGE +}; + +#endif diff --git a/accessible/base/nsAccessiblePivot.cpp b/accessible/base/nsAccessiblePivot.cpp new file mode 100644 index 000000000..4a6b9f850 --- /dev/null +++ b/accessible/base/nsAccessiblePivot.cpp @@ -0,0 +1,924 @@ +/* -*- 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 "nsAccessiblePivot.h" + +#include "HyperTextAccessible.h" +#include "nsAccUtils.h" +#include "States.h" +#include "xpcAccessibleDocument.h" + +using namespace mozilla::a11y; + + +/** + * An object that stores a given traversal rule during the pivot movement. + */ +class RuleCache +{ +public: + explicit RuleCache(nsIAccessibleTraversalRule* aRule) : mRule(aRule), + mAcceptRoles(nullptr) { } + ~RuleCache () { + if (mAcceptRoles) + free(mAcceptRoles); + } + + nsresult ApplyFilter(Accessible* aAccessible, uint16_t* aResult); + +private: + nsCOMPtr<nsIAccessibleTraversalRule> mRule; + uint32_t* mAcceptRoles; + uint32_t mAcceptRolesLength; + uint32_t mPreFilter; +}; + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessiblePivot + +nsAccessiblePivot::nsAccessiblePivot(Accessible* aRoot) : + mRoot(aRoot), mModalRoot(nullptr), mPosition(nullptr), + mStartOffset(-1), mEndOffset(-1) +{ + NS_ASSERTION(aRoot, "A root accessible is required"); +} + +nsAccessiblePivot::~nsAccessiblePivot() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_CYCLE_COLLECTION(nsAccessiblePivot, mRoot, mPosition, mObservers) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsAccessiblePivot) + NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivot) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessiblePivot) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAccessiblePivot) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAccessiblePivot) + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessiblePivot + +NS_IMETHODIMP +nsAccessiblePivot::GetRoot(nsIAccessible** aRoot) +{ + NS_ENSURE_ARG_POINTER(aRoot); + + NS_IF_ADDREF(*aRoot = ToXPC(mRoot)); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::GetPosition(nsIAccessible** aPosition) +{ + NS_ENSURE_ARG_POINTER(aPosition); + + NS_IF_ADDREF(*aPosition = ToXPC(mPosition)); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::SetPosition(nsIAccessible* aPosition) +{ + RefPtr<Accessible> position = nullptr; + + if (aPosition) { + position = aPosition->ToInternalAccessible(); + if (!position || !IsDescendantOf(position, GetActiveRoot())) + return NS_ERROR_INVALID_ARG; + } + + // Swap old position with new position, saves us an AddRef/Release. + mPosition.swap(position); + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mStartOffset = mEndOffset = -1; + NotifyOfPivotChange(position, oldStart, oldEnd, + nsIAccessiblePivot::REASON_NONE, false); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::GetModalRoot(nsIAccessible** aModalRoot) +{ + NS_ENSURE_ARG_POINTER(aModalRoot); + + NS_IF_ADDREF(*aModalRoot = ToXPC(mModalRoot)); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::SetModalRoot(nsIAccessible* aModalRoot) +{ + Accessible* modalRoot = nullptr; + + if (aModalRoot) { + modalRoot = aModalRoot->ToInternalAccessible(); + if (!modalRoot || !IsDescendantOf(modalRoot, mRoot)) + return NS_ERROR_INVALID_ARG; + } + + mModalRoot = modalRoot; + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::GetStartOffset(int32_t* aStartOffset) +{ + NS_ENSURE_ARG_POINTER(aStartOffset); + + *aStartOffset = mStartOffset; + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::GetEndOffset(int32_t* aEndOffset) +{ + NS_ENSURE_ARG_POINTER(aEndOffset); + + *aEndOffset = mEndOffset; + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::SetTextRange(nsIAccessibleText* aTextAccessible, + int32_t aStartOffset, int32_t aEndOffset, + bool aIsFromUserInput, uint8_t aArgc) +{ + NS_ENSURE_ARG(aTextAccessible); + + // Check that start offset is smaller than end offset, and that if a value is + // smaller than 0, both should be -1. + NS_ENSURE_TRUE(aStartOffset <= aEndOffset && + (aStartOffset >= 0 || (aStartOffset != -1 && aEndOffset != -1)), + NS_ERROR_INVALID_ARG); + + nsCOMPtr<nsIAccessible> xpcAcc = do_QueryInterface(aTextAccessible); + NS_ENSURE_ARG(xpcAcc); + + RefPtr<Accessible> acc = xpcAcc->ToInternalAccessible(); + NS_ENSURE_ARG(acc); + + HyperTextAccessible* position = acc->AsHyperText(); + if (!position || !IsDescendantOf(position, GetActiveRoot())) + return NS_ERROR_INVALID_ARG; + + // Make sure the given offsets don't exceed the character count. + if (aEndOffset > static_cast<int32_t>(position->CharacterCount())) + return NS_ERROR_FAILURE; + + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mStartOffset = aStartOffset; + mEndOffset = aEndOffset; + + mPosition.swap(acc); + NotifyOfPivotChange(acc, oldStart, oldEnd, + nsIAccessiblePivot::REASON_TEXT, + (aArgc > 0) ? aIsFromUserInput : true); + + return NS_OK; +} + +// Traversal functions + +NS_IMETHODIMP +nsAccessiblePivot::MoveNext(nsIAccessibleTraversalRule* aRule, + nsIAccessible* aAnchor, bool aIncludeStart, + bool aIsFromUserInput, uint8_t aArgc, bool* aResult) +{ + NS_ENSURE_ARG(aResult); + NS_ENSURE_ARG(aRule); + *aResult = false; + + Accessible* anchor = mPosition; + if (aArgc > 0 && aAnchor) + anchor = aAnchor->ToInternalAccessible(); + + if (anchor && (anchor->IsDefunct() || !IsDescendantOf(anchor, GetActiveRoot()))) + return NS_ERROR_NOT_IN_TREE; + + nsresult rv = NS_OK; + Accessible* accessible = + SearchForward(anchor, aRule, (aArgc > 1) ? aIncludeStart : false, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (accessible) + *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_NEXT, + (aArgc > 2) ? aIsFromUserInput : true); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::MovePrevious(nsIAccessibleTraversalRule* aRule, + nsIAccessible* aAnchor, + bool aIncludeStart, bool aIsFromUserInput, + uint8_t aArgc, bool* aResult) +{ + NS_ENSURE_ARG(aResult); + NS_ENSURE_ARG(aRule); + *aResult = false; + + Accessible* anchor = mPosition; + if (aArgc > 0 && aAnchor) + anchor = aAnchor->ToInternalAccessible(); + + if (anchor && (anchor->IsDefunct() || !IsDescendantOf(anchor, GetActiveRoot()))) + return NS_ERROR_NOT_IN_TREE; + + nsresult rv = NS_OK; + Accessible* accessible = + SearchBackward(anchor, aRule, (aArgc > 1) ? aIncludeStart : false, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (accessible) + *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_PREV, + (aArgc > 2) ? aIsFromUserInput : true); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::MoveFirst(nsIAccessibleTraversalRule* aRule, + bool aIsFromUserInput, + uint8_t aArgc, bool* aResult) +{ + NS_ENSURE_ARG(aResult); + NS_ENSURE_ARG(aRule); + + Accessible* root = GetActiveRoot(); + NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE); + + nsresult rv = NS_OK; + Accessible* accessible = SearchForward(root, aRule, true, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (accessible) + *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_FIRST, + (aArgc > 0) ? aIsFromUserInput : true); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::MoveLast(nsIAccessibleTraversalRule* aRule, + bool aIsFromUserInput, + uint8_t aArgc, bool* aResult) +{ + NS_ENSURE_ARG(aResult); + NS_ENSURE_ARG(aRule); + + Accessible* root = GetActiveRoot(); + NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE); + + *aResult = false; + nsresult rv = NS_OK; + Accessible* lastAccessible = root; + Accessible* accessible = nullptr; + + // First go to the last accessible in pre-order + while (lastAccessible->HasChildren()) + lastAccessible = lastAccessible->LastChild(); + + // Search backwards from last accessible and find the last occurrence in the doc + accessible = SearchBackward(lastAccessible, aRule, true, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (accessible) + *aResult = MovePivotInternal(accessible, nsAccessiblePivot::REASON_LAST, + (aArgc > 0) ? aIsFromUserInput : true); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::MoveNextByText(TextBoundaryType aBoundary, + bool aIsFromUserInput, uint8_t aArgc, + bool* aResult) +{ + NS_ENSURE_ARG(aResult); + + *aResult = false; + + int32_t tempStart = mStartOffset, tempEnd = mEndOffset; + Accessible* tempPosition = mPosition; + Accessible* root = GetActiveRoot(); + while (true) { + Accessible* curPosition = tempPosition; + HyperTextAccessible* text = nullptr; + // Find the nearest text node using a preorder traversal starting from + // the current node. + if (!(text = tempPosition->AsHyperText())) { + text = SearchForText(tempPosition, false); + if (!text) + return NS_OK; + if (text != curPosition) + tempStart = tempEnd = -1; + tempPosition = text; + } + + // If the search led to the parent of the node we started on (e.g. when + // starting on a text leaf), start the text movement from the end of that + // node, otherwise we just default to 0. + if (tempEnd == -1) + tempEnd = text == curPosition->Parent() ? + text->GetChildOffset(curPosition) : 0; + + // If there's no more text on the current node, try to find the next text + // node; if there isn't one, bail out. + if (tempEnd == static_cast<int32_t>(text->CharacterCount())) { + if (tempPosition == root) + return NS_OK; + + // If we're currently sitting on a link, try move to either the next + // sibling or the parent, whichever is closer to the current end + // offset. Otherwise, do a forward search for the next node to land on + // (we don't do this in the first case because we don't want to go to the + // subtree). + Accessible* sibling = tempPosition->NextSibling(); + if (tempPosition->IsLink()) { + if (sibling && sibling->IsLink()) { + tempStart = tempEnd = -1; + tempPosition = sibling; + } else { + tempStart = tempPosition->StartOffset(); + tempEnd = tempPosition->EndOffset(); + tempPosition = tempPosition->Parent(); + } + } else { + tempPosition = SearchForText(tempPosition, false); + if (!tempPosition) + return NS_OK; + tempStart = tempEnd = -1; + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundary) { + case CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0, currentEnd = tempEnd; + text->TextAtOffset(tempEnd, endBoundary, &newStart, &tempEnd, unusedText); + text->TextBeforeOffset(tempEnd, startBoundary, &newStart, &newEnd, unusedText); + int32_t potentialStart = newEnd == tempEnd ? newStart : newEnd; + tempStart = potentialStart > tempStart ? potentialStart : currentEnd; + + // The offset range we've obtained might have embedded characters in it, + // limit the range to the start of the first occurrence of an embedded + // character. + Accessible* childAtOffset = nullptr; + for (int32_t i = tempStart; i < tempEnd; i++) { + childAtOffset = text->GetChildAtOffset(i); + if (childAtOffset && !childAtOffset->IsText()) { + tempEnd = i; + break; + } + } + // If there's an embedded character at the very start of the range, we + // instead want to traverse into it. So restart the movement with + // the child as the starting point. + if (childAtOffset && !childAtOffset->IsText() && + tempStart == static_cast<int32_t>(childAtOffset->StartOffset())) { + tempPosition = childAtOffset; + tempStart = tempEnd = -1; + continue; + } + + *aResult = true; + + Accessible* startPosition = mPosition; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mPosition = tempPosition; + mStartOffset = tempStart; + mEndOffset = tempEnd; + NotifyOfPivotChange(startPosition, oldStart, oldEnd, + nsIAccessiblePivot::REASON_TEXT, + (aArgc > 0) ? aIsFromUserInput : true); + return NS_OK; + } +} + +NS_IMETHODIMP +nsAccessiblePivot::MovePreviousByText(TextBoundaryType aBoundary, + bool aIsFromUserInput, uint8_t aArgc, + bool* aResult) +{ + NS_ENSURE_ARG(aResult); + + *aResult = false; + + int32_t tempStart = mStartOffset, tempEnd = mEndOffset; + Accessible* tempPosition = mPosition; + Accessible* root = GetActiveRoot(); + while (true) { + Accessible* curPosition = tempPosition; + HyperTextAccessible* text; + // Find the nearest text node using a reverse preorder traversal starting + // from the current node. + if (!(text = tempPosition->AsHyperText())) { + text = SearchForText(tempPosition, true); + if (!text) + return NS_OK; + if (text != curPosition) + tempStart = tempEnd = -1; + tempPosition = text; + } + + // If the search led to the parent of the node we started on (e.g. when + // starting on a text leaf), start the text movement from the end of that + // node, otherwise we just default to 0. + if (tempStart == -1) { + if (tempPosition != curPosition) + tempStart = text == curPosition->Parent() ? + text->GetChildOffset(curPosition) : text->CharacterCount(); + else + tempStart = 0; + } + + // If there's no more text on the current node, try to find the previous + // text node; if there isn't one, bail out. + if (tempStart == 0) { + if (tempPosition == root) + return NS_OK; + + // If we're currently sitting on a link, try move to either the previous + // sibling or the parent, whichever is closer to the current end + // offset. Otherwise, do a forward search for the next node to land on + // (we don't do this in the first case because we don't want to go to the + // subtree). + Accessible* sibling = tempPosition->PrevSibling(); + if (tempPosition->IsLink()) { + if (sibling && sibling->IsLink()) { + HyperTextAccessible* siblingText = sibling->AsHyperText(); + tempStart = tempEnd = siblingText ? + siblingText->CharacterCount() : -1; + tempPosition = sibling; + } else { + tempStart = tempPosition->StartOffset(); + tempEnd = tempPosition->EndOffset(); + tempPosition = tempPosition->Parent(); + } + } else { + HyperTextAccessible* tempText = SearchForText(tempPosition, true); + if (!tempText) + return NS_OK; + tempPosition = tempText; + tempStart = tempEnd = tempText->CharacterCount(); + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundary) { + case CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0, currentStart = tempStart, potentialEnd = 0; + text->TextBeforeOffset(tempStart, startBoundary, &newStart, &newEnd, unusedText); + if (newStart < tempStart) + tempStart = newEnd >= currentStart ? newStart : newEnd; + else // XXX: In certain odd cases newStart is equal to tempStart + text->TextBeforeOffset(tempStart - 1, startBoundary, &newStart, + &tempStart, unusedText); + text->TextAtOffset(tempStart, endBoundary, &newStart, &potentialEnd, + unusedText); + tempEnd = potentialEnd < tempEnd ? potentialEnd : currentStart; + + // The offset range we've obtained might have embedded characters in it, + // limit the range to the start of the last occurrence of an embedded + // character. + Accessible* childAtOffset = nullptr; + for (int32_t i = tempEnd - 1; i >= tempStart; i--) { + childAtOffset = text->GetChildAtOffset(i); + if (childAtOffset && !childAtOffset->IsText()) { + tempStart = childAtOffset->EndOffset(); + break; + } + } + // If there's an embedded character at the very end of the range, we + // instead want to traverse into it. So restart the movement with + // the child as the starting point. + if (childAtOffset && !childAtOffset->IsText() && + tempEnd == static_cast<int32_t>(childAtOffset->EndOffset())) { + tempPosition = childAtOffset; + tempStart = tempEnd = childAtOffset->AsHyperText()->CharacterCount(); + continue; + } + + *aResult = true; + + Accessible* startPosition = mPosition; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mPosition = tempPosition; + mStartOffset = tempStart; + mEndOffset = tempEnd; + + NotifyOfPivotChange(startPosition, oldStart, oldEnd, + nsIAccessiblePivot::REASON_TEXT, + (aArgc > 0) ? aIsFromUserInput : true); + return NS_OK; + } +} + +NS_IMETHODIMP +nsAccessiblePivot::MoveToPoint(nsIAccessibleTraversalRule* aRule, + int32_t aX, int32_t aY, bool aIgnoreNoMatch, + bool aIsFromUserInput, uint8_t aArgc, + bool* aResult) +{ + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aRule); + + *aResult = false; + + Accessible* root = GetActiveRoot(); + NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE); + + RuleCache cache(aRule); + Accessible* match = nullptr; + Accessible* child = root->ChildAtPoint(aX, aY, Accessible::eDeepestChild); + while (child && root != child) { + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + nsresult rv = cache.ApplyFilter(child, &filtered); + NS_ENSURE_SUCCESS(rv, rv); + + // Ignore any matching nodes that were below this one + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) + match = nullptr; + + // Match if no node below this is a match + if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && !match) { + nsIntRect childRect = child->Bounds(); + // Double-check child's bounds since the deepest child may have been out + // of bounds. This assures we don't return a false positive. + if (aX >= childRect.x && aX < childRect.x + childRect.width && + aY >= childRect.y && aY < childRect.y + childRect.height) + match = child; + } + + child = child->Parent(); + } + + if (match || !aIgnoreNoMatch) + *aResult = MovePivotInternal(match, nsIAccessiblePivot::REASON_POINT, + (aArgc > 0) ? aIsFromUserInput : true); + + return NS_OK; +} + +// Observer functions + +NS_IMETHODIMP +nsAccessiblePivot::AddObserver(nsIAccessiblePivotObserver* aObserver) +{ + NS_ENSURE_ARG(aObserver); + + mObservers.AppendElement(aObserver); + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::RemoveObserver(nsIAccessiblePivotObserver* aObserver) +{ + NS_ENSURE_ARG(aObserver); + + return mObservers.RemoveElement(aObserver) ? NS_OK : NS_ERROR_FAILURE; +} + +// Private utility methods + +bool +nsAccessiblePivot::IsDescendantOf(Accessible* aAccessible, Accessible* aAncestor) +{ + if (!aAncestor || aAncestor->IsDefunct()) + return false; + + // XXX Optimize with IsInDocument() when appropriate. Blocked by bug 759875. + Accessible* accessible = aAccessible; + do { + if (accessible == aAncestor) + return true; + } while ((accessible = accessible->Parent())); + + return false; +} + +bool +nsAccessiblePivot::MovePivotInternal(Accessible* aPosition, + PivotMoveReason aReason, + bool aIsFromUserInput) +{ + RefPtr<Accessible> oldPosition = mPosition.forget(); + mPosition = aPosition; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mStartOffset = mEndOffset = -1; + + return NotifyOfPivotChange(oldPosition, oldStart, oldEnd, aReason, + aIsFromUserInput); +} + +Accessible* +nsAccessiblePivot::AdjustStartPosition(Accessible* aAccessible, + RuleCache& aCache, + uint16_t* aFilterResult, + nsresult* aResult) +{ + Accessible* matched = aAccessible; + *aResult = aCache.ApplyFilter(aAccessible, aFilterResult); + + if (aAccessible != mRoot && aAccessible != mModalRoot) { + for (Accessible* temp = aAccessible->Parent(); + temp && temp != mRoot && temp != mModalRoot; temp = temp->Parent()) { + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + *aResult = aCache.ApplyFilter(temp, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + *aFilterResult = filtered; + matched = temp; + } + } + } + + if (aAccessible == mPosition && mStartOffset != -1 && mEndOffset != -1) { + HyperTextAccessible* text = aAccessible->AsHyperText(); + if (text) { + matched = text->GetChildAtOffset(mStartOffset); + } + } + + return matched; +} + +Accessible* +nsAccessiblePivot::SearchBackward(Accessible* aAccessible, + nsIAccessibleTraversalRule* aRule, + bool aSearchCurrent, + nsresult* aResult) +{ + *aResult = NS_OK; + + // Initial position could be unset, in that case return null. + if (!aAccessible) + return nullptr; + + RuleCache cache(aRule); + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + Accessible* accessible = AdjustStartPosition(aAccessible, cache, + &filtered, aResult); + NS_ENSURE_SUCCESS(*aResult, nullptr); + + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return accessible; + } + + Accessible* root = GetActiveRoot(); + while (accessible != root) { + Accessible* parent = accessible->Parent(); + int32_t idxInParent = accessible->IndexInParent(); + while (idxInParent > 0) { + if (!(accessible = parent->GetChildAt(--idxInParent))) + continue; + + *aResult = cache.ApplyFilter(accessible, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + + Accessible* lastChild = nullptr; + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + (lastChild = accessible->LastChild())) { + parent = accessible; + accessible = lastChild; + idxInParent = accessible->IndexInParent(); + *aResult = cache.ApplyFilter(accessible, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + } + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) + return accessible; + } + + if (!(accessible = parent)) + break; + + *aResult = cache.ApplyFilter(accessible, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) + return accessible; + } + + return nullptr; +} + +Accessible* +nsAccessiblePivot::SearchForward(Accessible* aAccessible, + nsIAccessibleTraversalRule* aRule, + bool aSearchCurrent, + nsresult* aResult) +{ + *aResult = NS_OK; + + // Initial position could be not set, in that case begin search from root. + Accessible* root = GetActiveRoot(); + Accessible* accessible = (!aAccessible) ? root : aAccessible; + + RuleCache cache(aRule); + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + accessible = AdjustStartPosition(accessible, cache, &filtered, aResult); + NS_ENSURE_SUCCESS(*aResult, nullptr); + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) + return accessible; + + while (true) { + Accessible* firstChild = nullptr; + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + (firstChild = accessible->FirstChild())) { + accessible = firstChild; + *aResult = cache.ApplyFilter(accessible, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) + return accessible; + } + + Accessible* sibling = nullptr; + Accessible* temp = accessible; + do { + if (temp == root) + break; + + sibling = temp->NextSibling(); + + if (sibling) + break; + } while ((temp = temp->Parent())); + + if (!sibling) + break; + + accessible = sibling; + *aResult = cache.ApplyFilter(accessible, &filtered); + NS_ENSURE_SUCCESS(*aResult, nullptr); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) + return accessible; + } + + return nullptr; +} + +HyperTextAccessible* +nsAccessiblePivot::SearchForText(Accessible* aAccessible, bool aBackward) +{ + Accessible* root = GetActiveRoot(); + Accessible* accessible = aAccessible; + while (true) { + Accessible* child = nullptr; + + while ((child = (aBackward ? accessible->LastChild() : + accessible->FirstChild()))) { + accessible = child; + if (child->IsHyperText()) + return child->AsHyperText(); + } + + Accessible* sibling = nullptr; + Accessible* temp = accessible; + do { + if (temp == root) + break; + + if (temp != aAccessible && temp->IsHyperText()) + return temp->AsHyperText(); + + sibling = aBackward ? temp->PrevSibling() : temp->NextSibling(); + + if (sibling) + break; + } while ((temp = temp->Parent())); + + if (!sibling) + break; + + accessible = sibling; + if (accessible->IsHyperText()) + return accessible->AsHyperText(); + } + + return nullptr; +} + + +bool +nsAccessiblePivot::NotifyOfPivotChange(Accessible* aOldPosition, + int32_t aOldStart, int32_t aOldEnd, + int16_t aReason, bool aIsFromUserInput) +{ + if (aOldPosition == mPosition && + aOldStart == mStartOffset && aOldEnd == mEndOffset) + return false; + + nsCOMPtr<nsIAccessible> xpcOldPos = ToXPC(aOldPosition); // death grip + nsTObserverArray<nsCOMPtr<nsIAccessiblePivotObserver> >::ForwardIterator iter(mObservers); + while (iter.HasMore()) { + nsIAccessiblePivotObserver* obs = iter.GetNext(); + obs->OnPivotChanged(this, xpcOldPos, aOldStart, aOldEnd, aReason, + aIsFromUserInput); + } + + return true; +} + +nsresult +RuleCache::ApplyFilter(Accessible* aAccessible, uint16_t* aResult) +{ + *aResult = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (!mAcceptRoles) { + nsresult rv = mRule->GetMatchRoles(&mAcceptRoles, &mAcceptRolesLength); + NS_ENSURE_SUCCESS(rv, rv); + rv = mRule->GetPreFilter(&mPreFilter); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mPreFilter) { + uint64_t state = aAccessible->State(); + + if ((nsIAccessibleTraversalRule::PREFILTER_INVISIBLE & mPreFilter) && + (state & states::INVISIBLE)) + return NS_OK; + + if ((nsIAccessibleTraversalRule::PREFILTER_OFFSCREEN & mPreFilter) && + (state & states::OFFSCREEN)) + return NS_OK; + + if ((nsIAccessibleTraversalRule::PREFILTER_NOT_FOCUSABLE & mPreFilter) && + !(state & states::FOCUSABLE)) + return NS_OK; + + if (nsIAccessibleTraversalRule::PREFILTER_ARIA_HIDDEN & mPreFilter) { + if (aAccessible->IsARIAHidden()) { + *aResult |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + return NS_OK; + } + } + + if ((nsIAccessibleTraversalRule::PREFILTER_TRANSPARENT & mPreFilter) && + !(state & states::OPAQUE1)) { + nsIFrame* frame = aAccessible->GetFrame(); + if (frame->StyleEffects()->mOpacity == 0.0f) { + *aResult |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + return NS_OK; + } + } + } + + if (mAcceptRolesLength > 0) { + uint32_t accessibleRole = aAccessible->Role(); + bool matchesRole = false; + for (uint32_t idx = 0; idx < mAcceptRolesLength; idx++) { + matchesRole = mAcceptRoles[idx] == accessibleRole; + if (matchesRole) + break; + } + if (!matchesRole) + return NS_OK; + } + + return mRule->Match(ToXPC(aAccessible), aResult); +} diff --git a/accessible/base/nsAccessiblePivot.h b/accessible/base/nsAccessiblePivot.h new file mode 100644 index 000000000..a0e409aa5 --- /dev/null +++ b/accessible/base/nsAccessiblePivot.h @@ -0,0 +1,144 @@ +/* -*- 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 _nsAccessiblePivot_H_ +#define _nsAccessiblePivot_H_ + +#include "nsIAccessiblePivot.h" + +#include "Accessible-inl.h" +#include "nsTObserverArray.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" + +class RuleCache; + +/** + * Class represents an accessible pivot. + */ +class nsAccessiblePivot final : public nsIAccessiblePivot +{ +public: + typedef mozilla::a11y::Accessible Accessible; + + explicit nsAccessiblePivot(Accessible* aRoot); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsAccessiblePivot, nsIAccessiblePivot) + + NS_DECL_NSIACCESSIBLEPIVOT + + /* + * A simple getter for the pivot's position. + */ + Accessible* Position() { return mPosition; } + +private: + ~nsAccessiblePivot(); + nsAccessiblePivot() = delete; + nsAccessiblePivot(const nsAccessiblePivot&) = delete; + void operator = (const nsAccessiblePivot&) = delete; + + /* + * Notify all observers on a pivot change. Return true if it has changed and + * observers have been notified. + */ + bool NotifyOfPivotChange(Accessible* aOldAccessible, + int32_t aOldStart, int32_t aOldEnd, + PivotMoveReason aReason, + bool aIsFromUserInput); + + /* + * Check to see that the given accessible is a descendant of given ancestor + */ + bool IsDescendantOf(Accessible* aAccessible, Accessible* aAncestor); + + + /* + * Search in preorder for the first accessible to match the rule. + */ + Accessible* SearchForward(Accessible* aAccessible, + nsIAccessibleTraversalRule* aRule, + bool aSearchCurrent, + nsresult* aResult); + + /* + * Reverse search in preorder for the first accessible to match the rule. + */ + Accessible* SearchBackward(Accessible* aAccessible, + nsIAccessibleTraversalRule* aRule, + bool aSearchCurrent, + nsresult* aResult); + + /* + * Search in preorder for the first text accessible. + */ + mozilla::a11y::HyperTextAccessible* SearchForText(Accessible* aAccessible, + bool aBackward); + + /* + * Get the effective root for this pivot, either the true root or modal root. + */ + Accessible* GetActiveRoot() const + { + if (mModalRoot) { + NS_ENSURE_FALSE(mModalRoot->IsDefunct(), mRoot); + return mModalRoot; + } + + return mRoot; + } + + /* + * Update the pivot, and notify observers. Return true if it moved. + */ + bool MovePivotInternal(Accessible* aPosition, PivotMoveReason aReason, + bool aIsFromUserInput); + + /* + * Get initial node we should start a search from with a given rule. + * + * When we do a move operation from one position to another, + * the initial position can be inside of a subtree that is ignored by + * the given rule. We need to step out of the ignored subtree and start + * the search from there. + * + */ + Accessible* AdjustStartPosition(Accessible* aAccessible, RuleCache& aCache, + uint16_t* aFilterResult, nsresult* aResult); + + /* + * The root accessible. + */ + RefPtr<Accessible> mRoot; + + /* + * The temporary modal root accessible. + */ + RefPtr<Accessible> mModalRoot; + + /* + * The current pivot position. + */ + RefPtr<Accessible> mPosition; + + /* + * The text start offset ofthe pivot. + */ + int32_t mStartOffset; + + /* + * The text end offset ofthe pivot. + */ + int32_t mEndOffset; + + /* + * The list of pivot-changed observers. + */ + nsTObserverArray<nsCOMPtr<nsIAccessiblePivotObserver> > mObservers; +}; + +#endif diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp new file mode 100644 index 000000000..89823b223 --- /dev/null +++ b/accessible/base/nsCoreUtils.cpp @@ -0,0 +1,683 @@ +/* -*- 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 "nsCoreUtils.h" + +#include "nsIAccessibleTypes.h" + +#include "nsIBaseWindow.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIDocument.h" +#include "nsIDOMHTMLDocument.h" +#include "nsIDOMHTMLElement.h" +#include "nsRange.h" +#include "nsIBoxObject.h" +#include "nsIDOMXULElement.h" +#include "nsIDocShell.h" +#include "nsIObserverService.h" +#include "nsIPresShell.h" +#include "nsPresContext.h" +#include "nsIScrollableFrame.h" +#include "nsISelectionPrivate.h" +#include "nsISelectionController.h" +#include "nsISimpleEnumerator.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TouchEvents.h" +#include "nsView.h" +#include "nsGkAtoms.h" + +#include "nsComponentManagerUtils.h" + +#include "nsITreeBoxObject.h" +#include "nsITreeColumns.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLLabelElement.h" + +using namespace mozilla; + +//////////////////////////////////////////////////////////////////////////////// +// nsCoreUtils +//////////////////////////////////////////////////////////////////////////////// + +bool +nsCoreUtils::IsLabelWithControl(nsIContent* aContent) +{ + dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromContent(aContent); + if (label && label->GetControl()) + return true; + + return false; +} + +bool +nsCoreUtils::HasClickListener(nsIContent *aContent) +{ + NS_ENSURE_TRUE(aContent, false); + EventListenerManager* listenerManager = + aContent->GetExistingListenerManager(); + + return listenerManager && + (listenerManager->HasListenersFor(nsGkAtoms::onclick) || + listenerManager->HasListenersFor(nsGkAtoms::onmousedown) || + listenerManager->HasListenersFor(nsGkAtoms::onmouseup)); +} + +void +nsCoreUtils::DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj, + int32_t aRowIndex, nsITreeColumn *aColumn, + const nsAString& aPseudoElt) +{ + nsCOMPtr<nsIDOMElement> tcElm; + aTreeBoxObj->GetTreeBody(getter_AddRefs(tcElm)); + if (!tcElm) + return; + + nsCOMPtr<nsIContent> tcContent(do_QueryInterface(tcElm)); + nsIDocument *document = tcContent->GetUncomposedDoc(); + if (!document) + return; + + nsCOMPtr<nsIPresShell> presShell = document->GetShell(); + if (!presShell) + return; + + // Ensure row is visible. + aTreeBoxObj->EnsureRowIsVisible(aRowIndex); + + // Calculate x and y coordinates. + int32_t x = 0, y = 0, width = 0, height = 0; + nsresult rv = aTreeBoxObj->GetCoordsForCellItem(aRowIndex, aColumn, + aPseudoElt, + &x, &y, &width, &height); + if (NS_FAILED(rv)) + return; + + nsCOMPtr<nsIDOMXULElement> tcXULElm(do_QueryInterface(tcElm)); + nsCOMPtr<nsIBoxObject> tcBoxObj; + tcXULElm->GetBoxObject(getter_AddRefs(tcBoxObj)); + + int32_t tcX = 0; + tcBoxObj->GetX(&tcX); + + int32_t tcY = 0; + tcBoxObj->GetY(&tcY); + + // Dispatch mouse events. + nsWeakFrame tcFrame = tcContent->GetPrimaryFrame(); + nsIFrame* rootFrame = presShell->GetRootFrame(); + + nsPoint offset; + nsIWidget *rootWidget = + rootFrame->GetView()->GetNearestWidget(&offset); + + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + + int32_t cnvdX = presContext->CSSPixelsToDevPixels(tcX + x + 1) + + presContext->AppUnitsToDevPixels(offset.x); + int32_t cnvdY = presContext->CSSPixelsToDevPixels(tcY + y + 1) + + presContext->AppUnitsToDevPixels(offset.y); + + // XUL is just desktop, so there is no real reason for senfing touch events. + DispatchMouseEvent(eMouseDown, cnvdX, cnvdY, + tcContent, tcFrame, presShell, rootWidget); + + DispatchMouseEvent(eMouseUp, cnvdX, cnvdY, + tcContent, tcFrame, presShell, rootWidget); +} + +void +nsCoreUtils::DispatchMouseEvent(EventMessage aMessage, int32_t aX, int32_t aY, + nsIContent *aContent, nsIFrame *aFrame, + nsIPresShell *aPresShell, nsIWidget *aRootWidget) +{ + WidgetMouseEvent event(true, aMessage, aRootWidget, + WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal); + + event.mRefPoint = LayoutDeviceIntPoint(aX, aY); + + event.mClickCount = 1; + event.button = WidgetMouseEvent::eLeftButton; + event.mTime = PR_IntervalNow(); + event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN; + + nsEventStatus status = nsEventStatus_eIgnore; + aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status); +} + +void +nsCoreUtils::DispatchTouchEvent(EventMessage aMessage, int32_t aX, int32_t aY, + nsIContent* aContent, nsIFrame* aFrame, + nsIPresShell* aPresShell, nsIWidget* aRootWidget) +{ + nsIDocShell* docShell = nullptr; + if (aPresShell->GetDocument()) { + docShell = aPresShell->GetDocument()->GetDocShell(); + } + if (!dom::TouchEvent::PrefEnabled(docShell)) { + return; + } + + WidgetTouchEvent event(true, aMessage, aRootWidget); + + event.mTime = PR_IntervalNow(); + + // XXX: Touch has an identifier of -1 to hint that it is synthesized. + RefPtr<dom::Touch> t = new dom::Touch(-1, LayoutDeviceIntPoint(aX, aY), + LayoutDeviceIntPoint(1, 1), 0.0f, 1.0f); + t->SetTarget(aContent); + event.mTouches.AppendElement(t); + nsEventStatus status = nsEventStatus_eIgnore; + aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status); +} + +uint32_t +nsCoreUtils::GetAccessKeyFor(nsIContent* aContent) +{ + // Accesskeys are registered by @accesskey attribute only. At first check + // whether it is presented on the given element to avoid the slow + // EventStateManager::GetRegisteredAccessKey() method. + if (!aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::accesskey)) + return 0; + + nsIPresShell* presShell = aContent->OwnerDoc()->GetShell(); + if (!presShell) + return 0; + + nsPresContext *presContext = presShell->GetPresContext(); + if (!presContext) + return 0; + + EventStateManager *esm = presContext->EventStateManager(); + if (!esm) + return 0; + + return esm->GetRegisteredAccessKey(aContent); +} + +nsIContent * +nsCoreUtils::GetDOMElementFor(nsIContent *aContent) +{ + if (aContent->IsElement()) + return aContent; + + if (aContent->IsNodeOfType(nsINode::eTEXT)) + return aContent->GetFlattenedTreeParent(); + + return nullptr; +} + +nsINode * +nsCoreUtils::GetDOMNodeFromDOMPoint(nsINode *aNode, uint32_t aOffset) +{ + if (aNode && aNode->IsElement()) { + uint32_t childCount = aNode->GetChildCount(); + NS_ASSERTION(aOffset <= childCount, "Wrong offset of the DOM point!"); + + // The offset can be after last child of container node that means DOM point + // is placed immediately after the last child. In this case use the DOM node + // from the given DOM point is used as result node. + if (aOffset != childCount) + return aNode->GetChildAt(aOffset); + } + + return aNode; +} + +bool +nsCoreUtils::IsAncestorOf(nsINode *aPossibleAncestorNode, + nsINode *aPossibleDescendantNode, + nsINode *aRootNode) +{ + NS_ENSURE_TRUE(aPossibleAncestorNode && aPossibleDescendantNode, false); + + nsINode *parentNode = aPossibleDescendantNode; + while ((parentNode = parentNode->GetParentNode()) && + parentNode != aRootNode) { + if (parentNode == aPossibleAncestorNode) + return true; + } + + return false; +} + +nsresult +nsCoreUtils::ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange, + uint32_t aScrollType) +{ + nsIPresShell::ScrollAxis vertical, horizontal; + ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal); + + return ScrollSubstringTo(aFrame, aRange, vertical, horizontal); +} + +nsresult +nsCoreUtils::ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange, + nsIPresShell::ScrollAxis aVertical, + nsIPresShell::ScrollAxis aHorizontal) +{ + if (!aFrame) + return NS_ERROR_FAILURE; + + nsPresContext *presContext = aFrame->PresContext(); + + nsCOMPtr<nsISelectionController> selCon; + aFrame->GetSelectionController(presContext, getter_AddRefs(selCon)); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + + nsCOMPtr<nsISelection> selection; + selCon->GetSelection(nsISelectionController::SELECTION_ACCESSIBILITY, + getter_AddRefs(selection)); + + nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(selection)); + selection->RemoveAllRanges(); + selection->AddRange(aRange); + + privSel->ScrollIntoViewInternal( + nsISelectionController::SELECTION_ANCHOR_REGION, + true, aVertical, aHorizontal); + + selection->CollapseToStart(); + + return NS_OK; +} + +void +nsCoreUtils::ScrollFrameToPoint(nsIFrame *aScrollableFrame, + nsIFrame *aFrame, + const nsIntPoint& aPoint) +{ + nsIScrollableFrame* scrollableFrame = do_QueryFrame(aScrollableFrame); + if (!scrollableFrame) + return; + + nsPoint point = + ToAppUnits(aPoint, aFrame->PresContext()->AppUnitsPerDevPixel()); + nsRect frameRect = aFrame->GetScreenRectInAppUnits(); + nsPoint deltaPoint(point.x - frameRect.x, point.y - frameRect.y); + + nsPoint scrollPoint = scrollableFrame->GetScrollPosition(); + scrollPoint -= deltaPoint; + + scrollableFrame->ScrollTo(scrollPoint, nsIScrollableFrame::INSTANT); +} + +void +nsCoreUtils::ConvertScrollTypeToPercents(uint32_t aScrollType, + nsIPresShell::ScrollAxis *aVertical, + nsIPresShell::ScrollAxis *aHorizontal) +{ + int16_t whereY, whereX; + nsIPresShell::WhenToScroll whenY, whenX; + switch (aScrollType) + { + case nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT: + whereY = nsIPresShell::SCROLL_TOP; + whenY = nsIPresShell::SCROLL_ALWAYS; + whereX = nsIPresShell::SCROLL_LEFT; + whenX = nsIPresShell::SCROLL_ALWAYS; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT: + whereY = nsIPresShell::SCROLL_BOTTOM; + whenY = nsIPresShell::SCROLL_ALWAYS; + whereX = nsIPresShell::SCROLL_RIGHT; + whenX = nsIPresShell::SCROLL_ALWAYS; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE: + whereY = nsIPresShell::SCROLL_TOP; + whenY = nsIPresShell::SCROLL_ALWAYS; + whereX = nsIPresShell::SCROLL_MINIMUM; + whenX = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_EDGE: + whereY = nsIPresShell::SCROLL_BOTTOM; + whenY = nsIPresShell::SCROLL_ALWAYS; + whereX = nsIPresShell::SCROLL_MINIMUM; + whenX = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_LEFT_EDGE: + whereY = nsIPresShell::SCROLL_MINIMUM; + whenY = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + whereX = nsIPresShell::SCROLL_LEFT; + whenX = nsIPresShell::SCROLL_ALWAYS; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_RIGHT_EDGE: + whereY = nsIPresShell::SCROLL_MINIMUM; + whenY = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + whereX = nsIPresShell::SCROLL_RIGHT; + whenX = nsIPresShell::SCROLL_ALWAYS; + break; + default: + whereY = nsIPresShell::SCROLL_MINIMUM; + whenY = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + whereX = nsIPresShell::SCROLL_MINIMUM; + whenX = nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE; + } + *aVertical = nsIPresShell::ScrollAxis(whereY, whenY); + *aHorizontal = nsIPresShell::ScrollAxis(whereX, whenX); +} + +nsIntPoint +nsCoreUtils::GetScreenCoordsForWindow(nsINode *aNode) +{ + nsIntPoint coords(0, 0); + nsCOMPtr<nsIDocShellTreeItem> treeItem(GetDocShellFor(aNode)); + if (!treeItem) + return coords; + + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + treeItem->GetTreeOwner(getter_AddRefs(treeOwner)); + if (!treeOwner) + return coords; + + nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(treeOwner); + if (baseWindow) + baseWindow->GetPosition(&coords.x, &coords.y); // in device pixels + + return coords; +} + +already_AddRefed<nsIDocShell> +nsCoreUtils::GetDocShellFor(nsINode *aNode) +{ + if (!aNode) + return nullptr; + + nsCOMPtr<nsIDocShell> docShell = aNode->OwnerDoc()->GetDocShell(); + return docShell.forget(); +} + +bool +nsCoreUtils::IsRootDocument(nsIDocument *aDocument) +{ + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = aDocument->GetDocShell(); + NS_ASSERTION(docShellTreeItem, "No document shell for document!"); + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + docShellTreeItem->GetParent(getter_AddRefs(parentTreeItem)); + + return !parentTreeItem; +} + +bool +nsCoreUtils::IsContentDocument(nsIDocument *aDocument) +{ + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = aDocument->GetDocShell(); + NS_ASSERTION(docShellTreeItem, "No document shell tree item for document!"); + + return (docShellTreeItem->ItemType() == nsIDocShellTreeItem::typeContent); +} + +bool +nsCoreUtils::IsTabDocument(nsIDocument* aDocumentNode) +{ + nsCOMPtr<nsIDocShellTreeItem> treeItem(aDocumentNode->GetDocShell()); + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + treeItem->GetParent(getter_AddRefs(parentTreeItem)); + + // Tab document running in own process doesn't have parent. + if (XRE_IsContentProcess()) + return !parentTreeItem; + + // Parent of docshell for tab document running in chrome process is root. + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetRootTreeItem(getter_AddRefs(rootTreeItem)); + + return parentTreeItem == rootTreeItem; +} + +bool +nsCoreUtils::IsErrorPage(nsIDocument *aDocument) +{ + nsIURI *uri = aDocument->GetDocumentURI(); + bool isAboutScheme = false; + uri->SchemeIs("about", &isAboutScheme); + if (!isAboutScheme) + return false; + + nsAutoCString path; + uri->GetPath(path); + + NS_NAMED_LITERAL_CSTRING(neterror, "neterror"); + NS_NAMED_LITERAL_CSTRING(certerror, "certerror"); + + return StringBeginsWith(path, neterror) || StringBeginsWith(path, certerror); +} + +bool +nsCoreUtils::GetID(nsIContent *aContent, nsAString& aID) +{ + return aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, aID); +} + +bool +nsCoreUtils::GetUIntAttr(nsIContent *aContent, nsIAtom *aAttr, int32_t *aUInt) +{ + nsAutoString value; + aContent->GetAttr(kNameSpaceID_None, aAttr, value); + if (!value.IsEmpty()) { + nsresult error = NS_OK; + int32_t integer = value.ToInteger(&error); + if (NS_SUCCEEDED(error) && integer > 0) { + *aUInt = integer; + return true; + } + } + + return false; +} + +void +nsCoreUtils::GetLanguageFor(nsIContent *aContent, nsIContent *aRootContent, + nsAString& aLanguage) +{ + aLanguage.Truncate(); + + nsIContent *walkUp = aContent; + while (walkUp && walkUp != aRootContent && + !walkUp->GetAttr(kNameSpaceID_None, nsGkAtoms::lang, aLanguage)) + walkUp = walkUp->GetParent(); +} + +already_AddRefed<nsIBoxObject> +nsCoreUtils::GetTreeBodyBoxObject(nsITreeBoxObject *aTreeBoxObj) +{ + nsCOMPtr<nsIDOMElement> tcElm; + aTreeBoxObj->GetTreeBody(getter_AddRefs(tcElm)); + nsCOMPtr<nsIDOMXULElement> tcXULElm(do_QueryInterface(tcElm)); + if (!tcXULElm) + return nullptr; + + nsCOMPtr<nsIBoxObject> boxObj; + tcXULElm->GetBoxObject(getter_AddRefs(boxObj)); + return boxObj.forget(); +} + +already_AddRefed<nsITreeBoxObject> +nsCoreUtils::GetTreeBoxObject(nsIContent *aContent) +{ + // Find DOMNode's parents recursively until reach the <tree> tag + nsIContent* currentContent = aContent; + while (currentContent) { + if (currentContent->NodeInfo()->Equals(nsGkAtoms::tree, + kNameSpaceID_XUL)) { + // We will get the nsITreeBoxObject from the tree node + nsCOMPtr<nsIDOMXULElement> xulElement(do_QueryInterface(currentContent)); + if (xulElement) { + nsCOMPtr<nsIBoxObject> box; + xulElement->GetBoxObject(getter_AddRefs(box)); + nsCOMPtr<nsITreeBoxObject> treeBox(do_QueryInterface(box)); + if (treeBox) + return treeBox.forget(); + } + } + currentContent = currentContent->GetFlattenedTreeParent(); + } + + return nullptr; +} + +already_AddRefed<nsITreeColumn> +nsCoreUtils::GetFirstSensibleColumn(nsITreeBoxObject *aTree) +{ + nsCOMPtr<nsITreeColumns> cols; + aTree->GetColumns(getter_AddRefs(cols)); + if (!cols) + return nullptr; + + nsCOMPtr<nsITreeColumn> column; + cols->GetFirstColumn(getter_AddRefs(column)); + if (column && IsColumnHidden(column)) + return GetNextSensibleColumn(column); + + return column.forget(); +} + +uint32_t +nsCoreUtils::GetSensibleColumnCount(nsITreeBoxObject *aTree) +{ + uint32_t count = 0; + + nsCOMPtr<nsITreeColumns> cols; + aTree->GetColumns(getter_AddRefs(cols)); + if (!cols) + return count; + + nsCOMPtr<nsITreeColumn> column; + cols->GetFirstColumn(getter_AddRefs(column)); + + while (column) { + if (!IsColumnHidden(column)) + count++; + + nsCOMPtr<nsITreeColumn> nextColumn; + column->GetNext(getter_AddRefs(nextColumn)); + column.swap(nextColumn); + } + + return count; +} + +already_AddRefed<nsITreeColumn> +nsCoreUtils::GetSensibleColumnAt(nsITreeBoxObject *aTree, uint32_t aIndex) +{ + uint32_t idx = aIndex; + + nsCOMPtr<nsITreeColumn> column = GetFirstSensibleColumn(aTree); + while (column) { + if (idx == 0) + return column.forget(); + + idx--; + column = GetNextSensibleColumn(column); + } + + return nullptr; +} + +already_AddRefed<nsITreeColumn> +nsCoreUtils::GetNextSensibleColumn(nsITreeColumn *aColumn) +{ + nsCOMPtr<nsITreeColumn> nextColumn; + aColumn->GetNext(getter_AddRefs(nextColumn)); + + while (nextColumn && IsColumnHidden(nextColumn)) { + nsCOMPtr<nsITreeColumn> tempColumn; + nextColumn->GetNext(getter_AddRefs(tempColumn)); + nextColumn.swap(tempColumn); + } + + return nextColumn.forget(); +} + +already_AddRefed<nsITreeColumn> +nsCoreUtils::GetPreviousSensibleColumn(nsITreeColumn *aColumn) +{ + nsCOMPtr<nsITreeColumn> prevColumn; + aColumn->GetPrevious(getter_AddRefs(prevColumn)); + + while (prevColumn && IsColumnHidden(prevColumn)) { + nsCOMPtr<nsITreeColumn> tempColumn; + prevColumn->GetPrevious(getter_AddRefs(tempColumn)); + prevColumn.swap(tempColumn); + } + + return prevColumn.forget(); +} + +bool +nsCoreUtils::IsColumnHidden(nsITreeColumn *aColumn) +{ + nsCOMPtr<nsIDOMElement> element; + aColumn->GetElement(getter_AddRefs(element)); + nsCOMPtr<nsIContent> content = do_QueryInterface(element); + return content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters); +} + +void +nsCoreUtils::ScrollTo(nsIPresShell* aPresShell, nsIContent* aContent, + uint32_t aScrollType) +{ + nsIPresShell::ScrollAxis vertical, horizontal; + ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal); + aPresShell->ScrollContentIntoView(aContent, vertical, horizontal, + nsIPresShell::SCROLL_OVERFLOW_HIDDEN); +} + +bool +nsCoreUtils::IsWhitespaceString(const nsSubstring& aString) +{ + nsSubstring::const_char_iterator iterBegin, iterEnd; + + aString.BeginReading(iterBegin); + aString.EndReading(iterEnd); + + while (iterBegin != iterEnd && IsWhitespace(*iterBegin)) + ++iterBegin; + + return iterBegin == iterEnd; +} + +bool +nsCoreUtils::AccEventObserversExist() +{ + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + NS_ENSURE_TRUE(obsService, false); + + nsCOMPtr<nsISimpleEnumerator> observers; + obsService->EnumerateObservers(NS_ACCESSIBLE_EVENT_TOPIC, + getter_AddRefs(observers)); + NS_ENSURE_TRUE(observers, false); + + bool hasObservers = false; + observers->HasMoreElements(&hasObservers); + + return hasObservers; +} + +void +nsCoreUtils::DispatchAccEvent(RefPtr<nsIAccessibleEvent> event) +{ + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obsService); + + obsService->NotifyObservers(event, NS_ACCESSIBLE_EVENT_TOPIC, nullptr); +} + +void +nsCoreUtils::XBLBindingRole(const nsIContent* aEl, nsAString& aRole) +{ + for (const nsXBLBinding* binding = aEl->GetXBLBinding(); binding; + binding = binding->GetBaseBinding()) { + nsIContent* bindingElm = binding->PrototypeBinding()->GetBindingElement(); + bindingElm->GetAttr(kNameSpaceID_None, nsGkAtoms::role, aRole); + if (!aRole.IsEmpty()) + break; + } +} diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h new file mode 100644 index 000000000..8d561e55b --- /dev/null +++ b/accessible/base/nsCoreUtils.h @@ -0,0 +1,329 @@ +/* -*- 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 nsCoreUtils_h_ +#define nsCoreUtils_h_ + +#include "mozilla/EventForwards.h" +#include "nsIAccessibleEvent.h" +#include "nsIContent.h" +#include "nsIDocument.h" // for GetShell() +#include "nsIPresShell.h" + +#include "nsPoint.h" +#include "nsTArray.h" + +class nsRange; +class nsIBoxObject; +class nsIFrame; +class nsIDocShell; +class nsITreeColumn; +class nsITreeBoxObject; +class nsIWidget; + +/** + * Core utils. + */ +class nsCoreUtils +{ +public: + /** + * Return true if the given node is a label of a control. + */ + static bool IsLabelWithControl(nsIContent *aContent); + + /** + * Return true if the given node has registered click, mousedown or mouseup + * event listeners. + */ + static bool HasClickListener(nsIContent *aContent); + + /** + * Dispatch click event to XUL tree cell. + * + * @param aTreeBoxObj [in] tree box object + * @param aRowIndex [in] row index + * @param aColumn [in] column object + * @param aPseudoElm [in] pseudo elemenet inside the cell, see + * nsITreeBoxObject for available values + */ + static void DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj, + int32_t aRowIndex, nsITreeColumn *aColumn, + const nsAString& aPseudoElt = EmptyString()); + + /** + * Send mouse event to the given element. + * + * @param aMessage [in] an event message (see EventForwards.h) + * @param aX [in] x coordinate in dev pixels + * @param aY [in] y coordinate in dev pixels + * @param aContent [in] the element + * @param aFrame [in] frame of the element + * @param aPresShell [in] the presshell for the element + * @param aRootWidget [in] the root widget of the element + */ + static void DispatchMouseEvent(mozilla::EventMessage aMessage, + int32_t aX, int32_t aY, + nsIContent *aContent, nsIFrame *aFrame, + nsIPresShell *aPresShell, nsIWidget *aRootWidget); + + /** + * Send a touch event with a single touch point to the given element. + * + * @param aMessage [in] an event message (see EventForwards.h) + * @param aX [in] x coordinate in dev pixels + * @param aY [in] y coordinate in dev pixels + * @param aContent [in] the element + * @param aFrame [in] frame of the element + * @param aPresShell [in] the presshell for the element + * @param aRootWidget [in] the root widget of the element + */ + static void DispatchTouchEvent(mozilla::EventMessage aMessage, + int32_t aX, int32_t aY, + nsIContent* aContent, nsIFrame* aFrame, + nsIPresShell* aPresShell, nsIWidget* aRootWidget); + + /** + * Return an accesskey registered on the given element by + * EventStateManager or 0 if there is no registered accesskey. + * + * @param aContent - the given element. + */ + static uint32_t GetAccessKeyFor(nsIContent *aContent); + + /** + * Return DOM element related with the given node, i.e. + * a) itself if it is DOM element + * b) parent element if it is text node + * c) otherwise nullptr + * + * @param aNode [in] the given DOM node + */ + static nsIContent* GetDOMElementFor(nsIContent *aContent); + + /** + * Return DOM node for the given DOM point. + */ + static nsINode *GetDOMNodeFromDOMPoint(nsINode *aNode, uint32_t aOffset); + + /** + * Is the first passed in node an ancestor of the second? + * Note: A node is not considered to be the ancestor of itself. + * + * @param aPossibleAncestorNode [in] node to test for ancestor-ness of + * aPossibleDescendantNode + * @param aPossibleDescendantNode [in] node to test for descendant-ness of + * aPossibleAncestorNode + * @param aRootNode [in, optional] the root node that search + * search should be performed within + * @return true if aPossibleAncestorNode is an ancestor of + * aPossibleDescendantNode + */ + static bool IsAncestorOf(nsINode *aPossibleAncestorNode, + nsINode *aPossibleDescendantNode, + nsINode *aRootNode = nullptr); + + /** + * Helper method to scroll range into view, used for implementation of + * nsIAccessibleText::scrollSubstringTo(). + * + * @param aFrame the frame for accessible the range belongs to. + * @param aRange the range to scroll to + * @param aScrollType the place a range should be scrolled to + */ + static nsresult ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange, + uint32_t aScrollType); + + /** Helper method to scroll range into view, used for implementation of + * nsIAccessibleText::scrollSubstringTo[Point](). + * + * @param aFrame the frame for accessible the range belongs to. + * @param aRange the range to scroll to + * @param aVertical how to align vertically, specified in percents, and when. + * @param aHorizontal how to align horizontally, specified in percents, and when. + */ + static nsresult ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange, + nsIPresShell::ScrollAxis aVertical, + nsIPresShell::ScrollAxis aHorizontal); + + /** + * Scrolls the given frame to the point, used for implememntation of + * nsIAccessible::scrollToPoint and nsIAccessibleText::scrollSubstringToPoint. + * + * @param aScrollableFrame the scrollable frame + * @param aFrame the frame to scroll + * @param aPoint the point scroll to + */ + static void ScrollFrameToPoint(nsIFrame *aScrollableFrame, + nsIFrame *aFrame, const nsIntPoint& aPoint); + + /** + * Converts scroll type constant defined in nsIAccessibleScrollType to + * vertical and horizontal parameters. + */ + static void ConvertScrollTypeToPercents(uint32_t aScrollType, + nsIPresShell::ScrollAxis *aVertical, + nsIPresShell::ScrollAxis *aHorizontal); + + /** + * Returns coordinates in device pixels relative screen for the top level + * window. + * + * @param aNode the DOM node hosted in the window. + */ + static nsIntPoint GetScreenCoordsForWindow(nsINode *aNode); + + /** + * Return document shell for the given DOM node. + */ + static already_AddRefed<nsIDocShell> GetDocShellFor(nsINode *aNode); + + /** + * Return true if the given document is root document. + */ + static bool IsRootDocument(nsIDocument *aDocument); + + /** + * Return true if the given document is content document (not chrome). + */ + static bool IsContentDocument(nsIDocument *aDocument); + + /** + * Return true if the given document node is for tab document accessible. + */ + static bool IsTabDocument(nsIDocument* aDocumentNode); + + /** + * Return true if the given document is an error page. + */ + static bool IsErrorPage(nsIDocument *aDocument); + + /** + * Return presShell for the document containing the given DOM node. + */ + static nsIPresShell *GetPresShellFor(nsINode *aNode) + { + return aNode->OwnerDoc()->GetShell(); + } + + /** + * Get the ID for an element, in some types of XML this may not be the ID attribute + * @param aContent Node to get the ID for + * @param aID Where to put ID string + * @return true if there is an ID set for this node + */ + static bool GetID(nsIContent *aContent, nsAString& aID); + + /** + * Convert attribute value of the given node to positive integer. If no + * attribute or wrong value then false is returned. + */ + static bool GetUIntAttr(nsIContent *aContent, nsIAtom *aAttr, + int32_t* aUInt); + + /** + * Returns language for the given node. + * + * @param aContent [in] the given node + * @param aRootContent [in] container of the given node + * @param aLanguage [out] language + */ + static void GetLanguageFor(nsIContent *aContent, nsIContent *aRootContent, + nsAString& aLanguage); + + /** + * Return box object for XUL treechildren element by tree box object. + */ + static already_AddRefed<nsIBoxObject> + GetTreeBodyBoxObject(nsITreeBoxObject *aTreeBoxObj); + + /** + * Return tree box object from any levels DOMNode under the XUL tree. + */ + static already_AddRefed<nsITreeBoxObject> + GetTreeBoxObject(nsIContent* aContent); + + /** + * Return first sensible column for the given tree box object. + */ + static already_AddRefed<nsITreeColumn> + GetFirstSensibleColumn(nsITreeBoxObject *aTree); + + /** + * Return sensible columns count for the given tree box object. + */ + static uint32_t GetSensibleColumnCount(nsITreeBoxObject *aTree); + + /** + * Return sensible column at the given index for the given tree box object. + */ + static already_AddRefed<nsITreeColumn> + GetSensibleColumnAt(nsITreeBoxObject *aTree, uint32_t aIndex); + + /** + * Return next sensible column for the given column. + */ + static already_AddRefed<nsITreeColumn> + GetNextSensibleColumn(nsITreeColumn *aColumn); + + /** + * Return previous sensible column for the given column. + */ + static already_AddRefed<nsITreeColumn> + GetPreviousSensibleColumn(nsITreeColumn *aColumn); + + /** + * Return true if the given column is hidden (i.e. not sensible). + */ + static bool IsColumnHidden(nsITreeColumn *aColumn); + + /** + * Scroll content into view. + */ + static void ScrollTo(nsIPresShell* aPresShell, nsIContent* aContent, + uint32_t aScrollType); + + /** + * Return true if the given node is table header element. + */ + static bool IsHTMLTableHeader(nsIContent *aContent) + { + return aContent->NodeInfo()->Equals(nsGkAtoms::th) || + aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::scope); + } + + /** + * Returns true if the given string is empty or contains whitespace symbols + * only. In contrast to nsWhitespaceTokenizer class it takes into account + * non-breaking space (0xa0). + */ + static bool IsWhitespaceString(const nsSubstring& aString); + + /** + * Returns true if the given character is whitespace symbol. + */ + static bool IsWhitespace(char16_t aChar) + { + return aChar == ' ' || aChar == '\n' || + aChar == '\r' || aChar == '\t' || aChar == 0xa0; + } + + /* + * Return true if there are any observers of accessible events. + */ + static bool AccEventObserversExist(); + + /** + * Notify accessible event observers of an event. + */ + static void DispatchAccEvent(RefPtr<nsIAccessibleEvent> aEvent); + + /** + * Return a role attribute on XBL bindings of the element. + */ + static void XBLBindingRole(const nsIContent* aEl, nsAString& aRole); +}; + +#endif diff --git a/accessible/base/nsEventShell.cpp b/accessible/base/nsEventShell.cpp new file mode 100644 index 000000000..e070acee5 --- /dev/null +++ b/accessible/base/nsEventShell.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "nsEventShell.h" + +#include "nsAccUtils.h" + +#include "mozilla/StaticPtr.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// nsEventShell +//////////////////////////////////////////////////////////////////////////////// + +void +nsEventShell::FireEvent(AccEvent* aEvent) +{ + if (!aEvent || aEvent->mEventRule == AccEvent::eDoNotEmit) + return; + + Accessible* accessible = aEvent->GetAccessible(); + NS_ENSURE_TRUE_VOID(accessible); + + nsINode* node = accessible->GetNode(); + if (node) { + sEventTargetNode = node; + sEventFromUserInput = aEvent->IsFromUserInput(); + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eEvents)) { + logging::MsgBegin("EVENTS", "events fired"); + nsAutoString type; + GetAccService()->GetStringEventType(aEvent->GetEventType(), type); + logging::MsgEntry("type: %s", NS_ConvertUTF16toUTF8(type).get()); + logging::AccessibleInfo("target", aEvent->GetAccessible()); + logging::MsgEnd(); + } +#endif + + accessible->HandleAccEvent(aEvent); + aEvent->mEventRule = AccEvent::eDoNotEmit; + + sEventTargetNode = nullptr; +} + +void +nsEventShell::FireEvent(uint32_t aEventType, Accessible* aAccessible, + EIsFromUserInput aIsFromUserInput) +{ + NS_ENSURE_TRUE_VOID(aAccessible); + + RefPtr<AccEvent> event = new AccEvent(aEventType, aAccessible, + aIsFromUserInput); + + FireEvent(event); +} + +void +nsEventShell::GetEventAttributes(nsINode *aNode, + nsIPersistentProperties *aAttributes) +{ + if (aNode != sEventTargetNode) + return; + + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::eventFromInput, + sEventFromUserInput ? NS_LITERAL_STRING("true") : + NS_LITERAL_STRING("false")); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsEventShell: private + +bool nsEventShell::sEventFromUserInput = false; +StaticRefPtr<nsINode> nsEventShell::sEventTargetNode; diff --git a/accessible/base/nsEventShell.h b/accessible/base/nsEventShell.h new file mode 100644 index 000000000..169af6dad --- /dev/null +++ b/accessible/base/nsEventShell.h @@ -0,0 +1,67 @@ +/* -*- 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 _nsEventShell_H_ +#define _nsEventShell_H_ + +#include "AccEvent.h" + +namespace mozilla { +template<typename T> class StaticRefPtr; +} +class nsIPersistentProperties; + +/** + * Used for everything about events. + */ +class nsEventShell +{ +public: + + /** + * Fire the accessible event. + */ + static void FireEvent(mozilla::a11y::AccEvent* aEvent); + + /** + * Fire accessible event of the given type for the given accessible. + * + * @param aEventType [in] the event type + * @param aAccessible [in] the event target + */ + static void FireEvent(uint32_t aEventType, + mozilla::a11y::Accessible* aAccessible, + mozilla::a11y::EIsFromUserInput aIsFromUserInput = mozilla::a11y::eAutoDetect); + + /** + * Fire state change event. + */ + static void FireEvent(mozilla::a11y::Accessible* aTarget, uint64_t aState, + bool aIsEnabled, bool aIsFromUserInput) + { + RefPtr<mozilla::a11y::AccStateChangeEvent> stateChangeEvent = + new mozilla::a11y::AccStateChangeEvent(aTarget, aState, aIsEnabled, + (aIsFromUserInput ? + mozilla::a11y::eFromUserInput : + mozilla::a11y::eNoUserInput)); + FireEvent(stateChangeEvent); + } + + /** + * Append 'event-from-input' object attribute if the accessible event has + * been fired just now for the given node. + * + * @param aNode [in] the DOM node + * @param aAttributes [in, out] the attributes + */ + static void GetEventAttributes(nsINode *aNode, + nsIPersistentProperties *aAttributes); + +private: + static mozilla::StaticRefPtr<nsINode> sEventTargetNode; + static bool sEventFromUserInput; +}; + +#endif diff --git a/accessible/base/nsTextEquivUtils.cpp b/accessible/base/nsTextEquivUtils.cpp new file mode 100644 index 000000000..bdf14d097 --- /dev/null +++ b/accessible/base/nsTextEquivUtils.cpp @@ -0,0 +1,364 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 "nsTextEquivUtils.h" + +#include "Accessible-inl.h" +#include "AccIterator.h" +#include "nsCoreUtils.h" +#include "nsIDOMXULLabeledControlEl.h" + +using namespace mozilla::a11y; + +/** + * The accessible for which we are computing a text equivalent. It is useful + * for bailing out during recursive text computation, or for special cases + * like step f. of the ARIA implementation guide. + */ +static Accessible* sInitiatorAcc = nullptr; + +//////////////////////////////////////////////////////////////////////////////// +// nsTextEquivUtils. Public. + +nsresult +nsTextEquivUtils::GetNameFromSubtree(Accessible* aAccessible, + nsAString& aName) +{ + aName.Truncate(); + + if (sInitiatorAcc) + return NS_OK; + + sInitiatorAcc = aAccessible; + if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) { + //XXX: is it necessary to care the accessible is not a document? + if (aAccessible->IsContent()) { + nsAutoString name; + AppendFromAccessibleChildren(aAccessible, &name); + name.CompressWhitespace(); + if (!nsCoreUtils::IsWhitespaceString(name)) + aName = name; + } + } + + sInitiatorAcc = nullptr; + + return NS_OK; +} + +nsresult +nsTextEquivUtils::GetTextEquivFromIDRefs(Accessible* aAccessible, + nsIAtom *aIDRefsAttr, + nsAString& aTextEquiv) +{ + aTextEquiv.Truncate(); + + nsIContent* content = aAccessible->GetContent(); + if (!content) + return NS_OK; + + nsIContent* refContent = nullptr; + IDRefsIterator iter(aAccessible->Document(), content, aIDRefsAttr); + while ((refContent = iter.NextElem())) { + if (!aTextEquiv.IsEmpty()) + aTextEquiv += ' '; + + nsresult rv = AppendTextEquivFromContent(aAccessible, refContent, + &aTextEquiv); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsTextEquivUtils::AppendTextEquivFromContent(Accessible* aInitiatorAcc, + nsIContent *aContent, + nsAString *aString) +{ + // Prevent recursion which can cause infinite loops. + if (sInitiatorAcc) + return NS_OK; + + sInitiatorAcc = aInitiatorAcc; + + // If the given content is not visible or isn't accessible then go down + // through the DOM subtree otherwise go down through accessible subtree and + // calculate the flat string. + nsIFrame *frame = aContent->GetPrimaryFrame(); + bool isVisible = frame && frame->StyleVisibility()->IsVisible(); + + nsresult rv = NS_ERROR_FAILURE; + bool goThroughDOMSubtree = true; + + if (isVisible) { + Accessible* accessible = + sInitiatorAcc->Document()->GetAccessible(aContent); + if (accessible) { + rv = AppendFromAccessible(accessible, aString); + goThroughDOMSubtree = false; + } + } + + if (goThroughDOMSubtree) + rv = AppendFromDOMNode(aContent, aString); + + sInitiatorAcc = nullptr; + return rv; +} + +nsresult +nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent *aContent, + nsAString *aString) +{ + if (aContent->IsNodeOfType(nsINode::eTEXT)) { + bool isHTMLBlock = false; + + nsIContent *parentContent = aContent->GetFlattenedTreeParent(); + if (parentContent) { + nsIFrame *frame = parentContent->GetPrimaryFrame(); + if (frame) { + // If this text is inside a block level frame (as opposed to span + // level), we need to add spaces around that block's text, so we don't + // get words jammed together in final name. + const nsStyleDisplay* display = frame->StyleDisplay(); + if (display->IsBlockOutsideStyle() || + display->mDisplay == StyleDisplay::TableCell) { + isHTMLBlock = true; + if (!aString->IsEmpty()) { + aString->Append(char16_t(' ')); + } + } + } + } + + if (aContent->TextLength() > 0) { + nsIFrame *frame = aContent->GetPrimaryFrame(); + if (frame) { + nsIFrame::RenderedText text = frame->GetRenderedText(0, + UINT32_MAX, nsIFrame::TextOffsetType::OFFSETS_IN_CONTENT_TEXT, + nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE); + aString->Append(text.mString); + } else { + // If aContent is an object that is display: none, we have no a frame. + aContent->AppendTextTo(*aString); + } + if (isHTMLBlock && !aString->IsEmpty()) { + aString->Append(char16_t(' ')); + } + } + + return NS_OK; + } + + if (aContent->IsHTMLElement() && + aContent->NodeInfo()->Equals(nsGkAtoms::br)) { + aString->AppendLiteral("\r\n"); + return NS_OK; + } + + return NS_OK_NO_NAME_CLAUSE_HANDLED; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsTextEquivUtils. Private. + +nsresult +nsTextEquivUtils::AppendFromAccessibleChildren(Accessible* aAccessible, + nsAString *aString) +{ + nsresult rv = NS_OK_NO_NAME_CLAUSE_HANDLED; + + uint32_t childCount = aAccessible->ChildCount(); + for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { + Accessible* child = aAccessible->GetChildAt(childIdx); + rv = AppendFromAccessible(child, aString); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +nsresult +nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible, + nsAString *aString) +{ + //XXX: is it necessary to care the accessible is not a document? + if (aAccessible->IsContent()) { + nsresult rv = AppendTextEquivFromTextContent(aAccessible->GetContent(), + aString); + if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) + return rv; + } + + bool isEmptyTextEquiv = true; + + // If the name is from tooltip then append it to result string in the end + // (see h. step of name computation guide). + nsAutoString text; + if (aAccessible->Name(text) != eNameFromTooltip) + isEmptyTextEquiv = !AppendString(aString, text); + + // Implementation of f. step. + nsresult rv = AppendFromValue(aAccessible, aString); + NS_ENSURE_SUCCESS(rv, rv); + + if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) + isEmptyTextEquiv = false; + + // Implementation of g) step of text equivalent computation guide. Go down + // into subtree if accessible allows "text equivalent from subtree rule" or + // it's not root and not control. + if (isEmptyTextEquiv) { + uint32_t nameRule = GetRoleRule(aAccessible->Role()); + if (nameRule & eNameFromSubtreeIfReqRule) { + rv = AppendFromAccessibleChildren(aAccessible, aString); + NS_ENSURE_SUCCESS(rv, rv); + + if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) + isEmptyTextEquiv = false; + } + } + + // Implementation of h. step + if (isEmptyTextEquiv && !text.IsEmpty()) { + AppendString(aString, text); + return NS_OK; + } + + return rv; +} + +nsresult +nsTextEquivUtils::AppendFromValue(Accessible* aAccessible, + nsAString *aString) +{ + if (GetRoleRule(aAccessible->Role()) != eNameFromValueRule) + return NS_OK_NO_NAME_CLAUSE_HANDLED; + + // Implementation of step f. of text equivalent computation. If the given + // accessible is not root accessible (the accessible the text equivalent is + // computed for in the end) then append accessible value. Otherwise append + // value if and only if the given accessible is in the middle of its parent. + + nsAutoString text; + if (aAccessible != sInitiatorAcc) { + aAccessible->Value(text); + + return AppendString(aString, text) ? + NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; + } + + //XXX: is it necessary to care the accessible is not a document? + if (aAccessible->IsDoc()) + return NS_ERROR_UNEXPECTED; + + nsIContent *content = aAccessible->GetContent(); + + for (nsIContent* childContent = content->GetPreviousSibling(); childContent; + childContent = childContent->GetPreviousSibling()) { + // check for preceding text... + if (!childContent->TextIsOnlyWhitespace()) { + for (nsIContent* siblingContent = content->GetNextSibling(); siblingContent; + siblingContent = siblingContent->GetNextSibling()) { + // .. and subsequent text + if (!siblingContent->TextIsOnlyWhitespace()) { + aAccessible->Value(text); + + return AppendString(aString, text) ? + NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; + break; + } + } + break; + } + } + + return NS_OK_NO_NAME_CLAUSE_HANDLED; +} + +nsresult +nsTextEquivUtils::AppendFromDOMChildren(nsIContent *aContent, + nsAString *aString) +{ + for (nsIContent* childContent = aContent->GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + nsresult rv = AppendFromDOMNode(childContent, aString); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsTextEquivUtils::AppendFromDOMNode(nsIContent *aContent, nsAString *aString) +{ + nsresult rv = AppendTextEquivFromTextContent(aContent, aString); + NS_ENSURE_SUCCESS(rv, rv); + + if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) + return NS_OK; + + if (aContent->IsXULElement()) { + nsAutoString textEquivalent; + nsCOMPtr<nsIDOMXULLabeledControlElement> labeledEl = + do_QueryInterface(aContent); + + if (labeledEl) { + labeledEl->GetLabel(textEquivalent); + } else { + if (aContent->NodeInfo()->Equals(nsGkAtoms::label, + kNameSpaceID_XUL)) + aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::value, + textEquivalent); + + if (textEquivalent.IsEmpty()) + aContent->GetAttr(kNameSpaceID_None, + nsGkAtoms::tooltiptext, textEquivalent); + } + + AppendString(aString, textEquivalent); + } + + return AppendFromDOMChildren(aContent, aString); +} + +bool +nsTextEquivUtils::AppendString(nsAString *aString, + const nsAString& aTextEquivalent) +{ + if (aTextEquivalent.IsEmpty()) + return false; + + // Insert spaces to insure that words from controls aren't jammed together. + if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last())) + aString->Append(char16_t(' ')); + + aString->Append(aTextEquivalent); + + if (!nsCoreUtils::IsWhitespace(aString->Last())) + aString->Append(char16_t(' ')); + + return true; +} + +uint32_t +nsTextEquivUtils::GetRoleRule(role aRole) +{ +#define ROLE(geckoRole, stringRole, atkRole, \ + macRole, msaaRole, ia2Role, nameRule) \ + case roles::geckoRole: \ + return nameRule; + + switch (aRole) { +#include "RoleMap.h" + default: + MOZ_CRASH("Unknown role."); + } + +#undef ROLE +} diff --git a/accessible/base/nsTextEquivUtils.h b/accessible/base/nsTextEquivUtils.h new file mode 100644 index 000000000..3c35d0c07 --- /dev/null +++ b/accessible/base/nsTextEquivUtils.h @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 _nsTextEquivUtils_H_ +#define _nsTextEquivUtils_H_ + +#include "Accessible.h" +#include "Role.h" + +class nsIContent; + +/** + * Text equivalent computation rules (see nsTextEquivUtils::gRoleToNameRulesMap) + */ +enum ETextEquivRule +{ + // No rule. + eNoNameRule = 0x00, + + // Walk into subtree only if the currently navigated accessible is not root + // accessible (i.e. if the accessible is part of text equivalent computation). + eNameFromSubtreeIfReqRule = 0x01, + + // Text equivalent computation from subtree is allowed. + eNameFromSubtreeRule = 0x03, + + // The accessible allows to append its value to text equivalent. + // XXX: This is temporary solution. Once we move accessible value of links + // and linkable accessibles to MSAA part we can remove this. + eNameFromValueRule = 0x04 +}; + +/** + * The class provides utils methods to compute the accessible name and + * description. + */ +class nsTextEquivUtils +{ +public: + typedef mozilla::a11y::Accessible Accessible; + + /** + * Determines if the accessible has a given name rule. + * + * @param aAccessible [in] the given accessible + * @param aRule [in] a given name rule + * @return true if the accessible has the rule + */ + static inline bool HasNameRule(Accessible* aAccessible, ETextEquivRule aRule) + { + return (GetRoleRule(aAccessible->Role()) & aRule) == aRule; + } + + /** + * Calculates the name from accessible subtree if allowed. + * + * @param aAccessible [in] the given accessible + * @param aName [out] accessible name + */ + static nsresult GetNameFromSubtree(Accessible* aAccessible, + nsAString& aName); + + /** + * Calculates text equivalent from the subtree. Similar to GetNameFromSubtree. + * However it returns not empty result for things like HTML p. + */ + static void GetTextEquivFromSubtree(Accessible* aAccessible, + nsString& aTextEquiv) + { + aTextEquiv.Truncate(); + + AppendFromAccessibleChildren(aAccessible, &aTextEquiv); + aTextEquiv.CompressWhitespace(); + } + + /** + * Calculates text equivalent for the given accessible from its IDRefs + * attribute (like aria-labelledby or aria-describedby). + * + * @param aAccessible [in] the accessible text equivalent is computed for + * @param aIDRefsAttr [in] IDRefs attribute on DOM node of the accessible + * @param aTextEquiv [out] result text equivalent + */ + static nsresult GetTextEquivFromIDRefs(Accessible* aAccessible, + nsIAtom *aIDRefsAttr, + nsAString& aTextEquiv); + + /** + * Calculates the text equivalent from the given content and its subtree if + * allowed and appends it to the given string. + * + * @param aInitiatorAcc [in] the accessible text equivalent is computed for + * in the end (root accessible of text equivalent + * calculation recursion) + * @param aContent [in] the given content the text equivalent is + * computed from + * @param aString [in, out] the string + */ + static nsresult AppendTextEquivFromContent(Accessible* aInitiatorAcc, + nsIContent *aContent, + nsAString *aString); + + /** + * Calculates the text equivalent from the given text content (may be text + * node or html:br) and appends it to the given string. + * + * @param aContent [in] the text content + * @param aString [in, out] the string + */ + static nsresult AppendTextEquivFromTextContent(nsIContent *aContent, + nsAString *aString); + +private: + /** + * Iterates accessible children and calculates text equivalent from each + * child. + */ + static nsresult AppendFromAccessibleChildren(Accessible* aAccessible, + nsAString *aString); + + /** + * Calculates text equivalent from the given accessible and its subtree if + * allowed. + */ + static nsresult AppendFromAccessible(Accessible* aAccessible, + nsAString *aString); + + /** + * Calculates text equivalent from the value of given accessible. + */ + static nsresult AppendFromValue(Accessible* aAccessible, + nsAString *aString); + /** + * Iterates DOM children and calculates text equivalent from each child node. + */ + static nsresult AppendFromDOMChildren(nsIContent *aContent, + nsAString *aString); + + /** + * Calculates text equivalent from the given DOM node and its subtree if + * allowed. + */ + static nsresult AppendFromDOMNode(nsIContent *aContent, nsAString *aString); + + /** + * Concatenates strings and appends space between them. Returns true if + * text equivalent string was appended. + */ + static bool AppendString(nsAString *aString, + const nsAString& aTextEquivalent); + + /** + * Returns the rule (constant of ETextEquivRule) for a given role. + */ + static uint32_t GetRoleRule(mozilla::a11y::roles::Role aRole); +}; + +#endif |