diff options
Diffstat (limited to 'accessible/base')
70 files changed, 23902 insertions, 0 deletions
diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp new file mode 100644 index 0000000000..113460269e --- /dev/null +++ b/accessible/base/ARIAMap.cpp @@ -0,0 +1,1516 @@ +/* -*- 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[] = { + // clang-format off + { // alert + nsGkAtoms::alert, + roles::ALERT, + kUseMapRole, + eNoValue, + eNoAction, +#if defined(XP_MACOSX) + eAssertiveLiveAttr, +#else + eNoLiveAttr, +#endif + 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::ARTICLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // banner + nsGkAtoms::banner, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // blockquote + nsGkAtoms::blockquote, + roles::BLOCKQUOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // button + nsGkAtoms::button, + roles::PUSHBUTTON, + kUseMapRole, + eNoValue, + ePressAction, + eNoLiveAttr, + eButton, + kNoReqStates + // eARIAPressed is auto applied on any button + }, + { // caption + nsGkAtoms::caption, + roles::CAPTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // cell + nsGkAtoms::cell, + roles::CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates + }, + { // checkbox + nsGkAtoms::checkbox, + roles::CHECKBUTTON, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed, + eARIAReadonly + }, + { // code + nsGkAtoms::code, + roles::CODE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // columnheader + nsGkAtoms::columnheader, + roles::COLUMNHEADER, + kUseMapRole, + eNoValue, + eSortAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectableIfDefined, + eARIAReadonly + }, + { // combobox, which consists of text input and popup + nsGkAtoms::combobox, + roles::EDITCOMBOBOX, + kUseMapRole, + eNoValue, + eOpenCloseAction, + eNoLiveAttr, + eCombobox, + states::COLLAPSED | states::HASPOPUP, + eARIAAutoComplete, + eARIAReadonly, + eARIAOrientation + }, + { // comment + nsGkAtoms::comment, + roles::COMMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // complementary + nsGkAtoms::complementary, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // contentinfo + nsGkAtoms::contentinfo, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // deletion + nsGkAtoms::deletion, + roles::CONTENT_DELETION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // dialog + nsGkAtoms::dialog, + roles::DIALOG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // directory + nsGkAtoms::directory, + roles::LIST, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eList, + states::READONLY + }, + { // doc-abstract + nsGkAtoms::docAbstract, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-acknowledgments + nsGkAtoms::docAcknowledgments, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-afterword + nsGkAtoms::docAfterword, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-appendix + nsGkAtoms::docAppendix, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-backlink + nsGkAtoms::docBacklink, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-biblioentry + nsGkAtoms::docBiblioentry, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // doc-bibliography + nsGkAtoms::docBibliography, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-biblioref + nsGkAtoms::docBiblioref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-chapter + nsGkAtoms::docChapter, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-colophon + nsGkAtoms::docColophon, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-conclusion + nsGkAtoms::docConclusion, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-cover + nsGkAtoms::docCover, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-credit + nsGkAtoms::docCredit, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-credits + nsGkAtoms::docCredits, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-dedication + nsGkAtoms::docDedication, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-endnote + nsGkAtoms::docEndnote, + roles::LISTITEM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // doc-endnotes + nsGkAtoms::docEndnotes, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-epigraph + nsGkAtoms::docEpigraph, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-epilogue + nsGkAtoms::docEpilogue, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-errata + nsGkAtoms::docErrata, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-example + nsGkAtoms::docExample, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-footnote + nsGkAtoms::docFootnote, + roles::FOOTNOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-foreword + nsGkAtoms::docForeword, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-glossary + nsGkAtoms::docGlossary, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-glossref + nsGkAtoms::docGlossref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-index + nsGkAtoms::docIndex, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-introduction + nsGkAtoms::docIntroduction, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-noteref + nsGkAtoms::docNoteref, + roles::LINK, + kUseMapRole, + eNoValue, + eJumpAction, + eNoLiveAttr, + kGenericAccType, + states::LINKED + }, + { // doc-notice + nsGkAtoms::docNotice, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-pagebreak + nsGkAtoms::docPagebreak, + roles::SEPARATOR, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-pagelist + nsGkAtoms::docPagelist, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-part + nsGkAtoms::docPart, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-preface + nsGkAtoms::docPreface, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-prologue + nsGkAtoms::docPrologue, + roles::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // doc-pullquote + nsGkAtoms::docPullquote, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-qna + nsGkAtoms::docQna, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-subtitle + nsGkAtoms::docSubtitle, + roles::HEADING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-tip + nsGkAtoms::docTip, + roles::NOTE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // doc-toc + nsGkAtoms::docToc, + roles::NAVIGATION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // document + nsGkAtoms::document, + roles::NON_NATIVE_DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // feed + nsGkAtoms::feed, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // figure + nsGkAtoms::figure, + roles::FIGURE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // form + nsGkAtoms::form, + roles::FORM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // graphics-document + nsGkAtoms::graphicsDocument, + roles::NON_NATIVE_DOCUMENT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eReadonlyUntilEditable + }, + { // graphics-object + nsGkAtoms::graphicsObject, + roles::GROUPING, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // graphics-symbol + nsGkAtoms::graphicsSymbol, + roles::GRAPHIC, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // grid + nsGkAtoms::grid, + roles::TABLE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eSelect | eTable, + kNoReqStates, + eARIAMultiSelectable, + eARIAReadonly, + eFocusableUntilDisabled + }, + { // gridcell + nsGkAtoms::gridcell, + roles::GRID_CELL, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eTableCell, + kNoReqStates, + eARIASelectable, + eARIAReadonly + }, + { // 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 + }, + { // insertion + nsGkAtoms::insertion, + roles::CONTENT_INSERTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // 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::LANDMARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + kNoReqStates + }, + { // mark + nsGkAtoms::mark, + roles::MARK, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // 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 + }, + { // menuitemcheckbox + nsGkAtoms::menuitemcheckbox, + roles::CHECK_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableMixed, + eARIAReadonly + }, + { // menuitemradio + nsGkAtoms::menuitemradio, + roles::RADIO_MENU_ITEM, + kUseMapRole, + eNoValue, + eClickAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool, + eARIAReadonly + }, + { // navigation + nsGkAtoms::navigation, + roles::LANDMARK, + kUseMapRole, + 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 + }, + { // paragraph + nsGkAtoms::paragraph, + roles::PARAGRAPH, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // 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, + eARIAReadonly + }, + { // region + nsGkAtoms::region, + roles::REGION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + eLandmark, + 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, + eARIAReadonly + }, + { // scrollbar + nsGkAtoms::scrollbar, + roles::SCROLLBAR, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::VERTICAL, + eARIAOrientation, + eARIAReadonly + }, + { // search + nsGkAtoms::search, + roles::LANDMARK, + kUseMapRole, + 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, + eHasValueMinMaxIfFocusable, + 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 + }, + { // suggestion + nsGkAtoms::suggestion, + roles::SUGGESTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // switch + nsGkAtoms::svgSwitch, + roles::SWITCH, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool, + eARIAReadonly + }, + { // 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, + eARIAMultiSelectable + }, + { // tabpanel + nsGkAtoms::tabpanel, + roles::PROPERTYPAGE, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // term + nsGkAtoms::term, + roles::TERM, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // 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, + kNoReqStates, + eARIAReadonly, + 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 + } + // clang-format on +}; + +static const nsRoleMapEntry sLandmarkRoleMap = { + nsGkAtoms::_empty, roles::NOTHING, kUseNativeRole, eNoValue, + eNoAction, eNoLiveAttr, kGenericAccType, kNoReqStates}; + +nsRoleMapEntry aria::gEmptyRoleMap = { + nsGkAtoms::_empty, roles::TEXT_CONTAINER, 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, eARIACurrent, eARIADisabled, + eARIAExpanded, // Currently under spec review but precedent exists + eARIAHasPopup, // Note this is a tokenised attribute starting in ARIA 1.1 + 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 { + const nsStaticAtom* const attributeName; + const uint8_t characteristics; +}; + +static const AttrCharacteristics gWAIUnivAttrMap[] = { + // clang-format off + {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_current, ATTR_BYPASSOBJ_IF_FALSE | ATTR_VALTOKEN | 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_IF_FALSE | 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 } + // clang-format on +}; + +namespace { + +struct RoleComparator { + const nsDependentSubstring& mRole; + explicit RoleComparator(const nsDependentSubstring& aRole) : mRole(aRole) {} + int operator()(const nsRoleMapEntry& aEntry) const { + return Compare(mRole, aEntry.ARIARoleString()); + } +}; + +} // namespace + +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(nsAtom* 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 && aContent->IsElement() && + aContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::aria_hidden, + nsGkAtoms::_true, eCaseMatters); +} + +//////////////////////////////////////////////////////////////////////////////// +// AttrIterator class + +AttrIterator::AttrIterator(nsIContent* aContent) + : mElement(dom::Element::FromNode(aContent)), mAttrIdx(0) { + mAttrCount = mElement ? mElement->GetAttrCount() : 0; +} + +bool AttrIterator::Next(nsAString& aAttrName, nsAString& aAttrValue) { + while (mAttrIdx < mAttrCount) { + const nsAttrName* attr = mElement->GetAttrNameAt(mAttrIdx); + mAttrIdx++; + if (attr->NamespaceEquals(kNameSpaceID_None)) { + nsAtom* attrAtom = attr->Atom(); + nsDependentAtomString attrStr(attrAtom); + if (!StringBeginsWith(attrStr, u"aria-"_ns)) 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(mElement, attrAtom)) + continue; // only expose token based attributes if they are defined + + if ((attrFlags & ATTR_BYPASSOBJ_IF_FALSE) && + mElement->AttrValueIs(kNameSpaceID_None, attrAtom, nsGkAtoms::_false, + eCaseMatters)) { + continue; // only expose token based attribute if value is not 'false'. + } + + nsAutoString value; + if (mElement->GetAttr(kNameSpaceID_None, attrAtom, value)) { + aAttrName.Assign(Substring(attrStr, 5)); + if (attrFlags & ATTR_VALTOKEN) { + nsAtom* normalizedValue = + nsAccUtils::NormalizeARIAToken(mElement, attrAtom); + if (normalizedValue) { + nsDependentAtomString normalizedValueStr(normalizedValue); + aAttrValue.Assign(normalizedValueStr); + return true; + } + } + aAttrValue.Assign(value); + return true; + } + } + } + + return false; +} diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h new file mode 100644 index 0000000000..b45f2202d2 --- /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 "nsAtom.h" +#include "nsIContent.h" + +class nsINode; + +namespace mozilla::dom { +class Element; +} + +//////////////////////////////////////////////////////////////////////////////// +// 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, + + /** + * Value interface is implemented, but only if the element is focusable. + * For instance, in ARIA 1.1 the ability for authors to create adjustable + * splitters was provided by supporting the value interface on separators + * that are focusable. Non-focusable separators expose no value information. + */ + eHasValueMinMaxIfFocusable +}; + +//////////////////////////////////////////////////////////////////////////////// +// 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, + eAssertiveLiveAttr +}; + +//////////////////////////////////////////////////////////////////////////////// +// 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(nsAtom* 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" + nsStaticAtom* const 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(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(nsAtom* 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); + + bool Next(nsAString& aAttrName, nsAString& aAttrValue); + + private: + AttrIterator() = delete; + AttrIterator(const AttrIterator&) = delete; + AttrIterator& operator=(const AttrIterator&) = delete; + + dom::Element* mElement; + 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 0000000000..0a2f256037 --- /dev/null +++ b/accessible/base/ARIAStateMap.cpp @@ -0,0 +1,330 @@ +/* -*- 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. + nsStaticAtom* const mAttrName; + + // States if the attribute value is matched to the enum value. Used as + // Element::AttrValuesArray, last item must be nullptr. + nsStaticAtom* 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(nsAtom* 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. + nsAtom* 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 eARIACurrent: { + static const TokenTypeData data(nsGkAtoms::aria_current, eBoolType, 0, + states::CURRENT); + + 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 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 (aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, + nsGkAtoms::mixed, eCaseMatters)) { + if (aData.mType & eMixedType) + *aState |= aData.mPermanentState | states::MIXED; + else // unsupported use of 'mixed' is an authoring error + *aState |= aData.mPermanentState | aData.mFalseState; + 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 0000000000..20490aa901 --- /dev/null +++ b/accessible/base/ARIAStateMap.h @@ -0,0 +1,66 @@ +/* -*- 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, + eARIACurrent, + eARIADisabled, + eARIAExpanded, + eARIAHasPopup, + eARIAInvalid, + eARIAModal, + eARIAMultiline, + eARIAMultiSelectable, + eARIAOrientation, + eARIAPressed, + eARIAReadonly, + eARIAReadonlyOrEditable, + 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 0000000000..021909c60b --- /dev/null +++ b/accessible/base/AccEvent.cpp @@ -0,0 +1,305 @@ +/* -*- 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 "TextRange.h" +#include "xpcAccessibleDocument.h" +#include "xpcAccessibleTextRange.h" + +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/UserActivation.h" + +#include "nsComponentManagerUtils.h" +#include "nsIMutableArray.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 = dom::UserActivation::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); +} + +void AccTextSelChangeEvent::SelectionRanges( + nsTArray<TextRange>* aRanges) const { + TextRange::TextRangesFromSelection(mSel, aRanges); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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, + Accessible* aNewAccessible, + int32_t aNewStart, int32_t aNewEnd, + int16_t aReason, int16_t aBoundaryType, + EIsFromUserInput aIsFromUserInput) + : AccEvent(::nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED, aAccessible, + aIsFromUserInput), + mOldAccessible(aOldAccessible), + mNewAccessible(aNewAccessible), + mOldStart(aOldStart), + mNewStart(aNewStart), + mOldEnd(aOldEnd), + mNewEnd(aNewEnd), + mReason(aReason), + mBoundaryType(aBoundaryType) {} + +already_AddRefed<nsIAccessibleEvent> a11y::MakeXPCEvent(AccEvent* aEvent) { + DocAccessible* doc = aEvent->Document(); + Accessible* acc = aEvent->GetAccessible(); + nsINode* node = acc->GetNode(); + 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), + node, 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), node, 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), node, + 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), + node, fromUser, cm->GetCaretOffset(), + cm->IsSelectionCollapsed()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eTextSelChangeEvent)) { + AccTextSelChangeEvent* tsc = downcast_accEvent(aEvent); + AutoTArray<TextRange, 1> ranges; + tsc->SelectionRanges(&ranges); + + nsCOMPtr<nsIMutableArray> xpcRanges = + do_CreateInstance(NS_ARRAY_CONTRACTID); + uint32_t len = ranges.Length(); + for (uint32_t idx = 0; idx < len; idx++) { + xpcRanges->AppendElement( + new xpcAccessibleTextRange(std::move(ranges[idx]))); + } + + xpEvent = new xpcAccTextSelectionChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, xpcRanges); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eVirtualCursorChangeEvent)) { + AccVCChangeEvent* vcc = downcast_accEvent(aEvent); + xpEvent = new xpcAccVirtualCursorChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, + ToXPC(vcc->OldAccessible()), vcc->OldStartOffset(), vcc->OldEndOffset(), + ToXPC(vcc->NewAccessible()), vcc->NewStartOffset(), vcc->NewEndOffset(), + vcc->Reason(), vcc->BoundaryType()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eObjectAttrChangedEvent)) { + AccObjectAttrChangedEvent* oac = downcast_accEvent(aEvent); + nsString attribute; + oac->GetAttribute()->ToString(attribute); + xpEvent = new xpcAccObjectAttributeChangedEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, attribute); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eScrollingEvent)) { + AccScrollingEvent* sa = downcast_accEvent(aEvent); + xpEvent = new xpcAccScrollingEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, sa->ScrollX(), + sa->ScrollY(), sa->MaxScrollX(), sa->MaxScrollY()); + return xpEvent.forget(); + } + + if (eventGroup & (1 << AccEvent::eAnnouncementEvent)) { + AccAnnouncementEvent* aa = downcast_accEvent(aEvent); + xpEvent = new xpcAccAnnouncementEvent(type, ToXPC(acc), ToXPCDocument(doc), + node, fromUser, aa->Announcement(), + aa->Priority()); + return xpEvent.forget(); + } + + xpEvent = + new xpcAccEvent(type, ToXPC(acc), ToXPCDocument(doc), node, fromUser); + return xpEvent.forget(); +} diff --git a/accessible/base/AccEvent.h b/accessible/base/AccEvent.h new file mode 100644 index 0000000000..ade3e01b27 --- /dev/null +++ b/accessible/base/AccEvent.h @@ -0,0 +1,617 @@ +/* -*- 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; +class EventQueue; +class TextRange; + +// 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, + eScrollingEvent, + eAnnouncementEvent, + }; + + 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, + bool aIsSelectionCollapsed, + EIsFromUserInput aIsFromUserInput = eAutoDetect) + : AccEvent(::nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED, aAccessible, + aIsFromUserInput), + mCaretOffset(aCaretOffset), + mIsSelectionCollapsed(aIsSelectionCollapsed) {} + 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; } + + bool IsSelectionCollapsed() const { return mIsSelectionCollapsed; } + + private: + int32_t mCaretOffset; + + bool mIsSelectionCollapsed; +}; + +/** + * 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; + + /** + * Return selection ranges in document/control. + */ + void SelectionRanges(nsTArray<a11y::TextRange>* aRanges) 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, + Accessible* aNewAccessible, int32_t aNewStart, + int32_t aNewEnd, int16_t aReason, int16_t aBoundaryType, + EIsFromUserInput aIsFromUserInput = eFromUserInput); + + virtual ~AccVCChangeEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eVirtualCursorChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eVirtualCursorChangeEvent); + } + + // AccVCChangeEvent + Accessible* OldAccessible() const { return mOldAccessible; } + int32_t OldStartOffset() const { return mOldStart; } + int32_t OldEndOffset() const { return mOldEnd; } + Accessible* NewAccessible() const { return mNewAccessible; } + int32_t NewStartOffset() const { return mNewStart; } + int32_t NewEndOffset() const { return mNewEnd; } + int32_t Reason() const { return mReason; } + int32_t BoundaryType() const { return mBoundaryType; } + + private: + RefPtr<Accessible> mOldAccessible; + RefPtr<Accessible> mNewAccessible; + int32_t mOldStart; + int32_t mNewStart; + int32_t mOldEnd; + int32_t mNewEnd; + int16_t mReason; + int16_t mBoundaryType; +}; + +/** + * Accessible object attribute changed event. + */ +class AccObjectAttrChangedEvent : public AccEvent { + public: + AccObjectAttrChangedEvent(Accessible* aAccessible, nsAtom* 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 + nsAtom* GetAttribute() const { return mAttribute; } + + private: + RefPtr<nsAtom> mAttribute; + + virtual ~AccObjectAttrChangedEvent() {} +}; + +/** + * Accessible scroll event. + */ +class AccScrollingEvent : public AccEvent { + public: + AccScrollingEvent(uint32_t aEventType, Accessible* aAccessible, + uint32_t aScrollX, uint32_t aScrollY, uint32_t aMaxScrollX, + uint32_t aMaxScrollY) + : AccEvent(aEventType, aAccessible), + mScrollX(aScrollX), + mScrollY(aScrollY), + mMaxScrollX(aMaxScrollX), + mMaxScrollY(aMaxScrollY) {} + + virtual ~AccScrollingEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eScrollingEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eScrollingEvent); + } + + // The X scrolling offset of the container when the event was fired. + uint32_t ScrollX() { return mScrollX; } + // The Y scrolling offset of the container when the event was fired. + uint32_t ScrollY() { return mScrollY; } + // The max X offset of the container. + uint32_t MaxScrollX() { return mMaxScrollX; } + // The max Y offset of the container. + uint32_t MaxScrollY() { return mMaxScrollY; } + + private: + uint32_t mScrollX; + uint32_t mScrollY; + uint32_t mMaxScrollX; + uint32_t mMaxScrollY; +}; + +/** + * Accessible announcement event. + */ +class AccAnnouncementEvent : public AccEvent { + public: + AccAnnouncementEvent(Accessible* aAccessible, const nsAString& aAnnouncement, + uint16_t aPriority) + : AccEvent(nsIAccessibleEvent::EVENT_ANNOUNCEMENT, aAccessible), + mAnnouncement(aAnnouncement), + mPriority(aPriority) {} + + virtual ~AccAnnouncementEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eAnnouncementEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eAnnouncementEvent); + } + + const nsString& Announcement() const { return mAnnouncement; } + + uint16_t Priority() { return mPriority; } + + private: + nsString mAnnouncement; + uint16_t mPriority; +}; + +/** + * 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 0000000000..31153b87b5 --- /dev/null +++ b/accessible/base/AccGroupInfo.cpp @@ -0,0 +1,288 @@ +/* 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 "TableAccessible.h" + +#include "Role.h" +#include "States.h" + +using namespace mozilla::a11y; + +AccGroupInfo::AccGroupInfo(const Accessible* aItem, role aRole) + : mPosInSet(0), mSetSize(0), mParent(nullptr), mItem(aItem), mRole(aRole) { + MOZ_COUNT_CTOR(AccGroupInfo); + Update(); +} + +void AccGroupInfo::Update() { + mParent = nullptr; + + 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 (BaseRole(siblingRole) != mRole) { + continue; + } + bool siblingHasGroupInfo = + sibling->mBits.groupInfo && !sibling->HasDirtyGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingHasGroupInfo && 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 (siblingHasGroupInfo) { + 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 (BaseRole(siblingRole) != mRole) { + continue; + } + bool siblingHasGroupInfo = + sibling->mBits.groupInfo && !sibling->HasDirtyGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingHasGroupInfo && 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 (siblingHasGroupInfo) { + 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(const 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; +} + +uint32_t AccGroupInfo::TotalItemCount(Accessible* aContainer, + bool* aIsHierarchical) { + uint32_t itemCount = 0; + switch (aContainer->Role()) { + case roles::TABLE: + if (nsCoreUtils::GetUIntAttr(aContainer->GetContent(), + nsGkAtoms::aria_rowcount, + (int32_t*)&itemCount)) { + break; + } + + if (TableAccessible* tableAcc = aContainer->AsTable()) { + return tableAcc->RowCount(); + } + + break; + case roles::ROW: + if (Accessible* table = nsAccUtils::TableFor(aContainer)) { + if (nsCoreUtils::GetUIntAttr(table->GetContent(), + nsGkAtoms::aria_colcount, + (int32_t*)&itemCount)) { + break; + } + + if (TableAccessible* tableAcc = table->AsTable()) { + return tableAcc->ColCount(); + } + } + + break; + case roles::OUTLINE: + case roles::LIST: + case roles::MENUBAR: + case roles::MENUPOPUP: + case roles::COMBOBOX: + case roles::GROUPING: + case roles::TREE_TABLE: + case roles::COMBOBOX_LIST: + case roles::LISTBOX: + case roles::DEFINITION_LIST: + case roles::EDITCOMBOBOX: + case roles::RADIO_GROUP: + case roles::PAGETABLIST: { + Accessible* childItem = AccGroupInfo::FirstItemOf(aContainer); + if (!childItem) { + childItem = aContainer->FirstChild(); + if (childItem && childItem->IsTextLeaf()) { + // First child can be a text leaf, check its sibling for an item. + childItem = childItem->NextSibling(); + } + } + + if (childItem) { + GroupPos groupPos = childItem->GroupPosition(); + itemCount = groupPos.setSize; + if (groupPos.level && aIsHierarchical) { + *aIsHierarchical = true; + } + } + break; + } + default: + break; + } + + return itemCount; +} + +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; + } + } + + MOZ_ASSERT_UNREACHABLE( + "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 0000000000..070e258ea3 --- /dev/null +++ b/accessible/base/AccGroupInfo.h @@ -0,0 +1,118 @@ +/* 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: + MOZ_COUNTED_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(const 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 && + role != mozilla::a11y::roles::COMMENT) + return nullptr; + + AccGroupInfo* info = new AccGroupInfo(aAccessible, BaseRole(role)); + return info; + } + + /** + * Return a first item for the given container. + */ + static Accessible* FirstItemOf(const Accessible* aContainer); + + /** + * Return total number of items in container, and if it is has nested + * collections. + */ + static uint32_t TotalItemCount(Accessible* aContainer, bool* aIsHierarchical); + + /** + * Return next item of the same group to the given item. + */ + static Accessible* NextItemTo(Accessible* aItem); + + protected: + AccGroupInfo(const 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; + const Accessible* mItem; + a11y::role mRole; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccIterator.cpp b/accessible/base/AccIterator.cpp new file mode 100644 index 0000000000..2314843f22 --- /dev/null +++ b/accessible/base/AccIterator.cpp @@ -0,0 +1,338 @@ +/* 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/DocumentOrShadowRoot.h" +#include "mozilla/dom/HTMLLabelElement.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// AccIterator +//////////////////////////////////////////////////////////////////////////////// + +AccIterator::AccIterator(const 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(const Accessible* aParent, + IteratorState* mParentState) + : mParent(aParent), mIndex(0), mParentState(mParentState) {} + +//////////////////////////////////////////////////////////////////////////////// +// RelatedAccIterator +//////////////////////////////////////////////////////////////////////////////// + +RelatedAccIterator::RelatedAccIterator(DocAccessible* aDocument, + nsIContent* aDependentContent, + nsAtom* aRelAttr) + : mDocument(aDocument), mRelAttr(aRelAttr), mProviders(nullptr), mIndex(0) { + nsAutoString id; + if (aDependentContent->IsElement() && + aDependentContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, + id)) { + mProviders = mDocument->GetRelProviders(aDependentContent->AsElement(), id); + } +} + +Accessible* RelatedAccIterator::Next() { + if (!mProviders) return nullptr; + + while (mIndex < mProviders->Length()) { + const auto& provider = (*mProviders)[mIndex++]; + + // Return related accessible for the given attribute. + if (provider->mRelAttr == mRelAttr) { + 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::FromNode(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->AsElement()->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, + nsAtom* aIDRefsAttr) + : mContent(aContent), mDoc(aDoc), mCurrIdx(0) { + if (mContent->IsElement()) { + mContent->AsElement()->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; +} + +dom::Element* IDRefsIterator::GetElem(nsIContent* aContent, + const nsAString& 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 (!aContent->IsInNativeAnonymousSubtree()) { + dom::DocumentOrShadowRoot* docOrShadowRoot = + aContent->GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadowRoot) { + dom::Element* refElm = docOrShadowRoot->GetElementById(aID); + if (refElm) { + return refElm; + } + } + } + return nullptr; +} + +dom::Element* IDRefsIterator::GetElem(const nsDependentSubstring& aID) { + return GetElem(mContent, aID); +} + +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(const 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 0000000000..7cd48c870f --- /dev/null +++ b/accessible/base/AccIterator.h @@ -0,0 +1,303 @@ +/* -*- 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(const 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(const Accessible* aParent, + IteratorState* mParentState = nullptr); + + const 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, + nsAtom* 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; + nsAtom* mRelAttr; + DocAccessible::AttrRelProviders* mProviders; + 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, + nsAtom* aIDRefsAttr); + virtual ~IDRefsIterator() {} + + /** + * Return next ID. + */ + const nsDependentSubstring NextID(); + + /** + * Return next element. + */ + nsIContent* NextElem(); + + /** + * Return the element with the given ID. + */ + static dom::Element* GetElem(nsIContent* aContent, const nsAString& aID); + dom::Element* 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(const Accessible* aItemContainer) + : mContainer(aItemContainer), mAnchor(nullptr) {} + virtual ~ItemIterator() {} + + virtual Accessible* Next() override; + + private: + ItemIterator() = delete; + ItemIterator(const ItemIterator&) = delete; + ItemIterator& operator=(const ItemIterator&) = delete; + + const Accessible* mContainer; + Accessible* mAnchor; +}; + +/** + * Used to iterate through XUL tree items of the same level. + */ +class XULTreeItemIterator : public AccIterable { + public: + XULTreeItemIterator(const 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; + + const 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 0000000000..aa5dabab9c --- /dev/null +++ b/accessible/base/AccTypes.h @@ -0,0 +1,97 @@ +/* -*- 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, + eHTMLTextPasswordFieldType, + eHyperTextType, + eImageType, + eOuterDocType, + ePluginType, + eTextLeafType, + + /** + * Other accessible types. + */ + eApplicationType, + eHTMLLinkType, + eHTMLOptGroupType, + eImageMapType, + eMenuPopupType, + eProxyType, + eProgressType, + eRootType, + eXULLabelType, + eXULListItemType, + eXULTabpanelsType, + eXULTooltipType, + 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 0000000000..3826e6968c --- /dev/null +++ b/accessible/base/AccessibleOrProxy.cpp @@ -0,0 +1,97 @@ +/* -*- 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" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/OuterDocAccessible.h" + +namespace mozilla { +namespace a11y { + +int32_t AccessibleOrProxy::IndexInParent() const { + if (IsAccessible()) { + return AsAccessible()->IndexInParent(); + } + + ProxyAccessible* proxy = AsProxy(); + if (!proxy) { + return -1; + } + + if (proxy->Parent()) { + return proxy->IndexInParent(); + } + + if (proxy->OuterDocOfRemoteBrowser()) { + return 0; + } + + MOZ_ASSERT_UNREACHABLE("Proxy should have parent or outer doc."); + return -1; +} + +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(); +} + +AccessibleOrProxy AccessibleOrProxy::ChildAtPoint( + int32_t aX, int32_t aY, Accessible::EWhichChildAtPoint aWhichChild) { + if (IsProxy()) { + return AsProxy()->ChildAtPoint(aX, aY, aWhichChild); + } + ProxyAccessible* childDoc = RemoteChildDoc(); + if (childDoc) { + // This is an OuterDocAccessible. + nsIntRect docRect = AsAccessible()->Bounds(); + if (!docRect.Contains(aX, aY)) { + return nullptr; + } + if (aWhichChild == Accessible::eDirectChild) { + return childDoc; + } + return childDoc->ChildAtPoint(aX, aY, aWhichChild); + } + AccessibleOrProxy target = AsAccessible()->ChildAtPoint(aX, aY, aWhichChild); + if (target.IsNull() || aWhichChild == Accessible::eDirectChild) { + return target; + } + childDoc = target.RemoteChildDoc(); + if (childDoc) { + // Accessible::ChildAtPoint stopped at an OuterDocAccessible, since it + // can't traverse into ProxyAccessibles. Continue the search from childDoc. + return childDoc->ChildAtPoint(aX, aY, aWhichChild); + } + return target; +} + +ProxyAccessible* AccessibleOrProxy::RemoteChildDoc() const { + MOZ_ASSERT(!IsNull()); + if (IsProxy()) { + return nullptr; + } + OuterDocAccessible* outerDoc = AsAccessible()->AsOuterDoc(); + if (!outerDoc) { + return nullptr; + } + return outerDoc->RemoteChildDoc(); +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/AccessibleOrProxy.h b/accessible/base/AccessibleOrProxy.h new file mode 100644 index 0000000000..415ce7273d --- /dev/null +++ b/accessible/base/AccessibleOrProxy.h @@ -0,0 +1,180 @@ +/* -*- 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) {} + MOZ_IMPLICIT AccessibleOrProxy() : 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(); + } + + if (RemoteChildDoc()) { + return 1; + } + + return AsAccessible()->ChildCount(); + } + + /** + * Return true if the object has children, false otherwise + */ + bool HasChildren() const { return ChildCount() > 0; } + + /** + * Return the child object either an accessible or a proxied accessible at + * the given index. + */ + AccessibleOrProxy ChildAt(uint32_t aIdx) const { + if (IsProxy()) { + return AsProxy()->ChildAt(aIdx); + } + + ProxyAccessible* childDoc = RemoteChildDoc(); + if (childDoc && aIdx == 0) { + return childDoc; + } + + return AsAccessible()->GetChildAt(aIdx); + } + + /** + * Return the first child object. + */ + AccessibleOrProxy FirstChild() { + if (IsProxy()) { + return AsProxy()->FirstChild(); + } + + ProxyAccessible* childDoc = RemoteChildDoc(); + if (childDoc) { + return childDoc; + } + + return AsAccessible()->FirstChild(); + } + + /** + * Return the last child object. + */ + AccessibleOrProxy LastChild() { + if (IsProxy()) { + return AsProxy()->LastChild(); + } + + ProxyAccessible* childDoc = RemoteChildDoc(); + if (childDoc) { + return childDoc; + } + + return AsAccessible()->LastChild(); + } + + /** + * Return the next sibling object. + */ + AccessibleOrProxy NextSibling() { + if (IsProxy()) { + return AsProxy()->NextSibling(); + } + + return AsAccessible()->NextSibling(); + } + + /** + * Return the prev sibling object. + */ + AccessibleOrProxy PrevSibling() { + if (IsProxy()) { + return AsProxy()->PrevSibling(); + } + + return AsAccessible()->PrevSibling(); + } + + role Role() const { + if (IsProxy()) { + return AsProxy()->Role(); + } + + return AsAccessible()->Role(); + } + + int32_t IndexInParent() const; + + AccessibleOrProxy Parent() const; + + AccessibleOrProxy ChildAtPoint(int32_t aX, int32_t aY, + Accessible::EWhichChildAtPoint aWhichChild); + + bool operator!=(const AccessibleOrProxy& aOther) const { + return mBits != aOther.mBits; + } + + bool operator==(const AccessibleOrProxy& aOther) const { + return mBits == aOther.mBits; + } + + // 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: + /** + * If this is an OuterDocAccessible, return the remote child document. + */ + ProxyAccessible* RemoteChildDoc() const; + + uintptr_t mBits; + static const uintptr_t IS_PROXY = 0x1; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/Asserts.cpp b/accessible/base/Asserts.cpp new file mode 100644 index 0000000000..0d7fb88c71 --- /dev/null +++ b/accessible/base/Asserts.cpp @@ -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/. */ + +#include "nsIAccessibleRelation.h" +#include "nsIAccessibleRole.h" +#include "RelationType.h" +#include "Role.h" + +using namespace mozilla::a11y; + +#define ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, \ + ia2Role, androidClass, 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 0000000000..9011f20dad --- /dev/null +++ b/accessible/base/DocManager.cpp @@ -0,0 +1,562 @@ +/* -*- 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" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +#include "mozilla/Components.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Event.h" // for Event +#include "nsContentUtils.h" +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebNavigation.h" +#include "nsServiceManagerUtils.h" +#include "nsIWebProgress.h" +#include "nsCoreUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/dom/BrowserChild.h" +#include "xpcAccessibleDocument.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +StaticAutoPtr<nsTArray<DocAccessibleParent*>> DocManager::sRemoteDocuments; +StaticAutoPtr<nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, + xpcAccessibleDocument>> + DocManager::sRemoteXPCDocumentCache; + +//////////////////////////////////////////////////////////////////////////////// +// DocManager +//////////////////////////////////////////////////////////////////////////////// + +DocManager::DocManager() : mDocAccessibleCache(2), mXPCDocumentCache(0) {} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager public + +DocAccessible* DocManager::GetDocAccessible(Document* aDocument) { + if (!aDocument) return nullptr; + + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (docAcc) return docAcc; + + return CreateDocOrRootAccessible(aDocument); +} + +DocAccessible* DocManager::GetDocAccessible(const PresShell* aPresShell) { + if (!aPresShell) { + return nullptr; + } + + DocAccessible* doc = aPresShell->GetDocAccessible(); + if (doc) { + return doc; + } + + return GetDocAccessible(aPresShell->GetDocument()); +} + +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::RemoveFromXPCDocumentCache(DocAccessible* aDocument) { + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (xpcDoc) { + xpcDoc->Shutdown(); + mXPCDocumentCache.Remove(aDocument); + + if (!HasXPCDocuments()) { + MaybeShutdownAccService(nsAccessibilityService::eXPCOM); + } + } +} + +void DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument, + Document* 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; + } + + RemoveFromXPCDocumentCache(aDocument); + mDocAccessibleCache.Remove(aDOMDocument); +} + +void DocManager::RemoveFromRemoteXPCDocumentCache(DocAccessibleParent* aDoc) { + xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc); + if (doc) { + doc->Shutdown(); + sRemoteXPCDocumentCache->Remove(aDoc); + } + + if (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() == 0) { + MaybeShutdownAccService(nsAccessibilityService::eXPCOM); + } +} + +void DocManager::NotifyOfRemoteDocShutdown(DocAccessibleParent* aDoc) { + RemoveFromRemoteXPCDocumentCache(aDoc); +} + +xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessible* aDocument) { + if (!aDocument) return nullptr; + + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (!xpcDoc) { + xpcDoc = new xpcAccessibleDocument(aDocument); + mXPCDocumentCache.Put(aDocument, RefPtr{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>; + ClearOnShutdown(&sRemoteXPCDocumentCache); + } + + MOZ_ASSERT(!aDoc->IsShutdown(), "Adding a shutdown doc to remote XPC cache"); + doc = new xpcAccessibleDocument(aDoc, + Interfaces::DOCUMENT | Interfaces::HYPERTEXT); + sRemoteXPCDocumentCache->Put(aDoc, RefPtr{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 = components::DocLoader::Service(); + + if (!progress) return false; + + progress->AddProgressListener(static_cast<nsIWebProgressListener*>(this), + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + + return true; +} + +void DocManager::Shutdown() { + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + + 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<Document> 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) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aLocation, uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +DocManager::HandleEvent(Event* aEvent) { + nsAutoString type; + aEvent->GetType(type); + + nsCOMPtr<Document> document = do_QueryInterface(aEvent->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(Document* 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(Document* aDocument, + bool aAddDOMContentLoadedListener) { + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + EventTarget* target = window->GetChromeEventHandler(); + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture()); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) + logging::Text("added 'pagehide' listener"); +#endif + + if (aAddDOMContentLoadedListener) { + elm->AddEventListenerByType(this, u"DOMContentLoaded"_ns, + TrustedEventsAtCapture()); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) + logging::Text("added 'DOMContentLoaded' listener"); +#endif + } +} + +void DocManager::RemoveListeners(Document* aDocument) { + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + if (!window) return; + + EventTarget* target = window->GetChromeEventHandler(); + if (!target) return; + + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->RemoveEventListenerByType(this, u"pagehide"_ns, + TrustedEventsAtCapture()); + + elm->RemoveEventListenerByType(this, u"DOMContentLoaded"_ns, + TrustedEventsAtCapture()); +} + +DocAccessible* DocManager::CreateDocOrRootAccessible(Document* aDocument) { + // Ignore hidden documents, resource documents, static clone + // (printing) documents and documents without a docshell. + if (!aDocument->IsVisibleConsideringAncestors() || + aDocument->IsResourceDoc() || aDocument->IsStaticDocument() || + !aDocument->IsActive()) { + return nullptr; + } + + nsIDocShell* docShell = aDocument->GetDocShell(); + if (!docShell || docShell->IsInvisible()) { + return nullptr; + } + + nsIWidget* widget = nsContentUtils::WidgetForDocument(aDocument); + if (!widget || widget->WindowType() == eWindowType_invisible) { + return nullptr; + } + + // Ignore documents without presshell and not having root frame. + PresShell* presShell = aDocument->GetPresShell(); + 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->GetInProcessParentDocument()); + 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, RefPtr{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()); + + } 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); +} + +DocAccessible* mozilla::a11y::GetExistingDocAccessible( + const dom::Document* aDocument) { + PresShell* presShell = aDocument->GetPresShell(); + return presShell ? presShell->GetDocAccessible() : nullptr; +} diff --git a/accessible/base/DocManager.h b/accessible/base/DocManager.h new file mode 100644 index 0000000000..5fd820ff46 --- /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 "nsIDOMEventListener.h" +#include "nsRefPtrHashtable.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" +#include "mozilla/StaticPtr.h" +#include "nsINode.h" + +namespace mozilla::dom { +class Document; +} + +namespace mozilla { +class PresShell; + +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(dom::Document* aDocument); + + /** + * Return document accessible for the given presshell. + */ + DocAccessible* GetDocAccessible(const PresShell* aPresShell); + + /** + * 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, + dom::Document* aDOMDocument); + + void RemoveFromXPCDocumentCache(DocAccessible* aDocument); + + /** + * 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); + + static void RemoveFromRemoteXPCDocumentCache(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() = default; + + /** + * Initialize the manager. + */ + bool Init(); + + /** + * Shutdown the manager. + */ + void Shutdown(); + + bool HasXPCDocuments() { + return mXPCDocumentCache.Count() > 0 || + (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() > 0); + } + + 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(dom::Document* aDocument, uint32_t aLoadEventType); + + /** + * Add/remove 'pagehide' and 'DOMContentLoaded' event listeners. + */ + void AddListeners(dom::Document* aDocument, bool aAddPageShowListener); + void RemoveListeners(dom::Document* aDocument); + + /** + * Create document or root accessible. + */ + DocAccessible* CreateDocOrRootAccessible(dom::Document* aDocument); + + /** + * Clear the cache and shutdown the document accessibles. + */ + void ClearDocCache(); + + typedef nsRefPtrHashtable<nsPtrHashKey<const dom::Document>, DocAccessible> + DocAccessibleHashtable; + DocAccessibleHashtable mDocAccessibleCache; + + typedef nsRefPtrHashtable<nsPtrHashKey<const DocAccessible>, + xpcAccessibleDocument> + XPCDocumentHashtable; + XPCDocumentHashtable mXPCDocumentCache; + static StaticAutoPtr<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. + */ +DocAccessible* GetExistingDocAccessible(const dom::Document* aDocument); + +} // 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 0000000000..96d0038d8b --- /dev/null +++ b/accessible/base/EmbeddedObjCollector.cpp @@ -0,0 +1,63 @@ +/* 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 0000000000..3b18552795 --- /dev/null +++ b/accessible/base/EmbeddedObjCollector.h @@ -0,0 +1,68 @@ +/* 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 0000000000..638b2c5b30 --- /dev/null +++ b/accessible/base/EventQueue.cpp @@ -0,0 +1,335 @@ +/* -*- 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 + +namespace mozilla { +namespace 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!"); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mEvents.AppendElement(aEvent); + + // 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. + const nsTArray<RefPtr<AccEvent> > events = std::move(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; + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/EventQueue.h b/accessible/base/EventQueue.h new file mode 100644 index 0000000000..dceaf2843a --- /dev/null +++ b/accessible/base/EventQueue.h @@ -0,0 +1,76 @@ +/* -*- 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 0000000000..f804275775 --- /dev/null +++ b/accessible/base/EventTree.cpp @@ -0,0 +1,601 @@ +/* -*- 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 "EmbeddedObjCollector.h" +#include "NotificationController.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; + } + + for (uint32_t idx = 0; idx < length; idx++) { + 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 = std::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 = std::move(nodeOwnerRef); + nodeOwnerRef = std::move(newNode); + nodeOwnerRef->mNext = std::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 = std::move(*nodeRef); + insNode = insNode->mNext.get(); + + prevNode->mNext = std::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( + std::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 = std::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 0000000000..a6efd81b79 --- /dev/null +++ b/accessible/base/EventTree.h @@ -0,0 +1,128 @@ +/* -*- 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/a11y/DocAccessible.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace a11y { + +class NotificationController; + +/** + * 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 0000000000..e0d55113e5 --- /dev/null +++ b/accessible/base/Filters.cpp @@ -0,0 +1,44 @@ +/* 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) { + if (aAccessible->IsTableRow()) return eMatch | eSkipSubtree; + + // Look for rows inside rowgroup or wrapping text containers. + a11y::role role = aAccessible->Role(); + const nsRoleMapEntry* roleMapEntry = aAccessible->ARIARoleMap(); + if (role == roles::GROUPING || + (aAccessible->IsGenericHyperText() && !roleMapEntry)) { + 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 0000000000..dbc7a79595 --- /dev/null +++ b/accessible/base/Filters.h @@ -0,0 +1,46 @@ +/* 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 0000000000..423b21dd9a --- /dev/null +++ b/accessible/base/FocusManager.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 "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/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/DocManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowserParent.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; +} + +bool FocusManager::WasLastFocused(const Accessible* aAccessible) const { + return mLastFocus == aAccessible; +} + +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()) { + dom::Document* 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 mActiveItem is null we may need to shift a11y focus back to a remote + // document. For example, when combobox popup is closed, then + // the focus should be moved back to the combobox. + if (!mActiveItem && XRE_IsParentProcess()) { + dom::BrowserParent* browser = dom::BrowserParent::GetFocused(); + if (browser) { + a11y::DocAccessibleParent* dap = browser->GetTopLevelDocAccessible(); + if (dap) { + Unused << dap->SendRestoreFocus(); + } + } + } + + // 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) { + MOZ_ASSERT(aDocument, "No document for focused accessible!"); + if (aDocument) { + RefPtr<AccEvent> event = + new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, eAutoDetect, + AccEvent::eCoalesceOfSameType); + aDocument->FireDelayedEvent(event); + mLastFocus = aTarget; + +#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) { + MOZ_ASSERT(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(); + MOZ_ASSERT(!target->IsDefunct()); + 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; + MOZ_ASSERT(!target->IsDefunct()); + } + } + + // 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); + + if (NS_WARN_IF(target->IsDefunct())) { + // target died during nsEventShell::FireEvent. + return; + } + + // 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(); + MOZ_ASSERT(targetDocument); + 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->GetFocusedElement(); + nsIFrame* focusedFrame = focusedElm ? focusedElm->GetPrimaryFrame() : nullptr; + // DOM elements retain their focused state when they get styled as display: + // none/content or visibility: hidden. We should treat those cases as if those + // elements were removed, and focus on doc. + if (focusedFrame && focusedFrame->StyleVisibility()->IsVisible()) { + // Print preview documents don't get DocAccessibles, but we still want a11y + // focus to go somewhere useful. Therefore, we allow a11y focus to land on + // the OuterDocAccessible in this case. + // Note that this code only handles remote print preview documents. + if (EventStateManager::IsTopLevelRemoteTarget(focusedElm) && + focusedElm->AsElement()->HasAttribute(u"printpreview"_ns)) { + return focusedElm; + } + // 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. Similarly, suppress focus on OOP iframes because an + // element in another content process should now have the focus. + if (EventStateManager::IsRemoteTarget(focusedElm)) { + return nullptr; + } + return focusedElm; + } + + // Otherwise the focus can be on DOM document. + dom::BrowsingContext* context = DOMFocusManager->GetFocusedBrowsingContext(); + if (context) { + // GetDocShell will return null if the document isn't in our process. + nsIDocShell* shell = context->GetDocShell(); + if (shell) { + return shell->GetDocument(); + } + } + + // Focus isn't in this process. + return nullptr; +} + +dom::Document* 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 0000000000..475229376c --- /dev/null +++ b/accessible/base/FocusManager.h @@ -0,0 +1,146 @@ +/* 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_ + +#include "mozilla/RefPtr.h" + +class nsINode; +class nsISupports; + +namespace mozilla { +namespace dom { +class Document; +} + +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 DOM node having DOM focus. + */ + nsINode* FocusedDOMNode() const; + + /** + * 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; + + /** + * Return true if the given accessible was the last accessible focused. + * This is useful to detect the case where the last focused accessible was + * removed before something else was focused. This can happen in one of two + * ways: + * 1. The DOM focus was removed. DOM doesn't fire a blur event when this + * happens; see bug 559561. + * 2. The accessibility focus was an active item (e.g. aria-activedescendant) + * and that item was removed. + */ + bool WasLastFocused(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 document having DOM focus. + */ + dom::Document* FocusedDOMDocument() const; + + private: + RefPtr<Accessible> mActiveItem; + RefPtr<Accessible> mLastFocus; + RefPtr<Accessible> mActiveARIAMenubar; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/IDSet.h b/accessible/base/IDSet.h new file mode 100644 index 0000000000..a149bf95a3 --- /dev/null +++ b/accessible/base/IDSet.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +/** + * A class to generate unique IDs in the range [ - 2^31, 0 ) + */ + +#ifndef MOZILLA_A11Y_IDSet_h_ +#define MOZILLA_A11Y_IDSet_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/SplayTree.h" + +namespace mozilla { +namespace a11y { + +/** + * On windows an accessible's id must be a negative 32 bit integer. It is + * important to support recycling arbitrary IDs because accessibles can be + * created and destroyed at any time in the life of a page. IDSet provides 2 + * operations: generate an ID in the range (0, mMaxId], and release an ID so + * it can be allocated again. Allocated ID are tracked by a sparse bitmap + * implemented with a splay tree. Nodes in the tree are keyed by the upper N + * bits of the ID, and the node contains a bitmap tracking the allocation of + * 2^(ceil(log2(mMaxId)) - N) IDs. + * + * Note that negation is handled by MsaaIdGenerator as it performs additional + * decoration on the ID generated by IDSet. + * @see mozilla::a11y::MsaaIdGenerator + */ +class IDSet { + public: + constexpr explicit IDSet(const uint32_t aMaxIdBits) + : mBitSet(), + mIdx(0), + mMaxId((1UL << aMaxIdBits) - 1UL), + mMaxIdx(mMaxId / bitsPerElt) {} + + /** + * Return a new unique id. + */ + uint32_t GetID() { + uint32_t idx = mIdx; + while (true) { + BitSetElt* elt = mBitSet.findOrInsert(BitSetElt(idx)); + if (elt->mBitvec[0] != UINT64_MAX) { + uint32_t i = CountTrailingZeroes64(~elt->mBitvec[0]); + + elt->mBitvec[0] |= (1ull << i); + mIdx = idx; + return (elt->mIdx * bitsPerElt + i); + } + + if (elt->mBitvec[1] != UINT64_MAX) { + uint32_t i = CountTrailingZeroes64(~elt->mBitvec[1]); + + elt->mBitvec[1] |= (1ull << i); + mIdx = idx; + return (elt->mIdx * bitsPerElt + bitsPerWord + i); + } + + idx++; + if (idx > mMaxIdx) { + idx = 0; + } + + if (idx == mIdx) { + MOZ_CRASH("used up all the available ids"); + } + } + } + + /** + * Free a no longer required id so it may be allocated again. + */ + void ReleaseID(uint32_t aID) { + MOZ_ASSERT(aID < mMaxId); + + uint32_t idx = aID / bitsPerElt; + mIdx = idx; + BitSetElt* elt = mBitSet.find(BitSetElt(idx)); + MOZ_ASSERT(elt); + + uint32_t vecIdx = (aID % bitsPerElt) / bitsPerWord; + elt->mBitvec[vecIdx] &= ~(1ull << (aID % bitsPerWord)); + if (elt->mBitvec[0] == 0 && elt->mBitvec[1] == 0) { + delete mBitSet.remove(*elt); + } + } + + private: + static const unsigned int wordsPerElt = 2; + static const unsigned int bitsPerWord = 64; + static const unsigned int bitsPerElt = wordsPerElt * bitsPerWord; + + struct BitSetElt : mozilla::SplayTreeNode<BitSetElt> { + explicit BitSetElt(uint32_t aIdx) : mIdx(aIdx) { + mBitvec[0] = mBitvec[1] = 0; + } + + uint64_t mBitvec[wordsPerElt]; + uint32_t mIdx; + + static int compare(const BitSetElt& a, const BitSetElt& b) { + if (a.mIdx == b.mIdx) { + return 0; + } + + if (a.mIdx < b.mIdx) { + return -1; + } + return 1; + } + }; + + SplayTree<BitSetElt, BitSetElt> mBitSet; + uint32_t mIdx; + const uint32_t mMaxId; + const uint32_t mMaxIdx; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/Logging.cpp b/accessible/base/Logging.cpp new file mode 100644 index 0000000000..c6caaa3827 --- /dev/null +++ b/accessible/base/Logging.cpp @@ -0,0 +1,925 @@ +/* -*- 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 "nsTraceRefcnt.h" +#include "nsIWebProgress.h" +#include "prenv.h" +#include "nsIDocShellTreeItem.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/BorrowedAttrInfo.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/Selection.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +using mozilla::dom::BorrowedAttrInfo; + +//////////////////////////////////////////////////////////////////////////////// +// 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(dom::Document* aDocumentNode) { + printf("uri: %s", aDocumentNode->GetDocumentURI()->GetSpecOrDefault().get()); +} + +static void LogDocShellState(dom::Document* aDocumentNode) { + printf("docshell busy: "); + + nsAutoCString docShellBusy; + nsCOMPtr<nsIDocShell> docShell = aDocumentNode->GetDocShell(); + nsIDocShell::BusyFlags 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(dom::Document* aDocumentNode) { + if (aDocumentNode->IsActive()) { + bool isContent = nsCoreUtils::IsContentDocument(aDocumentNode); + printf("%s document", (isContent ? "content" : "chrome")); + } else { + printf("document type: [failed]"); + } +} + +static void LogDocShellTree(dom::Document* aDocumentNode) { + if (aDocumentNode->IsActive()) { + nsCOMPtr<nsIDocShellTreeItem> treeItem(aDocumentNode->GetDocShell()); + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem)); + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem)); + printf( + "in-process docshell hierarchy, parent: %p, root: %p, " + "is top level: %s;", + static_cast<void*>(parentTreeItem), static_cast<void*>(rootTreeItem), + (nsCoreUtils::IsTopLevelContentDocInProcess(aDocumentNode) ? "yes" + : "no")); + } +} + +static void LogDocState(dom::Document* aDocumentNode) { + const char* docState = nullptr; + dom::Document::ReadyState docStateFlag = aDocumentNode->GetReadyStateEnum(); + switch (docStateFlag) { + case dom::Document::READYSTATE_UNINITIALIZED: + docState = "uninitialized"; + break; + case dom::Document::READYSTATE_LOADING: + docState = "loading"; + break; + case dom::Document::READYSTATE_INTERACTIVE: + docState = "interactive"; + break; + case dom::Document::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(dom::Document* aDocumentNode) { + PresShell* presShell = aDocumentNode->GetPresShell(); + printf("presshell: %p", static_cast<void*>(presShell)); + + nsIScrollableFrame* sf = nullptr; + if (presShell) { + printf(", is %s destroying", (presShell->IsDestroying() ? "" : "not")); + sf = presShell->GetRootScrollFrameAsScrollable(); + } + printf(", root scroll frame: %p", static_cast<void*>(sf)); +} + +static void LogDocLoadGroup(dom::Document* aDocumentNode) { + nsCOMPtr<nsILoadGroup> loadGroup = aDocumentNode->GetDocumentLoadGroup(); + printf("load group: %p", static_cast<void*>(loadGroup)); +} + +static void LogDocParent(dom::Document* aDocumentNode) { + dom::Document* parentDoc = aDocumentNode->GetInProcessParentDocument(); + 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(dom::Document* 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_BYPASS_URL_CLASSIFIER) { + printf("bypass 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"); + } + } +} + +static void DescribeNode(nsINode* aNode, nsAString& aOutDescription) { + if (!aNode) { + aOutDescription.AppendLiteral("null"); + return; + } + + aOutDescription.AppendPrintf("0x%p, ", (void*)aNode); + aOutDescription.Append(aNode->NodeInfo()->QualifiedName()); + + if (!aNode->IsElement()) { + return; + } + + dom::Element* elm = aNode->AsElement(); + + nsAtom* idAtom = elm->GetID(); + if (idAtom) { + nsAutoCString id; + idAtom->ToUTF8String(id); + aOutDescription.AppendPrintf("@id=\"%s\" ", id.get()); + } else { + aOutDescription.Append(' '); + } + + uint32_t attrCount = elm->GetAttrCount(); + if (!attrCount || (idAtom && attrCount == 1)) { + return; + } + + aOutDescription.AppendLiteral("[ "); + + for (uint32_t index = 0; index < attrCount; index++) { + BorrowedAttrInfo info = elm->GetAttrInfoAt(index); + + // Skip redundant display of id attribute. + if (info.mName->Equals(nsGkAtoms::id)) { + continue; + } + + // name + nsAutoString name; + info.mName->GetQualifiedName(name); + aOutDescription.Append(name); + + aOutDescription.AppendLiteral("=\""); + + // value + nsAutoString value; + info.mValue->ToString(value); + for (uint32_t i = value.Length(); i > 0; --i) { + if (value[i - 1] == char16_t('"')) value.Insert(char16_t('\\'), i - 1); + } + aOutDescription.Append(value); + aOutDescription.AppendLiteral("\" "); + } + + aOutDescription.Append(']'); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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, "%s", aMsg); + + nsCOMPtr<mozIDOMWindowProxy> DOMWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow)); + nsPIDOMWindowOuter* window = nsPIDOMWindowOuter::From(DOMWindow); + if (!window) { + MsgEnd(); + return; + } + + nsCOMPtr<dom::Document> 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, dom::Document* aDocumentNode) { + MsgBegin(sDocLoadTitle, "%s", 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, dom::Document* aDocumentNode, + DocAccessible* aDocument) { + DocAccessible* document = + aDocument ? aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocCreateTitle, "%s", aMsg); + LogDocInfo(aDocumentNode, document); + MsgEnd(); +} + +void logging::DocDestroy(const char* aMsg, dom::Document* aDocumentNode, + DocAccessible* aDocument) { + DocAccessible* document = + aDocument ? aDocument : GetExistingDocAccessible(aDocumentNode); + + MsgBegin(sDocDestroyTitle, "%s", 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, "%s", aMsg); + AccessibleNNode(aTargetDescr, aTarget); + MsgEnd(); +} + +void logging::FocusNotificationTarget(const char* aMsg, + const char* aTargetDescr, + nsINode* aTargetNode) { + MsgBegin(sFocusTitle, "%s", aMsg); + Node(aTargetDescr, aTargetNode); + MsgEnd(); +} + +void logging::FocusNotificationTarget(const char* aMsg, + const char* aTargetDescr, + nsISupports* aTargetThing) { + MsgBegin(sFocusTitle, "%s", 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(dom::Selection* aSelection, DocAccessible* aDocument, + int16_t aReason) { + SelectionType type = aSelection->GetType(); + + const char* strType = 0; + if (type == SelectionType::eNormal) + strType = "normal"; + else if (type == SelectionType::eSpellCheck) + 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", "%s", 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, "%s", 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.AppendLiteral(u" "); + 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, "%s", aMsgText); + nsAutoString level; + nsINode* root = aDocument->DocumentNode(); + do { + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::Node("", root); + if (root->GetFirstChild()) { + level.AppendLiteral(u" "); + 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("; %02u:%02u.%03u", 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(); + dom::Document* 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) { + nsINode* parentNode = aNode ? aNode->GetParentNode() : nullptr; + int32_t idxInParent = parentNode ? parentNode->ComputeIndexOf(aNode) : -1; + + nsAutoString nodeDesc; + DescribeNode(aNode, nodeDesc); + printf(" %s: %s, idx in parent %d\n", aDescr, + NS_ConvertUTF16toUTF8(nodeDesc).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()); + + nsAutoString nodeDesc; + DescribeNode(aAccessible->GetNode(), nodeDesc); + printf(", node: %s\n", NS_ConvertUTF16toUTF8(nodeDesc).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 nsCString& 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 0000000000..6e2bc69ec4 --- /dev/null +++ b/accessible/base/Logging.h @@ -0,0 +1,230 @@ +/* -*- 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" +#include "mozilla/Attributes.h" + +class nsINode; +class nsIRequest; +class nsISupports; +class nsIWebProgress; + +namespace mozilla { + +namespace dom { +class Document; +class Selection; +} // namespace dom + +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, dom::Document* 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, dom::Document* aDocumentNode, + DocAccessible* aDocument = nullptr); + +/** + * Log the document was destroyed. + */ +void DocDestroy(const char* aMsg, dom::Document* 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(dom::Selection* 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, ...) + MOZ_FORMAT_PRINTF(2, 3); +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, ...) MOZ_FORMAT_PRINTF(1, 2); + +/** + * 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 nsCString& 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 0000000000..3adfd9ec1d --- /dev/null +++ b/accessible/base/MarkupMap.h @@ -0,0 +1,561 @@ +/* -*- 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, + [](Element* aElement, Accessible* aContext) -> Accessible* { + // Only some roles truly enjoy life as HTMLLinkAccessibles, for + // details see closed bug 494807. + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(aElement); + if (roleMapEntry && roleMapEntry->role != roles::NOTHING && + roleMapEntry->role != roles::LINK) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); + } + + return new HTMLLinkAccessible(aElement, aContext->Document()); + }, + roles::LINK) + +MARKUPMAP(abbr, New_HyperText, 0) + +MARKUPMAP(acronym, New_HyperText, 0) + +MARKUPMAP(article, New_HyperText, roles::ARTICLE, Attr(xmlroles, article)) + +MARKUPMAP(aside, New_HyperText, roles::LANDMARK) + +MARKUPMAP(blockquote, New_HyperText, roles::BLOCKQUOTE) + +MARKUPMAP( + button, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLButtonAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + caption, + [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aContext->IsTable()) { + dom::HTMLTableElement* tableEl = + dom::HTMLTableElement::FromNode(aContext->GetContent()); + if (tableEl && tableEl == aElement->GetParent() && + tableEl->GetCaption() == aElement) { + return new HTMLCaptionAccessible(aElement, aContext->Document()); + } + } + return nullptr; + }, + 0) + +// XXX: Uncomment this once HTML-aam agrees to map to same as ARIA. +// MARKUPMAP(code, New_HyperText, roles::CODE) + +MARKUPMAP(dd, New_HTMLDtOrDd<HyperTextAccessibleWrap>, roles::DEFINITION) + +MARKUPMAP(del, New_HyperText, roles::CONTENT_DELETION) + +MARKUPMAP(details, New_HyperText, roles::DETAILS) + +MARKUPMAP(dialog, New_HyperText, roles::DIALOG) + +MARKUPMAP( + div, + [](Element* aElement, Accessible* aContext) -> Accessible* { + // Never create an accessible if we're part of an anonymous + // subtree. + if (aElement->IsInNativeAnonymousSubtree()) { + return nullptr; + } + // Always create an accessible if the div has an id. + if (aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::id)) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); + } + // Never create an accessible if the div is not display:block; or + // display:inline-block; + nsAutoString displayValue; + StyleInfo styleInfo(aElement); + styleInfo.Display(displayValue); + if (displayValue != u"block"_ns && displayValue != u"inline-block"_ns) { + return nullptr; + } + // Check for various conditions to determine if this is a block + // break and needs to be rendered. + // If its previous sibling is an inline element, we probably want + // to break, so render. + nsIContent* prevSibling = aElement->GetPreviousSibling(); + if (prevSibling) { + nsIFrame* prevSiblingFrame = prevSibling->GetPrimaryFrame(); + if (prevSiblingFrame && prevSiblingFrame->IsInlineOutside()) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); + } + } + // Now, check the children. + nsIContent* firstChild = aElement->GetFirstChild(); + if (firstChild) { + nsIFrame* firstChildFrame = firstChild->GetPrimaryFrame(); + if (!firstChildFrame) { + // The first child is invisible, but this might be due to an + // invisible text node. Try the next. + firstChild = firstChild->GetNextSibling(); + if (!firstChild) { + // If there's no next sibling, there's only one child, so there's + // nothing more we can do. + return nullptr; + } + firstChildFrame = firstChild->GetPrimaryFrame(); + } + // Check to see if first child has an inline frame. + if (firstChildFrame && firstChildFrame->IsInlineOutside()) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); + } + nsIContent* lastChild = aElement->GetLastChild(); + MOZ_ASSERT(lastChild); + if (lastChild != firstChild) { + nsIFrame* lastChildFrame = lastChild->GetPrimaryFrame(); + if (!lastChildFrame) { + // The last child is invisible, but this might be due to an + // invisible text node. Try the next. + lastChild = lastChild->GetPreviousSibling(); + MOZ_ASSERT(lastChild); + if (lastChild == firstChild) { + return nullptr; + } + lastChildFrame = lastChild->GetPrimaryFrame(); + } + // Check to see if last child has an inline frame. + if (lastChildFrame && lastChildFrame->IsInlineOutside()) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); + } + } + } + return nullptr; + }, + roles::SECTION) + +MARKUPMAP( + dl, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::DEFINITION_LIST) + +MARKUPMAP(dt, New_HTMLDtOrDd<HTMLLIAccessible>, roles::TERM) + +MARKUPMAP( + figcaption, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLFigcaptionAccessible(aElement, aContext->Document()); + }, + roles::CAPTION) + +MARKUPMAP( + figure, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLFigureAccessible(aElement, aContext->Document()); + }, + roles::FIGURE, Attr(xmlroles, figure)) + +MARKUPMAP( + fieldset, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLGroupboxAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + form, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLFormAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + footer, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLHeaderOrFooterAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + header, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLHeaderOrFooterAccessible(aElement, aContext->Document()); + }, + 0) + +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( + hr, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLHRAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + input, + [](Element* aElement, Accessible* aContext) -> Accessible* { + // TODO(emilio): This would be faster if it used + // HTMLInputElement's already-parsed representation. + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::checkbox, eIgnoreCase)) { + return new CheckboxAccessible(aElement, aContext->Document()); + } + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::image, eIgnoreCase)) { + return new HTMLButtonAccessible(aElement, aContext->Document()); + } + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::radio, eIgnoreCase)) { + return new HTMLRadioButtonAccessible(aElement, aContext->Document()); + } + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::time, eIgnoreCase)) { + return new HTMLDateTimeAccessible<roles::TIME_EDITOR>( + aElement, aContext->Document()); + } + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::date, eIgnoreCase)) { + return new HTMLDateTimeAccessible<roles::DATE_EDITOR>( + aElement, aContext->Document()); + } + return nullptr; + }, + 0) + +MARKUPMAP(ins, New_HyperText, roles::CONTENT_INSERTION) + +MARKUPMAP( + label, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLLabelAccessible(aElement, aContext->Document()); + }, + roles::LABEL) + +MARKUPMAP( + legend, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLLegendAccessible(aElement, aContext->Document()); + }, + roles::LABEL) + +MARKUPMAP( + li, + [](Element* aElement, Accessible* aContext) -> Accessible* { + // 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() == aElement->GetParent()) { + return new HTMLLIAccessible(aElement, aContext->Document()); + } + + return nullptr; + }, + 0) + +MARKUPMAP(main, New_HyperText, roles::LANDMARK) + +MARKUPMAP(map, nullptr, roles::TEXT_CONTAINER) + +MARKUPMAP(mark, New_HyperText, roles::MARK, Attr(xmlroles, mark)) + +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_, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLTableAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE, AttrFromDOM(align, align), + AttrFromDOM(columnlines_, columnlines_), AttrFromDOM(rowlines_, rowlines_)) + +MARKUPMAP( + mlabeledtr_, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_LABELED_ROW) + +MARKUPMAP( + mtr_, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE_ROW) + +MARKUPMAP( + mtd_, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLTableCellAccessible(aElement, aContext->Document()); + }, + roles::MATHML_CELL) + +MARKUPMAP(maction_, New_HyperText, roles::MATHML_ACTION, + AttrFromDOM(actiontype_, actiontype_), + AttrFromDOM(selection_, selection_)) + +MARKUPMAP( + menu, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +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::LANDMARK) + +MARKUPMAP( + ol, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +MARKUPMAP( + option, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLSelectOptionAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + optgroup, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLSelectOptGroupAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + output, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLOutputAccessible(aElement, aContext->Document()); + }, + roles::STATUSBAR, Attr(live, polite)) + +MARKUPMAP(p, nullptr, roles::PARAGRAPH) + +MARKUPMAP( + progress, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLProgressAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(q, New_HyperText, 0) + +MARKUPMAP( + section, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLSectionAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + summary, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLSummaryAccessible(aElement, aContext->Document()); + }, + roles::SUMMARY) + +MARKUPMAP( + table, + [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aElement->GetPrimaryFrame() && + aElement->GetPrimaryFrame()->AccessibleType() != eHTMLTableType) { + return new ARIAGridAccessibleWrap(aElement, aContext->Document()); + } + + // Make sure that our children are proper layout table parts + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tfoot, + nsGkAtoms::tbody, nsGkAtoms::tr)) { + // These children elements need to participate in the layout table + // and need table row(group) frames. + nsIFrame* childFrame = child->GetPrimaryFrame(); + if (childFrame && (!childFrame->IsTableRowGroupFrame() && + !childFrame->IsTableRowFrame())) { + return new ARIAGridAccessibleWrap(aElement, aContext->Document()); + } + } + } + return nullptr; + }, + 0) + +MARKUPMAP(time, New_HyperText, 0, Attr(xmlroles, time), + AttrFromDOM(datetime, datetime)) + +MARKUPMAP(tbody, nullptr, roles::GROUPING) + +MARKUPMAP( + td, + [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aContext->IsTableRow() && + aContext->GetContent() == aElement->GetParent()) { + // If HTML:td element is part of its HTML:table, which has CSS + // display style other than 'table', then create a generic table + // cell accessible, because there's no underlying table layout and + // thus native HTML table cell class doesn't work. The same is + // true if the cell itself has CSS display:block;. + if (!aContext->IsHTMLTableRow() || + (aElement->GetPrimaryFrame() && + aElement->GetPrimaryFrame()->AccessibleType() != + eHTMLTableCellType)) { + return new ARIAGridCellAccessibleWrap(aElement, aContext->Document()); + } + if (aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::scope)) { + return new HTMLTableHeaderCellAccessibleWrap(aElement, + aContext->Document()); + } + } + return nullptr; + }, + 0) + +MARKUPMAP(tfoot, nullptr, roles::GROUPING) + +MARKUPMAP( + th, + [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aContext->IsTableRow() && + aContext->GetContent() == aElement->GetParent()) { + if (!aContext->IsHTMLTableRow()) { + return new ARIAGridCellAccessibleWrap(aElement, aContext->Document()); + } + return new HTMLTableHeaderCellAccessibleWrap(aElement, + aContext->Document()); + } + return nullptr; + }, + 0) + +MARKUPMAP(thead, nullptr, roles::GROUPING) + +MARKUPMAP( + tr, + [](Element* aElement, Accessible* aContext) -> Accessible* { + // If HTML:tr element is part of its HTML:table, which has CSS + // display style other than 'table', then create a generic table row + // accessible, because there's no underlying table layout and thus + // native HTML table row class doesn't work. Refer to + // CreateAccessibleByFrameType dual logic. + Accessible* table = aContext->IsTable() ? aContext : nullptr; + if (!table && aContext->Parent() && aContext->Parent()->IsTable()) { + table = aContext->Parent(); + } + if (table) { + nsIContent* parentContent = aElement->GetParent(); + nsIFrame* parentFrame = parentContent->GetPrimaryFrame(); + if (parentFrame && !parentFrame->IsTableWrapperFrame()) { + parentContent = parentContent->GetParent(); + parentFrame = parentContent->GetPrimaryFrame(); + if (table->GetContent() == parentContent && + ((parentFrame && !parentFrame->IsTableWrapperFrame()) || + (aElement->GetPrimaryFrame() && + aElement->GetPrimaryFrame()->AccessibleType() != + eHTMLTableRowType))) { + return new ARIARowAccessible(aElement, aContext->Document()); + } + } + } + return nullptr; + }, + 0) + +MARKUPMAP( + ul, + [](Element* aElement, Accessible* aContext) -> Accessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp new file mode 100644 index 0000000000..d5a678e321 --- /dev/null +++ b/accessible/base/NotificationController.cpp @@ -0,0 +1,974 @@ +/* -*- 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 "GeckoProfiler.h" +#include "nsEventShell.h" +#include "TextLeafAccessible.h" +#include "TextUpdater.h" + +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/PresShell.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector +//////////////////////////////////////////////////////////////////////////////// + +NotificationController::NotificationController(DocAccessible* aDocument, + PresShell* 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, FlushType::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) { + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + // 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->GetAccessible()->ShowEventTarget()) { + AccTreeMutationEvent* showEvent = + mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent); + DropMutationEvent(showEvent); + return false; + } + + // If this is an additional hide event, the accessible may be hidden, or + // moved again after a move. Preserve the original hide event since + // its properties are consistent with the tree that existed before + // the next batch of mutation events is processed. + if (aEvent->GetAccessible()->HideEventTarget()) { + 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 { + aEvent->GetAccessible()->SetHideEventTarget(false); + + 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, nsTArray<nsCOMPtr<nsIContent>>& aInsertions) { + if (!aInsertions.IsEmpty()) { + mContentInsertions.LookupOrAdd(aContainer)->AppendElements(aInsertions); + 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, FlushType::Display, + "Accessibility notifications")) + mObservingState = eRefreshObserving; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: protected + +bool NotificationController::IsUpdatePending() { + return mPresShell->IsLayoutFlushObserver() || + mObservingState == eRefreshProcessingForUpdate || WaitingForParent() || + mContentInsertions.Count() != 0 || mNotifications.Length() != 0 || + mTextHash.Count() != 0 || + !mDocument->HasLoadState(DocAccessible::eTreeConstructed); +} + +bool NotificationController::WaitingForParent() { + DocAccessible* parentdoc = mDocument->ParentDocument(); + if (!parentdoc) { + return false; + } + + NotificationController* parent = parentdoc->mNotificationController; + if (!parent || parent == this) { + // Do not wait for nothing or ourselves + return false; + } + + // Wait for parent's notifications processing + return parent->mContentInsertions.Count() != 0 || + parent->mNotifications.Length() != 0; +} + +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) { + Telemetry::AutoTimer<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> timer; + + AUTO_PROFILER_LABEL("NotificationController::WillRefresh", OTHER); + + // 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; + + // Wait until an update, we have started, or an interruptible reflow is + // finished. + if (mObservingState == eRefreshProcessing || + mObservingState == eRefreshProcessingForUpdate || + mPresShell->IsReflowInterrupted()) { + return; + } + + // Process parent's notifications before ours, to get proper ordering between + // e.g. tab event and content event. + if (WaitingForParent()) { + mDocument->ParentDocument()->mNotificationController->WillRefresh(aTime); + if (!mDocument) { + 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!"); + } + + // 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 a frame, or placed in + // another document, then this case should have been handled already by + // content removal notifications. + nsINode* containerNode = textNode->GetFlattenedTreeParentNode(); + if (!containerNode || textNode->OwnerDoc() != mDocument->DocumentNode()) { + MOZ_ASSERT(!textAcc, + "Text node was removed but accessible is kept alive!"); + continue; + } + + nsIFrame* textFrame = textNode->GetPrimaryFrame(); + if (!textFrame) { + MOZ_ASSERT(!textAcc, + "Text node isn't rendered but accessible is kept alive!"); + continue; + } + +#ifdef A11Y_LOG + nsIContent* containerElm = + containerNode->IsElement() ? containerNode->AsElement() : nullptr; +#endif + + nsIFrame::RenderedText text = textFrame->GetRenderedText( + 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + + // 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(textAcc); + 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 + + MOZ_ASSERT(mDocument->AccessibleOrTrueContainer(containerNode), + "Text node having rendered text hasn't accessible document!"); + + Accessible* container = + mDocument->AccessibleOrTrueContainer(containerNode, true); + 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 unless we are using IPC and the + // document has no IPC actor. If we fail to bind the child doc then + // shut it down. + 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; + + if (IPCAccessibilityActive() && !mDocument->IPCDoc()) { + childDoc->Shutdown(); + 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(std::move(mHangingChildDocuments[idx])); + continue; + } + + outerDocAcc->RemoveChild(childDoc); + } + + // Failed to bind the child document, destroy it. + childDoc->Shutdown(); + } + } + + // Clear the hanging documents list, even if we didn't bind them. + mHangingChildDocuments.Clear(); + MOZ_ASSERT(mDocument, "Illicit document shutdown"); + if (!mDocument) { + return; + } + + // 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 invalidation list of the document after all accessible tree + // mutation is done. + mDocument->ProcessInvalidationList(); + + // Process relocation list. + for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) { + // owner should be in a document and have na associated DOM node (docs + // sometimes don't) + if (mRelocations[idx]->IsInDocument() && + mRelocations[idx]->HasOwnContent()) { + mDocument->DoARIAOwnsRelocation(mRelocations[idx]); + } + } + mRelocations.Clear(); + + // Process only currently queued generic notifications. + // These are used for processing aria-activedescendant, DOMMenuItemActive, + // etc. Therefore, they must be processed after relocations, since relocated + // subtrees might not have been created before relocation processing and the + // target might be inside a relocated subtree. + const nsTArray<RefPtr<Notification>> notifications = + std::move(mNotifications); + + uint32_t notificationCount = notifications.Length(); + for (uint32_t idx = 0; idx < notificationCount; idx++) { + notifications[idx]->Process(); + if (!mDocument) return; + } + + // 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 = std::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(); + MOZ_DIAGNOSTIC_ASSERT(parentIPCDoc); + uint64_t id = reinterpret_cast<uintptr_t>(parent->UniqueID()); + MOZ_DIAGNOSTIC_ASSERT(id); + DocAccessibleChild* ipcDoc = childDoc->IPCDoc(); + if (ipcDoc) { + parentIPCDoc->SendBindChildDoc(ipcDoc, id); + continue; + } + + ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager()); + childDoc->SetIPCDoc(ipcDoc); + +#if defined(XP_WIN) + parentIPCDoc->ConstructChildDocInParentProcess( + ipcDoc, id, AccessibleWrap::GetChildIDFor(childDoc)); +#else + nsCOMPtr<nsIBrowserChild> browserChild = + do_GetInterface(mDocument->DocumentNode()->GetDocShell()); + if (browserChild) { + static_cast<BrowserChild*>(browserChild.get()) + ->SendPDocAccessibleConstructor(ipcDoc, parentIPCDoc, id, 0, 0); + ipcDoc->SendPDocAccessiblePlatformExtConstructor(); + } +#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, FlushType::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, RefPtr{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 0000000000..0a5ebee659 --- /dev/null +++ b/accessible/base/NotificationController.h @@ -0,0 +1,462 @@ +/* -*- 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/Tuple.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefreshObservers.h" + +#include <utility> + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +namespace mozilla { + +class PresShell; + +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(std::index_sequence_for<Args...>{}); + } + + private: + TNotification(const TNotification&); + TNotification& operator=(const TNotification&); + + template <size_t... Indices> + void ProcessHelper(std::index_sequence<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, PresShell* 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, + nsTArray<nsCOMPtr<nsIContent>>& aInsertions); + + /** + * Pend an accessible subtree relocation. + */ + void ScheduleRelocation(Accessible* aOwner) { + if (!mRelocations.Contains(aOwner)) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + 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... Args> + inline void HandleNotification( + Class* aInstance, + typename TNotification<Class, Args...>::Callback aMethod, + Args*... aArgs) { + if (!IsUpdatePending()) { +#ifdef A11Y_LOG + if (mozilla::a11y::logging::IsEnabled( + mozilla::a11y::logging::eNotifications)) + mozilla::a11y::logging::Text("sync notification processing"); +#endif + (aInstance->*aMethod)(aArgs...); + return; + } + + RefPtr<Notification> notification = + new TNotification<Class, Args...>(aInstance, aMethod, aArgs...); + if (notification) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + 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) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mNotifications.AppendElement(notification); + ScheduleProcessing(); + } + } + + template <class Class, class Arg> + inline void ScheduleNotification( + Class* aInstance, typename TNotification<Class, Arg>::Callback aMethod, + Arg* aArg) { + RefPtr<Notification> notification = + new TNotification<Class, Arg>(aInstance, aMethod, aArg); + if (notification) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + 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(); + + /** + * Return true if we should wait for processing from the parent before we can + * process our own queue. + */ + bool WaitingForParent(); + + 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(std::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. + */ + PresShell* 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)) {} + nsCOMPtrHashKey(nsCOMPtrHashKey<T>&& aOther) + : PLDHashEntryHdr(std::move(aOther)), mKey(std::move(aOther.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/Pivot.cpp b/accessible/base/Pivot.cpp new file mode 100644 index 0000000000..f654fb9333 --- /dev/null +++ b/accessible/base/Pivot.cpp @@ -0,0 +1,596 @@ +/* -*- 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 "Pivot.h" + +#include "AccIterator.h" +#include "Accessible.h" +#include "DocAccessible.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" + +#include "mozilla/dom/ChildIterator.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Pivot +//////////////////////////////////////////////////////////////////////////////// + +Pivot::Pivot(const AccessibleOrProxy& aRoot) : mRoot(aRoot) { + MOZ_COUNT_CTOR(Pivot); +} + +Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot); } + +AccessibleOrProxy Pivot::AdjustStartPosition(AccessibleOrProxy& aAnchor, + PivotRule& aRule, + uint16_t* aFilterResult) { + AccessibleOrProxy matched = aAnchor; + *aFilterResult = aRule.Match(aAnchor); + + if (aAnchor != mRoot) { + for (AccessibleOrProxy temp = aAnchor.Parent(); + !temp.IsNull() && temp != mRoot; temp = temp.Parent()) { + uint16_t filtered = aRule.Match(temp); + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + *aFilterResult = filtered; + matched = temp; + } + } + } + + return matched; +} + +AccessibleOrProxy Pivot::SearchBackward(AccessibleOrProxy& aAnchor, + PivotRule& aRule, bool aSearchCurrent) { + // Initial position could be unset, in that case return null AoP. + if (aAnchor.IsNull()) { + return aAnchor; + } + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + + AccessibleOrProxy accOrProxy = AdjustStartPosition(aAnchor, aRule, &filtered); + + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return accOrProxy; + } + + while (accOrProxy != mRoot) { + AccessibleOrProxy parent = accOrProxy.Parent(); + int32_t idxInParent = accOrProxy.IndexInParent(); + while (idxInParent > 0) { + accOrProxy = parent.ChildAt(--idxInParent); + if (accOrProxy.IsNull()) { + continue; + } + + filtered = aRule.Match(accOrProxy); + + AccessibleOrProxy lastChild = accOrProxy.LastChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + !lastChild.IsNull()) { + parent = accOrProxy; + accOrProxy = lastChild; + idxInParent = accOrProxy.IndexInParent(); + filtered = aRule.Match(accOrProxy); + lastChild = accOrProxy.LastChild(); + } + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return accOrProxy; + } + } + + accOrProxy = parent; + if (accOrProxy.IsNull()) { + break; + } + + filtered = aRule.Match(accOrProxy); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return accOrProxy; + } + } + + return AccessibleOrProxy(); +} + +AccessibleOrProxy Pivot::SearchForward(AccessibleOrProxy& aAnchor, + PivotRule& aRule, bool aSearchCurrent) { + // Initial position could be not set, in that case begin search from root. + AccessibleOrProxy accOrProxy = !aAnchor.IsNull() ? aAnchor : mRoot; + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + accOrProxy = AdjustStartPosition(accOrProxy, aRule, &filtered); + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return accOrProxy; + } + + while (true) { + AccessibleOrProxy firstChild = accOrProxy.FirstChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + !firstChild.IsNull()) { + accOrProxy = firstChild; + filtered = aRule.Match(accOrProxy); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return accOrProxy; + } + firstChild = accOrProxy.FirstChild(); + } + + AccessibleOrProxy sibling = AccessibleOrProxy(); + AccessibleOrProxy temp = accOrProxy; + do { + if (temp == mRoot) { + break; + } + + sibling = temp.NextSibling(); + + if (!sibling.IsNull()) { + break; + } + temp = temp.Parent(); + } while (!temp.IsNull()); + + if (sibling.IsNull()) { + break; + } + + accOrProxy = sibling; + filtered = aRule.Match(accOrProxy); + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return accOrProxy; + } + } + + return AccessibleOrProxy(); +} + +// TODO: This method does not work for proxy accessibles +HyperTextAccessible* Pivot::SearchForText(Accessible* aAnchor, bool aBackward) { + if (!mRoot.IsAccessible()) { + return nullptr; + } + Accessible* accessible = aAnchor; + 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 == mRoot.AsAccessible()) { + break; + } + + // Unlike traditional pre-order traversal we revisit the parent + // nodes when we go up the tree. This is because our starting point + // may be a subtree or a leaf. If it's parent matches, it should + // take precedent over a sibling. + if (temp != aAnchor && temp->IsHyperText()) { + return temp->AsHyperText(); + } + + if (sibling) { + break; + } + + sibling = aBackward ? temp->PrevSibling() : temp->NextSibling(); + } while ((temp = temp->Parent())); + + if (!sibling) { + break; + } + + accessible = sibling; + if (accessible->IsHyperText()) { + return accessible->AsHyperText(); + } + } + + return nullptr; +} + +AccessibleOrProxy Pivot::Next(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchForward(aAnchor, aRule, aIncludeStart); +} + +AccessibleOrProxy Pivot::Prev(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchBackward(aAnchor, aRule, aIncludeStart); +} + +AccessibleOrProxy Pivot::First(PivotRule& aRule) { + return SearchForward(mRoot, aRule, true); +} + +AccessibleOrProxy Pivot::Last(PivotRule& aRule) { + AccessibleOrProxy lastAccOrProxy = mRoot; + + // First go to the last accessible in pre-order + while (lastAccOrProxy.HasChildren()) { + lastAccOrProxy = lastAccOrProxy.LastChild(); + } + + // Search backwards from last accessible and find the last occurrence in the + // doc + return SearchBackward(lastAccOrProxy, aRule, true); +} + +// TODO: This method does not work for proxy accessibles +Accessible* Pivot::NextText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType) { + if (!mRoot.IsAccessible()) { + return nullptr; + } + + int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset; + Accessible* tempPosition = aAnchor; + + // if we're starting on a text leaf, translate the offsets to the + // HyperTextAccessible parent and start from there. + if (aAnchor->IsTextLeaf() && aAnchor->Parent() && + aAnchor->Parent()->IsHyperText()) { + HyperTextAccessible* text = aAnchor->Parent()->AsHyperText(); + tempPosition = text; + int32_t childOffset = text->GetChildOffset(aAnchor); + if (tempEnd == -1) { + tempStart = 0; + tempEnd = 0; + } + tempStart += childOffset; + tempEnd += childOffset; + } + + while (true) { + MOZ_ASSERT(tempPosition); + 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 nullptr; + } + + 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 == mRoot.AsAccessible()) { + return nullptr; + } + + // 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 nullptr; + } + + tempStart = tempEnd = -1; + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundaryType) { + case nsIAccessiblePivot::CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case nsIAccessiblePivot::WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + case nsIAccessiblePivot::LINE_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_LINE_START; + endBoundary = nsIAccessibleText::BOUNDARY_LINE_END; + break; + default: + return nullptr; + } + + 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->IsHyperText()) { + 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->IsHyperText() && + tempStart == static_cast<int32_t>(childAtOffset->StartOffset())) { + tempPosition = childAtOffset; + tempStart = tempEnd = -1; + continue; + } + + *aStartOffset = tempStart; + *aEndOffset = tempEnd; + + MOZ_ASSERT(tempPosition); + return tempPosition; + } +} + +// TODO: This method does not work for proxy accessibles +Accessible* Pivot::PrevText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType) { + if (!mRoot.IsAccessible()) { + return nullptr; + } + + int32_t tempStart = *aStartOffset, tempEnd = *aEndOffset; + Accessible* tempPosition = aAnchor; + + // if we're starting on a text leaf, translate the offsets to the + // HyperTextAccessible parent and start from there. + if (aAnchor->IsTextLeaf() && aAnchor->Parent() && + aAnchor->Parent()->IsHyperText()) { + HyperTextAccessible* text = aAnchor->Parent()->AsHyperText(); + tempPosition = text; + int32_t childOffset = text->GetChildOffset(aAnchor); + if (tempStart == -1) { + tempStart = nsAccUtils::TextLength(aAnchor); + tempEnd = tempStart; + } + tempStart += childOffset; + tempEnd += childOffset; + } + + while (true) { + MOZ_ASSERT(tempPosition); + + 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 nullptr; + } + + 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 offset + // of that node. Otherwise we just default to the last offset in the parent. + if (tempStart == -1) { + if (tempPosition != curPosition && text == curPosition->Parent()) { + tempStart = text->GetChildOffset(curPosition) + + nsAccUtils::TextLength(curPosition); + } else { + tempStart = text->CharacterCount(); + } + } + + // 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 == mRoot.AsAccessible()) { + return nullptr; + } + + // 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 nullptr; + } + + tempPosition = tempText; + tempStart = tempEnd = tempText->CharacterCount(); + } + continue; + } + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundaryType) { + case nsIAccessiblePivot::CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case nsIAccessiblePivot::WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + case nsIAccessiblePivot::LINE_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_LINE_START; + endBoundary = nsIAccessibleText::BOUNDARY_LINE_END; + break; + default: + return nullptr; + } + + 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; + } + + *aStartOffset = tempStart; + *aEndOffset = tempEnd; + + MOZ_ASSERT(tempPosition); + return tempPosition; + } +} + +AccessibleOrProxy Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) { + AccessibleOrProxy match = AccessibleOrProxy(); + AccessibleOrProxy child = + mRoot.ChildAtPoint(aX, aY, Accessible::eDeepestChild); + while (!child.IsNull() && (mRoot != child)) { + uint16_t filtered = aRule.Match(child); + + // Ignore any matching nodes that were below this one + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + match = AccessibleOrProxy(); + } + + // Match if no node below this is a match + if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && + match.IsNull()) { + nsIntRect childRect = child.IsAccessible() + ? child.AsAccessible()->Bounds() + : child.AsProxy()->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 (childRect.Contains(aX, aY)) { + match = child; + } + } + + child = child.Parent(); + } + + return match; +} + +// Role Rule + +PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole) + : mRole(aRole), mDirectDescendantsFrom(nullptr) {} + +PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole, + AccessibleOrProxy& aDirectDescendantsFrom) + : mRole(aRole), mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +uint16_t PivotRoleRule::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAccOrProxy)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (!mDirectDescendantsFrom.IsNull() && + (aAccOrProxy != mDirectDescendantsFrom)) { + // If we've specified mDirectDescendantsFrom, we should ignore + // non-direct descendants of from the specified AoP. Because + // pivot performs a preorder traversal, the first aAccOrProxy + // object(s) that don't equal mDirectDescendantsFrom will be + // mDirectDescendantsFrom's children. We'll process them, but ignore + // their subtrees thereby processing direct descendants of + // mDirectDescendantsFrom only. + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (aAccOrProxy.Role() == mRole) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} diff --git a/accessible/base/Pivot.h b/accessible/base/Pivot.h new file mode 100644 index 0000000000..e4b5b1c058 --- /dev/null +++ b/accessible/base/Pivot.h @@ -0,0 +1,108 @@ +/* -*- 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_Pivot_h_ +#define mozilla_a11y_Pivot_h_ + +#include <stdint.h> +#include "Role.h" +#include "mozilla/dom/ChildIterator.h" +#include "AccessibleOrProxy.h" + +namespace mozilla { +namespace a11y { + +class Accessible; +class HyperTextAccessible; +class DocAccessible; + +class PivotRule { + public: + // A filtering function that returns a bitmask from + // nsIAccessibleTraversalRule: FILTER_IGNORE (0x0): Don't match this + // accessible. FILTER_MATCH (0x1): Match this accessible FILTER_IGNORE_SUBTREE + // (0x2): Ignore accessible's subtree. + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) = 0; +}; + +// The Pivot class is used for searching for accessible nodes in a given subtree +// with a given criteria. Since it only holds a weak reference to the root, +// this class is meant to be used primarily on the stack. +class Pivot final { + public: + explicit Pivot(const AccessibleOrProxy& aRoot); + Pivot() = delete; + Pivot(const Pivot&) = delete; + Pivot& operator=(const Pivot&) = delete; + + ~Pivot(); + + // Return the next accessible after aAnchor in pre-order that matches the + // given rule. If aIncludeStart, return aAnchor if it matches the rule. + AccessibleOrProxy Next(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aIncludeStart = false); + + // Return the previous accessible before aAnchor in pre-order that matches the + // given rule. If aIncludeStart, return aAnchor if it matches the rule. + AccessibleOrProxy Prev(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aIncludeStart = false); + + // Return the first accessible within the root that matches the pivot rule. + AccessibleOrProxy First(PivotRule& aRule); + + // Return the last accessible within the root that matches the pivot rule. + AccessibleOrProxy Last(PivotRule& aRule); + + // Return the next range of text according to the boundary type. + Accessible* NextText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType); + + // Return the previous range of text according to the boundary type. + Accessible* PrevText(Accessible* aAnchor, int32_t* aStartOffset, + int32_t* aEndOffset, int32_t aBoundaryType); + + // Return the accessible at the given screen coordinate if it matches the + // pivot rule. + AccessibleOrProxy AtPoint(int32_t aX, int32_t aY, PivotRule& aRule); + + private: + AccessibleOrProxy AdjustStartPosition(AccessibleOrProxy& aAnchor, + PivotRule& aRule, + uint16_t* aFilterResult); + + // Search in preorder for the first accessible to match the rule. + AccessibleOrProxy SearchForward(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + // Reverse search in preorder for the first accessible to match the rule. + AccessibleOrProxy SearchBackward(AccessibleOrProxy& aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + // Search in preorder for the first text accessible. + HyperTextAccessible* SearchForText(Accessible* aAnchor, bool aBackward); + + AccessibleOrProxy mRoot; +}; + +/** + * This rule matches accessibles on a given role, filtering out non-direct + * descendants if necessary. + */ +class PivotRoleRule : public PivotRule { + public: + explicit PivotRoleRule(role aRole); + explicit PivotRoleRule(role aRole, AccessibleOrProxy& aDirectDescendantsFrom); + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + protected: + role mRole; + AccessibleOrProxy mDirectDescendantsFrom; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_Pivot_h_ diff --git a/accessible/base/Platform.h b/accessible/base/Platform.h new file mode 100644 index 0000000000..d2fefea68b --- /dev/null +++ b/accessible/base/Platform.h @@ -0,0 +1,160 @@ +/* -*- 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> +#include "nsStringFwd.h" + +#if defined(ANDROID) +# include "nsTArray.h" +# include "nsRect.h" +#endif + +#ifdef MOZ_WIDGET_COCOA +# include "mozilla/a11y/Role.h" +#endif + +#if defined(XP_WIN) +# include "Units.h" +#endif + +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 + +#if defined(XP_WIN) +/* + * Do we have AccessibleHandler.dll registered. + */ +bool IsHandlerRegistered(); + +/* + * Name of platform service that instantiated accessibility + */ +void SetInstantiator(const uint32_t aInstantiatorPid); +bool GetInstantiator(nsIFile** aOutInstantiator); +#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); + +#if defined(XP_WIN) +void ProxyFocusEvent(ProxyAccessible* aTarget, + const LayoutDeviceIntRect& aCaretRect); +void ProxyCaretMoveEvent(ProxyAccessible* aTarget, + const LayoutDeviceIntRect& aCaretRect); +#else +void ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed); +#endif +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); + +#if defined(ANDROID) +MOZ_CAN_RUN_SCRIPT +void ProxyVirtualCursorChangeEvent(ProxyAccessible* aTarget, + ProxyAccessible* aOldPosition, + int32_t aOldStartOffset, + int32_t aOldEndOffset, + ProxyAccessible* aNewPosition, + int32_t aNewStartOffset, + int32_t aNewEndOffset, int16_t aReason, + int16_t aBoundaryType, bool aFromUser); + +void ProxyScrollingEvent(ProxyAccessible* aTarget, uint32_t aEventType, + uint32_t aScrollX, uint32_t aScrollY, + uint32_t aMaxScrollX, uint32_t aMaxScrollY); + +void ProxyAnnouncementEvent(ProxyAccessible* aTarget, + const nsString& aAnnouncement, uint16_t aPriority); + +class BatchData; + +void ProxyBatch(ProxyAccessible* aDocument, const uint64_t aBatchType, + const nsTArray<ProxyAccessible*>& aAccessibles, + const nsTArray<BatchData>& aData); + +bool LocalizeString( + const char* aToken, nsAString& aLocalized, + const nsTArray<nsString>& aFormatString = nsTArray<nsString>()); +#endif + +#ifdef MOZ_WIDGET_COCOA +class TextRangeData; +void ProxyTextSelectionChangeEvent(ProxyAccessible* aTarget, + const nsTArray<TextRangeData>& aSelection); + +void ProxyRoleChangedEvent(ProxyAccessible* aTarget, const a11y::role& aRole); +#endif + +} // 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 0000000000..7b7f521eaf --- /dev/null +++ b/accessible/base/Relation.h @@ -0,0 +1,98 @@ +/* -*- 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(std::move(aOther.mFirstIter)), mLastIter(aOther.mLastIter) { + aOther.mLastIter = nullptr; + } + + Relation& operator=(Relation&& aRH) { + mFirstIter = std::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 0000000000..e59cf1f7b8 --- /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 0000000000..eecb184ed8 --- /dev/null +++ b/accessible/base/RelationTypeMap.h @@ -0,0 +1,87 @@ +/* -*- 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_WINDOW, "containing window", ATK_RELATION_NULL, + NAVRELATION_CONTAINING_WINDOW, IA2_RELATION_CONTAINING_WINDOW) + +RELATIONTYPE(CONTAINING_APPLICATION, "containing application", + ATK_RELATION_NULL, NAVRELATION_CONTAINING_APPLICATION, + IA2_RELATION_CONTAINING_APPLICATION) + +RELATIONTYPE(DETAILS, "details", ATK_RELATION_DETAILS, NAVRELATION_DETAILS, + IA2_RELATION_DETAILS) + +RELATIONTYPE(DETAILS_FOR, "details for", ATK_RELATION_DETAILS_FOR, + NAVRELATION_DETAILS_FOR, IA2_RELATION_DETAILS_FOR) + +RELATIONTYPE(ERRORMSG, "error", ATK_RELATION_ERROR_MESSAGE, NAVRELATION_ERROR, + IA2_RELATION_ERROR) + +RELATIONTYPE(ERRORMSG_FOR, "error for", ATK_RELATION_ERROR_FOR, + NAVRELATION_ERROR_FOR, IA2_RELATION_ERROR_FOR) diff --git a/accessible/base/Role.h b/accessible/base/Role.h new file mode 100644 index 0000000000..5c0a18cab2 --- /dev/null +++ b/accessible/base/Role.h @@ -0,0 +1,1090 @@ +/* -*- 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. For role="document", see NON_NATIVE_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 and xul:tree cell. + * 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; a popup button with an associated list box that + * provides a set of predefined choices. It is used for html:select with a + * size of 1 and xul:menulist. See also ROLE_EDITCOMBOBOX. + */ + 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 html:progress, + * 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 input[type=number] spin buttons. + */ + 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, + + /** + * An element containing content that assistive technology users may want to + * browse in a reading mode, rather than a focus/interactive/application mode. + * This role is used for role="document". For the container which holds the + * content of a web page, see DOCUMENT. + */ + NON_NATIVE_DOCUMENT = 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, + + /** + * An ARIA landmark. See related NAVIGATION role. + */ + LANDMARK = 169, + + /** + * A specific type of ARIA landmark. The ability to distinguish navigation + * landmarks from other types of landmarks is, for example, needed on macOS + * where specific AXSubrole and AXRoleDescription for navigation landmarks + * are used. + */ + NAVIGATION = 170, + + /** + * An object that contains the text of a footnote. + */ + FOOTNOTE = 171, + + /** + * A complete or self-contained composition in a document, page, application, + * or site and that is, in principle, independently distributable or reusable, + * e.g. in syndication. + */ + ARTICLE = 172, + + /** + * A perceivable section containing content that is relevant to a specific, + * author-specified purpose and sufficiently important that users will likely + * want to be able to navigate to the section easily and to have it listed in + * a summary of the page. + */ + REGION = 173, + + /** + * Represents a control with a text input and a popup with a set of predefined + * choices. It is used for ARIA's combobox role. See also COMBOBOX. + */ + EDITCOMBOBOX = 174, + + /** + * A section of content that is quoted from another source. + */ + BLOCKQUOTE = 175, + + /** + * Content previously deleted or proposed for deletion, e.g. in revision + * history or a content view providing suggestions from reviewers. + */ + CONTENT_DELETION = 176, + + /** + * Content previously inserted or proposed for insertion, e.g. in revision + * history or a content view providing suggestions from reviewers. + */ + CONTENT_INSERTION = 177, + + /** + * An html:form element with a label provided by WAI-ARIA. + * This may also be used if role="form" with a label should be exposed + * differently in the future. + */ + FORM_LANDMARK = 178, + + /** + * The html:mark element. + * This is also used for the equivalent WAI-ARIA role. + */ + MARK = 179, + + /** + * The WAI-ARIA suggestion role. + */ + SUGGESTION = 180, + + /** + * The WAI-ARIA comment role. + */ + COMMENT = 181, + + /** + * A snippet of program code. ATs might want to treat this differently. + */ + CODE = 182, + + /** + * Represents control whose purpose is to allow a user to edit a time. + */ + TIME_EDITOR = 183, + + /** + * Represents the marker associated with a list item. In unordered lists, + * this is a bullet, while in ordered lists this is a number. + */ + LISTITEM_MARKER = 184, + + LAST_ROLE = LISTITEM_MARKER +}; + +} // namespace roles + +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 0000000000..8dbba7e8ec --- /dev/null +++ b/accessible/base/RoleMap.h @@ -0,0 +1,1879 @@ +/* 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/. */ + +// clang-format off +/** + * Usage: declare the macro ROLE()with the following arguments: + * ROLE(geckoRole, stringRole, atkRole, macRole, macSubrole, msaaRole, ia2Role, nameRule) + */ + +ROLE(NOTHING, + "nothing", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TITLEBAR, + "titlebar", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Irrelevant on OS X; windows are always native. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TITLEBAR, + ROLE_SYSTEM_TITLEBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + 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. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUBAR, + ROLE_SYSTEM_MENUBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + 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). + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SCROLLBAR, + ROLE_SYSTEM_SCROLLBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromValueRule) + +ROLE(GRIP, + "grip", + ATK_ROLE_UNKNOWN, + NSAccessibilitySplitterRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRIP, + ROLE_SYSTEM_GRIP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SOUND, + "sound", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SOUND, + ROLE_SYSTEM_SOUND, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CURSOR, + "cursor", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CURSOR, + ROLE_SYSTEM_CURSOR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CARET, + "caret", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CARET, + ROLE_SYSTEM_CARET, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ALERT, + "alert", + ATK_ROLE_ALERT, + NSAccessibilityGroupRole, + @"AXApplicationAlert", + ROLE_SYSTEM_ALERT, + ROLE_SYSTEM_ALERT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(WINDOW, + "window", + ATK_ROLE_WINDOW, + NSAccessibilityWindowRole, //Irrelevant on OS X; all window a11y is handled by the system. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_WINDOW, + ROLE_SYSTEM_WINDOW, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(INTERNAL_FRAME, + "internal frame", + ATK_ROLE_INTERNAL_FRAME, + NSAccessibilityScrollAreaRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_INTERNAL_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MENUPOPUP, + "menupopup", + ATK_ROLE_MENU, + NSAccessibilityMenuRole, //The parent of menuitems. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUPOPUP, + ROLE_SYSTEM_MENUPOPUP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MENUITEM, + "menuitem", + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(TOOLTIP, + "tooltip", + ATK_ROLE_TOOL_TIP, + NSAccessibilityGroupRole, + @"AXUserInterfaceTooltip", + ROLE_SYSTEM_TOOLTIP, + ROLE_SYSTEM_TOOLTIP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(APPLICATION, + "application", + ATK_ROLE_EMBEDDED, + NSAccessibilityGroupRole, //Unused on OS X. the system will take care of this. + @"AXLandmarkApplication", + ROLE_SYSTEM_APPLICATION, + ROLE_SYSTEM_APPLICATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DOCUMENT, + "document", + ATK_ROLE_DOCUMENT_WEB, + @"AXWebArea", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + 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, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CHART, + "chart", + ATK_ROLE_CHART, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHART, + ROLE_SYSTEM_CHART, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DIALOG, + "dialog", + ATK_ROLE_DIALOG, + NSAccessibilityGroupRole, //There's a dialog subrole. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIALOG, + ROLE_SYSTEM_DIALOG, + java::SessionAccessibility::CLASSNAME_DIALOG, + eNoNameRule) + +ROLE(BORDER, + "border", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BORDER, + ROLE_SYSTEM_BORDER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GROUPING, + "grouping", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(SEPARATOR, + "separator", + ATK_ROLE_SEPARATOR, + NSAccessibilitySplitterRole, + @"AXContentSeparator", + ROLE_SYSTEM_SEPARATOR, + ROLE_SYSTEM_SEPARATOR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TOOLBAR, + "toolbar", + ATK_ROLE_TOOL_BAR, + NSAccessibilityToolbarRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TOOLBAR, + ROLE_SYSTEM_TOOLBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(STATUSBAR, + "statusbar", + ATK_ROLE_STATUSBAR, + NSAccessibilityGroupRole, + @"AXApplicationStatus", + ROLE_SYSTEM_STATUSBAR, + ROLE_SYSTEM_STATUSBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TABLE, + "table", + ATK_ROLE_TABLE, + NSAccessibilityTableRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TABLE, + ROLE_SYSTEM_TABLE, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(COLUMNHEADER, + "columnheader", + ATK_ROLE_COLUMN_HEADER, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COLUMNHEADER, + ROLE_SYSTEM_COLUMNHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(ROWHEADER, + "rowheader", + ATK_ROLE_ROW_HEADER, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_ROWHEADER, + ROLE_SYSTEM_ROWHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(COLUMN, + "column", + ATK_ROLE_UNKNOWN, + NSAccessibilityColumnRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COLUMN, + ROLE_SYSTEM_COLUMN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(ROW, + "row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityRowRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_ROW, + ROLE_SYSTEM_ROW, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(CELL, + "cell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LINK, + "link", + ATK_ROLE_LINK, + NSAccessibilityLinkRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LINK, + ROLE_SYSTEM_LINK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(HELPBALLOON, + "helpballoon", + ATK_ROLE_UNKNOWN, + NSAccessibilityHelpTagRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_HELPBALLOON, + ROLE_SYSTEM_HELPBALLOON, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(CHARACTER, + "character", + ATK_ROLE_IMAGE, + NSAccessibilityUnknownRole, //Unused on OS X. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHARACTER, + ROLE_SYSTEM_CHARACTER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(LIST, + "list", + ATK_ROLE_LIST, + NSAccessibilityListRole, + NSAccessibilityContentListSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LISTITEM, + "listitem", + ATK_ROLE_LIST_ITEM, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(OUTLINE, + "outline", + ATK_ROLE_TREE, + NSAccessibilityOutlineRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(OUTLINEITEM, + "outlineitem", + ATK_ROLE_TREE_ITEM, + NSAccessibilityRowRole, + NSAccessibilityOutlineRowSubrole, + ROLE_SYSTEM_OUTLINEITEM, + ROLE_SYSTEM_OUTLINEITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(PAGETAB, + "pagetab", + ATK_ROLE_PAGE_TAB, + NSAccessibilityRadioButtonRole, + @"AXTabButton", // Can be upgraded to NSAccessibilityTabButtonSubrole in 10.13 + ROLE_SYSTEM_PAGETAB, + ROLE_SYSTEM_PAGETAB, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(PROPERTYPAGE, + "propertypage", + ATK_ROLE_SCROLL_PANE, + NSAccessibilityGroupRole, + @"AXTabPanel", + ROLE_SYSTEM_PROPERTYPAGE, + ROLE_SYSTEM_PROPERTYPAGE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(INDICATOR, + "indicator", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_INDICATOR, + ROLE_SYSTEM_INDICATOR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GRAPHIC, + "graphic", + ATK_ROLE_IMAGE, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(STATICTEXT, + "statictext", + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TEXT_LEAF, + "text leaf", + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PUSHBUTTON, + "pushbutton", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(CHECKBUTTON, + "checkbutton", + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(RADIOBUTTON, + "radiobutton", + ATK_ROLE_RADIO_BUTTON, + NSAccessibilityRadioButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_RADIOBUTTON, + ROLE_SYSTEM_RADIOBUTTON, + java::SessionAccessibility::CLASSNAME_RADIOBUTTON, + eNameFromSubtreeRule) + +// Equivalent of HTML select element with size="1". See also EDITCOMBOBOX. +ROLE(COMBOBOX, + "combobox", + ATK_ROLE_COMBO_BOX, + NSAccessibilityPopUpButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNameFromValueRule) + +ROLE(DROPLIST, + "droplist", + ATK_ROLE_COMBO_BOX, + NSAccessibilityPopUpButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DROPLIST, + ROLE_SYSTEM_DROPLIST, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PROGRESSBAR, + "progressbar", + ATK_ROLE_PROGRESS_BAR, + NSAccessibilityProgressIndicatorRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PROGRESSBAR, + ROLE_SYSTEM_PROGRESSBAR, + java::SessionAccessibility::CLASSNAME_PROGRESSBAR, + eNameFromValueRule) + +ROLE(DIAL, + "dial", + ATK_ROLE_DIAL, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIAL, + ROLE_SYSTEM_DIAL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(HOTKEYFIELD, + "hotkeyfield", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_HOTKEYFIELD, + ROLE_SYSTEM_HOTKEYFIELD, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SLIDER, + "slider", + ATK_ROLE_SLIDER, + NSAccessibilitySliderRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SLIDER, + ROLE_SYSTEM_SLIDER, + java::SessionAccessibility::CLASSNAME_SEEKBAR, + eNameFromValueRule) + +ROLE(SPINBUTTON, + "spinbutton", + ATK_ROLE_SPIN_BUTTON, + NSAccessibilityIncrementorRole, //Subroles: Increment/Decrement. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SPINBUTTON, + ROLE_SYSTEM_SPINBUTTON, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(DIAGRAM, + "diagram", + ATK_ROLE_IMAGE, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIAGRAM, + ROLE_SYSTEM_DIAGRAM, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(ANIMATION, + "animation", + ATK_ROLE_ANIMATION, + NSAccessibilityUnknownRole, + @"AXApplicationMarquee", + ROLE_SYSTEM_ANIMATION, + ROLE_SYSTEM_ANIMATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(EQUATION, + "equation", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(BUTTONDROPDOWN, + "buttondropdown", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityPopUpButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BUTTONDROPDOWN, + ROLE_SYSTEM_BUTTONDROPDOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(BUTTONMENU, + "buttonmenu", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityMenuButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BUTTONMENU, + ROLE_SYSTEM_BUTTONMENU, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNameFromSubtreeRule) + +ROLE(BUTTONDROPDOWNGRID, + "buttondropdowngrid", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BUTTONDROPDOWNGRID, + ROLE_SYSTEM_BUTTONDROPDOWNGRID, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(WHITESPACE, + "whitespace", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_WHITESPACE, + ROLE_SYSTEM_WHITESPACE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PAGETABLIST, + "pagetablist", + ATK_ROLE_PAGE_TAB_LIST, + NSAccessibilityTabGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PAGETABLIST, + ROLE_SYSTEM_PAGETABLIST, + java::SessionAccessibility::CLASSNAME_TABWIDGET, + eNoNameRule) + +ROLE(CLOCK, + "clock", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, //Unused on OS X + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CLOCK, + ROLE_SYSTEM_CLOCK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SPLITBUTTON, + "splitbutton", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SPLITBUTTON, + ROLE_SYSTEM_SPLITBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNoNameRule) + +ROLE(IPADDRESS, + "ipaddress", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_IPADDRESS, + ROLE_SYSTEM_IPADDRESS, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ACCEL_LABEL, + "accel label", + ATK_ROLE_ACCEL_LABEL, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ARROW, + "arrow", + ATK_ROLE_ARROW, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_INDICATOR, + ROLE_SYSTEM_INDICATOR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CANVAS, + "canvas", + ATK_ROLE_CANVAS, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_CANVAS, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(CHECK_MENU_ITEM, + "check menu item", + ATK_ROLE_CHECK_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_CHECK_MENU_ITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(COLOR_CHOOSER, + "color chooser", + ATK_ROLE_COLOR_CHOOSER, + NSAccessibilityColorWellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIALOG, + IA2_ROLE_COLOR_CHOOSER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DATE_EDITOR, + "date editor", + ATK_ROLE_DATE_EDITOR, + @"AXDateField", + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_DATE_EDITOR, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNoNameRule) + +ROLE(DESKTOP_ICON, + "desktop icon", + ATK_ROLE_DESKTOP_ICON, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_DESKTOP_ICON, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DESKTOP_FRAME, + "desktop frame", + ATK_ROLE_DESKTOP_FRAME, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_DESKTOP_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DIRECTORY_PANE, + "directory pane", + ATK_ROLE_DIRECTORY_PANE, + NSAccessibilityBrowserRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_DIRECTORY_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FILE_CHOOSER, + "file chooser", + ATK_ROLE_FILE_CHOOSER, + NSAccessibilityUnknownRole, //Unused on OS X + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_FILE_CHOOSER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FONT_CHOOSER, + "font chooser", + ATK_ROLE_FONT_CHOOSER, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_FONT_CHOOSER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CHROME_WINDOW, + "chrome window", + ATK_ROLE_FRAME, + NSAccessibilityGroupRole, //Contains the main Firefox UI + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_APPLICATION, + IA2_ROLE_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GLASS_PANE, + "glass pane", + ATK_ROLE_GLASS_PANE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_GLASS_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(HTML_CONTAINER, + "html container", + ATK_ROLE_HTML_CONTAINER, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(ICON, + "icon", + ATK_ROLE_ICON, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PUSHBUTTON, + IA2_ROLE_ICON, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(LABEL, + "label", + ATK_ROLE_LABEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + IA2_ROLE_LABEL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(LAYERED_PANE, + "layered pane", + ATK_ROLE_LAYERED_PANE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_LAYERED_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(OPTION_PANE, + "option pane", + ATK_ROLE_OPTION_PANE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_OPTION_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PASSWORD_TEXT, + "password text", + ATK_ROLE_PASSWORD_TEXT, + NSAccessibilityTextFieldRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNoNameRule) + +ROLE(POPUP_MENU, + "popup menu", + ATK_ROLE_POPUP_MENU, + NSAccessibilityUnknownRole, //Unused + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUPOPUP, + ROLE_SYSTEM_MENUPOPUP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(RADIO_MENU_ITEM, + "radio menu item", + ATK_ROLE_RADIO_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_RADIO_MENU_ITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(ROOT_PANE, + "root pane", + ATK_ROLE_ROOT_PANE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_ROOT_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SCROLL_PANE, + "scroll pane", + ATK_ROLE_SCROLL_PANE, + NSAccessibilityScrollAreaRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_SCROLL_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SPLIT_PANE, + "split pane", + ATK_ROLE_SPLIT_PANE, + NSAccessibilitySplitGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_SPLIT_PANE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TABLE_COLUMN_HEADER, + "table column header", + ATK_ROLE_TABLE_COLUMN_HEADER, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COLUMNHEADER, + ROLE_SYSTEM_COLUMNHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(TABLE_ROW_HEADER, + "table row header", + ATK_ROLE_TABLE_ROW_HEADER, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_ROWHEADER, + ROLE_SYSTEM_ROWHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(TEAR_OFF_MENU_ITEM, + "tear off menu item", + ATK_ROLE_TEAR_OFF_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_TEAR_OFF_MENU, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(TERMINAL, + "terminal", + ATK_ROLE_TERMINAL, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_TERMINAL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TEXT_CONTAINER, + "text container", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TOGGLE_BUTTON, + "toggle button", + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + NSAccessibilityToggleSubrole, + ROLE_SYSTEM_PUSHBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + java::SessionAccessibility::CLASSNAME_TOGGLEBUTTON, + eNameFromSubtreeRule) + +ROLE(TREE_TABLE, + "tree table", + ATK_ROLE_TREE_TABLE, + NSAccessibilityTableRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNoNameRule) + +ROLE(VIEWPORT, + "viewport", + ATK_ROLE_VIEWPORT, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PANE, + IA2_ROLE_VIEW_PORT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(HEADER, + "header", + ATK_ROLE_HEADER, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_HEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FOOTER, + "footer", + ATK_ROLE_FOOTER, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_FOOTER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PARAGRAPH, + "paragraph", + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_PARAGRAPH, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(RULER, + "ruler", + ATK_ROLE_RULER, + NSAccessibilityRulerRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_RULER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(AUTOCOMPLETE, + "autocomplete", + ATK_ROLE_AUTOCOMPLETE, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNoNameRule) + +ROLE(EDITBAR, + "editbar", + ATK_ROLE_EDITBAR, + NSAccessibilityTextFieldRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + IA2_ROLE_EDITBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ENTRY, + "entry", + ATK_ROLE_ENTRY, + NSAccessibilityTextFieldRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(CAPTION, + "caption", + ATK_ROLE_CAPTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_CAPTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(NON_NATIVE_DOCUMENT, + "non-native document", + ATK_ROLE_DOCUMENT_FRAME, + NSAccessibilityGroupRole, + @"AXDocument", + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(HEADING, + "heading", + ATK_ROLE_HEADING, + @"AXHeading", + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_HEADING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(PAGE, + "page", + ATK_ROLE_PAGE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_PAGE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SECTION, + "section", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_SECTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(REDUNDANT_OBJECT, + "redundant object", + ATK_ROLE_REDUNDANT_OBJECT, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_REDUNDANT_OBJECT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FORM, + "form", + ATK_ROLE_FORM, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_FORM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(IME, + "ime", + ATK_ROLE_INPUT_METHOD_WINDOW, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_INPUT_METHOD_WINDOW, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(APP_ROOT, + "app root", + ATK_ROLE_APPLICATION, + NSAccessibilityUnknownRole, //Unused on OS X + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_APPLICATION, + ROLE_SYSTEM_APPLICATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PARENT_MENUITEM, + "parent menuitem", + ATK_ROLE_MENU, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(CALENDAR, + "calendar", + ATK_ROLE_CALENDAR, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CLIENT, + ROLE_SYSTEM_CLIENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(COMBOBOX_LIST, + "combobox list", + ATK_ROLE_MENU, + NSAccessibilityMenuRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(COMBOBOX_OPTION, + "combobox option", + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(IMAGE_MAP, + "image map", + ATK_ROLE_IMAGE, + @"AXImageMap", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(OPTION, + "listbox option", + ATK_ROLE_LIST_ITEM, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(RICH_OPTION, + "listbox rich option", + ATK_ROLE_LIST_ITEM, + NSAccessibilityRowRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(LISTBOX, + "listbox", + ATK_ROLE_LIST_BOX, + NSAccessibilityListRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNoNameRule) + +ROLE(FLAT_EQUATION, + "flat equation", + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + @"AXDocumentMath", + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GRID_CELL, + "gridcell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(EMBEDDED_OBJECT, + "embedded object", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_EMBEDDED_OBJECT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(NOTE, + "note", + ATK_ROLE_COMMENT, + NSAccessibilityGroupRole, + @"AXDocumentNote", + USE_ROLE_STRING, + IA2_ROLE_NOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(FIGURE, + "figure", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CHECK_RICH_OPTION, + "check rich option", + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(DEFINITION_LIST, + "definitionlist", + ATK_ROLE_LIST, + NSAccessibilityListRole, + @"AXDescriptionList", + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TERM, + "term", + ATK_ROLE_DESCRIPTION_TERM, + NSAccessibilityGroupRole, + @"AXTerm", + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(DEFINITION, + "definition", + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + @"AXDescription", + USE_ROLE_STRING, + IA2_ROLE_PARAGRAPH, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(KEY, + "key", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(SWITCH, + "switch", + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + NSAccessibilitySwitchSubrole, + ROLE_SYSTEM_CHECKBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(MATHML_MATH, + "math", + ATK_ROLE_MATH, + NSAccessibilityGroupRole, + @"AXDocumentMath", + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_IDENTIFIER, + "mathml identifier", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathIdentifier", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_NUMBER, + "mathml number", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathNumber", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_OPERATOR, + "mathml operator", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathOperator", + // XXX: NSAccessibility also uses subroles AXMathSeparatorOperator and + // AXMathFenceOperator. We should use the NS_MATHML_OPERATOR_FENCE and + // NS_MATHML_OPERATOR_SEPARATOR bits of nsOperatorFlags, but currently they + // are only available from the MathML layout code. Hence we just fallback + // to subrole AXMathOperator for now. + // XXX bug 1175747 WebKit also creates anonymous operators for <mfenced> + // which have subroles AXMathSeparatorOperator and AXMathFenceOperator. + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_TEXT, + "mathml text", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_STRING_LITERAL, + "mathml string literal", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_GLYPH, + "mathml glyph", + ATK_ROLE_IMAGE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNameFromSubtreeRule) + +ROLE(MATHML_ROW, + "mathml row", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_FRACTION, + "mathml fraction", + ATK_ROLE_MATH_FRACTION, + NSAccessibilityGroupRole, + @"AXMathFraction", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SQUARE_ROOT, + "mathml square root", + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + @"AXMathSquareRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ROOT, + "mathml root", + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + @"AXMathRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_FENCED, + "mathml fenced", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathFenced", // XXX bug 1176970 This should be AXMathFence, but doing so without implementing the whole fence interface seems to make VoiceOver crash, so we present it as a row for now. + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ENCLOSED, + "mathml enclosed", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STYLE, + "mathml style", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUB, + "mathml sub", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUP, + "mathml sup", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUB_SUP, + "mathml sub sup", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_UNDER, + "mathml under", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_OVER, + "mathml over", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_UNDER_OVER, + "mathml under over", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_MULTISCRIPTS, + "mathml multiscripts", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathMultiscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_TABLE, + "mathml table", + ATK_ROLE_TABLE, + NSAccessibilityGroupRole, + @"AXMathTable", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNoNameRule) + +ROLE(MATHML_LABELED_ROW, + "mathml labeled row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_TABLE_ROW, + "mathml table row", + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + @"AXMathTableRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_CELL, + "mathml cell", + ATK_ROLE_TABLE_CELL, + NSAccessibilityGroupRole, + @"AXMathTableCell", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ACTION, + "mathml action", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ERROR, + "mathml error", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK, + "mathml stack", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_LONG_DIVISION, + "mathml long division", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_GROUP, + "mathml stack group", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_ROW, + "mathml stack row", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_CARRIES, + "mathml stack carries", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_CARRY, + "mathml stack carry", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_LINE, + "mathml stack line", + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(RADIO_GROUP, + "grouping", + ATK_ROLE_PANEL, + NSAccessibilityRadioGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TEXT, + "text", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(DETAILS, + "details", + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + @"AXDetails", + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SUMMARY, + "summary", + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + @"AXSummary", + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(LANDMARK, + "landmark", + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_LANDMARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(NAVIGATION, + "navigation", + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkNavigation", + USE_ROLE_STRING, + IA2_ROLE_LANDMARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FOOTNOTE, + "footnote", + ATK_ROLE_FOOTNOTE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_FOOTNOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ARTICLE, + "article", + ATK_ROLE_ARTICLE, + NSAccessibilityGroupRole, + @"AXDocumentArticle", + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(REGION, + "region", + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkRegion", + USE_ROLE_STRING, + IA2_ROLE_LANDMARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +// A composite widget with a text input and popup. Used for ARIA role combobox. +// See also COMBOBOX. +ROLE(EDITCOMBOBOX, + "editcombobox", + ATK_ROLE_COMBO_BOX, + NSAccessibilityComboBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(BLOCKQUOTE, + "blockquote", + ATK_ROLE_BLOCK_QUOTE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_BLOCK_QUOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CONTENT_DELETION, + "content deletion", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXDeleteStyleGroup", + USE_ROLE_STRING, + IA2_ROLE_CONTENT_DELETION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CONTENT_INSERTION, + "content insertion", + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXInsertStyleGroup", + USE_ROLE_STRING, + IA2_ROLE_CONTENT_INSERTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FORM_LANDMARK, + "form", + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkForm", + USE_ROLE_STRING, + IA2_ROLE_FORM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MARK, + "mark", + ATK_ROLE_MARK, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_MARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(SUGGESTION, + "suggestion", + ATK_ROLE_SUGGESTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_SUGGESTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(COMMENT, + "comment", + ATK_ROLE_COMMENT, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + USE_ROLE_STRING, + IA2_ROLE_COMMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CODE, + "code", + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXCodeStyleGroup", + USE_ROLE_STRING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TIME_EDITOR, + "time editor", + ATK_ROLE_PANEL, + @"AXTimeField", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LISTITEM_MARKER, + "list item marker", + ATK_ROLE_UNKNOWN, + @"AXListMarker", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) +// clang-format on diff --git a/accessible/base/SelectionManager.cpp b/accessible/base/SelectionManager.cpp new file mode 100644 index 0000000000..0c95f6a206 --- /dev/null +++ b/accessible/base/SelectionManager.cpp @@ -0,0 +1,207 @@ +/* -*- 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 "mozilla/PresShell.h" +#include "mozilla/dom/Document.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. + if (mCurrCtrlNormalSel) { + mCurrCtrlNormalSel->RemoveSelectionListener(this); + mCurrCtrlNormalSel = nullptr; + } + + // Remove 'this' registered as selection listener for the spellcheck + // selection. + if (mCurrCtrlSpellSel) { + mCurrCtrlSpellSel->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. + Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal); + normalSel->AddSelectionListener(this); + mCurrCtrlNormalSel = normalSel; + + // Register 'this' as selection listener for the spell check selection. + Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck); + spellSel->AddSelectionListener(this); + mCurrCtrlSpellSel = spellSel; +} + +void SelectionManager::AddDocSelectionListener(PresShell* 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(PresShell* 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, selection->IsCollapsed(), + aEvent->FromUserInput()); + nsEventShell::FireEvent(caretMoveEvent); + } +} + +NS_IMETHODIMP +SelectionManager::NotifySelectionChanged(dom::Document* aDocument, + Selection* aSelection, + int16_t aReason) { + if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + DocAccessible* document = GetAccService()->GetDocAccessible(aDocument); + +#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, 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->GetClosestCommonInclusiveAncestor(); + } + + 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) { + // FIXME bug 1126649 + NS_ERROR("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); + } +} + +SelectionManager::~SelectionManager() = default; diff --git a/accessible/base/SelectionManager.h b/accessible/base/SelectionManager.h new file mode 100644 index 0000000000..34a841d525 --- /dev/null +++ b/accessible/base/SelectionManager.h @@ -0,0 +1,130 @@ +/* -*- 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 "nsISelectionListener.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla { + +class PresShell; + +namespace dom { +class Element; +class Selection; +} // namespace dom + +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(PresShell* aPresShell); + + /** + * Stop listening to selection events for a given document + */ + void RemoveDocSelectionListener(PresShell* aPresShell); + + /** + * 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; + } + + ~SelectionManager(); + + protected: + SelectionManager(); + + /** + * Process DOM selection change. Fire selection and caret move events. + */ + void ProcessSelectionChanged(SelData* aSelData); + + private: + // Currently focused control. + int32_t mCaretOffset; + HyperTextAccessible* mAccWithCaret; + WeakPtr<dom::Selection> mCurrCtrlNormalSel; + WeakPtr<dom::Selection> mCurrCtrlSpellSel; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/States.h b/accessible/base/States.h new file mode 100644 index 0000000000..5aa7db28cf --- /dev/null +++ b/accessible/base/States.h @@ -0,0 +1,297 @@ +/* -*- 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; + +/** + * The object is the current item within a container or set of related elements. + */ +const uint64_t CURRENT = ((uint64_t)0x1) << 48; + +/** + * Not a real state, used for static assertions. + */ +const uint64_t LAST_ENTRY = CURRENT; + +} // namespace states +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/Statistics.h b/accessible/base/Statistics.h new file mode 100644 index 0000000000..19f7166317 --- /dev/null +++ b/accessible/base/Statistics.h @@ -0,0 +1,42 @@ +/* -*- 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 0000000000..294ed50562 --- /dev/null +++ b/accessible/base/StyleInfo.cpp @@ -0,0 +1,107 @@ +/* -*- 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) : mElement(aElement) { + mComputedStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(aElement, nullptr); +} + +void StyleInfo::Display(nsAString& aValue) { + aValue.Truncate(); + nsAutoCString value; + mComputedStyle->GetComputedPropertyValue(eCSSProperty_display, value); + CopyUTF8toUTF16(value, aValue); +} + +void StyleInfo::TextAlign(nsAString& aValue) { + aValue.Truncate(); + nsAutoCString value; + mComputedStyle->GetComputedPropertyValue(eCSSProperty_text_align, value); + CopyUTF8toUTF16(value, aValue); +} + +void StyleInfo::TextIndent(nsAString& aValue) { + aValue.Truncate(); + + const auto& textIndent = mComputedStyle->StyleText()->mTextIndent; + if (textIndent.ConvertsToLength()) { + aValue.AppendFloat(textIndent.ToLengthInCSSPixels()); + aValue.AppendLiteral("px"); + return; + } + if (textIndent.ConvertsToPercentage()) { + aValue.AppendFloat(textIndent.ToPercentage() * 100); + aValue.AppendLiteral("%"); + return; + } + // FIXME: This doesn't handle calc in any meaningful way? Probably should just + // use the Servo serialization code... + aValue.AppendLiteral("0px"); +} + +void StyleInfo::Margin(Side aSide, nsAString& aValue) { + MOZ_ASSERT(mElement->GetPrimaryFrame(), + " mElement->GetPrimaryFrame() needs to be valid pointer"); + aValue.Truncate(); + + nsIFrame* frame = mElement->GetPrimaryFrame(); + + // This is here only to guarantee that we do the same as getComputedStyle + // does, so that we don't hit precision errors in tests. + auto& margin = frame->StyleMargin()->mMargin.Get(aSide); + if (margin.ConvertsToLength()) { + aValue.AppendFloat(margin.AsLengthPercentage().ToLengthInCSSPixels()); + } else { + nscoord coordVal = frame->GetUsedMargin().Side(aSide); + aValue.AppendFloat(CSSPixel::FromAppUnits(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::FormatTextDecorationStyle(uint8_t aValue, + nsAString& aFormattedValue) { + // TODO: When these are enum classes that rust also understands we should just + // make an FFI call here. + switch (aValue) { + case NS_STYLE_TEXT_DECORATION_STYLE_NONE: + return aFormattedValue.AssignASCII("-moz-none"); + case NS_STYLE_TEXT_DECORATION_STYLE_SOLID: + return aFormattedValue.AssignASCII("solid"); + case NS_STYLE_TEXT_DECORATION_STYLE_DOUBLE: + return aFormattedValue.AssignASCII("double"); + case NS_STYLE_TEXT_DECORATION_STYLE_DOTTED: + return aFormattedValue.AssignASCII("dotted"); + case NS_STYLE_TEXT_DECORATION_STYLE_DASHED: + return aFormattedValue.AssignASCII("dashed"); + case NS_STYLE_TEXT_DECORATION_STYLE_WAVY: + return aFormattedValue.AssignASCII("wavy"); + default: + MOZ_ASSERT_UNREACHABLE("Unknown decoration style"); + break; + } +} diff --git a/accessible/base/StyleInfo.h b/accessible/base/StyleInfo.h new file mode 100644 index 0000000000..3423f545f3 --- /dev/null +++ b/accessible/base/StyleInfo.h @@ -0,0 +1,50 @@ +/* -*- 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 "mozilla/ComputedStyle.h" + +namespace mozilla { +namespace dom { +class Element; +} // namespace dom +namespace a11y { + +class StyleInfo { + public: + explicit StyleInfo(dom::Element* aElement); + ~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 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<ComputedStyle> mComputedStyle; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextAttrs.cpp b/accessible/base/TextAttrs.cpp new file mode 100644 index 0000000000..869a536c9d --- /dev/null +++ b/accessible/base/TextAttrs.cpp @@ -0,0 +1,750 @@ +/* -*- 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 "gfxTextRun.h" +#include "nsFontMetrics.h" +#include "nsLayoutUtils.h" +#include "nsContainerFrame.h" +#include "nsStyleUtil.h" +#include "HyperTextAccessible.h" +#include "mozilla/AppUnits.h" +#include "mozilla/gfx/2D.h" + +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. + MOZ_ASSERT( + 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, u"false"_ns); + break; + + case eGrammar: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, u"grammar"_ns); + break; + + case eSpelling: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, u"spelling"_ns); + break; + + case eTrue: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, u"true"_ns); + break; + } +} + +bool TextAttrsMgr::InvalidTextAttr::GetValue(nsIContent* aElm, + uint32_t* aValue) { + nsIContent* elm = aElm; + do { + if (nsAccUtils::HasDefinedARIAToken(elm, nsGkAtoms::aria_invalid)) { + static dom::Element::AttrValuesArray tokens[] = { + nsGkAtoms::_false, nsGkAtoms::grammar, nsGkAtoms::spelling, nullptr}; + + int32_t idx = elm->AsElement()->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) { + nscolor backgroundColor = aFrame->StyleBackground()->BackgroundColor(aFrame); + if (NS_GET_A(backgroundColor) > 0) { + *aColor = backgroundColor; + 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->StyleText()->mColor.ToColor(); + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleText()->mColor.ToColor(); + mIsDefined = true; + } +} + +bool TextAttrsMgr::ColorTextAttr::GetValueFor(Accessible* aAccessible, + nscolor* aValue) { + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + if (nsIFrame* frame = elm->GetPrimaryFrame()) { + *aValue = frame->StyleText()->mColor.ToColor(); + 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.Append(NS_ConvertUTF8toUTF16(fontEntry->FamilyName())); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// FontSizeTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontSizeTextAttr::FontSizeTextAttr(nsIFrame* aRootFrame, + nsIFrame* aFrame) + : TTextAttr<nscoord>(!aFrame) { + mDC = aRootFrame->PresContext()->DeviceContext(); + + mRootNativeValue = aRootFrame->StyleFont()->mSize.ToAppUnits(); + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleFont()->mSize.ToAppUnits(); + 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.ToAppUnits(); + 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<FontSlantStyle>(!aFrame) { + mRootNativeValue = aRootFrame->StyleFont()->mFont.style; + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = aFrame->StyleFont()->mFont.style; + mIsDefined = true; + } +} + +bool TextAttrsMgr::FontStyleTextAttr::GetValueFor(Accessible* aAccessible, + FontSlantStyle* 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 FontSlantStyle& aValue) { + nsAutoString string; + nsStyleUtil::AppendFontSlantStyle(aValue, string); + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::font_style, string); +} + +//////////////////////////////////////////////////////////////////////////////// +// FontWeightTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::FontWeightTextAttr::FontWeightTextAttr(nsIFrame* aRootFrame, + nsIFrame* aFrame) + : TTextAttr<FontWeight>(!aFrame) { + mRootNativeValue = GetFontWeight(aRootFrame); + mIsRootDefined = true; + + if (aFrame) { + mNativeValue = GetFontWeight(aFrame); + mIsDefined = true; + } +} + +bool TextAttrsMgr::FontWeightTextAttr::GetValueFor(Accessible* aAccessible, + FontWeight* 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 FontWeight& aValue) { + nsAutoString formattedValue; + formattedValue.AppendFloat(aValue.ToFloat()); + + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::fontWeight, formattedValue); +} + +FontWeight 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 FontWeight::Bold(); + } + + // 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 + // range supported by the font face used, so we clamp the weight that was + // requested by style to what is actually supported by the font. + gfxFontEntry* fontEntry = font->GetFontEntry(); + return fontEntry->Weight().Clamp(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) || + (aAccessible->NativeRole() == roles::LISTITEM_MARKER)); +} + +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 ? u"true"_ns : u"false"_ns); +} + +//////////////////////////////////////////////////////////////////////////////// +// TextDecorTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::TextDecorValue::TextDecorValue(nsIFrame* aFrame) { + const nsStyleTextReset* textReset = aFrame->StyleTextReset(); + mStyle = textReset->mTextDecorationStyle; + mColor = textReset->mTextDecorationColor.CalcColor(aFrame); + mLine = + textReset->mTextDecorationLine & (StyleTextDecorationLine::UNDERLINE | + StyleTextDecorationLine::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, + u"baseline"_ns); + break; + + case eTextPosSub: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textPosition, u"sub"_ns); + break; + + case eTextPosSuper: + nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::textPosition, u"super"_ns); + break; + + case eTextPosNone: + break; + } +} + +TextAttrsMgr::TextPosValue TextAttrsMgr::TextPosTextAttr::GetTextPosValue( + nsIFrame* aFrame) const { + const auto& verticalAlign = aFrame->StyleDisplay()->mVerticalAlign; + if (verticalAlign.IsKeyword()) { + switch (verticalAlign.AsKeyword()) { + case StyleVerticalAlignKeyword::Baseline: + return eTextPosBaseline; + case StyleVerticalAlignKeyword::Sub: + return eTextPosSub; + case StyleVerticalAlignKeyword::Super: + return eTextPosSuper; + // No good guess for the rest, so do not expose value of text-position + // attribute. + default: + return eTextPosNone; + } + } + + const auto& length = verticalAlign.AsLength(); + if (length.ConvertsToPercentage()) { + float percentValue = length.ToPercentage(); + return percentValue > 0 + ? eTextPosSuper + : (percentValue < 0 ? eTextPosSub : eTextPosBaseline); + } + + if (length.ConvertsToLength()) { + nscoord coordValue = length.ToLength(); + return coordValue > 0 ? eTextPosSuper + : (coordValue < 0 ? eTextPosSub : eTextPosBaseline); + } + + if (const nsIContent* content = aFrame->GetContent()) { + 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 0000000000..66321c944b --- /dev/null +++ b/accessible/base/TextAttrs.h @@ -0,0 +1,433 @@ +/* -*- 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 "mozilla/FontPropertyTypes.h" +#include "nsCOMPtr.h" +#include "nsColor.h" +#include "nsString.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<mozilla::FontSlantStyle> { + public: + FontStyleTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontStyleTextAttr() {} + + protected: + // TTextAttr + virtual bool GetValueFor(Accessible* aContent, + mozilla::FontSlantStyle* aValue) override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const mozilla::FontSlantStyle& aValue) override; + }; + + /** + * Class is used for the work with "font-weight" text attribute. + */ + class FontWeightTextAttr : public TTextAttr<mozilla::FontWeight> { + public: + FontWeightTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~FontWeightTextAttr() {} + + protected: + // TTextAttr + virtual bool GetValueFor(Accessible* aAccessible, + mozilla::FontWeight* aValue) override; + virtual void ExposeValue(nsIPersistentProperties* aAttributes, + const mozilla::FontWeight& aValue) override; + + private: + mozilla::FontWeight 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() + : mColor{0}, + mLine{StyleTextDecorationLine::NONE}, + mStyle{NS_STYLE_TEXT_DECORATION_STYLE_NONE} {} + 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 bool(mLine & mozilla::StyleTextDecorationLine::UNDERLINE); + } + bool IsLineThrough() const { + return bool(mLine & mozilla::StyleTextDecorationLine::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; + mozilla::StyleTextDecorationLine 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 0000000000..af6761bc0d --- /dev/null +++ b/accessible/base/TextRange-inl.h @@ -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/. */ + +#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 0000000000..679e46c6ee --- /dev/null +++ b/accessible/base/TextRange.cpp @@ -0,0 +1,518 @@ +/* -*- 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 "HyperTextAccessible-inl.h" +#include "mozilla/dom/Selection.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(); + } + + if (pos1 != 0) { + // If parents1 is a superset of parents2 then mContainer is a + // descendant of aPoint.mContainer. The next element down in parents1 + // is mContainer's ancestor that is the child of aPoint.mContainer. + // We compare its end offset in aPoint.mContainer with aPoint.mOffset. + Accessible* child = parents1.ElementAt(pos1 - 1); + MOZ_ASSERT(child->Parent() == aPoint.mContainer); + return child->EndOffset() < static_cast<uint32_t>(aPoint.mOffset); + } + + if (pos2 != 0) { + // If parents2 is a superset of parents1 then aPoint.mContainer is a + // descendant of mContainer. The next element down in parents2 + // is aPoint.mContainer's ancestor that is the child of mContainer. + // We compare its start offset in mContainer with mOffset. + Accessible* child = parents2.ElementAt(pos2 - 1); + MOZ_ASSERT(child->Parent() == mContainer); + return static_cast<uint32_t>(mOffset) < child->StartOffset(); + } + + 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 {} + +bool TextRange::SetSelectionAt(int32_t aSelectionNum) const { + RefPtr<dom::Selection> domSel = mRoot->DOMSelection(); + if (!domSel) { + return false; + } + + RefPtr<nsRange> range = nsRange::Create(mRoot->GetContent()); + uint32_t rangeCount = domSel->RangeCount(); + if (aSelectionNum == static_cast<int32_t>(rangeCount)) { + range = nsRange::Create(mRoot->GetContent()); + } else { + range = domSel->GetRangeAt(aSelectionNum); + } + + if (!range) { + return false; + } + + bool reversed; + AssignDOMRange(range, &reversed); + + // If this is not a new range, notify selection listeners that the existing + // selection range has changed. Otherwise, just add the new range. + if (aSelectionNum != static_cast<int32_t>(rangeCount)) { + domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, + IgnoreErrors()); + } + + IgnoredErrorResult err; + domSel->AddRangeAndSelectFramesAndNotifyListeners(*range, err); + if (!err.Failed()) { + // Changing the direction of the selection assures that the caret + // will be at the logical end of the selection. + domSel->SetDirection(reversed ? eDirPrevious : eDirNext); + return true; + } + + return false; +} + +void TextRange::ScrollIntoView(uint32_t aScrollType) const { + RefPtr<nsRange> range = nsRange::Create(mRoot->GetContent()); + if (AssignDOMRange(range)) { + nsCoreUtils::ScrollSubstringTo(mStartContainer->GetFrame(), range, + aScrollType); + } +} + +/** + * Convert the given DOM point to a DOM point in non-generated contents. + * + * If aDOMPoint is in ::before, the result is immediately after it. + * If aDOMPoint is in ::after, the result is immediately before it. + */ +static DOMPoint ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint, + nsIContent* aElementContent) { + MOZ_ASSERT(aDOMPoint.node, "The node must not be null"); + + // ::before pseudo element + if (aElementContent && + aElementContent->IsGeneratedContentContainerForBefore()) { + MOZ_ASSERT(aElementContent->GetParent(), + "::before must have parent element"); + // The first child of its parent (i.e., immediately after the ::before) is + // good point for a DOM range. + return DOMPoint(aElementContent->GetParent(), 0); + } + + // ::after pseudo element + if (aElementContent && + aElementContent->IsGeneratedContentContainerForAfter()) { + MOZ_ASSERT(aElementContent->GetParent(), + "::after must have parent element"); + // The end of its parent (i.e., immediately before the ::after) is good + // point for a DOM range. + return DOMPoint(aElementContent->GetParent(), + aElementContent->GetParent()->GetChildCount()); + } + + return aDOMPoint; +} + +/** + * GetElementAsContentOf() returns a content representing an element which is + * or includes aNode. + * + * XXX This method is enough to retrieve ::before or ::after pseudo element. + * So, if you want to use this for other purpose, you might need to check + * ancestors too. + */ +static nsIContent* GetElementAsContentOf(nsINode* aNode) { + if (auto* element = dom::Element::FromNode(aNode)) { + return element; + } + return aNode->GetParentElement(); +} + +bool TextRange::AssignDOMRange(nsRange* aRange, bool* aReversed) const { + bool reversed = EndPoint() < StartPoint(); + if (aReversed) { + *aReversed = reversed; + } + + DOMPoint startPoint = reversed + ? mEndContainer->OffsetToDOMPoint(mEndOffset) + : mStartContainer->OffsetToDOMPoint(mStartOffset); + if (!startPoint.node) { + return false; + } + + // HyperTextAccessible manages pseudo elements generated by ::before or + // ::after. However, contents of them are not in the DOM tree normally. + // Therefore, they are not selectable and editable. So, when this creates + // a DOM range, it should not start from nor end in any pseudo contents. + + nsIContent* container = GetElementAsContentOf(startPoint.node); + DOMPoint startPointForDOMRange = + ClosestNotGeneratedDOMPoint(startPoint, container); + aRange->SetStart(startPointForDOMRange.node, startPointForDOMRange.idx); + + // If the caller wants collapsed range, let's collapse the range to its start. + if (mEndContainer == mStartContainer && mEndOffset == mStartOffset) { + aRange->Collapse(true); + return true; + } + + DOMPoint endPoint = reversed ? mStartContainer->OffsetToDOMPoint(mStartOffset) + : mEndContainer->OffsetToDOMPoint(mEndOffset); + if (!endPoint.node) { + return false; + } + + if (startPoint.node != endPoint.node) { + container = GetElementAsContentOf(endPoint.node); + } + + DOMPoint endPointForDOMRange = + ClosestNotGeneratedDOMPoint(endPoint, container); + aRange->SetEnd(endPointForDOMRange.node, endPointForDOMRange.idx); + return true; +} + +void TextRange::TextRangesFromSelection(dom::Selection* aSelection, + nsTArray<TextRange>* aRanges) { + MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty"); + + aRanges->SetCapacity(aSelection->RangeCount()); + + for (uint32_t idx = 0; idx < aSelection->RangeCount(); idx++) { + const nsRange* DOMRange = aSelection->GetRangeAt(idx); + HyperTextAccessible* startContainer = + nsAccUtils::GetTextContainer(DOMRange->GetStartContainer()); + HyperTextAccessible* endContainer = + nsAccUtils::GetTextContainer(DOMRange->GetEndContainer()); + HyperTextAccessible* commonAncestor = nsAccUtils::GetTextContainer( + DOMRange->GetClosestCommonInclusiveAncestor()); + if (!startContainer || !endContainer) { + continue; + } + + int32_t startOffset = startContainer->DOMPointToOffset( + DOMRange->GetStartContainer(), DOMRange->StartOffset(), false); + int32_t endOffset = endContainer->DOMPointToOffset( + DOMRange->GetEndContainer(), DOMRange->EndOffset(), true); + + TextRange tr(commonAncestor && commonAncestor->IsTextField() + ? commonAncestor + : startContainer->Document(), + startContainer, startOffset, endContainer, endOffset); + *(aRanges->AppendElement()) = std::move(tr); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 0000000000..20b1e78a86 --- /dev/null +++ b/accessible/base/TextRange.h @@ -0,0 +1,283 @@ +/* -*- 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 <utility> + +#include "nsCaseTreatment.h" +#include "nsRect.h" +#include "nsTArray.h" + +class nsIVariant; +class nsRange; + +namespace mozilla { +namespace dom { +class Selection; +} // namespace dom +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() : mStartOffset{0}, mEndOffset{0} {} + TextRange(TextRange&& aRange) + : mRoot(std::move(aRange.mRoot)), + mStartContainer(std::move(aRange.mStartContainer)), + mEndContainer(std::move(aRange.mEndContainer)), + mStartOffset(aRange.mStartOffset), + mEndOffset(aRange.mEndOffset) {} + + TextRange& operator=(TextRange&& aRange) { + mRoot = std::move(aRange.mRoot); + mStartContainer = std::move(aRange.mStartContainer); + mEndContainer = std::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; + MOZ_CAN_RUN_SCRIPT bool SetSelectionAt(int32_t aSelectionNum) const; + + /** + * Scroll the text range into view. + */ + void ScrollIntoView(uint32_t aScrollType) const; + + /** + * Convert stored hypertext offsets into DOM offsets and assign it to DOM + * range. + * + * Note that if start and/or end accessible offsets are in generated content + * such as ::before or + * ::after, the result range excludes the generated content. See also + * ClosestNotGeneratedDOMPoint() for more information. + * + * @param aRange [in, out] the range whose bounds to set + * @param aReversed [out] whether the start/end offsets were reversed. + * @return true if conversion was successful + */ + bool AssignDOMRange(nsRange* aRange, bool* aReversed = nullptr) 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; + } + + static void TextRangesFromSelection(dom::Selection* aSelection, + nsTArray<TextRange>* aRanges); + + 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 0000000000..0ebe53d01b --- /dev/null +++ b/accessible/base/TextUpdater.cpp @@ -0,0 +1,189 @@ +/* -*- 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) { + MOZ_ASSERT_UNREACHABLE("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; + } + MOZ_ASSERT_UNREACHABLE("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 0000000000..87905cf082 --- /dev/null +++ b/accessible/base/TextUpdater.h @@ -0,0 +1,95 @@ +/* -*- 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 0000000000..a865088271 --- /dev/null +++ b/accessible/base/TreeWalker.cpp @@ -0,0 +1,348 @@ +/* -*- 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 |= 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 |= nsIContent::eAllChildren; + + MOZ_COUNT_CTOR(TreeWalker); +} + +TreeWalker::TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode) + : mDoc(aDocument), + mContext(nullptr), + mAnchorNode(aAnchorNode), + mARIAOwnsIdx(0), + mChildFilter(nsIContent::eSkipPlaceholderContent | + nsIContent::eAllChildren), + mFlags(eWalkCache), + mPhase(eAtStart) { + MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker"); + MOZ_COUNT_CTOR(TreeWalker); +} + +TreeWalker::~TreeWalker() { MOZ_COUNT_DTOR(TreeWalker); } + +Accessible* TreeWalker::Scope(nsIContent* aAnchorNode) { + Reset(); + + mAnchorNode = aAnchorNode; + + mFlags |= eScoped; + + 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->GetFlattenedTreeParent(); + + // Handle the special case of XBL binding child under a shadow root. + if (parentNode && parentNode->IsShadowRoot()) { + parentNode = childNode->GetFlattenedTreeParent(); + if (parentNode == mAnchorNode) { + return true; + } + continue; + } + + if (!parentNode || !parentNode->IsElement()) { + return false; + } + + // If ARIA owned child. + Accessible* child = mDoc->GetAccessible(childNode); + if (child && child->IsRelocated()) { + MOZ_ASSERT( + !(mFlags & eScoped), + "Walker should not be scoped when seeking into relocated children"); + 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) { + if (!(mFlags & eScoped)) { + mPhase = eAtARIAOwns; + Accessible* child = mDoc->ARIAOwnedAt(mContext, mARIAOwnsIdx); + if (child) { + mARIAOwnsIdx++; + return child; + } + } + MOZ_ASSERT(!(mFlags & eScoped) || mPhase != eAtARIAOwns, + "Don't walk relocated children in scoped mode"); + 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) { + if (mFlags & eScoped) { + mPhase = eAtDOM; + } else { + mPhase = eAtARIAOwns; + mARIAOwnsIdx = mDoc->ARIAOwnedCount(mContext); + } + } + + if (mPhase == eAtARIAOwns) { + MOZ_ASSERT(!(mFlags & eScoped), + "Should not walk relocated children in scoped mode"); + 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() { + mStateStack.RemoveLastElement(); + return mStateStack.IsEmpty() ? nullptr : &mStateStack.LastElement(); +} diff --git a/accessible/base/TreeWalker.h b/accessible/base/TreeWalker.h new file mode 100644 index 0000000000..94d06d524a --- /dev/null +++ b/accessible/base/TreeWalker.h @@ -0,0 +1,142 @@ +/* -*- 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, + eScoped = 4 + }; + + /** + * 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); + + /** + * Navigates the accessible children within the anchor node subtree. + */ + TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode); + + ~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/XULMap.h b/accessible/base/XULMap.h new file mode 100644 index 0000000000..4ebe7a6245 --- /dev/null +++ b/accessible/base/XULMap.h @@ -0,0 +1,126 @@ +/* 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/. */ + +XULMAP_TYPE(browser, OuterDocAccessible) +XULMAP_TYPE(button, XULButtonAccessible) +XULMAP_TYPE(checkbox, CheckboxAccessible) +XULMAP_TYPE(dropMarker, XULDropmarkerAccessible) +XULMAP_TYPE(editor, OuterDocAccessible) +XULMAP_TYPE(findbar, XULToolbarAccessible) +XULMAP_TYPE(groupbox, XULGroupboxAccessible) +XULMAP_TYPE(iframe, OuterDocAccessible) +XULMAP_TYPE(listheader, XULColumAccessible) +XULMAP_TYPE(menu, XULMenuitemAccessibleWrap) +XULMAP_TYPE(menubar, XULMenubarAccessible) +XULMAP_TYPE(menucaption, XULMenuitemAccessibleWrap) +XULMAP_TYPE(menuitem, XULMenuitemAccessibleWrap) +XULMAP_TYPE(menulist, XULComboboxAccessible) +XULMAP_TYPE(menuseparator, XULMenuSeparatorAccessible) +XULMAP_TYPE(notification, XULAlertAccessible) +XULMAP_TYPE(radio, XULRadioButtonAccessible) +XULMAP_TYPE(radiogroup, XULRadioGroupAccessible) +XULMAP_TYPE(richlistbox, XULListboxAccessibleWrap) +XULMAP_TYPE(richlistitem, XULListitemAccessible) +XULMAP_TYPE(statusbar, XULStatusBarAccessible) +XULMAP_TYPE(tab, XULTabAccessible) +XULMAP_TYPE(tabpanels, XULTabpanelsAccessible) +XULMAP_TYPE(tabs, XULTabsAccessible) +XULMAP_TYPE(toolbarseparator, XULToolbarSeparatorAccessible) +XULMAP_TYPE(toolbarspacer, XULToolbarSeparatorAccessible) +XULMAP_TYPE(toolbarspring, XULToolbarSeparatorAccessible) +XULMAP_TYPE(treecol, XULColumnItemAccessible) +XULMAP_TYPE(treecolpicker, XULButtonAccessible) +XULMAP_TYPE(treecols, XULTreeColumAccessible) +XULMAP_TYPE(toolbar, XULToolbarAccessible) +XULMAP_TYPE(toolbarbutton, XULToolbarButtonAccessible) + +XULMAP(description, [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aElement->ClassList()->Contains(u"tooltip-label"_ns)) { + return nullptr; + } + + return new XULLabelAccessible(aElement, aContext->Document()); +}) + +XULMAP(tooltip, [](Element* aElement, Accessible* aContext) -> Accessible* { + nsIFrame* frame = aElement->GetPrimaryFrame(); + if (!frame) { + return nullptr; + } + + nsMenuPopupFrame* popupFrame = do_QueryFrame(frame); + if (!popupFrame) { + return nullptr; + } + + nsPopupState popupState = popupFrame->PopupState(); + if (popupState == ePopupHiding || popupState == ePopupInvisible || + popupState == ePopupClosed) { + return nullptr; + } + + return new XULTooltipAccessible(aElement, aContext->Document()); +}) + +XULMAP(label, [](Element* aElement, Accessible* aContext) -> Accessible* { + if (aElement->ClassList()->Contains(u"text-link"_ns)) { + return new XULLinkAccessible(aElement, aContext->Document()); + } + return new XULLabelAccessible(aElement, aContext->Document()); +}) + +XULMAP(image, [](Element* aElement, Accessible* aContext) -> Accessible* { + // Don't include nameless images in accessible tree. + if (!aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext)) { + return nullptr; + } + + return new ImageAccessibleWrap(aElement, aContext->Document()); +}) + +XULMAP(menupopup, [](Element* aElement, Accessible* aContext) { + return CreateMenupopupAccessible(aElement, aContext); +}) + +XULMAP(panel, [](Element* aElement, Accessible* aContext) -> Accessible* { + static const Element::AttrValuesArray sIgnoreTypeVals[] = { + nsGkAtoms::autocomplete_richlistbox, nsGkAtoms::autocomplete, nullptr}; + + if (aElement->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + sIgnoreTypeVals, eIgnoreCase) >= 0) { + return nullptr; + } + + if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::noautofocus, + nsGkAtoms::_true, eCaseMatters)) { + return new XULAlertAccessible(aElement, aContext->Document()); + } + + return new EnumRoleAccessible<roles::PANE>(aElement, aContext->Document()); +}) + +XULMAP(popup, [](Element* aElement, Accessible* aContext) { + return CreateMenupopupAccessible(aElement, aContext); +}) + +XULMAP(tree, [](Element* aElement, Accessible* aContext) -> Accessible* { + nsIContent* child = + nsTreeUtils::GetDescendantChild(aElement, nsGkAtoms::treechildren); + if (!child) return nullptr; + + nsTreeBodyFrame* treeFrame = do_QueryFrame(child->GetPrimaryFrame()); + if (!treeFrame) return nullptr; + + RefPtr<nsTreeColumns> treeCols = treeFrame->Columns(); + uint32_t count = treeCols->Count(); + + // Outline of list accessible. + if (count == 1) { + return new XULTreeAccessible(aElement, aContext->Document(), treeFrame); + } + + // Table or tree table accessible. + return new XULTreeGridAccessibleWrap(aElement, aContext->Document(), + treeFrame); +}) diff --git a/accessible/base/moz.build b/accessible/base/moz.build new file mode 100644 index 0000000000..6eb2d96b04 --- /dev/null +++ b/accessible/base/moz.build @@ -0,0 +1,117 @@ +# -*- 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", + "IDSet.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", + "Pivot.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", + "/dom/base", + "/dom/xul", +] + +if CONFIG["OS_ARCH"] == "WINNT": + LOCAL_INCLUDES += [ + "/accessible/ipc/win", + ] +else: + LOCAL_INCLUDES += [ + "/accessible/ipc/other", + ] + +LOCAL_INCLUDES += [ + "/accessible/xpcom", + "/accessible/xul", + "/dom/base", + "/ipc/chromium/src", + "/layout/generic", + "/layout/style", + "/layout/xul", + "/layout/xul/tree/", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + 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", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + LOCAL_INCLUDES += [ + "/accessible/android", + ] +else: + LOCAL_INCLUDES += [ + "/accessible/other", + ] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/accessible/base/nsAccCache.h b/accessible/base/nsAccCache.h new file mode 100644 index 0000000000..a6d5874867 --- /dev/null +++ b/accessible/base/nsAccCache.h @@ -0,0 +1,24 @@ +/* -*- 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_ + +//////////////////////////////////////////////////////////////////////////////// +// 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 0000000000..49d0af6bd3 --- /dev/null +++ b/accessible/base/nsAccUtils.cpp @@ -0,0 +1,519 @@ +/* -*- 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 "nsISimpleEnumerator.h" +#include "mozilla/a11y/PDocAccessibleChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsAccessibilityService.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +void nsAccUtils::GetAccAttr(nsIPersistentProperties* aAttributes, + nsAtom* aAttrName, nsAString& aAttrValue) { + aAttrValue.Truncate(); + + aAttributes->GetStringProperty(nsAtomCString(aAttrName), aAttrValue); +} + +void nsAccUtils::SetAccAttr(nsIPersistentProperties* aAttributes, + nsAtom* aAttrName, const nsAString& aAttrValue) { + nsAutoString oldValue; + aAttributes->SetStringProperty(nsAtomCString(aAttrName), aAttrValue, + oldValue); +} + +void nsAccUtils::SetAccAttr(nsIPersistentProperties* aAttributes, + nsAtom* aAttrName, nsAtom* 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(const 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(const 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 = + aContent->AsElement()->AsXULContainerItem(); + if (!item) return 0; + + nsCOMPtr<dom::Element> containerElement; + item->GetParentContainer(getter_AddRefs(containerElement)); + nsCOMPtr<nsIDOMXULContainerElement> container = + containerElement ? containerElement->AsXULContainer() : nullptr; + if (!container) return 0; + + // Get level of the item. + int32_t level = -1; + while (container) { + level++; + + container->GetParentContainer(getter_AddRefs(containerElement)); + container = containerElement ? containerElement->AsXULContainer() : nullptr; + } + + return level; +} + +void nsAccUtils::SetLiveContainerAttributes( + nsIPersistentProperties* aAttributes, nsIContent* aStartContent) { + nsAutoString live, relevant, busy; + dom::Document* doc = aStartContent->GetComposedDoc(); + if (!doc) { + return; + } + dom::Element* topEl = doc->GetRootElement(); + nsIContent* ancestor = aStartContent; + while (ancestor) { + // container-relevant attribute + if (relevant.IsEmpty() && + HasDefinedARIAToken(ancestor, nsGkAtoms::aria_relevant) && + ancestor->AsElement()->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->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_live, + live); + } else if (role) { + GetLiveAttrValue(role->liveAttRule, live); + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + ancestor, nsGkAtoms::live)) { + value->ToString(live); + } + + if (!live.IsEmpty()) { + SetAccAttr(aAttributes, nsGkAtoms::containerLive, live); + if (role) { + SetAccAttr(aAttributes, nsGkAtoms::containerLiveRole, + role->ARIARoleString()); + } + } + } + + // container-atomic attribute + if (ancestor->IsElement() && ancestor->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::aria_atomic, + nsGkAtoms::_true, eCaseMatters)) { + SetAccAttr(aAttributes, nsGkAtoms::containerAtomic, u"true"_ns); + } + + // container-busy attribute + if (busy.IsEmpty() && HasDefinedARIAToken(ancestor, nsGkAtoms::aria_busy) && + ancestor->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_busy, + busy)) + SetAccAttr(aAttributes, nsGkAtoms::containerBusy, busy); + + if (ancestor == topEl) { + break; + } + + ancestor = ancestor->GetParent(); + if (!ancestor) { + ancestor = topEl; // Use <body>/<frameset> + } + } +} + +bool nsAccUtils::HasDefinedARIAToken(nsIContent* aContent, nsAtom* aAtom) { + NS_ASSERTION(aContent, "aContent is null in call to HasDefinedARIAToken!"); + + if (!aContent->IsElement()) return false; + + dom::Element* element = aContent->AsElement(); + if (!element->HasAttr(kNameSpaceID_None, aAtom) || + element->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_empty, + eCaseMatters) || + element->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_undefined, + eCaseMatters)) { + return false; + } + return true; +} + +nsStaticAtom* nsAccUtils::GetARIAToken(dom::Element* aElement, nsAtom* aAttr) { + if (!HasDefinedARIAToken(aElement, aAttr)) return nsGkAtoms::_empty; + + static dom::Element::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; +} + +nsStaticAtom* nsAccUtils::NormalizeARIAToken(dom::Element* aElement, + nsAtom* aAttr) { + if (!HasDefinedARIAToken(aElement, aAttr)) { + return nsGkAtoms::_empty; + } + + if (aAttr == nsGkAtoms::aria_current) { + static dom::Element::AttrValuesArray tokens[] = { + nsGkAtoms::page, nsGkAtoms::step, nsGkAtoms::location_, + nsGkAtoms::date, nsGkAtoms::time, nsGkAtoms::_true, + nullptr}; + int32_t idx = aElement->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, + eCaseMatters); + // If the token is present, return it, otherwise TRUE as per spec. + return (idx >= 0) ? tokens[idx] : nsGkAtoms::_true; + } + + 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::IsDOMAttrTrue(const Accessible* aAccessible, nsAtom* aAttr) { + dom::Element* el = aAccessible->Elm(); + return el && el->AttrValueIs(kNameSpaceID_None, aAttr, nsGkAtoms::_true, + eCaseMatters); +} + +Accessible* nsAccUtils::TableFor(Accessible* aRow) { + if (aRow) { + Accessible* table = aRow->Parent(); + if (table) { + roles::Role tableRole = table->Role(); + const nsRoleMapEntry* roleMapEntry = table->ARIARoleMap(); + if (tableRole == roles::GROUPING || // if there's a rowgroup. + (table->IsGenericHyperText() && !roleMapEntry && + !table->IsTable())) { // or there is a wrapping text container + 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: + MOZ_ASSERT_UNREACHABLE("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: + MOZ_ASSERT_UNREACHABLE("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 = u"off"_ns; + return true; + case ePoliteLiveAttr: + aValue = u"polite"_ns; + return true; + case eAssertiveLiveAttr: + aValue = u"assertive"_ns; + 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(AccessibleOrProxy aAccessible) { + MOZ_ASSERT(!aAccessible.IsNull()); + roles::Role role = aAccessible.Role(); + + if (role == roles::SLIDER) { + // Always prune the tree for sliders, as it doesn't make sense for a + // slider to have descendants and this confuses NVDA. + return true; + } + + if (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::PROGRESSBAR && + role != roles::SEPARATOR) { + // If it doesn't match any of these roles, don't prune its children. + return false; + } + + if (aAccessible.ChildCount() != 1) { + // If the accessible has more than one child, don't prune it. + return false; + } + + roles::Role childRole = aAccessible.FirstChild().Role(); + // If the accessible's child is a text leaf, prune the accessible. + return childRole == roles::TEXT_LEAF || childRole == roles::STATICTEXT; +} + +bool nsAccUtils::PersistentPropertiesToArray(nsIPersistentProperties* aProps, + nsTArray<Attribute>* aAttributes) { + if (!aProps) { + return true; + } + nsCOMPtr<nsISimpleEnumerator> propEnum; + nsresult rv = aProps->Enumerate(getter_AddRefs(propEnum)); + NS_ENSURE_SUCCESS(rv, false); + + bool hasMore; + while (NS_SUCCEEDED(propEnum->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> sup; + rv = propEnum->GetNext(getter_AddRefs(sup)); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIPropertyElement> propElem(do_QueryInterface(sup)); + NS_ENSURE_TRUE(propElem, false); + + nsAutoCString name; + rv = propElem->GetKey(name); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoString value; + rv = propElem->GetValue(value); + NS_ENSURE_SUCCESS(rv, false); + + aAttributes->AppendElement(Attribute(name, value)); + } + + return true; +} + +bool nsAccUtils::IsARIALive(const Accessible* aAccessible) { + // Get computed aria-live property based on the closest container with the + // attribute. Inner nodes override outer nodes within the same + // document. + // This should be the same as the container-live attribute, but we don't need + // the other container-* attributes, so we can't use the same function. + nsIContent* ancestor = aAccessible->GetContent(); + if (!ancestor) { + return false; + } + dom::Document* doc = ancestor->GetComposedDoc(); + if (!doc) { + return false; + } + dom::Element* topEl = doc->GetRootElement(); + while (ancestor) { + const nsRoleMapEntry* role = nullptr; + if (ancestor->IsElement()) { + role = aria::GetRoleMap(ancestor->AsElement()); + } + nsAutoString live; + if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) { + ancestor->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_live, + live); + } else if (role) { + GetLiveAttrValue(role->liveAttRule, live); + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + ancestor, nsGkAtoms::live)) { + value->ToString(live); + } + if (!live.IsEmpty() && !live.EqualsLiteral("off")) { + return true; + } + + if (ancestor == topEl) { + break; + } + + ancestor = ancestor->GetParent(); + if (!ancestor) { + ancestor = topEl; // Use <body>/<frameset> + } + } + + return false; +} diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h new file mode 100644 index 0000000000..b44caed20a --- /dev/null +++ b/accessible/base/nsAccUtils.h @@ -0,0 +1,270 @@ +/* -*- 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 "mozilla/a11y/DocManager.h" + +#include "AccessibleOrProxy.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" + +#include "nsIDocShell.h" +#include "nsPoint.h" + +namespace mozilla { + +class PresShell; + +namespace dom { +class Element; +} + +namespace a11y { + +class HyperTextAccessible; +class DocAccessible; +class Attribute; + +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, + nsAtom* 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, + nsAtom* aAttrName, const nsAString& aAttrValue); + + static void SetAccAttr(nsIPersistentProperties* aAttributes, + nsAtom* aAttrName, nsAtom* 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(const Accessible* aAcc); + + /** + * Return ARIA level value or the default one if ARIA is missed for the + * given accessible. + */ + static int32_t GetARIAOrDefaultLevel(const 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 + */ + static void SetLiveContainerAttributes(nsIPersistentProperties* aAttributes, + nsIContent* aStartContent); + + /** + * 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, nsAtom* aAtom); + + /** + * Return atomic value of ARIA attribute of boolean or NMTOKEN type. + */ + static nsStaticAtom* GetARIAToken(mozilla::dom::Element* aElement, + nsAtom* aAttr); + + /** + * If the given ARIA attribute has a specific known token value, return it. + * If the specification demands for a fallback value for unknown attribute + * values, return that. For all others, return a nullptr. + */ + static nsStaticAtom* NormalizeARIAToken(mozilla::dom::Element* aElement, + nsAtom* aAttr); + + /** + * Return document accessible for the given DOM node. + */ + static DocAccessible* GetDocAccessibleFor(nsINode* aNode) { + return GetAccService()->GetDocAccessible( + nsCoreUtils::GetPresShellFor(aNode)); + } + + /** + * 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 a given accessible has a given attribute + * with a value of "true". + */ + static bool IsDOMAttrTrue(const Accessible* aAccessible, nsAtom* aAttr); + + /** + * Return true if the DOM node of given accessible has aria-selected="true" + * attribute. + */ + static inline bool IsARIASelected(const Accessible* aAccessible) { + return IsDOMAttrTrue(aAccessible, nsGkAtoms::aria_selected); + } + + /** + * Return true if the DOM node of given accessible has + * aria-multiselectable="true" attribute. + */ + static inline bool IsARIAMultiSelectable(const Accessible* aAccessible) { + return IsDOMAttrTrue(aAccessible, nsGkAtoms::aria_multiselectable); + } + + /** + * 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(AccessibleOrProxy aAccessible); + + static bool PersistentPropertiesToArray(nsIPersistentProperties* aProps, + nsTArray<Attribute>* aAttributes); + + /** + * Return true if the given accessible is within an ARIA live region; i.e. + * the container-live attribute would be something other than "off" or empty. + */ + static bool IsARIALive(const Accessible* aAccessible); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp new file mode 100644 index 0000000000..7b14106d64 --- /dev/null +++ b/accessible/base/nsAccessibilityService.cpp @@ -0,0 +1,1768 @@ +/* -*- 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 "StyleInfo.h" +#include "nsAccUtils.h" +#include "nsArrayUtils.h" +#include "nsAttrName.h" +#include "nsDOMTokenList.h" +#include "nsCRT.h" +#include "nsEventShell.h" +#include "nsIFrameInlines.h" +#include "nsServiceManagerUtils.h" +#include "nsTextFormatter.h" +#include "OuterDocAccessible.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" + +#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 + +#include "nsExceptionHandler.h" +#include "nsImageFrame.h" +#include "nsINamed.h" +#include "nsIObserverService.h" +#include "nsMenuPopupFrame.h" +#include "nsLayoutUtils.h" +#include "nsPluginFrame.h" +#include "nsTreeBodyFrame.h" +#include "nsTreeColumns.h" +#include "nsTreeUtils.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/HTMLTableElement.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/Services.h" +#include "mozilla/SVGGeometryFrame.h" +#include "nsDeckFrame.h" + +#ifdef MOZ_XUL +# include "XULAlertAccessible.h" +# include "XULComboboxAccessible.h" +# include "XULElementAccessibles.h" +# include "XULFormControlAccessible.h" +# include "XULListboxAccessibleWrap.h" +# include "XULMenuAccessibleWrap.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; + +/** + * Accessibility service force enable/disable preference. + * Supported values: + * Accessibility is force enabled (accessibility should always be enabled): -1 + * Accessibility is enabled (will be started upon a request, default value): 0 + * Accessibility is force disabled (never enable accessibility): 1 + */ +#define PREF_ACCESSIBILITY_FORCE_DISABLED "accessibility.force_disabled" + +//////////////////////////////////////////////////////////////////////////////// +// Statics +//////////////////////////////////////////////////////////////////////////////// + +/** + * Return true if the element must be accessible. + */ +static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) { + if (aContent->GetPrimaryFrame()->IsFocusable()) return true; + + if (aContent->IsElement()) { + uint32_t attrCount = aContent->AsElement()->GetAttrCount(); + for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) { + const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx); + if (attr->NamespaceEquals(kNameSpaceID_None)) { + nsAtom* attrAtom = attr->Atom(); + if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) { + // If the author provided a title on an element that would not + // be accessible normally, assume an intent and make it accessible. + return true; + } + + nsDependentAtomString attrStr(attrAtom); + if (!StringBeginsWith(attrStr, u"aria-"_ns)) 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(aContent->AsElement(), id); + } + } + + return false; +} + +/** + * Return true if the SVG element should be accessible + */ +static bool MustSVGElementBeAccessible(nsIContent* aContent) { + // https://w3c.github.io/svg-aam/#include_elements + for (nsIContent* childElm = aContent->GetFirstChild(); childElm; + childElm = childElm->GetNextSibling()) { + if (childElm->IsAnyOfSVGElements(nsGkAtoms::title, nsGkAtoms::desc)) { + return true; + } + } + return false; +} + +/** + * Used by XULMap.h to map both menupopup and popup elements + */ +#ifdef MOZ_XUL +Accessible* CreateMenupopupAccessible(Element* aElement, Accessible* aContext) { +# 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 = aElement->GetParent(); + if (parent && parent->IsXULElement(nsGkAtoms::menu)) return nullptr; +# endif + + return new XULMenupopupAccessible(aElement, aContext->Document()); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Accessible constructors + +static Accessible* New_HyperText(Element* aElement, Accessible* aContext) { + return new HyperTextAccessibleWrap(aElement, aContext->Document()); +} + +template <typename AccClass> +static Accessible* New_HTMLDtOrDd(Element* aElement, Accessible* aContext) { + nsIContent* parent = aContext->GetContent(); + if (parent->IsHTMLElement(nsGkAtoms::div)) { + // It is conforming in HTML to use a div to group dt/dd elements. + parent = parent->GetParent(); + } + + if (parent && parent->IsHTMLElement(nsGkAtoms::dl)) { + return new AccClass(aElement, aContext->Document()); + } + + return nullptr; +} + +/** + * Cached value of the PREF_ACCESSIBILITY_FORCE_DISABLED preference. + */ +static int32_t sPlatformDisabledState = 0; + +//////////////////////////////////////////////////////////////////////////////// +// 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 HTMLMarkupMapInfo sHTMLMarkupMapList[] = { +#include "MarkupMap.h" +}; + +#undef MARKUPMAP + +#ifdef MOZ_XUL +# define XULMAP(atom, ...) {nsGkAtoms::atom, __VA_ARGS__}, + +# define XULMAP_TYPE(atom, new_type) \ + XULMAP(atom, [](Element* aElement, Accessible* aContext) -> Accessible* { \ + return new new_type(aElement, aContext->Document()); \ + }) + +static const XULMarkupMapInfo sXULMarkupMapList[] = { +# include "XULMap.h" +}; + +# undef XULMAP_TYPE +# undef XULMAP +#endif + +#undef Attr +#undef AttrFromDOM +#undef AttrFromDOMIf + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService +//////////////////////////////////////////////////////////////////////////////// + +nsAccessibilityService* nsAccessibilityService::gAccessibilityService = nullptr; +ApplicationAccessible* nsAccessibilityService::gApplicationAccessible = nullptr; +xpcAccessibleApplication* nsAccessibilityService::gXPCApplicationAccessible = + nullptr; +uint32_t nsAccessibilityService::gConsumers = 0; + +nsAccessibilityService::nsAccessibilityService() + : DocManager(), + FocusManager(), + mHTMLMarkupMap(ArrayLength(sHTMLMarkupMapList)) +#ifdef MOZ_XUL + , + mXULMarkupMap(ArrayLength(sXULMarkupMapList)) +#endif +{ +} + +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); + + RefPtr<EventTarget> target; + change->GetTarget(getter_AddRefs(target)); + nsCOMPtr<nsIContent> node(do_QueryInterface(target)); + if (!node || !node->IsHTMLElement()) { + continue; + } + + uint32_t changeCount; + change->GetCountOfEventListenerChangesAffectingAccessibility(&changeCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (changeCount) { + Document* ownerDoc = node->OwnerDoc(); + DocAccessible* document = GetExistingDocAccessible(ownerDoc); + + if (document) { + Accessible* acc = document->GetAccessible(node); + if (!acc && nsCoreUtils::HasClickListener(node)) { + // Create an accessible for a inaccessible element having click event + // handler. + document->ContentInserted(node, node->GetNextSibling()); + } else if (acc && acc->IsHTMLLink() && !acc->AsHTMLLink()->IsLinked()) { + // Notify of a LINKED state change if an HTML link gets a click + // listener but does not have an href attribute. + RefPtr<AccEvent> linkedChangeEvent = + new AccStateChangeEvent(acc, states::LINKED); + document->FireDelayedEvent(linkedChangeEvent); + } + } + } + } + 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) { + Document* 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); +} + +void nsAccessibilityService::NotifyOfImageSizeAvailable( + mozilla::PresShell* aPresShell, nsIContent* aContent) { + // If the size of an image is initially unknown, it will have the invisible + // state (and a 0 width and height), causing it to be ignored by some screen + // readers. Fire a state change event to update any client caches. + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) { + Accessible* accessible = document->GetAccessible(aContent); + // The accessible may not be an ImageAccessible if this was previously a + // broken image with an alt attribute. In that case, do nothing; the + // accessible will be recreated if this becomes a valid image. + if (accessible && accessible->IsImage()) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::INVISIBLE, false); + document->FireDelayedEvent(event); + } + } +} + +Accessible* nsAccessibilityService::GetRootDocumentAccessible( + PresShell* aPresShell, bool aCanCreate) { + PresShell* presShell = aPresShell; + Document* documentNode = aPresShell->GetDocument(); + if (documentNode) { + nsCOMPtr<nsIDocShellTreeItem> treeItem(documentNode->GetDocShell()); + if (treeItem) { + nsCOMPtr<nsIDocShellTreeItem> rootTreeItem; + treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem)); + if (treeItem != rootTreeItem) { + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootTreeItem)); + presShell = docShell->GetPresShell(); + } + + return aCanCreate ? GetDocAccessible(presShell) + : presShell->GetDocAccessible(); + } + } + return nullptr; +} + +#ifdef XP_WIN +static StaticAutoPtr<nsTArray<nsCOMPtr<nsIContent> > > sPendingPlugins; +static StaticAutoPtr<nsTArray<nsCOMPtr<nsITimer> > > sPluginTimers; + +class PluginTimerCallBack final : public nsITimerCallback, public nsINamed { + ~PluginTimerCallBack() {} + + public: + explicit PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD Notify(nsITimer* aTimer) final { + if (!mContent->IsInUncomposedDoc()) return NS_OK; + + PresShell* presShell = mContent->OwnerDoc()->GetPresShell(); + if (presShell) { + DocAccessible* doc = presShell->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; + } + + NS_IMETHOD GetName(nsACString& aName) final { + aName.AssignLiteral("PluginTimerCallBack"); + return NS_OK; + } + + private: + nsCOMPtr<nsIContent> mContent; +}; + +NS_IMPL_ISUPPORTS(PluginTimerCallBack, nsITimerCallback, nsINamed) +#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 = aFrame->GetPluginInstance(); + if (pluginInstance) { +# ifdef XP_WIN + if (!sPendingPlugins->Contains(aContent) && + (Preferences::GetBool("accessibility.delay_plugins") || + Compatibility::IsJAWS() || Compatibility::IsWE())) { + RefPtr<PluginTimerCallBack> cb = new PluginTimerCallBack(aContent); + nsCOMPtr<nsITimer> timer; + NS_NewTimerWithCallback( + getter_AddRefs(timer), 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 unexpected 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(PresShell* aPresShell, + nsIContent* aDeckNode, + nsIFrame* aPrevBoxFrame, + nsIFrame* aCurrentBoxFrame) { + DocAccessible* document = GetDocAccessible(aPresShell); + if (!document) { + return; + } + // A deck with an Accessible is a tabpanels element. + const bool isTabPanels = document->HasAccessible(aDeckNode); + MOZ_ASSERT(!isTabPanels || aDeckNode->IsXULElement(nsGkAtoms::tabpanels), + "A deck with an Accessible should be a tabpanels element"); + + 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 + if (isTabPanels) { + // Tabpanels are accessible even when not selected. + if (Accessible* acc = document->GetAccessible(panelNode)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(acc, states::OFFSCREEN, true); + document->FireDelayedEvent(event); + } + } else { + document->ContentRemoved(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 + if (isTabPanels) { + // Tabpanels are accessible even when not selected, so we don't have to + // insert an Accessible. + if (Accessible* acc = document->GetAccessible(panelNode)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(acc, states::OFFSCREEN, false); + document->FireDelayedEvent(event); + } + } else { + document->ContentInserted(panelNode, panelNode->GetNextSibling()); + } + } +} + +void nsAccessibilityService::ContentRangeInserted(PresShell* aPresShell, + 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", aStartChild->GetParent()); + for (nsIContent* child = aStartChild; child != aEndChild; + child = child->GetNextSibling()) { + logging::Node("content", child); + } + logging::MsgEnd(); + logging::Stack(); + } +#endif + + if (document) { + document->ContentInserted(aStartChild, aEndChild); + } +} + +void nsAccessibilityService::ContentRemoved(PresShell* 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) { + document->ContentRemoved(aChildNode); + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgEnd(); + logging::Stack(); + } +#endif +} + +void nsAccessibilityService::UpdateText(PresShell* aPresShell, + nsIContent* aContent) { + DocAccessible* document = GetDocAccessible(aPresShell); + if (document) document->UpdateText(aContent); +} + +void nsAccessibilityService::TreeViewChanged(PresShell* 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(PresShell* 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(PresShell* 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) { + PresShell* presShell = aImageFrame->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(PresShell* 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(PresShell* 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(PresShell* 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, macSubrole, msaaRole, \ + ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + aString.AssignLiteral(stringRole); \ + 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 = + GetStringStates(nsAccUtils::To64State(aState, aExtraState)); + + // unknown state + if (!stringStates->Length()) { + stringStates->Add(u"unknown"_ns); + } + + stringStates.forget(aStringStates); +} + +already_AddRefed<DOMStringList> nsAccessibilityService::GetStringStates( + uint64_t aStates) const { + RefPtr<DOMStringList> stringStates = new DOMStringList(); + + if (aStates & states::UNAVAILABLE) { + stringStates->Add(u"unavailable"_ns); + } + if (aStates & states::SELECTED) { + stringStates->Add(u"selected"_ns); + } + if (aStates & states::FOCUSED) { + stringStates->Add(u"focused"_ns); + } + if (aStates & states::PRESSED) { + stringStates->Add(u"pressed"_ns); + } + if (aStates & states::CHECKED) { + stringStates->Add(u"checked"_ns); + } + if (aStates & states::MIXED) { + stringStates->Add(u"mixed"_ns); + } + if (aStates & states::READONLY) { + stringStates->Add(u"readonly"_ns); + } + if (aStates & states::HOTTRACKED) { + stringStates->Add(u"hottracked"_ns); + } + if (aStates & states::DEFAULT) { + stringStates->Add(u"default"_ns); + } + if (aStates & states::EXPANDED) { + stringStates->Add(u"expanded"_ns); + } + if (aStates & states::COLLAPSED) { + stringStates->Add(u"collapsed"_ns); + } + if (aStates & states::BUSY) { + stringStates->Add(u"busy"_ns); + } + if (aStates & states::FLOATING) { + stringStates->Add(u"floating"_ns); + } + if (aStates & states::ANIMATED) { + stringStates->Add(u"animated"_ns); + } + if (aStates & states::INVISIBLE) { + stringStates->Add(u"invisible"_ns); + } + if (aStates & states::OFFSCREEN) { + stringStates->Add(u"offscreen"_ns); + } + if (aStates & states::SIZEABLE) { + stringStates->Add(u"sizeable"_ns); + } + if (aStates & states::MOVEABLE) { + stringStates->Add(u"moveable"_ns); + } + if (aStates & states::SELFVOICING) { + stringStates->Add(u"selfvoicing"_ns); + } + if (aStates & states::FOCUSABLE) { + stringStates->Add(u"focusable"_ns); + } + if (aStates & states::SELECTABLE) { + stringStates->Add(u"selectable"_ns); + } + if (aStates & states::LINKED) { + stringStates->Add(u"linked"_ns); + } + if (aStates & states::TRAVERSED) { + stringStates->Add(u"traversed"_ns); + } + if (aStates & states::MULTISELECTABLE) { + stringStates->Add(u"multiselectable"_ns); + } + if (aStates & states::EXTSELECTABLE) { + stringStates->Add(u"extselectable"_ns); + } + if (aStates & states::PROTECTED) { + stringStates->Add(u"protected"_ns); + } + if (aStates & states::HASPOPUP) { + stringStates->Add(u"haspopup"_ns); + } + if (aStates & states::REQUIRED) { + stringStates->Add(u"required"_ns); + } + if (aStates & states::ALERT) { + stringStates->Add(u"alert"_ns); + } + if (aStates & states::INVALID) { + stringStates->Add(u"invalid"_ns); + } + if (aStates & states::CHECKABLE) { + stringStates->Add(u"checkable"_ns); + } + if (aStates & states::SUPPORTS_AUTOCOMPLETION) { + stringStates->Add(u"autocompletion"_ns); + } + if (aStates & states::DEFUNCT) { + stringStates->Add(u"defunct"_ns); + } + if (aStates & states::SELECTABLE_TEXT) { + stringStates->Add(u"selectable text"_ns); + } + if (aStates & states::EDITABLE) { + stringStates->Add(u"editable"_ns); + } + if (aStates & states::ACTIVE) { + stringStates->Add(u"active"_ns); + } + if (aStates & states::MODAL) { + stringStates->Add(u"modal"_ns); + } + if (aStates & states::MULTI_LINE) { + stringStates->Add(u"multi line"_ns); + } + if (aStates & states::HORIZONTAL) { + stringStates->Add(u"horizontal"_ns); + } + if (aStates & states::OPAQUE1) { + stringStates->Add(u"opaque"_ns); + } + if (aStates & states::SINGLE_LINE) { + stringStates->Add(u"single line"_ns); + } + if (aStates & states::TRANSIENT) { + stringStates->Add(u"transient"_ns); + } + if (aStates & states::VERTICAL) { + stringStates->Add(u"vertical"_ns); + } + if (aStates & states::STALE) { + stringStates->Add(u"stale"_ns); + } + if (aStates & states::ENABLED) { + stringStates->Add(u"enabled"_ns); + } + if (aStates & states::SENSITIVE) { + stringStates->Add(u"sensitive"_ns); + } + if (aStates & states::PINNED) { + stringStates->Add(u"pinned"_ns); + } + if (aStates & states::CURRENT) { + stringStates->Add(u"current"_ns); + } + + return stringStates.forget(); +} + +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; + } + + aString.AssignASCII(kEventTypeNames[aEventType]); +} + +void nsAccessibilityService::GetStringEventType(uint32_t aEventType, + nsACString& aString) { + MOZ_ASSERT( + nsIAccessibleEvent::EVENT_LAST_ENTRY == ArrayLength(kEventTypeNames), + "nsIAccessibleEvent constants are out of sync to kEventTypeNames"); + + if (aEventType >= ArrayLength(kEventTypeNames)) { + aString.AssignLiteral("unknown"); + return; + } + + aString = nsDependentCString(kEventTypeNames[aEventType]); +} + +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->IsDocument()) { + // If it's document node then ask accessible document loader for + // document accessible, otherwise return null. + return GetDocAccessible(aNode->AsDocument()); + } + + // 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(); + if (aria::HasDefinedARIAHidden(content)) { + if (aIsSubtreeHidden) { + *aIsSubtreeHidden = true; + } + return nullptr; + } + + // Check frame and its visibility. Note, hidden frame allows visible + // elements in subtree. + nsIFrame* frame = content->GetPrimaryFrame(); + if (!frame || !frame->StyleVisibility()->IsVisible()) { + // display:contents element doesn't have a frame, but retains the semantics. + // All its children are unaffected. + if (nsCoreUtils::IsDisplayContents(content)) { + const HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.Get(content->NodeInfo()->NameAtom()); + if (markupMap && markupMap->new_func) { + RefPtr<Accessible> newAcc = + markupMap->new_func(content->AsElement(), aContext); + if (newAcc) { + document->BindToDocument(newAcc, + aria::GetRoleMap(content->AsElement())); + } + return newAcc; + } + return nullptr; + } + + 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->IsText()) { + nsIFrame::RenderedText text = frame->GetRenderedText( + 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + // 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 || + // We should always use OuterDocAccessible for OuterDocs, even for + // ARIA table roles. + frame->AccessibleType() == eOuterDocType) { + // Prefer to use markup to decide if and what kind of accessible to + // create, + const HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.Get(content->NodeInfo()->NameAtom()); + if (markupMap && markupMap->new_func) + newAcc = markupMap->new_func(content->AsElement(), 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() || + (aContext->Parent() && aContext->Parent()->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; + } + } + } + + // XUL accessibles. + 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; + } + } + +#ifdef MOZ_XUL + // Prefer to use XUL to decide if and what kind of accessible to create. + const XULMarkupMapInfo* xulMap = + mXULMarkupMap.Get(content->NodeInfo()->NameAtom()); + if (xulMap && xulMap->new_func) { + newAcc = xulMap->new_func(content->AsElement(), aContext); + } +#endif + + // 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()) { + LayoutFrameType frameType = frame->Type(); + if (frameType == LayoutFrameType::Box || + frameType == LayoutFrameType::Scroll) { + newAcc = new XULTabpanelAccessible(content, document); + } + } + } + + if (!newAcc) { + if (content->IsSVGElement()) { + SVGGeometryFrame* geometryFrame = do_QueryFrame(frame); + if (geometryFrame && MustSVGElementBeAccessible(content)) { + // 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::text)) { + newAcc = new HyperTextAccessibleWrap(content->AsElement(), document); + } else if (content->IsSVGElement(nsGkAtoms::svg)) { + newAcc = new EnumRoleAccessible<roles::DIAGRAM>(content, document); + } else if (content->IsSVGElement(nsGkAtoms::g) && + MustSVGElementBeAccessible(content)) { + newAcc = new EnumRoleAccessible<roles::GROUPING>(content, document); + } + + } else if (content->IsMathMLElement()) { + const HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.Get(content->NodeInfo()->NameAtom()); + if (markupMap && markupMap->new_func) + newAcc = markupMap->new_func(content->AsElement(), 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); + +#if defined(XP_WIN) + // This information needs to be initialized before the observer fires. + if (XRE_IsParentProcess()) { + Compatibility::Init(); + } +#endif // defined(XP_WIN) + + // 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(sHTMLMarkupMapList); i++) + mHTMLMarkupMap.Put(sHTMLMarkupMapList[i].tag, &sHTMLMarkupMapList[i]); + +#ifdef MOZ_XUL + for (uint32_t i = 0; i < ArrayLength(sXULMarkupMapList); i++) + mXULMarkupMap.Put(sXULMarkupMapList[i].tag, &sXULMarkupMapList[i]); +#endif + +#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(); + } + + gApplicationAccessible = new ApplicationAccessibleWrap(); +#else + gApplicationAccessible = new ApplicationAccessible(); +#endif // defined(XP_WIN) + } + + NS_ADDREF(gApplicationAccessible); // will release in Shutdown() + gApplicationAccessible->Init(); + + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::Accessibility, + "Active"_ns); + +#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(); + + static const char16_t kInitIndicator[] = {'1', 0}; + observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", + kInitIndicator); + + 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"); + UnsetConsumers(eXPCOM | eMainProcess | ePlatformAPI); + + // Remove observers. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + // 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; + + if (observerService) { + static const char16_t kShutdownIndicator[] = {'0', 0}; + observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", + kShutdownIndicator); + } +} + +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 CheckboxAccessible(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->GetParentOrShadowHostNode()->AsContent(); + nsIFrame* parentFrame = nullptr; + if (parentContent) { + parentFrame = parentContent->GetPrimaryFrame(); + if (!parentFrame || !parentFrame->IsTableWrapperFrame()) { + parentContent = + parentContent->GetParentOrShadowHostNode()->AsContent(); + if (parentContent) { + parentFrame = parentContent->GetPrimaryFrame(); + } + } + } + + if (parentFrame && parentFrame->IsTableWrapperFrame() && + table->GetContent() == parentContent) { + newAcc = new HTMLTableRowAccessible(aContent, document); + } + } + break; + } + case eHTMLTextFieldType: + newAcc = new HTMLTextFieldAccessible(aContent, document); + break; + case eHyperTextType: { + if (aContext->IsTable() || aContext->IsTableRow()) { + // This is some generic hyperText, for example a block frame element + // inserted between a table and table row. Treat it as presentational. + return nullptr; + } + + if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd, + nsGkAtoms::div, nsGkAtoms::thead, + nsGkAtoms::tfoot, nsGkAtoms::tbody)) { + 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::HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.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->IsElement() && aContent->AsElement()->AttrValueIs( + kNameSpaceID_None, info->DOMAttrName, + info->DOMAttrValue, eCaseMatters)) { + nsAccUtils::SetAccAttr(aAttributes, info->name, info->DOMAttrValue); + } + continue; + } + + nsAutoString value; + + if (aContent->IsElement()) { + aContent->AsElement()->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(nsINode* aDOMNode) { + if (!aDOMNode) return false; + + Document* document = aDOMNode->OwnerDoc(); + if (!document) return false; + + DocAccessible* docAcc = GetExistingDocAccessible(aDOMNode->OwnerDoc()); + if (!docAcc) return false; + + return docAcc->HasAccessible(aDOMNode); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService private (DON'T put methods here) + +void nsAccessibilityService::SetConsumers(uint32_t aConsumers, bool aNotify) { + if (gConsumers & aConsumers) { + return; + } + + gConsumers |= aConsumers; + if (aNotify) { + NotifyOfConsumersChange(); + } +} + +void nsAccessibilityService::UnsetConsumers(uint32_t aConsumers) { + if (!(gConsumers & aConsumers)) { + return; + } + + gConsumers &= ~aConsumers; + NotifyOfConsumersChange(); +} + +void nsAccessibilityService::GetConsumers(nsAString& aString) { + const char16_t* kJSONFmt = + u"{ \"XPCOM\": %s, \"MainProcess\": %s, \"PlatformAPI\": %s }"; + nsString json; + nsTextFormatter::ssprintf(json, kJSONFmt, + gConsumers & eXPCOM ? "true" : "false", + gConsumers & eMainProcess ? "true" : "false", + gConsumers & ePlatformAPI ? "true" : "false"); + aString.Assign(json); +} + +void nsAccessibilityService::NotifyOfConsumersChange() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + if (!observerService) { + return; + } + + nsAutoString consumers; + GetConsumers(consumers); + observerService->NotifyObservers(nullptr, "a11y-consumers-changed", + consumers.get()); +} + +nsAccessibilityService* GetOrCreateAccService(uint32_t aNewConsumer) { + // Do not initialize accessibility if it is force disabled. + if (PlatformDisabledState() == ePlatformIsDisabled) { + return nullptr; + } + + 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::gAccessibilityService->SetConsumers(aNewConsumer); + return nsAccessibilityService::gAccessibilityService; +} + +void MaybeShutdownAccService(uint32_t aFormerConsumer) { + nsAccessibilityService* accService = + nsAccessibilityService::gAccessibilityService; + + if (!accService || nsAccessibilityService::IsShutdown()) { + return; + } + + // Still used by XPCOM + if (nsCoreUtils::AccEventObserversExist() || + xpcAccessibilityService::IsInUse() || accService->HasXPCDocuments()) { + // In case the XPCOM flag was unset (possibly because of the shutdown + // timer in the xpcAccessibilityService) ensure it is still present. Note: + // this should be fixed when all the consumer logic is taken out as a + // separate class. + accService->SetConsumers(nsAccessibilityService::eXPCOM, false); + + if (aFormerConsumer != nsAccessibilityService::eXPCOM) { + // Only unset non-XPCOM consumers. + accService->UnsetConsumers(aFormerConsumer); + } + return; + } + + if (nsAccessibilityService::gConsumers & ~aFormerConsumer) { + accService->UnsetConsumers(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 bool platformDisabledStateCached = false; + if (platformDisabledStateCached) { + return static_cast<EPlatformDisabledState>(sPlatformDisabledState); + } + + platformDisabledStateCached = true; + Preferences::RegisterCallback(PrefChanged, PREF_ACCESSIBILITY_FORCE_DISABLED); + return ReadPlatformDisabledState(); +} + +EPlatformDisabledState ReadPlatformDisabledState() { + sPlatformDisabledState = + Preferences::GetInt(PREF_ACCESSIBILITY_FORCE_DISABLED, 0); + if (sPlatformDisabledState < ePlatformIsForceEnabled) { + sPlatformDisabledState = ePlatformIsForceEnabled; + } else if (sPlatformDisabledState > ePlatformIsDisabled) { + sPlatformDisabledState = ePlatformIsDisabled; + } + + return static_cast<EPlatformDisabledState>(sPlatformDisabledState); +} + +void PrefChanged(const char* aPref, void* aClosure) { + if (ReadPlatformDisabledState() == ePlatformIsDisabled) { + // Force shut down accessibility. + nsAccessibilityService* accService = + nsAccessibilityService::gAccessibilityService; + if (accService && !nsAccessibilityService::IsShutdown()) { + accService->Shutdown(); + } + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/nsAccessibilityService.h b/accessible/base/nsAccessibilityService.h new file mode 100644 index 0000000000..8458478535 --- /dev/null +++ b/accessible/base/nsAccessibilityService.h @@ -0,0 +1,507 @@ +/* -*- 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/Platform.h" +#include "mozilla/a11y/Role.h" +#include "mozilla/a11y/SelectionManager.h" +#include "mozilla/Preferences.h" + +#include "nsIContent.h" +#include "nsIObserver.h" +#include "nsIAccessibleEvent.h" +#include "nsIEventListenerService.h" +#include "nsXULAppAPI.h" +#include "xpcAccessibilityService.h" + +class nsImageFrame; +class nsIArray; +class nsIPersistentProperties; +class nsPluginFrame; +class nsITreeView; + +namespace mozilla { + +class PresShell; + +namespace dom { +class DOMStringList; +class Element; +} // namespace dom + +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)(mozilla::dom::Element* aElement, + Accessible* aContext); + +// These fields are not `nsStaticAtom* const` because MSVC doesn't like it. +struct MarkupAttrInfo { + nsStaticAtom* name; + nsStaticAtom* value; + + nsStaticAtom* DOMAttrName; + nsStaticAtom* DOMAttrValue; +}; + +struct HTMLMarkupMapInfo { + const nsStaticAtom* const tag; + New_Accessible* new_func; + a11y::role role; + MarkupAttrInfo attrs[4]; +}; + +#ifdef MOZ_XUL +struct XULMarkupMapInfo { + const nsStaticAtom* const tag; + New_Accessible* new_func; +}; +#endif + +/** + * PREF_ACCESSIBILITY_FORCE_DISABLED preference change callback. + */ +void PrefChanged(const char* aPref, void* aClosure); + +/** + * Read and normalize PREF_ACCESSIBILITY_FORCE_DISABLED preference. + */ +EPlatformDisabledState ReadPlatformDisabledState(); + +} // 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(mozilla::PresShell* 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(nsINode* aDOMNode); + + /** + * Get a string equivalent for an accessible role value. + */ + void GetStringRole(uint32_t aRole, nsAString& aString); + + /** + * Get a string equivalent for an accessible state/extra state. + */ + already_AddRefed<mozilla::dom::DOMStringList> GetStringStates( + uint64_t aStates) const; + 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 event value. + */ + void GetStringEventType(uint32_t aEventType, nsACString& 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(mozilla::PresShell* aPresShell, nsIContent* aDeckNode, + nsIFrame* aPrevBoxFrame, nsIFrame* aCurrentBoxFrame); + + /** + * Notification used to update the accessible tree when new content is + * inserted. + */ + void ContentRangeInserted(mozilla::PresShell* aPresShell, + nsIContent* aStartChild, nsIContent* aEndChild); + + /** + * Notification used to update the accessible tree when content is removed. + */ + void ContentRemoved(mozilla::PresShell* aPresShell, nsIContent* aChild); + + void UpdateText(mozilla::PresShell* aPresShell, nsIContent* aContent); + + /** + * Update XUL:tree accessible tree when treeview is changed. + */ + void TreeViewChanged(mozilla::PresShell* aPresShell, nsIContent* aContent, + nsITreeView* aView); + + /** + * Notify of input@type="element" value change. + */ + void RangeValueChanged(mozilla::PresShell* aPresShell, nsIContent* aContent); + + /** + * Update list bullet accessible. + */ + void UpdateListBullet(mozilla::PresShell* 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(mozilla::PresShell* 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(mozilla::PresShell* aPresShell); + + /** + * Recreate an accessible for the given content node in the presshell. + */ + void RecreateAccessible(mozilla::PresShell* aPresShell, nsIContent* aContent); + + void FireAccessibleEvent(uint32_t aEvent, Accessible* aTarget); + + /** + * Notify accessibility that the size has become available for an image. + * This occurs when the size of an image is initially not known, but we've + * now loaded enough data to know the size. + * Called by layout. + */ + void NotifyOfImageSizeAvailable(mozilla::PresShell* aPresShell, + nsIContent* aContent); + + // 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::HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.Get(aContent->NodeInfo()->NameAtom()); + return markupMap ? markupMap->role : mozilla::a11y::roles::NOTHING; + } + + /** + * Return the associated value for a given attribute if + * it appears in the MarkupMap. Otherwise, it returns null. + */ + nsStaticAtom* MarkupAttribute(const nsIContent* aContent, + nsStaticAtom* aAtom) const { + const mozilla::a11y::HTMLMarkupMapInfo* markupMap = + mHTMLMarkupMap.Get(aContent->NodeInfo()->NameAtom()); + if (markupMap) { + for (size_t i = 0; i < mozilla::ArrayLength(markupMap->attrs); i++) { + const mozilla::a11y::MarkupAttrInfo* info = markupMap->attrs + i; + if (info->name == aAtom) { + return info->value; + } + } + } + return nullptr; + } + + /** + * 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 an accessible whose type depends on the given frame. + */ + already_AddRefed<Accessible> CreateAccessibleByFrameType( + nsIFrame* aFrame, nsIContent* aContent, Accessible* aContext); + + /** + * Notify observers about change of the accessibility service's consumers. + */ + void NotifyOfConsumersChange(); + + /** + * Get a JSON string representing the accessibility service consumers. + */ + void GetConsumers(nsAString& aString); + + /** + * Set accessibility service consumers. + */ + void SetConsumers(uint32_t aConsumers, bool aNotify = true); + + /** + * Unset accessibility service consumers. + */ + void UnsetConsumers(uint32_t aConsumers); + + /** + * 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 nsAtom>, + const mozilla::a11y::HTMLMarkupMapInfo*> + mHTMLMarkupMap; +#ifdef MOZ_XUL + nsDataHashtable<nsPtrHashKey<const nsAtom>, + const mozilla::a11y::XULMarkupMapInfo*> + mXULMarkupMap; +#endif + + friend nsAccessibilityService* GetAccService(); + friend nsAccessibilityService* GetOrCreateAccService(uint32_t); + friend void MaybeShutdownAccService(uint32_t); + friend void mozilla::a11y::PrefChanged(const char*, void*); + 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() { return XRE_IsContentProcess(); } + +/** + * 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 + "scrolling", // EVENT_SCROLLING + "announcement", // EVENT_ANNOUNCEMENT + "live region added", // EVENT_LIVE_REGION_ADDED + "live region removed", // EVENT_LIVE_REGION_REMOVED +}; + +#endif diff --git a/accessible/base/nsAccessiblePivot.cpp b/accessible/base/nsAccessiblePivot.cpp new file mode 100644 index 0000000000..456f6cbde3 --- /dev/null +++ b/accessible/base/nsAccessiblePivot.cpp @@ -0,0 +1,522 @@ +/* -*- 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 "Pivot.h" +#include "xpcAccessibleDocument.h" +#include "nsTArray.h" +#include "mozilla/Maybe.h" + +using namespace mozilla::a11y; +using mozilla::DebugOnly; +using mozilla::Maybe; + +/** + * An object that stores a given traversal rule during the pivot movement. + */ +class RuleCache : public PivotRule { + public: + explicit RuleCache(nsIAccessibleTraversalRule* aRule) + : mRule(aRule), mPreFilter{0} {} + ~RuleCache() {} + + virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override; + + private: + nsCOMPtr<nsIAccessibleTraversalRule> mRule; + Maybe<nsTArray<uint32_t>> mAcceptRoles; + 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, + nsIAccessiblePivot::NO_BOUNDARY, 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_NONE, + nsIAccessiblePivot::NO_BOUNDARY, + (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; + + Pivot pivot(GetActiveRoot()); + RuleCache rule(aRule); + AccessibleOrProxy wrappedAnchor = AccessibleOrProxy(anchor); + AccessibleOrProxy newPos = + pivot.Next(wrappedAnchor, rule, (aArgc > 1) ? aIncludeStart : false); + if (!newPos.IsNull() && newPos.IsAccessible()) { + *aResult = MovePivotInternal(newPos.AsAccessible(), + nsIAccessiblePivot::REASON_NEXT, + (aArgc > 2) ? aIsFromUserInput : true); + } else if (newPos.IsProxy()) { + // we shouldn't ever end up with a proxy here, but if we do for some + // reason something is wrong. we should still return OK if we're null + return NS_ERROR_FAILURE; + } + + 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; + + Pivot pivot(GetActiveRoot()); + RuleCache rule(aRule); + AccessibleOrProxy wrappedAnchor = AccessibleOrProxy(anchor); + AccessibleOrProxy newPos = + pivot.Prev(wrappedAnchor, rule, (aArgc > 1) ? aIncludeStart : false); + if (!newPos.IsNull() && newPos.IsAccessible()) { + *aResult = MovePivotInternal(newPos.AsAccessible(), + nsIAccessiblePivot::REASON_PREV, + (aArgc > 2) ? aIsFromUserInput : true); + } else if (newPos.IsProxy()) { + // we shouldn't ever end up with a proxy here, but if we do for some + // reason something is wrong. we should still return OK if we're null + return NS_ERROR_FAILURE; + } + + 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); + + Pivot pivot(GetActiveRoot()); + RuleCache rule(aRule); + AccessibleOrProxy newPos = pivot.First(rule); + if (!newPos.IsNull() && newPos.IsAccessible()) { + *aResult = MovePivotInternal(newPos.AsAccessible(), + nsIAccessiblePivot::REASON_FIRST, + (aArgc > 0) ? aIsFromUserInput : true); + } else if (newPos.IsProxy()) { + // we shouldn't ever end up with a proxy here, but if we do for some + // reason something is wrong. we should still return OK if we're null + return NS_ERROR_FAILURE; + } + + 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); + + Pivot pivot(root); + RuleCache rule(aRule); + AccessibleOrProxy newPos = pivot.Last(rule); + if (!newPos.IsNull() && newPos.IsAccessible()) { + *aResult = MovePivotInternal(newPos.AsAccessible(), + nsIAccessiblePivot::REASON_LAST, + (aArgc > 0) ? aIsFromUserInput : true); + } else if (newPos.IsProxy()) { + // we shouldn't ever end up with a proxy here, but if we do for some + // reason something is wrong. we should still return OK if we're null + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsAccessiblePivot::MoveNextByText(TextBoundaryType aBoundary, + bool aIsFromUserInput, uint8_t aArgc, + bool* aResult) { + NS_ENSURE_ARG(aResult); + + *aResult = false; + + Pivot pivot(GetActiveRoot()); + + int32_t newStart = mStartOffset, newEnd = mEndOffset; + Accessible* newPos = pivot.NextText(mPosition, &newStart, &newEnd, aBoundary); + if (newPos) { + *aResult = true; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + Accessible* oldPos = mPosition; + mStartOffset = newStart; + mEndOffset = newEnd; + mPosition = newPos; + NotifyOfPivotChange(oldPos, oldStart, oldEnd, + nsIAccessiblePivot::REASON_NEXT, aBoundary, + (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; + + Pivot pivot(GetActiveRoot()); + + int32_t newStart = mStartOffset, newEnd = mEndOffset; + Accessible* newPos = pivot.PrevText(mPosition, &newStart, &newEnd, aBoundary); + if (newPos) { + *aResult = true; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + Accessible* oldPos = mPosition; + mStartOffset = newStart; + mEndOffset = newEnd; + mPosition = newPos; + NotifyOfPivotChange(oldPos, oldStart, oldEnd, + nsIAccessiblePivot::REASON_PREV, aBoundary, + (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 rule(aRule); + Pivot pivot(root); + + AccessibleOrProxy newPos = pivot.AtPoint(aX, aY, rule); + if ((!newPos.IsNull() && newPos.IsAccessible()) || + !aIgnoreNoMatch) { // TODO does this need a proxy check? + *aResult = MovePivotInternal(newPos.AsAccessible(), + nsIAccessiblePivot::REASON_POINT, + (aArgc > 0) ? aIsFromUserInput : true); + } else if (newPos.IsProxy()) { + // we shouldn't ever end up with a proxy here, but if we do for some + // reason something is wrong. we should still return OK if we're null + return NS_ERROR_FAILURE; + } + + 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 = std::move(mPosition); + mPosition = aPosition; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + mStartOffset = mEndOffset = -1; + + return NotifyOfPivotChange(oldPosition, oldStart, oldEnd, aReason, + nsIAccessiblePivot::NO_BOUNDARY, aIsFromUserInput); +} + +bool nsAccessiblePivot::NotifyOfPivotChange(Accessible* aOldPosition, + int32_t aOldStart, int32_t aOldEnd, + int16_t aReason, + int16_t aBoundaryType, + bool aIsFromUserInput) { + if (aOldPosition == mPosition && aOldStart == mStartOffset && + aOldEnd == mEndOffset) + return false; + + nsCOMPtr<nsIAccessible> xpcOldPos = ToXPC(aOldPosition); // death grip + for (nsIAccessiblePivotObserver* obs : mObservers.ForwardRange()) { + obs->OnPivotChanged(this, xpcOldPos, aOldStart, aOldEnd, ToXPC(mPosition), + mStartOffset, mEndOffset, aReason, aBoundaryType, + aIsFromUserInput); + } + + return true; +} + +uint16_t RuleCache::Match(const AccessibleOrProxy& aAccOrProxy) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (!mAcceptRoles) { + mAcceptRoles.emplace(); + DebugOnly<nsresult> rv = mRule->GetMatchRoles(*mAcceptRoles); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRule->GetPreFilter(&mPreFilter); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (mPreFilter) { + uint64_t state; + if (aAccOrProxy.IsAccessible()) { + state = aAccOrProxy.AsAccessible()->State(); + } else { + state = aAccOrProxy.AsProxy()->State(); + } + + if ((nsIAccessibleTraversalRule::PREFILTER_PLATFORM_PRUNED & mPreFilter) && + nsAccUtils::MustPrune(aAccOrProxy)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if ((nsIAccessibleTraversalRule::PREFILTER_INVISIBLE & mPreFilter) && + (state & states::INVISIBLE)) + return result; + + if ((nsIAccessibleTraversalRule::PREFILTER_OFFSCREEN & mPreFilter) && + (state & states::OFFSCREEN)) + return result; + + if ((nsIAccessibleTraversalRule::PREFILTER_NOT_FOCUSABLE & mPreFilter) && + !(state & states::FOCUSABLE)) + return result; + + if (aAccOrProxy.IsAccessible() && + (nsIAccessibleTraversalRule::PREFILTER_TRANSPARENT & mPreFilter) && + !(state & states::OPAQUE1)) { + nsIFrame* frame = aAccOrProxy.AsAccessible()->GetFrame(); + if (frame->StyleEffects()->mOpacity == 0.0f) { + return result | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + } + } + + if (mAcceptRoles->Length() > 0) { + uint32_t accessibleRole = aAccOrProxy.Role(); + bool matchesRole = false; + for (uint32_t idx = 0; idx < mAcceptRoles->Length(); idx++) { + matchesRole = mAcceptRoles->ElementAt(idx) == accessibleRole; + if (matchesRole) break; + } + + if (!matchesRole) { + return result; + } + } + + uint16_t matchResult = nsIAccessibleTraversalRule::FILTER_IGNORE; + DebugOnly<nsresult> rv = mRule->Match(ToXPC(aAccOrProxy), &matchResult); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return result | matchResult; +} diff --git a/accessible/base/nsAccessiblePivot.h b/accessible/base/nsAccessiblePivot.h new file mode 100644 index 0000000000..b9d7406c1a --- /dev/null +++ b/accessible/base/nsAccessiblePivot.h @@ -0,0 +1,138 @@ +/* -*- 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; } + + int32_t StartOffset() { return mStartOffset; } + + int32_t EndOffset() { return mEndOffset; } + + 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, + TextBoundaryType aBoundaryType, + 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); + + /* + * 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 0000000000..5d4abb5c1f --- /dev/null +++ b/accessible/base/nsCoreUtils.cpp @@ -0,0 +1,587 @@ +/* -*- 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 "mozilla/dom/Document.h" +#include "nsRange.h" +#include "nsXULElement.h" +#include "nsIDocShell.h" +#include "nsIObserverService.h" +#include "nsPresContext.h" +#include "nsIScrollableFrame.h" +#include "nsISelectionController.h" +#include "nsISimpleEnumerator.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/TouchEvents.h" +#include "nsView.h" +#include "nsGkAtoms.h" + +#include "nsComponentManagerUtils.h" + +#include "XULTreeElement.h" +#include "nsTreeColumns.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/Selection.h" + +using namespace mozilla; + +using mozilla::dom::DOMRect; +using mozilla::dom::Element; +using mozilla::dom::Selection; +using mozilla::dom::XULTreeElement; + +//////////////////////////////////////////////////////////////////////////////// +// nsCoreUtils +//////////////////////////////////////////////////////////////////////////////// + +bool nsCoreUtils::IsLabelWithControl(nsIContent* aContent) { + dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromNode(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(XULTreeElement* aTree, int32_t aRowIndex, + nsTreeColumn* aColumn, + const nsAString& aPseudoElt) { + RefPtr<dom::Element> tcElm = aTree->GetTreeBody(); + if (!tcElm) return; + + Document* document = tcElm->GetUncomposedDoc(); + if (!document) return; + + RefPtr<PresShell> presShell = document->GetPresShell(); + if (!presShell) { + return; + } + + // Ensure row is visible. + aTree->EnsureRowIsVisible(aRowIndex); + + // Calculate x and y coordinates. + nsresult rv; + nsIntRect rect = + aTree->GetCoordsForCellItem(aRowIndex, aColumn, aPseudoElt, rv); + if (NS_FAILED(rv)) { + return; + } + + RefPtr<DOMRect> treeBodyRect = tcElm->GetBoundingClientRect(); + int32_t tcX = (int32_t)treeBodyRect->X(); + int32_t tcY = (int32_t)treeBodyRect->Y(); + + // Dispatch mouse events. + AutoWeakFrame tcFrame = tcElm->GetPrimaryFrame(); + nsIFrame* rootFrame = presShell->GetRootFrame(); + + nsPoint offset; + nsCOMPtr<nsIWidget> rootWidget = + rootFrame->GetView()->GetNearestWidget(&offset); + + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + + int32_t cnvdX = presContext->CSSPixelsToDevPixels(tcX + int32_t(rect.x) + 1) + + presContext->AppUnitsToDevPixels(offset.x); + int32_t cnvdY = presContext->CSSPixelsToDevPixels(tcY + int32_t(rect.y) + 1) + + presContext->AppUnitsToDevPixels(offset.y); + + // XUL is just desktop, so there is no real reason for senfing touch events. + DispatchMouseEvent(eMouseDown, cnvdX, cnvdY, tcElm, tcFrame, presShell, + rootWidget); + + DispatchMouseEvent(eMouseUp, cnvdX, cnvdY, tcElm, tcFrame, presShell, + rootWidget); +} + +void nsCoreUtils::DispatchMouseEvent(EventMessage aMessage, int32_t aX, + int32_t aY, nsIContent* aContent, + nsIFrame* aFrame, PresShell* aPresShell, + nsIWidget* aRootWidget) { + WidgetMouseEvent event(true, aMessage, aRootWidget, WidgetMouseEvent::eReal, + WidgetMouseEvent::eNormal); + + event.mRefPoint = LayoutDeviceIntPoint(aX, aY); + + event.mClickCount = 1; + event.mButton = MouseButton::ePrimary; + event.mTime = PR_IntervalNow(); + event.mInputSource = dom::MouseEvent_Binding::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, PresShell* 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->SetTouchTarget(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->IsElement() || + !aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::accesskey)) + return 0; + + nsPresContext* presContext = aContent->OwnerDoc()->GetPresContext(); + if (!presContext) return 0; + + EventStateManager* esm = presContext->EventStateManager(); + if (!esm) return 0; + + return esm->GetRegisteredAccessKey(aContent->AsElement()); +} + +nsIContent* nsCoreUtils::GetDOMElementFor(nsIContent* aContent) { + if (aContent->IsElement()) return aContent; + + if (aContent->IsText()) 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_Deprecated(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) { + ScrollAxis vertical, horizontal; + ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal); + + return ScrollSubstringTo(aFrame, aRange, vertical, horizontal); +} + +nsresult nsCoreUtils::ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange, + ScrollAxis aVertical, + ScrollAxis aHorizontal) { + if (!aFrame || !aRange) { + return NS_ERROR_FAILURE; + } + + nsPresContext* presContext = aFrame->PresContext(); + + nsCOMPtr<nsISelectionController> selCon; + aFrame->GetSelectionController(presContext, getter_AddRefs(selCon)); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + + RefPtr<dom::Selection> selection = + selCon->GetSelection(nsISelectionController::SELECTION_ACCESSIBILITY); + + selection->RemoveAllRanges(IgnoreErrors()); + selection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, IgnoreErrors()); + + selection->ScrollIntoView(nsISelectionController::SELECTION_ANCHOR_REGION, + aVertical, aHorizontal, + Selection::SCROLL_SYNCHRONOUS); + + selection->CollapseToStart(IgnoreErrors()); + + 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 - frameRect.TopLeft(); + + nsPoint scrollPoint = scrollableFrame->GetScrollPosition(); + scrollPoint -= deltaPoint; + + scrollableFrame->ScrollTo(scrollPoint, ScrollMode::Instant); +} + +void nsCoreUtils::ConvertScrollTypeToPercents(uint32_t aScrollType, + ScrollAxis* aVertical, + ScrollAxis* aHorizontal) { + WhereToScroll whereY, whereX; + WhenToScroll whenY, whenX; + switch (aScrollType) { + case nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT: + whereY = kScrollToTop; + whenY = WhenToScroll::Always; + whereX = kScrollToLeft; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT: + whereY = kScrollToBottom; + whenY = WhenToScroll::Always; + whereX = kScrollToRight; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE: + whereY = kScrollToTop; + whenY = WhenToScroll::Always; + whereX = kScrollMinimum; + whenX = WhenToScroll::IfNotFullyVisible; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_EDGE: + whereY = kScrollToBottom; + whenY = WhenToScroll::Always; + whereX = kScrollMinimum; + whenX = WhenToScroll::IfNotFullyVisible; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_LEFT_EDGE: + whereY = kScrollMinimum; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = kScrollToLeft; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_RIGHT_EDGE: + whereY = kScrollMinimum; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = kScrollToRight; + whenX = WhenToScroll::Always; + break; + default: + whereY = kScrollMinimum; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = kScrollMinimum; + whenX = WhenToScroll::IfNotFullyVisible; + } + *aVertical = ScrollAxis(whereY, whenY); + *aHorizontal = 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(Document* aDocument) { + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = aDocument->GetDocShell(); + NS_ASSERTION(docShellTreeItem, "No document shell for document!"); + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + docShellTreeItem->GetInProcessParent(getter_AddRefs(parentTreeItem)); + + return !parentTreeItem; +} + +bool nsCoreUtils::IsContentDocument(Document* aDocument) { + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = aDocument->GetDocShell(); + NS_ASSERTION(docShellTreeItem, "No document shell tree item for document!"); + + return (docShellTreeItem->ItemType() == nsIDocShellTreeItem::typeContent); +} + +bool nsCoreUtils::IsTopLevelContentDocInProcess(Document* aDocumentNode) { + mozilla::dom::BrowsingContext* bc = aDocumentNode->GetBrowsingContext(); + return bc->IsContent() && ( + // Tab document. + bc->IsTop() || + // Out-of-process iframe. + !bc->GetParent()->IsInProcess()); +} + +bool nsCoreUtils::IsErrorPage(Document* aDocument) { + nsIURI* uri = aDocument->GetDocumentURI(); + if (!uri->SchemeIs("about")) { + return false; + } + + nsAutoCString path; + uri->GetPathQueryRef(path); + + constexpr auto neterror = "neterror"_ns; + constexpr auto certerror = "certerror"_ns; + + return StringBeginsWith(path, neterror) || StringBeginsWith(path, certerror); +} + +PresShell* nsCoreUtils::GetPresShellFor(nsINode* aNode) { + return aNode->OwnerDoc()->GetPresShell(); +} + +bool nsCoreUtils::GetID(nsIContent* aContent, nsAString& aID) { + return aContent->IsElement() && + aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, aID); +} + +bool nsCoreUtils::GetUIntAttr(nsIContent* aContent, nsAtom* aAttr, + int32_t* aUInt) { + nsAutoString value; + if (!aContent->IsElement()) { + return false; + } + aContent->AsElement()->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->IsElement() || + !walkUp->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::lang, + aLanguage))) + walkUp = walkUp->GetParent(); +} + +XULTreeElement* nsCoreUtils::GetTree(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)) { + return XULTreeElement::FromNode(currentContent); + } + currentContent = currentContent->GetFlattenedTreeParent(); + } + + return nullptr; +} + +already_AddRefed<nsTreeColumn> nsCoreUtils::GetFirstSensibleColumn( + XULTreeElement* aTree, FlushType aFlushType) { + if (!aTree) { + return nullptr; + } + + RefPtr<nsTreeColumns> cols = aTree->GetColumns(aFlushType); + if (!cols) { + return nullptr; + } + + RefPtr<nsTreeColumn> column = cols->GetFirstColumn(); + if (column && IsColumnHidden(column)) return GetNextSensibleColumn(column); + + return column.forget(); +} + +uint32_t nsCoreUtils::GetSensibleColumnCount(XULTreeElement* aTree) { + uint32_t count = 0; + if (!aTree) { + return count; + } + + RefPtr<nsTreeColumns> cols = aTree->GetColumns(); + if (!cols) { + return count; + } + + nsTreeColumn* column = cols->GetFirstColumn(); + + while (column) { + if (!IsColumnHidden(column)) count++; + + column = column->GetNext(); + } + + return count; +} + +already_AddRefed<nsTreeColumn> nsCoreUtils::GetSensibleColumnAt( + XULTreeElement* aTree, uint32_t aIndex) { + if (!aTree) { + return nullptr; + } + + uint32_t idx = aIndex; + + nsCOMPtr<nsTreeColumn> column = GetFirstSensibleColumn(aTree); + while (column) { + if (idx == 0) return column.forget(); + + idx--; + column = GetNextSensibleColumn(column); + } + + return nullptr; +} + +already_AddRefed<nsTreeColumn> nsCoreUtils::GetNextSensibleColumn( + nsTreeColumn* aColumn) { + if (!aColumn) { + return nullptr; + } + + RefPtr<nsTreeColumn> nextColumn = aColumn->GetNext(); + + while (nextColumn && IsColumnHidden(nextColumn)) { + nextColumn = nextColumn->GetNext(); + } + + return nextColumn.forget(); +} + +already_AddRefed<nsTreeColumn> nsCoreUtils::GetPreviousSensibleColumn( + nsTreeColumn* aColumn) { + if (!aColumn) { + return nullptr; + } + + RefPtr<nsTreeColumn> prevColumn = aColumn->GetPrevious(); + + while (prevColumn && IsColumnHidden(prevColumn)) { + prevColumn = prevColumn->GetPrevious(); + } + + return prevColumn.forget(); +} + +bool nsCoreUtils::IsColumnHidden(nsTreeColumn* aColumn) { + if (!aColumn) { + return false; + } + + Element* element = aColumn->Element(); + return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters); +} + +void nsCoreUtils::ScrollTo(PresShell* aPresShell, nsIContent* aContent, + uint32_t aScrollType) { + ScrollAxis vertical, horizontal; + ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal); + aPresShell->ScrollContentIntoView(aContent, vertical, horizontal, + ScrollFlags::ScrollOverflowHidden); +} + +bool nsCoreUtils::IsHTMLTableHeader(nsIContent* aContent) { + return aContent->NodeInfo()->Equals(nsGkAtoms::th) || + (aContent->IsElement() && + aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::scope)); +} + +bool nsCoreUtils::IsWhitespaceString(const nsAString& aString) { + nsAString::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); +} + +bool nsCoreUtils::IsDisplayContents(nsIContent* aContent) { + return aContent && aContent->IsElement() && + aContent->AsElement()->IsDisplayContents(); +} diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h new file mode 100644 index 0000000000..a61409eca5 --- /dev/null +++ b/accessible/base/nsCoreUtils.h @@ -0,0 +1,328 @@ +/* -*- 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 "mozilla/FlushType.h" +#include "mozilla/PresShellForwards.h" + +#include "nsPoint.h" +#include "nsTArray.h" + +class nsRange; +class nsTreeColumn; +class nsIFrame; +class nsIDocShell; +class nsIWidget; + +namespace mozilla { +class PresShell; +namespace dom { +class Document; +class XULTreeElement; +} // namespace dom +} // namespace mozilla + +/** + * Core utils. + */ +class nsCoreUtils { + public: + typedef mozilla::PresShell PresShell; + typedef mozilla::dom::Document Document; + + /** + * 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 aTree [in] tree + * @param aRowIndex [in] row index + * @param aColumn [in] column object + * @param aPseudoElm [in] pseudo element inside the cell, see + * XULTreeElement for available values + */ + MOZ_CAN_RUN_SCRIPT + static void DispatchClickEvent(mozilla::dom::XULTreeElement* aTree, + int32_t aRowIndex, nsTreeColumn* aColumn, + const nsAString& aPseudoElt = u""_ns); + + /** + * 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 + */ + MOZ_CAN_RUN_SCRIPT + static void DispatchMouseEvent(mozilla::EventMessage aMessage, int32_t aX, + int32_t aY, nsIContent* aContent, + nsIFrame* aFrame, PresShell* 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 + */ + MOZ_CAN_RUN_SCRIPT + static void DispatchTouchEvent(mozilla::EventMessage aMessage, int32_t aX, + int32_t aY, nsIContent* aContent, + nsIFrame* aFrame, PresShell* 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 + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY 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. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult ScrollSubstringTo( + nsIFrame* aFrame, nsRange* aRange, mozilla::ScrollAxis aVertical, + mozilla::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, + mozilla::ScrollAxis* aVertical, + mozilla::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(Document* aDocument); + + /** + * Return true if the given document is content document (not chrome). + */ + static bool IsContentDocument(Document* aDocument); + + /** + * Return true if the given document is a top level content document in this + * process. + * This will be true for tab documents and out-of-process iframe documents. + */ + static bool IsTopLevelContentDocInProcess(Document* aDocumentNode); + + /** + * Return true if the given document is an error page. + */ + static bool IsErrorPage(Document* aDocument); + + /** + * Return presShell for the document containing the given DOM node. + */ + static PresShell* GetPresShellFor(nsINode* aNode); + + /** + * 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, nsAtom* 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 tree from any levels DOMNode under the XUL tree. + */ + static mozilla::dom::XULTreeElement* GetTree(nsIContent* aContent); + + /** + * Return first sensible column for the given tree box object. + */ + static already_AddRefed<nsTreeColumn> GetFirstSensibleColumn( + mozilla::dom::XULTreeElement* aTree, + mozilla::FlushType = mozilla::FlushType::Frames); + + /** + * Return sensible columns count for the given tree box object. + */ + static uint32_t GetSensibleColumnCount(mozilla::dom::XULTreeElement* aTree); + + /** + * Return sensible column at the given index for the given tree box object. + */ + static already_AddRefed<nsTreeColumn> GetSensibleColumnAt( + mozilla::dom::XULTreeElement* aTree, uint32_t aIndex); + + /** + * Return next sensible column for the given column. + */ + static already_AddRefed<nsTreeColumn> GetNextSensibleColumn( + nsTreeColumn* aColumn); + + /** + * Return previous sensible column for the given column. + */ + static already_AddRefed<nsTreeColumn> GetPreviousSensibleColumn( + nsTreeColumn* aColumn); + + /** + * Return true if the given column is hidden (i.e. not sensible). + */ + static bool IsColumnHidden(nsTreeColumn* aColumn); + + /** + * Scroll content into view. + */ + MOZ_CAN_RUN_SCRIPT + static void ScrollTo(PresShell* aPresShell, nsIContent* aContent, + uint32_t aScrollType); + + /** + * Return true if the given node is table header element. + */ + static bool IsHTMLTableHeader(nsIContent* aContent); + + /** + * 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 nsAString& 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); + + static bool IsDisplayContents(nsIContent* aContent); +}; + +#endif diff --git a/accessible/base/nsEventShell.cpp b/accessible/base/nsEventShell.cpp new file mode 100644 index 0000000000..d158102a79 --- /dev/null +++ b/accessible/base/nsEventShell.cpp @@ -0,0 +1,71 @@ +/* -*- 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 "Logging.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 ? u"true"_ns : u"false"_ns); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 0000000000..6ceb66a4f2 --- /dev/null +++ b/accessible/base/nsEventShell.h @@ -0,0 +1,66 @@ +/* -*- 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 0000000000..77a3f77823 --- /dev/null +++ b/accessible/base/nsTextEquivUtils.cpp @@ -0,0 +1,355 @@ +/* -*- 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 "mozilla/dom/Text.h" + +using namespace mozilla; +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 const Accessible* sInitiatorAcc = nullptr; + +//////////////////////////////////////////////////////////////////////////////// +// nsTextEquivUtils. Public. + +nsresult nsTextEquivUtils::GetNameFromSubtree(const 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(const Accessible* aAccessible, + nsAtom* 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( + const 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->IsText()) { + 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::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + aString->Append(text.mString); + } else { + // If aContent is an object that is display: none, we have no a frame. + aContent->GetAsText()->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( + const 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) { + if (ShouldIncludeInSubtreeCalculation(aAccessible)) { + 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; + if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL)) { + aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, + textEquivalent); + } else { + aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, + textEquivalent); + } + + if (textEquivalent.IsEmpty()) { + aContent->AsElement()->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, macSubrole, msaaRole, \ + ia2Role, androidClass, nameRule) \ + case roles::geckoRole: \ + return nameRule; + + switch (aRole) { +#include "RoleMap.h" + default: + MOZ_CRASH("Unknown role."); + } + +#undef ROLE +} + +bool nsTextEquivUtils::ShouldIncludeInSubtreeCalculation( + Accessible* aAccessible) { + uint32_t nameRule = GetRoleRule(aAccessible->Role()); + if (nameRule == eNameFromSubtreeRule) { + return true; + } + if (!(nameRule & eNameFromSubtreeIfReqRule)) { + return false; + } + + if (aAccessible == sInitiatorAcc) { + // We're calculating the text equivalent for this accessible, but this + // accessible should only be included when calculating the text equivalent + // for something else. + return false; + } + + // sInitiatorAcc can be null when, for example, Accessible::Value calls + // GetTextEquivFromSubtree. + role initiatorRole = sInitiatorAcc ? sInitiatorAcc->Role() : roles::NOTHING; + if (initiatorRole == roles::OUTLINEITEM && + aAccessible->Role() == roles::GROUPING) { + // Child treeitems are contained in a group. We don't want to include those + // in the parent treeitem's text equivalent. + return false; + } + + return true; +} diff --git a/accessible/base/nsTextEquivUtils.h b/accessible/base/nsTextEquivUtils.h new file mode 100644 index 0000000000..41fada6b10 --- /dev/null +++ b/accessible/base/nsTextEquivUtils.h @@ -0,0 +1,164 @@ +/* -*- 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(const 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(const 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(const Accessible* aAccessible, + nsAtom* 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(const 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(const 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); + + /** + * Returns true if a given accessible should be included when calculating + * the text equivalent for the initiator's subtree. + */ + static bool ShouldIncludeInSubtreeCalculation(Accessible* aAccessible); +}; + +#endif |