diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /accessible/base | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/base')
74 files changed, 25241 insertions, 0 deletions
diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp new file mode 100644 index 0000000000..01cc5d0417 --- /dev/null +++ b/accessible/base/ARIAMap.cpp @@ -0,0 +1,1687 @@ +/* -*- 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 "AccAttributes.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "mozilla/a11y/Role.h" +#include "States.h" + +#include "nsAttrName.h" +#include "nsWhitespaceTokenizer.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/dom/Element.h" + +#include "nsUnicharUtils.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". + * + * Note: the list must remain alphabetically ordered to support binary search. + */ + +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 + }, + { // definition + nsGkAtoms::definition, + roles::DEFINITION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // 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 + }, + { // emphasis + nsGkAtoms::emphasis, + roles::EMPHASIS, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // 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 + }, + { // generic + nsGkAtoms::generic, + roles::SECTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + 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 + }, + { // image + nsGkAtoms::image, + roles::GRAPHIC, + 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 + }, + { // meter + nsGkAtoms::meter, + roles::METER, + kUseMapRole, + eHasValueMinMax, + eNoAction, + eNoLiveAttr, + kGenericAccType, + states::READONLY + }, + { // 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 + }, + { // strong + nsGkAtoms::strong, + roles::STRONG, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates + }, + { // subscript + nsGkAtoms::subscript, + roles::SUBSCRIPT, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType + }, + { // suggestion + nsGkAtoms::suggestion, + roles::SUGGESTION, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kGenericAccType, + }, + { // superscript + nsGkAtoms::superscript, + roles::SUPERSCRIPT, + 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 + }, + { // time + nsGkAtoms::time, + roles::TIME, + kUseMapRole, + eNoValue, + eNoAction, + eNoLiveAttr, + kNoReqStates + }, + { // 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_colcount, ATTR_VALINT }, + {nsGkAtoms::aria_colindex, ATTR_VALINT }, + {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 }, + // XXX Ideally, aria-description shouldn't expose a description object + // attribute (i.e. it should have ATTR_BYPASSOBJ). However, until the + // description-from attribute is implemented (bug 1726087), clients such as + // NVDA depend on the description object attribute to work out whether the + // accDescription originated from aria-description. + {nsGkAtoms::aria_description, 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_GLOBAL }, + {nsGkAtoms::aria_required, ATTR_BYPASSOBJ | ATTR_VALTOKEN }, + {nsGkAtoms::aria_rowcount, ATTR_VALINT }, + {nsGkAtoms::aria_rowindex, ATTR_VALINT }, + {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 +}; + +const nsRoleMapEntry* aria::GetRoleMap(dom::Element* aEl) { + return GetRoleMapFromIndex(GetRoleMapIndex(aEl)); +} + +uint8_t aria::GetRoleMapIndex(dom::Element* aEl) { + nsAutoString roles; + if (!aEl || !nsAccUtils::GetARIAAttr(aEl, 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; + auto comparator = [&role](const nsRoleMapEntry& aEntry) { + return Compare(role, aEntry.ARIARoleString(), + nsCaseInsensitiveStringComparator); + }; + if (BinarySearchIf(sWAIRoleMaps, 0, ArrayLength(sWAIRoleMaps), comparator, + &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 { + uint8_t index = aRoleMapEntry - sWAIRoleMaps; + MOZ_ASSERT(aria::IsRoleMapIndexValid(index)); + return index; + } +} + +bool aria::IsRoleMapIndexValid(uint8_t aRoleMapIndex) { + switch (aRoleMapIndex) { + case NO_ROLE_MAP_ENTRY_INDEX: + case EMPTY_ROLE_MAP_ENTRY_INDEX: + case LANDMARK_ROLE_MAP_ENTRY_INDEX: + return true; + } + return aRoleMapIndex < ArrayLength(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() && + nsAccUtils::ARIAAttrValueIs(aContent->AsElement(), + nsGkAtoms::aria_hidden, nsGkAtoms::_true, + eCaseMatters); +} + +//////////////////////////////////////////////////////////////////////////////// +// AttrIterator class + +AttrIterator::AttrIterator(nsIContent* aContent) + : mElement(dom::Element::FromNode(aContent)), + mIteratingDefaults(false), + mAttrIdx(0), + mAttrCharacteristics(0) { + mAttrs = mElement ? &mElement->GetAttrs() : nullptr; + mAttrCount = mAttrs ? mAttrs->AttrCount() : 0; +} + +bool AttrIterator::Next() { + while (mAttrIdx < mAttrCount) { + const nsAttrName* attr = mAttrs->GetSafeAttrNameAt(mAttrIdx); + mAttrIdx++; + if (attr->NamespaceEquals(kNameSpaceID_None)) { + mAttrAtom = attr->Atom(); + nsDependentAtomString attrStr(mAttrAtom); + if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // Not ARIA + + if (mIteratingDefaults) { + if (mOverriddenAttrs.Contains(mAttrAtom)) { + continue; + } + } else { + mOverriddenAttrs.Insert(mAttrAtom); + } + + // AttrCharacteristicsFor has to search for the entry, so cache it here + // rather than having to search again later. + mAttrCharacteristics = aria::AttrCharacteristicsFor(mAttrAtom); + if (mAttrCharacteristics & ATTR_BYPASSOBJ) { + continue; // No need to handle exposing as obj attribute here + } + + if ((mAttrCharacteristics & ATTR_VALTOKEN) && + !nsAccUtils::HasDefinedARIAToken(mAttrs, mAttrAtom)) { + continue; // only expose token based attributes if they are defined + } + + if ((mAttrCharacteristics & ATTR_BYPASSOBJ_IF_FALSE) && + mAttrs->AttrValueIs(kNameSpaceID_None, mAttrAtom, nsGkAtoms::_false, + eCaseMatters)) { + continue; // only expose token based attribute if value is not 'false'. + } + + return true; + } + } + + mAttrCharacteristics = 0; + mAttrAtom = nullptr; + + if (const auto* defaults = nsAccUtils::GetARIADefaults(mElement); + !mIteratingDefaults && defaults) { + mIteratingDefaults = true; + mAttrs = defaults; + mAttrCount = mAttrs->AttrCount(); + mAttrIdx = 0; + return Next(); + } + + return false; +} + +nsAtom* AttrIterator::AttrName() const { return mAttrAtom; } + +void AttrIterator::AttrValue(nsAString& aAttrValue) const { + nsAutoString value; + if (mAttrs->GetAttr(mAttrAtom, value)) { + if (mAttrCharacteristics & ATTR_VALTOKEN) { + nsAtom* normalizedValue = + nsAccUtils::NormalizeARIAToken(mAttrs, mAttrAtom); + if (normalizedValue) { + nsDependentAtomString normalizedValueStr(normalizedValue); + aAttrValue.Assign(normalizedValueStr); + return; + } + } + aAttrValue.Assign(value); + } +} + +bool AttrIterator::ExposeAttr(AccAttributes* aTargetAttrs) const { + if (mAttrCharacteristics & ATTR_VALTOKEN) { + nsAtom* normalizedValue = nsAccUtils::NormalizeARIAToken(mAttrs, mAttrAtom); + if (normalizedValue) { + aTargetAttrs->SetAttribute(mAttrAtom, normalizedValue); + return true; + } + } else if (mAttrCharacteristics & ATTR_VALINT) { + int32_t intVal; + if (nsCoreUtils::GetUIntAttrValue(mAttrs->GetAttr(mAttrAtom), &intVal)) { + aTargetAttrs->SetAttribute(mAttrAtom, intVal); + return true; + } + if (mAttrAtom == nsGkAtoms::aria_colcount || + mAttrAtom == nsGkAtoms::aria_rowcount) { + // These attributes allow a value of -1. + if (mAttrs->AttrValueIs(kNameSpaceID_None, mAttrAtom, u"-1"_ns, + eCaseMatters)) { + aTargetAttrs->SetAttribute(mAttrAtom, -1); + return true; + } + } + return false; // Invalid value. + } + nsAutoString value; + if (mAttrs->GetAttr(mAttrAtom, value)) { + aTargetAttrs->SetAttribute(mAttrAtom, std::move(value)); + return true; + } + return false; +} diff --git a/accessible/base/ARIAMap.h b/accessible/base/ARIAMap.h new file mode 100644 index 0000000000..30cc1f0814 --- /dev/null +++ b/accessible/base/ARIAMap.h @@ -0,0 +1,335 @@ +/* -*- 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" +#include "nsTHashSet.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; + +/** + * Indicates that the attribute should have an integer value. + */ +const uint8_t ATTR_VALINT = 0x1 << 4; + +//////////////////////////////////////////////////////////////////////////////// +// 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; + + // LocalAccessible 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 { +class AccAttributes; + +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); + +/** + * Determine whether a role map entry index is valid. + */ +bool IsRoleMapIndexValid(uint8_t aRoleMapIndex); + +/** + * 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(); + + nsAtom* AttrName() const; + + void AttrValue(nsAString& aAttrValue) const; + + /** + * Expose this ARIA attribute in a specified AccAttributes. The appropriate + * type will be used for the attribute; e.g. an atom for a token value. + */ + bool ExposeAttr(AccAttributes* aTargetAttrs) const; + + private: + AttrIterator() = delete; + AttrIterator(const AttrIterator&) = delete; + AttrIterator& operator=(const AttrIterator&) = delete; + + dom::Element* mElement; + + bool mIteratingDefaults; + nsTHashSet<RefPtr<nsAtom>> mOverriddenAttrs; + + const AttrArray* mAttrs; + uint32_t mAttrIdx; + uint32_t mAttrCount; + RefPtr<nsAtom> mAttrAtom; + uint8_t mAttrCharacteristics; +}; + +} // 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..6bf20cf1cc --- /dev/null +++ b/accessible/base/ARIAStateMap.cpp @@ -0,0 +1,334 @@ +/* -*- 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 (!nsAccUtils::HasARIAAttr(aElement, nsGkAtoms::aria_valuenow) && + !nsAccUtils::HasARIAAttr(aElement, nsGkAtoms::aria_valuetext)) { + *aState |= states::MIXED; + } + + return true; + } + + case eFocusableUntilDisabled: { + if (!nsAccUtils::HasDefinedARIAToken(aElement, + nsGkAtoms::aria_disabled) || + nsAccUtils::ARIAAttrValueIs(aElement, 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 (nsAccUtils::FindARIAAttrValueIn(aElement, 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 (nsAccUtils::ARIAAttrValueIs(aElement, 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 (nsAccUtils::ARIAAttrValueIs(aElement, 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/AccAttributes.cpp b/accessible/base/AccAttributes.cpp new file mode 100644 index 0000000000..4018f09074 --- /dev/null +++ b/accessible/base/AccAttributes.cpp @@ -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/. */ + +#include "AccAttributes.h" +#include "StyleInfo.h" +#include "mozilla/ToString.h" +#include "nsAtom.h" + +using namespace mozilla::a11y; + +bool AccAttributes::GetAttribute(nsAtom* aAttrName, + nsAString& aAttrValue) const { + if (auto value = mData.Lookup(aAttrName)) { + StringFromValueAndName(aAttrName, *value, aAttrValue); + return true; + } + + return false; +} + +void AccAttributes::StringFromValueAndName(nsAtom* aAttrName, + const AttrValueType& aValue, + nsAString& aValueString) { + aValueString.Truncate(); + + aValue.match( + [&aValueString](const bool& val) { + aValueString.Assign(val ? u"true" : u"false"); + }, + [&aValueString](const float& val) { + aValueString.AppendFloat(val * 100); + aValueString.Append(u"%"); + }, + [&aValueString](const double& val) { aValueString.AppendFloat(val); }, + [&aValueString](const int32_t& val) { aValueString.AppendInt(val); }, + [&aValueString](const RefPtr<nsAtom>& val) { + val->ToString(aValueString); + }, + [&aValueString](const nsTArray<int32_t>& val) { + if (const size_t len = val.Length()) { + for (size_t i = 0; i < len - 1; i++) { + aValueString.AppendInt(val[i]); + aValueString.Append(u", "); + } + aValueString.AppendInt(val[len - 1]); + } else { + // The array is empty + NS_WARNING( + "Hmm, should we have used a DeleteEntry() for this instead?"); + aValueString.Append(u"[ ]"); + } + }, + [&aValueString](const CSSCoord& val) { + aValueString.AppendFloat(val); + aValueString.Append(u"px"); + }, + [&aValueString](const FontSize& val) { + aValueString.AppendInt(val.mValue); + aValueString.Append(u"pt"); + }, + [&aValueString](const Color& val) { + StyleInfo::FormatColor(val.mValue, aValueString); + }, + [&aValueString](const DeleteEntry& val) { + aValueString.Append(u"-delete-entry-"); + }, + [&aValueString](const UniquePtr<nsString>& val) { + aValueString.Assign(*val); + }, + [&aValueString](const RefPtr<AccAttributes>& val) { + aValueString.Assign(u"AccAttributes{...}"); + }, + [&aValueString](const uint64_t& val) { aValueString.AppendInt(val); }, + [&aValueString](const UniquePtr<AccGroupInfo>& val) { + aValueString.Assign(u"AccGroupInfo{...}"); + }, + [&aValueString](const UniquePtr<gfx::Matrix4x4>& val) { + aValueString.AppendPrintf("Matrix4x4=%s", ToString(*val).c_str()); + }, + [&aValueString](const nsTArray<uint64_t>& val) { + if (const size_t len = val.Length()) { + for (size_t i = 0; i < len - 1; i++) { + aValueString.AppendInt(val[i]); + aValueString.Append(u", "); + } + aValueString.AppendInt(val[len - 1]); + } else { + // The array is empty + NS_WARNING( + "Hmm, should we have used a DeleteEntry() for this instead?"); + aValueString.Append(u"[ ]"); + } + }); +} + +void AccAttributes::Update(AccAttributes* aOther) { + for (auto iter = aOther->mData.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data().is<DeleteEntry>()) { + mData.Remove(iter.Key()); + } else { + mData.InsertOrUpdate(iter.Key(), std::move(iter.Data())); + } + iter.Remove(); + } +} + +bool AccAttributes::Equal(const AccAttributes* aOther) const { + if (Count() != aOther->Count()) { + return false; + } + for (auto iter = mData.ConstIter(); !iter.Done(); iter.Next()) { + const auto otherEntry = aOther->mData.Lookup(iter.Key()); + if (!otherEntry) { + return false; + } + if (iter.Data().is<UniquePtr<nsString>>()) { + // Because we store nsString in a UniquePtr, we must handle it specially + // so we compare the string and not the pointer. + if (!otherEntry->is<UniquePtr<nsString>>()) { + return false; + } + const auto& thisStr = iter.Data().as<UniquePtr<nsString>>(); + const auto& otherStr = otherEntry->as<UniquePtr<nsString>>(); + if (*thisStr != *otherStr) { + return false; + } + } else if (iter.Data() != otherEntry.Data()) { + return false; + } + } + return true; +} + +void AccAttributes::CopyTo(AccAttributes* aDest) const { + for (auto iter = mData.ConstIter(); !iter.Done(); iter.Next()) { + iter.Data().match( + [&iter, &aDest](const bool& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const float& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const double& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const int32_t& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const RefPtr<nsAtom>& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const nsTArray<int32_t>& val) { + // We don't copy arrays. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an array"); + }, + [&iter, &aDest](const CSSCoord& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const FontSize& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [&iter, &aDest](const Color& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const DeleteEntry& val) { + // We don't copy DeleteEntry. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing a DeleteEntry"); + }, + [&iter, &aDest](const UniquePtr<nsString>& val) { + aDest->SetAttributeStringCopy(iter.Key(), *val); + }, + [](const RefPtr<AccAttributes>& val) { + // We don't copy nested AccAttributes. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an AccAttributes"); + }, + [&iter, &aDest](const uint64_t& val) { + aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val)); + }, + [](const UniquePtr<AccGroupInfo>& val) { + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an AccGroupInfo"); + }, + [](const UniquePtr<gfx::Matrix4x4>& val) { + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing a matrix"); + }, + [](const nsTArray<uint64_t>& val) { + // We don't copy arrays. + MOZ_ASSERT_UNREACHABLE( + "Trying to copy an AccAttributes containing an array"); + }); + } +} + +#ifdef A11Y_LOG +void AccAttributes::DebugPrint(const char* aPrefix, + const AccAttributes& aAttributes) { + nsAutoString prettyString; + prettyString.AssignLiteral("{\n"); + for (const auto& iter : aAttributes) { + nsAutoString name; + iter.NameAsString(name); + + nsAutoString value; + iter.ValueAsString(value); + prettyString.AppendLiteral(" "); + prettyString.Append(name); + prettyString.AppendLiteral(": "); + prettyString.Append(value); + prettyString.AppendLiteral("\n"); + } + + prettyString.AppendLiteral("}"); + printf("%s %s\n", aPrefix, NS_ConvertUTF16toUTF8(prettyString).get()); +} +#endif + +size_t AccAttributes::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = + aMallocSizeOf(this) + mData.ShallowSizeOfExcludingThis(aMallocSizeOf); + + for (auto iter : *this) { + size += iter.SizeOfExcludingThis(aMallocSizeOf); + } + + return size; +} + +size_t AccAttributes::Entry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = 0; + + // We don't count the size of Name() since it's counted by the atoms table + // memory reporter. + + if (mValue->is<nsTArray<int32_t>>()) { + size += mValue->as<nsTArray<int32_t>>().ShallowSizeOfExcludingThis( + aMallocSizeOf); + } else if (mValue->is<UniquePtr<nsString>>()) { + // String data will never be shared. + size += mValue->as<UniquePtr<nsString>>()->SizeOfIncludingThisIfUnshared( + aMallocSizeOf); + } else if (mValue->is<RefPtr<AccAttributes>>()) { + size += + mValue->as<RefPtr<AccAttributes>>()->SizeOfIncludingThis(aMallocSizeOf); + } else if (mValue->is<UniquePtr<AccGroupInfo>>()) { + size += mValue->as<UniquePtr<AccGroupInfo>>()->SizeOfIncludingThis( + aMallocSizeOf); + } else if (mValue->is<UniquePtr<gfx::Matrix4x4>>()) { + size += aMallocSizeOf(mValue->as<UniquePtr<gfx::Matrix4x4>>().get()); + } else if (mValue->is<nsTArray<uint64_t>>()) { + size += mValue->as<nsTArray<uint64_t>>().ShallowSizeOfExcludingThis( + aMallocSizeOf); + } else { + // This type is stored directly and already counted or is an atom and + // stored and counted in the atoms table. + // Assert that we have exhausted all the remaining variant types. + MOZ_ASSERT(mValue->is<RefPtr<nsAtom>>() || mValue->is<bool>() || + mValue->is<float>() || mValue->is<double>() || + mValue->is<int32_t>() || mValue->is<uint64_t>() || + mValue->is<CSSCoord>() || mValue->is<FontSize>() || + mValue->is<Color>() || mValue->is<DeleteEntry>()); + } + + return size; +} diff --git a/accessible/base/AccAttributes.h b/accessible/base/AccAttributes.h new file mode 100644 index 0000000000..0d7610b358 --- /dev/null +++ b/accessible/base/AccAttributes.h @@ -0,0 +1,293 @@ +/* -*- 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 AccAttributes_h_ +#define AccAttributes_h_ + +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/a11y/AccGroupInfo.h" +#include "mozilla/Variant.h" +#include "nsTHashMap.h" +#include "nsStringFwd.h" +#include "mozilla/gfx/Matrix.h" + +class nsVariant; + +namespace IPC { +template <typename T> +struct ParamTraits; +} // namespace IPC + +namespace mozilla { + +namespace dom { +class Element; +} + +namespace a11y { + +struct FontSize { + int32_t mValue; + + bool operator==(const FontSize& aOther) const { + return mValue == aOther.mValue; + } + + bool operator!=(const FontSize& aOther) const { + return mValue != aOther.mValue; + } +}; + +struct Color { + nscolor mValue; + + bool operator==(const Color& aOther) const { return mValue == aOther.mValue; } + + bool operator!=(const Color& aOther) const { return mValue != aOther.mValue; } +}; + +// A special type. If an entry has a value of this type, it instructs the +// target instance of an Update to remove the entry with the same key value. +struct DeleteEntry { + DeleteEntry() : mValue(true) {} + bool mValue; + + bool operator==(const DeleteEntry& aOther) const { return true; } + + bool operator!=(const DeleteEntry& aOther) const { return false; } +}; + +class AccAttributes { + // Warning! An AccAttributes can contain another AccAttributes. This is + // intended for object and text attributes. However, the nested + // AccAttributes should never itself contain another AccAttributes, nor + // should it create a cycle. We don't do cycle collection here for + // performance reasons, so violating this rule will cause leaks! + using AttrValueType = + Variant<bool, float, double, int32_t, RefPtr<nsAtom>, nsTArray<int32_t>, + CSSCoord, FontSize, Color, DeleteEntry, UniquePtr<nsString>, + RefPtr<AccAttributes>, uint64_t, UniquePtr<AccGroupInfo>, + UniquePtr<gfx::Matrix4x4>, nsTArray<uint64_t>>; + static_assert(sizeof(AttrValueType) <= 16); + using AtomVariantMap = nsTHashMap<RefPtr<nsAtom>, AttrValueType>; + + protected: + ~AccAttributes() = default; + + public: + AccAttributes() = default; + AccAttributes(const AccAttributes&) = delete; + AccAttributes& operator=(const AccAttributes&) = delete; + + NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::AccAttributes) + + template <typename T> + void SetAttribute(nsAtom* aAttrName, T&& aAttrValue) { + using ValType = + std::remove_const_t<std::remove_reference_t<decltype(aAttrValue)>>; + if constexpr (std::is_convertible_v<ValType, nsString>) { + static_assert(std::is_rvalue_reference_v<decltype(aAttrValue)>, + "Please only move strings into this function. To make a " + "copy, use SetAttributeStringCopy."); + UniquePtr<nsString> value = MakeUnique<nsString>(std::move(aAttrValue)); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_same_v<ValType, gfx::Matrix4x4>) { + UniquePtr<gfx::Matrix4x4> value = MakeUnique<gfx::Matrix4x4>(aAttrValue); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_same_v<ValType, AccGroupInfo*>) { + UniquePtr<AccGroupInfo> value(aAttrValue); + mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value))); + } else if constexpr (std::is_convertible_v<ValType, nsAtom*>) { + mData.InsertOrUpdate(aAttrName, AsVariant(RefPtr<nsAtom>(aAttrValue))); + } else { + mData.InsertOrUpdate(aAttrName, AsVariant(std::forward<T>(aAttrValue))); + } + } + + void SetAttributeStringCopy(nsAtom* aAttrName, nsString aAttrValue) { + SetAttribute(aAttrName, std::move(aAttrValue)); + } + + template <typename T> + Maybe<const T&> GetAttribute(nsAtom* aAttrName) const { + if (auto value = mData.Lookup(aAttrName)) { + if constexpr (std::is_same_v<nsString, T>) { + if (value->is<UniquePtr<nsString>>()) { + const T& val = *(value->as<UniquePtr<nsString>>().get()); + return SomeRef(val); + } + } else if constexpr (std::is_same_v<gfx::Matrix4x4, T>) { + if (value->is<UniquePtr<gfx::Matrix4x4>>()) { + const T& val = *(value->as<UniquePtr<gfx::Matrix4x4>>()); + return SomeRef(val); + } + } else { + if (value->is<T>()) { + const T& val = value->as<T>(); + return SomeRef(val); + } + } + } + return Nothing(); + } + + template <typename T> + RefPtr<const T> GetAttributeRefPtr(nsAtom* aAttrName) const { + if (auto value = mData.Lookup(aAttrName)) { + if (value->is<RefPtr<T>>()) { + RefPtr<const T> ref = value->as<RefPtr<T>>(); + return ref; + } + } + return nullptr; + } + + template <typename T> + Maybe<T&> GetMutableAttribute(nsAtom* aAttrName) const { + static_assert(std::is_same_v<nsTArray<int32_t>, T> || + std::is_same_v<nsTArray<uint64_t>, T>, + "Only arrays should be mutable attributes"); + if (auto value = mData.Lookup(aAttrName)) { + if (value->is<T>()) { + T& val = value->as<T>(); + return SomeRef(val); + } + } + return Nothing(); + } + + // Get stringified value + bool GetAttribute(nsAtom* aAttrName, nsAString& aAttrValue) const; + + bool HasAttribute(nsAtom* aAttrName) const { + return mData.Contains(aAttrName); + } + + bool Remove(nsAtom* aAttrName) { return mData.Remove(aAttrName); } + + uint32_t Count() const { return mData.Count(); } + + // Update one instance with the entries in another. The supplied AccAttributes + // will be emptied. + void Update(AccAttributes* aOther); + + /** + * Return true if all the attributes in this instance are equal to all the + * attributes in another instance. + */ + bool Equal(const AccAttributes* aOther) const; + + /** + * Copy attributes from this instance to another instance. + * This should only be used in very specific cases; e.g. merging two sets of + * cached attributes without modifying the cache. It can only copy simple + * value types; e.g. it can't copy array values. Attempting to copy an + * AccAttributes with uncopyable values will cause an assertion. + */ + void CopyTo(AccAttributes* aDest) const; + + // An entry class for our iterator. + class Entry { + public: + Entry(nsAtom* aAttrName, const AttrValueType* aAttrValue) + : mName(aAttrName), mValue(aAttrValue) {} + + nsAtom* Name() const { return mName; } + + template <typename T> + Maybe<const T&> Value() const { + if constexpr (std::is_same_v<nsString, T>) { + if (mValue->is<UniquePtr<nsString>>()) { + const T& val = *(mValue->as<UniquePtr<nsString>>().get()); + return SomeRef(val); + } + } else if constexpr (std::is_same_v<gfx::Matrix4x4, T>) { + if (mValue->is<UniquePtr<gfx::Matrix4x4>>()) { + const T& val = *(mValue->as<UniquePtr<gfx::Matrix4x4>>()); + return SomeRef(val); + } + } else { + if (mValue->is<T>()) { + const T& val = mValue->as<T>(); + return SomeRef(val); + } + } + return Nothing(); + } + + void NameAsString(nsString& aName) const { + mName->ToString(aName); + if (StringBeginsWith(aName, u"aria-"_ns)) { + // Found 'aria-' + aName.ReplaceLiteral(0, 5, u""); + } + } + + void ValueAsString(nsAString& aValueString) const { + StringFromValueAndName(mName, *mValue, aValueString); + } + + // Size of the pair in the hash table. + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf); + + private: + nsAtom* mName; + const AttrValueType* mValue; + + friend class AccAttributes; + }; + + class Iterator { + public: + explicit Iterator(AtomVariantMap::const_iterator aIter) + : mHashIterator(aIter) {} + + Iterator() = delete; + Iterator(const Iterator&) = delete; + Iterator& operator=(const Iterator&) = delete; + + bool operator!=(const Iterator& aOther) const { + return mHashIterator != aOther.mHashIterator; + } + + Iterator& operator++() { + mHashIterator++; + return *this; + } + + Entry operator*() const { + auto& entry = *mHashIterator; + return Entry(entry.GetKey(), &entry.GetData()); + } + + private: + AtomVariantMap::const_iterator mHashIterator; + }; + + friend class Iterator; + + Iterator begin() const { return Iterator(mData.begin()); } + Iterator end() const { return Iterator(mData.end()); } + +#ifdef A11Y_LOG + static void DebugPrint(const char* aPrefix, const AccAttributes& aAttributes); +#endif + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf); + + private: + static void StringFromValueAndName(nsAtom* aAttrName, + const AttrValueType& aValue, + nsAString& aValueString); + + AtomVariantMap mData; + + friend struct IPC::ParamTraits<AccAttributes*>; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccEvent.cpp b/accessible/base/AccEvent.cpp new file mode 100644 index 0000000000..1d1b4386f8 --- /dev/null +++ b/accessible/base/AccEvent.cpp @@ -0,0 +1,256 @@ +/* -*- 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 "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, LocalAccessible* 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 + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* 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(LocalAccessible* aTarget, bool aNeedsShutdown) + : AccMutationEvent(::nsIAccessibleEvent::EVENT_HIDE, aTarget), + mNeedsShutdown(aNeedsShutdown) { + mNextSibling = mAccessible->LocalNextSibling(); + mPrevSibling = mAccessible->LocalPrevSibling(); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccShowEvent +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// AccTextSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccTextSelChangeEvent::AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, + int32_t aReason, + int32_t aGranularity) + : AccEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, aTarget, + eAutoDetect, eCoalesceTextSelChange), + mSel(aSelection), + mReason(aReason), + mGranularity(aGranularity) {} + +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(LocalAccessible* aWidget, + LocalAccessible* 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; + } +} + +already_AddRefed<nsIAccessibleEvent> a11y::MakeXPCEvent(AccEvent* aEvent) { + DocAccessible* doc = aEvent->Document(); + LocalAccessible* 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(), cm->IsAtEndOfLine(), + cm->GetGranularity()); + 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(ranges[idx])); + } + + xpEvent = new xpcAccTextSelectionChangeEvent( + type, ToXPC(acc), ToXPCDocument(doc), node, fromUser, xpcRanges); + 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..a4ff82916a --- /dev/null +++ b/accessible/base/AccEvent.h @@ -0,0 +1,562 @@ +/* -*- 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/LocalAccessible.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, LocalAccessible* 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); + } + + LocalAccessible* 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, + 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<LocalAccessible> mAccessible; + + friend class EventQueue; + friend class EventTree; + friend class ::nsEventShell; + friend class NotificationController; +}; + +/** + * Accessible state change event. + */ +class AccStateChangeEvent : public AccEvent { + public: + AccStateChangeEvent(LocalAccessible* aAccessible, uint64_t aState, + bool aIsEnabled, + EIsFromUserInput aIsFromUserInput = eAutoDetect) + : AccEvent(nsIAccessibleEvent::EVENT_STATE_CHANGE, aAccessible, + aIsFromUserInput, eCoalesceStateChange), + mState(aState), + mIsEnabled(aIsEnabled) {} + + AccStateChangeEvent(LocalAccessible* 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(LocalAccessible* 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, LocalAccessible* 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, LocalAccessible* aTarget) + : AccTreeMutationEvent(aEventType, aTarget) { + // Don't coalesce these since they are coalesced by reorder event. Coalesce + // contained text change events. + mParent = mAccessible->LocalParent(); + } + 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; } + + LocalAccessible* LocalParent() const { return mParent; } + + protected: + RefPtr<LocalAccessible> mParent; + RefPtr<AccTextChangeEvent> mTextChangeEvent; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * Accessible hide event. + */ +class AccHideEvent : public AccMutationEvent { + public: + explicit AccHideEvent(LocalAccessible* aTarget, bool aNeedsShutdown = true); + + // Event + static const EventGroup kEventGroup = eHideEvent; + virtual unsigned int GetEventGroups() const override { + return AccMutationEvent::GetEventGroups() | (1U << eHideEvent); + } + + // AccHideEvent + LocalAccessible* TargetParent() const { return mParent; } + LocalAccessible* TargetNextSibling() const { return mNextSibling; } + LocalAccessible* TargetPrevSibling() const { return mPrevSibling; } + bool NeedsShutdown() const { return mNeedsShutdown; } + + protected: + bool mNeedsShutdown; + RefPtr<LocalAccessible> mNextSibling; + RefPtr<LocalAccessible> mPrevSibling; + + friend class EventTree; + friend class NotificationController; +}; + +/** + * Accessible show event. + */ +class AccShowEvent : public AccMutationEvent { + public: + explicit AccShowEvent(LocalAccessible* aTarget) + : AccMutationEvent(::nsIAccessibleEvent::EVENT_SHOW, aTarget) {} + + // Event + static const EventGroup kEventGroup = eShowEvent; + virtual unsigned int GetEventGroups() const override { + return AccMutationEvent::GetEventGroups() | (1U << eShowEvent); + } +}; + +/** + * Class for reorder accessible event. Takes care about + */ +class AccReorderEvent : public AccTreeMutationEvent { + public: + explicit AccReorderEvent(LocalAccessible* 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); + } + + /* + * Make this an inner reorder event that is coalesced into + * a reorder event of an ancestor. + */ + void SetInner() { mEventType = ::nsIAccessibleEvent::EVENT_INNER_REORDER; } +}; + +/** + * Accessible caret move event. + */ +class AccCaretMoveEvent : public AccEvent { + public: + AccCaretMoveEvent(LocalAccessible* aAccessible, int32_t aCaretOffset, + bool aIsSelectionCollapsed, bool aIsAtEndOfLine, + int32_t aGranularity, + EIsFromUserInput aIsFromUserInput = eAutoDetect) + : AccEvent(::nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED, aAccessible, + aIsFromUserInput), + mCaretOffset(aCaretOffset), + mIsSelectionCollapsed(aIsSelectionCollapsed), + mIsAtEndOfLine(aIsAtEndOfLine), + mGranularity(aGranularity) {} + 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; } + bool IsAtEndOfLine() { return mIsAtEndOfLine; } + + int32_t GetGranularity() const { return mGranularity; } + + private: + int32_t mCaretOffset; + + bool mIsSelectionCollapsed; + bool mIsAtEndOfLine; + int32_t mGranularity; +}; + +/** + * Accessible text selection change event. + */ +class AccTextSelChangeEvent : public AccEvent { + public: + AccTextSelChangeEvent(HyperTextAccessible* aTarget, + dom::Selection* aSelection, int32_t aReason, + int32_t aGranularity); + 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; + + int32_t GetGranularity() const { return mGranularity; } + + /** + * Return selection ranges in document/control. + */ + void SelectionRanges(nsTArray<a11y::TextRange>* aRanges) const; + + private: + RefPtr<dom::Selection> mSel; + int32_t mReason; + int32_t mGranularity; + + friend class EventQueue; + friend class SelectionManager; +}; + +/** + * Accessible widget selection change event. + */ +class AccSelChangeEvent : public AccEvent { + public: + enum SelChangeType { eSelectionAdd, eSelectionRemove }; + + AccSelChangeEvent(LocalAccessible* aWidget, LocalAccessible* aItem, + SelChangeType aSelChangeType); + + virtual ~AccSelChangeEvent() {} + + // AccEvent + static const EventGroup kEventGroup = eSelectionChangeEvent; + virtual unsigned int GetEventGroups() const override { + return AccEvent::GetEventGroups() | (1U << eSelectionChangeEvent); + } + + // AccSelChangeEvent + LocalAccessible* Widget() const { return mWidget; } + + private: + RefPtr<LocalAccessible> mWidget; + RefPtr<LocalAccessible> mItem; + SelChangeType mSelChangeType; + uint32_t mPreceedingCount; + AccSelChangeEvent* mPackedEvent; + + friend class EventQueue; +}; + +/** + * Accessible object attribute changed event. + */ +class AccObjectAttrChangedEvent : public AccEvent { + public: + AccObjectAttrChangedEvent(LocalAccessible* 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, LocalAccessible* 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(LocalAccessible* 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..3b536b1aa4 --- /dev/null +++ b/accessible/base/AccGroupInfo.cpp @@ -0,0 +1,397 @@ +/* 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 "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/TableAccessible.h" + +#include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" + +#include "Pivot.h" +#include "States.h" + +using namespace mozilla::a11y; + +static role BaseRole(role aRole); + +// This rule finds candidate siblings for compound widget children. +class CompoundWidgetSiblingRule : public PivotRule { + public: + CompoundWidgetSiblingRule() = delete; + explicit CompoundWidgetSiblingRule(role aRole) : mRole(aRole) {} + + uint16_t Match(Accessible* aAcc) override { + // If the acc has a matching role, that's a valid sibling. If the acc is + // separator then the group is ended. Return a match for separators with + // the assumption that the caller will check for the role of the returned + // accessible. + const role accRole = aAcc->Role(); + if (BaseRole(accRole) == mRole || accRole == role::SEPARATOR) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + // Ignore generic accessibles, but keep searching through the subtree for + // siblings. + if (aAcc->IsGeneric()) { + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + private: + role mRole = role::NOTHING; +}; + +AccGroupInfo::AccGroupInfo(const Accessible* aItem, role aRole) + : mPosInSet(0), mSetSize(0), mParentId(0), mItem(aItem), mRole(aRole) { + MOZ_COUNT_CTOR(AccGroupInfo); + Update(); +} + +void AccGroupInfo::Update() { + mParentId = 0; + + Accessible* parent = mItem->GetNonGenericParent(); + if (!parent) { + return; + } + + const int32_t level = GetARIAOrDefaultLevel(mItem); + + // Compute position in set. + mPosInSet = 1; + + // Search backwards through the tree for candidate siblings. + Accessible* candidateSibling = const_cast<Accessible*>(mItem); + Pivot pivot{parent}; + CompoundWidgetSiblingRule widgetSiblingRule{mRole}; + while ((candidateSibling = pivot.Prev(candidateSibling, widgetSiblingRule)) && + candidateSibling != parent) { + // If the sibling is separator then the group is ended. + if (candidateSibling->Role() == roles::SEPARATOR) { + break; + } + + const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingGroupInfo && candidateSibling->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). + const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); + if (siblingLevel < level) { + mParentId = candidateSibling->ID(); + 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 (siblingGroupInfo) { + mPosInSet += siblingGroupInfo->mPosInSet; + mParentId = siblingGroupInfo->mParentId; + mSetSize = siblingGroupInfo->mSetSize; + return; + } + + mPosInSet++; + } + + // Compute set size. + mSetSize = mPosInSet; + + candidateSibling = const_cast<Accessible*>(mItem); + while ((candidateSibling = pivot.Next(candidateSibling, widgetSiblingRule)) && + candidateSibling != parent) { + // If the sibling is separator then the group is ended. + if (candidateSibling->Role() == roles::SEPARATOR) { + break; + } + + const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); + // Skip invisible siblings. + // If the sibling has calculated group info, that means it's visible. + if (!siblingGroupInfo && candidateSibling->State() & states::INVISIBLE) { + continue; + } + + // and check if it's hierarchical flatten structure. + const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); + 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 (siblingGroupInfo) { + mParentId = siblingGroupInfo->mParentId; + mSetSize = siblingGroupInfo->mSetSize; + return; + } + + mSetSize++; + } + + if (mParentId) { + return; + } + + roles::Role parentRole = parent->Role(); + if (ShouldReportRelations(mRole, parentRole)) { + mParentId = parent->ID(); + } + + // 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) { + // Find the relevant grandparent of the item. Use that parent as the root + // and find the previous outline item sibling within that root. + Accessible* grandParent = parent->GetNonGenericParent(); + MOZ_ASSERT(grandParent); + Pivot pivot{grandParent}; + CompoundWidgetSiblingRule parentSiblingRule{mRole}; + Accessible* parentPrevSibling = pivot.Prev(parent, widgetSiblingRule); + if (parentPrevSibling && parentPrevSibling->Role() == mRole) { + mParentId = parentPrevSibling->ID(); + 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->GetNonGenericParent(); + if (grandParent && grandParent->Role() == mRole) { + mParentId = grandParent->ID(); + } + } +} + +AccGroupInfo* 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; +} + +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->GetOrCreateGroupInfo(); + 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->GetOrCreateGroupInfo(); + 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 (auto val = aContainer->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) { + if (*val >= 0) { + return *val; + } + } + if (TableAccessible* tableAcc = aContainer->AsTable()) { + return tableAcc->RowCount(); + } + break; + case roles::ROW: + if (Accessible* table = nsAccUtils::TableFor(aContainer)) { + if (auto val = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) { + if (*val >= 0) { + return *val; + } + } + 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->GetOrCreateGroupInfo(); + 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->ChildAt(idx); + AccGroupInfo* nextGroupInfo = nextItem->GetOrCreateGroupInfo(); + 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; +} + +size_t AccGroupInfo::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + // We don't count mParentId or mItem since they (should be) counted + // as part of the document. + return aMallocSizeOf(this); +} + +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; +} + +int32_t AccGroupInfo::GetARIAOrDefaultLevel(const Accessible* aAccessible) { + int32_t level = 0; + aAccessible->ARIAGroupPosition(&level, nullptr, nullptr); + + if (level != 0) return level; + + return aAccessible->GetLevel(true); +} + +Accessible* AccGroupInfo::ConceptualParent() const { + if (!mParentId) { + // The conceptual parent can never be the document, so id 0 means none. + return nullptr; + } + if (Accessible* doc = + nsAccUtils::DocumentFor(const_cast<Accessible*>(mItem))) { + return nsAccUtils::GetAccessibleByID(doc, mParentId); + } + return nullptr; +} + +static role BaseRole(role aRole) { + if (aRole == roles::CHECK_MENU_ITEM || aRole == roles::PARENT_MENUITEM || + aRole == roles::RADIO_MENU_ITEM) { + return roles::MENUITEM; + } + + if (aRole == roles::CHECK_RICH_OPTION) { + return roles::RICH_OPTION; + } + + return aRole; +} diff --git a/accessible/base/AccGroupInfo.h b/accessible/base/AccGroupInfo.h new file mode 100644 index 0000000000..a9afa14b8e --- /dev/null +++ b/accessible/base/AccGroupInfo.h @@ -0,0 +1,101 @@ +/* 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 "nsISupportsImpl.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/a11y/Role.h" + +namespace mozilla { +namespace a11y { + +class Accessible; + +/** + * Calculate and store group information. + */ +class AccGroupInfo { + public: + MOZ_COUNTED_DTOR(AccGroupInfo) + + AccGroupInfo() = default; + AccGroupInfo(AccGroupInfo&&) = default; + AccGroupInfo& operator=(AccGroupInfo&&) = default; + + /** + * 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; + + /** + * Update group information. + */ + void Update(); + + /** + * Create group info. + */ + static AccGroupInfo* CreateGroupInfo(const Accessible* aAccessible); + + /** + * 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); + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf); + + protected: + AccGroupInfo(const Accessible* aItem, a11y::role aRole); + + private: + AccGroupInfo(const AccGroupInfo&) = delete; + AccGroupInfo& operator=(const AccGroupInfo&) = delete; + + /** + * Return true if the given parent and child roles should have their node + * relations reported. + */ + static bool ShouldReportRelations(a11y::role aRole, a11y::role aParentRole); + + /** + * Return ARIA level value or the default one if ARIA is missed for the + * given accessible. + */ + static int32_t GetARIAOrDefaultLevel(const Accessible* aAccessible); + + uint32_t mPosInSet; + uint32_t mSetSize; + uint64_t mParentId; + 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..badd34c0d5 --- /dev/null +++ b/accessible/base/AccIterator.cpp @@ -0,0 +1,360 @@ +/* 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" +#include "DocAccessible-inl.h" +#include "LocalAccessible-inl.h" +#include "XULTreeAccessible.h" + +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/DocumentOrShadowRoot.h" +#include "mozilla/dom/HTMLLabelElement.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// AccIterator +//////////////////////////////////////////////////////////////////////////////// + +AccIterator::AccIterator(const LocalAccessible* aAccessible, + filters::FilterFuncPtr aFilterFunc) + : mFilterFunc(aFilterFunc) { + mState = new IteratorState(aAccessible); +} + +AccIterator::~AccIterator() { + while (mState) { + IteratorState* tmp = mState; + mState = tmp->mParentState; + delete tmp; + } +} + +LocalAccessible* AccIterator::Next() { + while (mState) { + LocalAccessible* child = mState->mParent->LocalChildAt(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 LocalAccessible* 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(nsGkAtoms::id, id)) { + mProviders = mDocument->GetRelProviders(aDependentContent->AsElement(), id); + } +} + +LocalAccessible* 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) { + LocalAccessible* 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 LocalAccessible* aAccessible, + LabelFilter aFilter) + : mRelIter(aDocument, aAccessible->GetContent(), nsGkAtoms::_for), + mAcc(aAccessible), + mLabelFilter(aFilter) {} + +bool HTMLLabelIterator::IsLabel(LocalAccessible* aLabel) { + dom::HTMLLabelElement* labelEl = + dom::HTMLLabelElement::FromNode(aLabel->GetContent()); + return labelEl && labelEl->GetControl() == mAcc->GetContent(); +} + +LocalAccessible* HTMLLabelIterator::Next() { + // Get either <label for="[id]"> element which explicitly points to given + // element, or <label> ancestor which implicitly point to it. + LocalAccessible* 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. + LocalAccessible* walkUp = mAcc->LocalParent(); + while (walkUp && !walkUp->IsDoc()) { + nsIContent* walkUpEl = walkUp->GetContent(); + if (IsLabel(walkUp) && !walkUpEl->AsElement()->HasAttr(nsGkAtoms::_for)) { + mLabelFilter = eSkipAncestorLabel; // prevent infinite loop + return walkUp; + } + + if (walkUpEl->IsHTMLElement(nsGkAtoms::form)) break; + + walkUp = walkUp->LocalParent(); + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// HTMLOutputIterator +//////////////////////////////////////////////////////////////////////////////// + +HTMLOutputIterator::HTMLOutputIterator(DocAccessible* aDocument, + nsIContent* aElement) + : mRelIter(aDocument, aElement, nsGkAtoms::_for) {} + +LocalAccessible* HTMLOutputIterator::Next() { + LocalAccessible* 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) {} + +LocalAccessible* XULLabelIterator::Next() { + LocalAccessible* 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) {} + +LocalAccessible* XULDescriptionIterator::Next() { + LocalAccessible* 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(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); +} + +LocalAccessible* IDRefsIterator::Next() { + nsIContent* nextEl = nullptr; + while ((nextEl = NextElem())) { + LocalAccessible* acc = mDoc->GetAccessible(nextEl); + if (acc) { + return acc; + } + } + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// SingleAccIterator +//////////////////////////////////////////////////////////////////////////////// + +Accessible* SingleAccIterator::Next() { + Accessible* nextAcc = mAcc; + mAcc = nullptr; + if (!nextAcc) { + return nullptr; + } + + MOZ_ASSERT(!nextAcc->IsLocal() || !nextAcc->AsLocal()->IsDefunct(), + "Iterator references defunct accessible?"); + return nextAcc; +} + +//////////////////////////////////////////////////////////////////////////////// +// ItemIterator +//////////////////////////////////////////////////////////////////////////////// + +Accessible* ItemIterator::Next() { + if (mContainer) { + mAnchor = AccGroupInfo::FirstItemOf(mContainer); + mContainer = nullptr; + return mAnchor; + } + + if (mAnchor) { + mAnchor = AccGroupInfo::NextItemTo(mAnchor); + } + + return mAnchor; +} + +//////////////////////////////////////////////////////////////////////////////// +// 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); +} + +LocalAccessible* 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; +} + +//////////////////////////////////////////////////////////////////////////////// +// RemoteAccIterator +//////////////////////////////////////////////////////////////////////////////// + +Accessible* RemoteAccIterator::Next() { + while (mIndex < mIds.Length()) { + uint64_t id = mIds[mIndex++]; + Accessible* acc = mDoc->GetAccessible(id); + if (acc) { + return acc; + } + } + return nullptr; +} diff --git a/accessible/base/AccIterator.h b/accessible/base/AccIterator.h new file mode 100644 index 0000000000..463e3e9d3e --- /dev/null +++ b/accessible/base/AccIterator.h @@ -0,0 +1,328 @@ +/* -*- 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 "Filters.h" +#include "mozilla/a11y/DocAccessible.h" +#include "nsTArray.h" + +#include <memory> + +class nsITreeView; + +namespace mozilla { +namespace a11y { +class DocAccessibleParent; + +/** + * 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 LocalAccessible* aRoot, filters::FilterFuncPtr aFilterFunc); + virtual ~AccIterator(); + + /** + * Return next accessible complying with filter function. Return the first + * accessible for the first time. + */ + virtual LocalAccessible* Next() override; + + private: + AccIterator(); + AccIterator(const AccIterator&); + AccIterator& operator=(const AccIterator&); + + struct IteratorState { + explicit IteratorState(const LocalAccessible* aParent, + IteratorState* mParentState = nullptr); + + const LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* aAccessible, + LabelFilter aFilter = eAllLabels); + + virtual ~HTMLLabelIterator() {} + + /** + * Return next label accessible associated with the given element. + */ + virtual LocalAccessible* Next() override; + + private: + HTMLLabelIterator(); + HTMLLabelIterator(const HTMLLabelIterator&); + HTMLLabelIterator& operator=(const HTMLLabelIterator&); + + bool IsLabel(LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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&); + + 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 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 LocalAccessible* 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; +}; + +/** + * Used to iterate through a sequence of RemoteAccessibles supplied as an array + * of ids. Such id arrays are included in the RemoteAccessible cache. + */ +class RemoteAccIterator : public AccIterable { + public: + /** + * Construct with a reference to an array owned somewhere else; e.g. a + * RemoteAccessible cache. + */ + RemoteAccIterator(const nsTArray<uint64_t>& aIds, DocAccessibleParent* aDoc) + : mIds(aIds), mDoc(aDoc), mIndex(0) {} + + virtual ~RemoteAccIterator() = default; + + virtual Accessible* Next() override; + + private: + const nsTArray<uint64_t>& mIds; + DocAccessibleParent* mDoc; + uint32_t mIndex; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/AccTypes.h b/accessible/base/AccTypes.h new file mode 100644 index 0000000000..3e9d88e486 --- /dev/null +++ b/accessible/base/AccTypes.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_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, + eHTMLDateTimeFieldType, + eHTMLFileInputType, + eHTMLGroupboxType, + eHTMLHRType, + eHTMLImageMapType, + eHTMLLiType, + eHTMLSelectListType, + eHTMLMediaType, + eHTMLRadioButtonType, + eHTMLRangeType, + eHTMLSpinnerType, + eHTMLTableType, + eHTMLTableCellType, + eHTMLTableRowType, + eHTMLTextFieldType, + eHTMLTextPasswordFieldType, + eHyperTextType, + eImageType, + eOuterDocType, + eTextLeafType, + + /** + * Other accessible types. + */ + eApplicationType, + eHTMLLinkType, + eHTMLOptGroupType, + eImageMapType, + eMenuPopupType, + 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, + eAutoCompletePopup = 1 << 1, + eButton = 1 << 2, + eCombobox = 1 << 3, + eDocument = 1 << 4, + eHyperText = 1 << 5, + eLandmark = 1 << 6, + eList = 1 << 7, + eListControl = 1 << 8, + eMenuButton = 1 << 9, + eSelect = 1 << 10, + eTable = 1 << 11, + eTableCell = 1 << 12, + eTableRow = 1 << 13, + eText = 1 << 14, + eNumericValue = 1 << 15, + eActionable = 1 << 16, // This is for remote accessibles + + eLastAccGenericType = eActionable, + eAllGenericTypes = (eLastAccGenericType << 1) - 1 +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_AccTypes_h diff --git a/accessible/base/Asserts.cpp b/accessible/base/Asserts.cpp new file mode 100644 index 0000000000..efdd733d9b --- /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 "mozilla/a11y/RelationType.h" +#include "mozilla/a11y/Role.h" + +using namespace mozilla::a11y; + +#define ROLE(geckoRole, stringRole, ariaRole, 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/CacheConstants.h b/accessible/base/CacheConstants.h new file mode 100644 index 0000000000..eb5cc79f5e --- /dev/null +++ b/accessible/base/CacheConstants.h @@ -0,0 +1,255 @@ +/* -*- 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 _CacheConstants_h_ +#define _CacheConstants_h_ + +#include "nsGkAtoms.h" +#include "mozilla/a11y/RelationType.h" + +namespace mozilla { +namespace a11y { + +class CacheDomain { + public: + static constexpr uint64_t NameAndDescription = ((uint64_t)0x1) << 0; + static constexpr uint64_t Value = ((uint64_t)0x1) << 1; + static constexpr uint64_t Bounds = ((uint64_t)0x1) << 2; + static constexpr uint64_t Resolution = ((uint64_t)0x1) << 3; + static constexpr uint64_t Text = ((uint64_t)0x1) << 4; + static constexpr uint64_t DOMNodeIDAndClass = ((uint64_t)0x1) << 5; + static constexpr uint64_t State = ((uint64_t)0x1) << 6; + static constexpr uint64_t GroupInfo = ((uint64_t)0x1) << 7; + static constexpr uint64_t Actions = ((uint64_t)0x1) << 8; + static constexpr uint64_t Style = ((uint64_t)0x1) << 9; + static constexpr uint64_t TransformMatrix = ((uint64_t)0x1) << 10; + static constexpr uint64_t ScrollPosition = ((uint64_t)0x1) << 11; + static constexpr uint64_t Table = ((uint64_t)0x1) << 12; + static constexpr uint64_t Spelling = ((uint64_t)0x1) << 13; + static constexpr uint64_t Viewport = ((uint64_t)0x1) << 14; + static constexpr uint64_t ARIA = ((uint64_t)0x1) << 15; + static constexpr uint64_t Relations = ((uint64_t)0x1) << 16; +#ifdef XP_WIN + // Used for MathML. + static constexpr uint64_t InnerHTML = ((uint64_t)0x1) << 17; +#endif + static constexpr uint64_t All = ~((uint64_t)0x0); +}; + +enum class CacheUpdateType { + /* + * An initial cache push of a loaded document or inserted subtree. + */ + Initial, + + /* + * An incremental cache push of one or more fields that have changed. + */ + Update, +}; + +struct RelationData { + nsStaticAtom* const mAtom; + nsStaticAtom* const mValidTag; + RelationType mType; + RelationType mReverseType; +}; + +/** + * This array of RelationData lists our relation types (explicit and reverse) + * and the cache attribute atoms that store their targets. Attributes may + * describe different kinds of relations, depending on the element they + * originate on. For example, an <output> element's `for` attribute describes a + * CONTROLLER_FOR relation, while the `for` attribute of a <label> describes a + * LABEL_FOR relation. To ensure we process these attributes appropriately, + * RelationData.mValidTag contains the atom for the tag this attribute/relation + * type pairing is valid on. If the pairing is valid for all tag types, this + * field is null. + */ +static constexpr RelationData kRelationTypeAtoms[] = { + {nsGkAtoms::aria_labelledby, nullptr, RelationType::LABELLED_BY, + RelationType::LABEL_FOR}, + {nsGkAtoms::_for, nsGkAtoms::label, RelationType::LABEL_FOR, + RelationType::LABELLED_BY}, + {nsGkAtoms::aria_controls, nullptr, RelationType::CONTROLLER_FOR, + RelationType::CONTROLLED_BY}, + {nsGkAtoms::_for, nsGkAtoms::output, RelationType::CONTROLLED_BY, + RelationType::CONTROLLER_FOR}, + {nsGkAtoms::aria_describedby, nullptr, RelationType::DESCRIBED_BY, + RelationType::DESCRIPTION_FOR}, + {nsGkAtoms::aria_flowto, nullptr, RelationType::FLOWS_TO, + RelationType::FLOWS_FROM}, + {nsGkAtoms::aria_details, nullptr, RelationType::DETAILS, + RelationType::DETAILS_FOR}, + {nsGkAtoms::aria_errormessage, nullptr, RelationType::ERRORMSG, + RelationType::ERRORMSG_FOR}, +}; + +// The count of numbers needed to serialize an nsRect. This is used when +// flattening character rects into an array of ints. +constexpr int32_t kNumbersInRect = 4; + +/** + * RemoteAccessible cache keys. + * Cache keys are nsAtoms, but this is mostly an implementation detail. Rather + * than creating new atoms specific to the RemoteAccessible cache, we often + * reuse existing atoms which are a reasonably close match for the value we're + * caching, though the choices aren't always clear or intuitive. For clarity, we + * alias the cache keys to atoms below. Code dealing with the RemoteAccessible + * cache should generally use these aliases rather than using nsAtoms directly. + * There are two exceptions: + * 1. Some ARIA attributes are copied directly from the DOM node, so these + * aren't aliased. Specifically, aria-level, aria-posinset and aria-setsize + * are copied as separate cache keys as part of CacheDomain::GroupInfo. + * 2. Keys for relations are defined in kRelationTypeAtoms above. + */ +class CacheKey { + public: + // uint64_t, CacheDomain::Actions + // As returned by Accessible::AccessKey. + static constexpr nsStaticAtom* AccessKey = nsGkAtoms::accesskey; + // int32_t, no domain + static constexpr nsStaticAtom* AppUnitsPerDevPixel = + nsGkAtoms::_moz_device_pixel_ratio; + // AccAttributes, CacheDomain::ARIA + // ARIA attributes that are exposed as object attributes; i.e. returned in + // Accessible::Attributes. + static constexpr nsStaticAtom* ARIAAttributes = nsGkAtoms::aria; + // nsString, CacheUpdateType::Initial + // The ARIA role attribute if the role is unknown or if there are multiple + // roles. + static constexpr nsStaticAtom* ARIARole = nsGkAtoms::role; + // bool, CacheDomain::State + // The aria-selected attribute. + static constexpr nsStaticAtom* ARIASelected = nsGkAtoms::aria_selected; + // nsTArray<uint64_t>, CacheDomain::Table + // The explicit headers of an HTML table cell. + static constexpr nsStaticAtom* CellHeaders = nsGkAtoms::headers; + // int32_t, CacheDomain::Table + // The colspan of an HTML table cell. + static constexpr nsStaticAtom* ColSpan = nsGkAtoms::colspan; + // nsTArray<int32_t, 2>, CacheDomain::Bounds + // The offset from an OuterDocAccessible (iframe) to its embedded document. + static constexpr nsStaticAtom* CrossDocOffset = nsGkAtoms::crossorigin; + // nsAtom, CacheDomain::Style + // CSS display; block, inline, etc. + static constexpr nsStaticAtom* CSSDisplay = nsGkAtoms::display; + // nsAtom, CacheDomain::Style + // CSS overflow; e.g. hidden. + static constexpr nsStaticAtom* CSSOverflow = nsGkAtoms::overflow; + // nsAtom, CacheDomain::Style + // CSS position; e.g. fixed. + static constexpr nsStaticAtom* CssPosition = nsGkAtoms::position; + // nsString, CacheDomain::NameAndDescription + static constexpr nsStaticAtom* Description = nsGkAtoms::description; + // nsString, CacheDomain::Relations + // The "name" DOM attribute. + static constexpr nsStaticAtom* DOMName = nsGkAtoms::attributeName; + // nsAtom, CacheDomain::DOMNodeIDAndClass + // The "class" DOM attribute. + static constexpr nsStaticAtom* DOMNodeClass = nsGkAtoms::_class; + // nsAtom, CacheDomain::DOMNodeIDAndClass + static constexpr nsStaticAtom* DOMNodeID = nsGkAtoms::id; + // AccGroupInfo, no domain + static constexpr nsStaticAtom* GroupInfo = nsGkAtoms::group; + // nsTArray<int32_t>, no domain + // As returned by HyperTextAccessibleBase::CachedHyperTextOffsets. + static constexpr nsStaticAtom* HyperTextOffsets = nsGkAtoms::offset; + // bool, CacheDomain::Actions + // Whether this image has a longdesc. + static constexpr nsStaticAtom* HasLongdesc = nsGkAtoms::longdesc; + // nsString, CacheDomain::NameAndDescription + static constexpr nsStaticAtom* HTMLPlaceholder = nsGkAtoms::placeholder; +#ifdef XP_WIN + // nsString, CacheDomain::InnerHTML + static constexpr nsStaticAtom* InnerHTML = nsGkAtoms::html; +#endif + // nsAtom, CacheUpdateType::Initial + // The type of an <input> element; tel, email, etc. + static constexpr nsStaticAtom* InputType = nsGkAtoms::textInputType; + // bool, CacheDomain::Bounds + // Whether the Accessible is fully clipped. + static constexpr nsStaticAtom* IsClipped = nsGkAtoms::clip_rule; + // nsString, CacheUpdateType::Initial + static constexpr nsStaticAtom* MimeType = nsGkAtoms::headerContentType; + // double, CacheDomain::Value + static constexpr nsStaticAtom* MaxValue = nsGkAtoms::max; + // double, CacheDomain::Value + static constexpr nsStaticAtom* MinValue = nsGkAtoms::min; + // nsString, CacheDomain::NameAndDescription + static constexpr nsStaticAtom* Name = nsGkAtoms::name; + // ENameValueFlag, CacheDomain::NameAndDescription + // Returned by Accessible::Name. + static constexpr nsStaticAtom* NameValueFlag = nsGkAtoms::explicit_name; + // double, CacheDomain::Value + // The numeric value returned by Accessible::CurValue. + static constexpr nsStaticAtom* NumericValue = nsGkAtoms::value; + // float, CacheDomain::Style + static constexpr nsStaticAtom* Opacity = nsGkAtoms::opacity; + // nsTArray<int32_t, 4>, CacheDomain::Bounds + // The screen bounds relative to the parent Accessible + // as returned by LocalAccessible::ParentRelativeBounds. + static constexpr nsStaticAtom* ParentRelativeBounds = + nsGkAtoms::relativeBounds; + // nsAtom, CacheUpdateType::Initial + // The type of a popup (used for HTML popover). + static constexpr nsStaticAtom* PopupType = nsGkAtoms::ispopup; + // nsAtom, CacheDomain::Actions + static constexpr nsStaticAtom* PrimaryAction = nsGkAtoms::action; + // float, no domain + // Document resolution. + static constexpr nsStaticAtom* Resolution = nsGkAtoms::resolution; + // int32_t, CacheDomain::Table + // The rowspan of an HTML table cell. + static constexpr nsStaticAtom* RowSpan = nsGkAtoms::rowspan; + // nsTArray<int32_t, 2>, CacheDomain::ScrollPosition + static constexpr nsStaticAtom* ScrollPosition = nsGkAtoms::scrollPosition; + // nsTArray<int32_t>, CacheDomain::Spelling | CacheDomain::Text + // The offsets of spelling errors. + static constexpr nsStaticAtom* SpellingErrors = nsGkAtoms::spelling; + // nsString, CacheDomain::Value + // The src URL of images. + static constexpr nsStaticAtom* SrcURL = nsGkAtoms::src; + // uint64_t, CacheDomain::State + // As returned by Accessible::State. + static constexpr nsStaticAtom* State = nsGkAtoms::state; + // double, CacheDomain::Value + // The value step returned by Accessible::Step. + static constexpr nsStaticAtom* Step = nsGkAtoms::step; + // nsAtom, CacheUpdateType::Initial + // The tag name of the element. + static constexpr nsStaticAtom* TagName = nsGkAtoms::tag; + // bool, CacheDomain::Table + // Whether this is a layout table. + static constexpr nsStaticAtom* TableLayoutGuess = nsGkAtoms::layout_guess; + // nsString, CacheDomain::Text + // The text of TextLeafAccessibles. + static constexpr nsStaticAtom* Text = nsGkAtoms::text; + // AccAttributes, CacheDomain::Text + // Text attributes; font, etc. + static constexpr nsStaticAtom* TextAttributes = nsGkAtoms::style; + // nsTArray<int32_t, 4 * n>, CacheDomain::Text | CacheDomain::Bounds + // The bounds of each character in a text leaf. + static constexpr nsStaticAtom* TextBounds = nsGkAtoms::characterData; + // nsTArray<int32_t>, CacheDomain::Text | CacheDomain::Bounds + // The text offsets where new lines start. + static constexpr nsStaticAtom* TextLineStarts = nsGkAtoms::line; + // nsString, CacheDomain::Value + // The textual value returned by Accessible::Value (as opposed to + // the numeric value returned by Accessible::CurValue). + static constexpr nsStaticAtom* TextValue = nsGkAtoms::aria_valuetext; + // gfx::Matrix4x4, CacheDomain::TransformMatrix + static constexpr nsStaticAtom* TransformMatrix = nsGkAtoms::transform; + // nsTArray<uint64_t>, CacheDomain::Viewport + // The list of Accessibles in the viewport used for hit testing and on-screen + // determination. + static constexpr nsStaticAtom* Viewport = nsGkAtoms::viewport; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/CachedTableAccessible.cpp b/accessible/base/CachedTableAccessible.cpp new file mode 100644 index 0000000000..e780bd2a89 --- /dev/null +++ b/accessible/base/CachedTableAccessible.cpp @@ -0,0 +1,429 @@ +/* -*- 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 "CachedTableAccessible.h" + +#include "AccIterator.h" +#include "HTMLTableAccessible.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" +#include "Pivot.h" +#include "RemoteAccessible.h" + +namespace mozilla::a11y { + +// Used to search for table descendants relevant to table structure. +class TablePartRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override { + role accRole = aAcc->Role(); + if (accRole == roles::CAPTION || aAcc->IsTableCell()) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + if (aAcc->IsTableRow()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + if (aAcc->IsTable() || + // Generic containers. + accRole == roles::TEXT || accRole == roles::TEXT_CONTAINER || + accRole == roles::SECTION || + // Row groups. + accRole == roles::GROUPING) { + // Walk inside these, but don't match them. + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + return nsIAccessibleTraversalRule::FILTER_IGNORE | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } +}; + +// The Accessible* keys should only be used for lookup. They should not be +// dereferenced. +using CachedTablesMap = nsTHashMap<Accessible*, CachedTableAccessible>; +// We use a global map rather than a map in each document for three reasons: +// 1. We don't have a common base class for local and remote documents. +// 2. It avoids wasting memory in a document that doesn't have any tables. +// 3. It allows the cache management to be encapsulated here in +// CachedTableAccessible. +static StaticAutoPtr<CachedTablesMap> sCachedTables; + +/* static */ +CachedTableAccessible* CachedTableAccessible::GetFrom(Accessible* aAcc) { + MOZ_ASSERT(aAcc->IsTable()); + if (!sCachedTables) { + sCachedTables = new CachedTablesMap(); + ClearOnShutdown(&sCachedTables); + } + return &sCachedTables->LookupOrInsertWith( + aAcc, [&] { return CachedTableAccessible(aAcc); }); +} + +/* static */ +void CachedTableAccessible::Invalidate(Accessible* aAcc) { + if (!sCachedTables) { + return; + } + + if (Accessible* table = nsAccUtils::TableFor(aAcc)) { + // Destroy the instance (if any). We'll create a new one the next time it + // is requested. + sCachedTables->Remove(table); + } +} + +CachedTableAccessible::CachedTableAccessible(Accessible* aAcc) : mAcc(aAcc) { + MOZ_ASSERT(mAcc); + // Build the cache. The cache can only be built once per instance. When it's + // invalidated, we just throw away the instance and create a new one when + // the cache is next needed. + int32_t rowIdx = -1; + uint32_t colIdx = 0; + // Maps a column index to the cell index of its previous implicit column + // header. + nsTHashMap<uint32_t, uint32_t> prevColHeaders; + Pivot pivot(mAcc); + TablePartRule rule; + for (Accessible* part = pivot.Next(mAcc, rule); part; + part = pivot.Next(part, rule)) { + role partRole = part->Role(); + if (partRole == roles::CAPTION) { + // If there are multiple captions, use the first. + if (!mCaptionAccID) { + mCaptionAccID = part->ID(); + } + continue; + } + if (part->IsTableRow()) { + ++rowIdx; + colIdx = 0; + // This might be an empty row, so ensure a row here, as our row count is + // based on the length of mRowColToCellIdx. + EnsureRow(rowIdx); + continue; + } + MOZ_ASSERT(part->IsTableCell()); + if (rowIdx == -1) { + // We haven't created a row yet, so this cell must be outside a row. + continue; + } + // Check for a cell spanning multiple rows which already occupies this + // position. Keep incrementing until we find a vacant position. + for (;;) { + EnsureRowCol(rowIdx, colIdx); + if (mRowColToCellIdx[rowIdx][colIdx] == kNoCellIdx) { + // This position is not occupied. + break; + } + // This position is occupied. + ++colIdx; + } + // Create the cell. + uint32_t cellIdx = mCells.Length(); + auto prevColHeader = prevColHeaders.MaybeGet(colIdx); + auto cell = mCells.AppendElement( + CachedTableCellAccessible(part->ID(), part, rowIdx, colIdx, + prevColHeader ? *prevColHeader : kNoCellIdx)); + mAccToCellIdx.InsertOrUpdate(part, cellIdx); + // Update our row/col map. + // This cell might span multiple rows and/or columns. In that case, we need + // to occupy multiple coordinates in the row/col map. + uint32_t lastRowForCell = + static_cast<uint32_t>(rowIdx) + cell->RowExtent() - 1; + MOZ_ASSERT(lastRowForCell >= static_cast<uint32_t>(rowIdx)); + uint32_t lastColForCell = colIdx + cell->ColExtent() - 1; + MOZ_ASSERT(lastColForCell >= colIdx); + for (uint32_t spannedRow = static_cast<uint32_t>(rowIdx); + spannedRow <= lastRowForCell; ++spannedRow) { + for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell; + ++spannedCol) { + EnsureRowCol(spannedRow, spannedCol); + auto& rowCol = mRowColToCellIdx[spannedRow][spannedCol]; + // If a cell already occupies this position, it overlaps with this one; + // e.g. r1..2c2 and r2c1..2. In that case, we want to prefer the first + // cell. + if (rowCol == kNoCellIdx) { + rowCol = cellIdx; + } + } + } + if (partRole == roles::COLUMNHEADER) { + for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell; + ++spannedCol) { + prevColHeaders.InsertOrUpdate(spannedCol, cellIdx); + } + } + // Increment for the next cell. + colIdx = lastColForCell + 1; + } +} + +void CachedTableAccessible::EnsureRow(uint32_t aRowIdx) { + if (mRowColToCellIdx.Length() <= aRowIdx) { + mRowColToCellIdx.AppendElements(aRowIdx - mRowColToCellIdx.Length() + 1); + } + MOZ_ASSERT(mRowColToCellIdx.Length() > aRowIdx); +} + +void CachedTableAccessible::EnsureRowCol(uint32_t aRowIdx, uint32_t aColIdx) { + EnsureRow(aRowIdx); + auto& row = mRowColToCellIdx[aRowIdx]; + if (mColCount <= aColIdx) { + mColCount = aColIdx + 1; + } + row.SetCapacity(mColCount); + for (uint32_t newCol = row.Length(); newCol <= aColIdx; ++newCol) { + // An entry doesn't yet exist for this column in this row. + row.AppendElement(kNoCellIdx); + } + MOZ_ASSERT(row.Length() > aColIdx); +} + +Accessible* CachedTableAccessible::Caption() const { + if (mCaptionAccID) { + Accessible* caption = nsAccUtils::GetAccessibleByID( + nsAccUtils::DocumentFor(mAcc), mCaptionAccID); + MOZ_ASSERT(caption, "Dead caption Accessible!"); + MOZ_ASSERT(caption->Role() == roles::CAPTION, "Caption has wrong role"); + return caption; + } + return nullptr; +} + +void CachedTableAccessible::Summary(nsString& aSummary) { + if (Caption()) { + // If there's a caption, we map caption to Name and summary to Description. + mAcc->Description(aSummary); + } else { + // If there's no caption, we map summary to Name. + mAcc->Name(aSummary); + } +} + +Accessible* CachedTableAccessible::CellAt(uint32_t aRowIdx, uint32_t aColIdx) { + int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx); + if (cellIdx == -1) { + return nullptr; + } + return mCells[cellIdx].Acc(mAcc); +} + +bool CachedTableAccessible::IsProbablyLayoutTable() { + if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) { + return remoteAcc->TableIsProbablyForLayout(); + } + if (auto* localTable = HTMLTableAccessible::GetFrom(mAcc->AsLocal())) { + return localTable->IsProbablyLayoutTable(); + } + return false; +} + +/* static */ +CachedTableCellAccessible* CachedTableCellAccessible::GetFrom( + Accessible* aAcc) { + MOZ_ASSERT(aAcc->IsTableCell()); + for (Accessible* parent = aAcc; parent; parent = parent->Parent()) { + if (parent->IsDoc()) { + break; // Never cross document boundaries. + } + TableAccessible* table = parent->AsTable(); + if (!table) { + continue; + } + if (LocalAccessible* local = parent->AsLocal()) { + nsIContent* content = local->GetContent(); + if (content && content->IsXULElement()) { + // XUL tables don't use CachedTableAccessible. + break; + } + } + // Non-XUL tables only use CachedTableAccessible. + auto* cachedTable = static_cast<CachedTableAccessible*>(table); + if (auto cellIdx = cachedTable->mAccToCellIdx.Lookup(aAcc)) { + return &cachedTable->mCells[*cellIdx]; + } + // We found a table, but it doesn't know about this cell. This can happen + // if a cell is outside of a row due to authoring error. We must not search + // ancestor tables, since this cell's data is not valid there and vice + // versa. + break; + } + return nullptr; +} + +Accessible* CachedTableCellAccessible::Acc(Accessible* aTableAcc) const { + Accessible* acc = + nsAccUtils::GetAccessibleByID(nsAccUtils::DocumentFor(aTableAcc), mAccID); + MOZ_DIAGNOSTIC_ASSERT(acc == mAcc, "Cell's cached mAcc is dead!"); + return acc; +} + +TableAccessible* CachedTableCellAccessible::Table() const { + for (const Accessible* acc = mAcc; acc; acc = acc->Parent()) { + // Since the caller has this cell, the table is already created, so it's + // okay to ignore the const restriction here. + if (TableAccessible* table = const_cast<Accessible*>(acc)->AsTable()) { + return table; + } + } + return nullptr; +} + +uint32_t CachedTableCellAccessible::ColExtent() const { + if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) { + if (remoteAcc->mCachedFields) { + if (auto colSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>( + CacheKey::ColSpan)) { + MOZ_ASSERT(*colSpan > 0); + return *colSpan; + } + } + } else if (auto* cell = HTMLTableCellAccessible::GetFrom(mAcc->AsLocal())) { + // For HTML table cells, we must use the HTMLTableCellAccessible + // GetColExtent method rather than using the DOM attributes directly. + // This is because of things like rowspan="0" which depend on knowing + // about thead, tbody, etc., which is info we don't have in the a11y tree. + uint32_t colExtent = cell->ColExtent(); + MOZ_ASSERT(colExtent > 0); + if (colExtent > 0) { + return colExtent; + } + } + return 1; +} + +uint32_t CachedTableCellAccessible::RowExtent() const { + if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) { + if (remoteAcc->mCachedFields) { + if (auto rowSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>( + CacheKey::RowSpan)) { + MOZ_ASSERT(*rowSpan > 0); + return *rowSpan; + } + } + } else if (auto* cell = HTMLTableCellAccessible::GetFrom(mAcc->AsLocal())) { + // For HTML table cells, we must use the HTMLTableCellAccessible + // GetRowExtent method rather than using the DOM attributes directly. + // This is because of things like rowspan="0" which depend on knowing + // about thead, tbody, etc., which is info we don't have in the a11y tree. + uint32_t rowExtent = cell->RowExtent(); + MOZ_ASSERT(rowExtent > 0); + if (rowExtent > 0) { + return rowExtent; + } + } + return 1; +} + +UniquePtr<AccIterable> CachedTableCellAccessible::GetExplicitHeadersIterator() { + if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) { + if (remoteAcc->mCachedFields) { + if (auto headers = + remoteAcc->mCachedFields->GetAttribute<nsTArray<uint64_t>>( + CacheKey::CellHeaders)) { + return MakeUnique<RemoteAccIterator>(*headers, remoteAcc->Document()); + } + } + } else if (LocalAccessible* localAcc = mAcc->AsLocal()) { + return MakeUnique<IDRefsIterator>( + localAcc->Document(), localAcc->GetContent(), nsGkAtoms::headers); + } + return nullptr; +} + +void CachedTableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) { + auto* table = static_cast<CachedTableAccessible*>(Table()); + if (!table) { + return; + } + if (auto iter = GetExplicitHeadersIterator()) { + while (Accessible* header = iter->Next()) { + role headerRole = header->Role(); + if (headerRole == roles::COLUMNHEADER) { + aCells->AppendElement(header); + } else if (headerRole != roles::ROWHEADER) { + // Treat this cell as a column header only if it's in the same column. + if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) { + CachedTableCellAccessible& cell = table->mCells[*cellIdx]; + if (cell.ColIdx() == ColIdx()) { + aCells->AppendElement(header); + } + } + } + } + if (!aCells->IsEmpty()) { + return; + } + } + Accessible* doc = nsAccUtils::DocumentFor(table->AsAccessible()); + // Each cell stores its previous implicit column header, effectively forming a + // linked list. We traverse that to get all the headers. + CachedTableCellAccessible* cell = this; + for (;;) { + if (cell->mPrevColHeaderCellIdx == kNoCellIdx) { + break; // No more headers. + } + cell = &table->mCells[cell->mPrevColHeaderCellIdx]; + Accessible* cellAcc = nsAccUtils::GetAccessibleByID(doc, cell->mAccID); + aCells->AppendElement(cellAcc); + } +} + +void CachedTableCellAccessible::RowHeaderCells(nsTArray<Accessible*>* aCells) { + auto* table = static_cast<CachedTableAccessible*>(Table()); + if (!table) { + return; + } + if (auto iter = GetExplicitHeadersIterator()) { + while (Accessible* header = iter->Next()) { + role headerRole = header->Role(); + if (headerRole == roles::ROWHEADER) { + aCells->AppendElement(header); + } else if (headerRole != roles::COLUMNHEADER) { + // Treat this cell as a row header only if it's in the same row. + if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) { + CachedTableCellAccessible& cell = table->mCells[*cellIdx]; + if (cell.RowIdx() == RowIdx()) { + aCells->AppendElement(header); + } + } + } + } + if (!aCells->IsEmpty()) { + return; + } + } + Accessible* doc = nsAccUtils::DocumentFor(table->AsAccessible()); + // We don't cache implicit row headers because there are usually not that many + // cells per row. Get all the row headers on the row before this cell. + uint32_t row = RowIdx(); + uint32_t thisCol = ColIdx(); + for (uint32_t col = thisCol - 1; col < thisCol; --col) { + int32_t cellIdx = table->CellIndexAt(row, col); + if (cellIdx == -1) { + continue; + } + CachedTableCellAccessible& cell = table->mCells[cellIdx]; + Accessible* cellAcc = nsAccUtils::GetAccessibleByID(doc, cell.mAccID); + MOZ_ASSERT(cellAcc); + // cell might span multiple columns. We don't want to visit it multiple + // times, so ensure col is set to cell's starting column. + col = cell.ColIdx(); + if (cellAcc->Role() != roles::ROWHEADER) { + continue; + } + aCells->AppendElement(cellAcc); + } +} + +bool CachedTableCellAccessible::Selected() { + return mAcc->State() & states::SELECTED; +} + +} // namespace mozilla::a11y diff --git a/accessible/base/CachedTableAccessible.h b/accessible/base/CachedTableAccessible.h new file mode 100644 index 0000000000..7803343070 --- /dev/null +++ b/accessible/base/CachedTableAccessible.h @@ -0,0 +1,294 @@ +/* -*- 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 CACHED_TABLE_ACCESSIBLE_H +#define CACHED_TABLE_ACCESSIBLE_H + +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "mozilla/UniquePtr.h" +#include "nsTHashMap.h" + +namespace mozilla::a11y { + +const uint32_t kNoCellIdx = UINT32_MAX; + +class AccIterable; + +class CachedTableAccessible; + +class CachedTableCellAccessible final : public TableCellAccessible { + public: + static CachedTableCellAccessible* GetFrom(Accessible* aAcc); + + virtual TableAccessible* Table() const override; + + virtual uint32_t ColIdx() const override { + return static_cast<int32_t>(mColIdx); + } + + virtual uint32_t RowIdx() const override { + return static_cast<int32_t>(mRowIdx); + } + + virtual uint32_t ColExtent() const override; + + virtual uint32_t RowExtent() const override; + + virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) override; + + virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) override; + + virtual bool Selected() override; + + private: + CachedTableCellAccessible(uint64_t aAccID, Accessible* aAcc, uint32_t aRowIdx, + uint32_t aColIdx, uint32_t aPrevColHeaderCellIdx) + : mAccID(aAccID), + mAcc(aAcc), + mRowIdx(aRowIdx), + mColIdx(aColIdx), + mPrevColHeaderCellIdx(aPrevColHeaderCellIdx) {} + + // Get the Accessible for this table cell given its ancestor table Accessible, + // verifying that the Accessible is valid. + Accessible* Acc(Accessible* aTableAcc) const; + + UniquePtr<AccIterable> GetExplicitHeadersIterator(); + + uint64_t mAccID; + // CachedTableAccessible methods which fetch a cell should retrieve the + // Accessible using Acc() rather than using mAcc. We need mAcc for some + // methods because we can't fetch a document by id. It's okay to use mAcc in + // these methods because the caller has to hold the Accessible in order to + // call them. + Accessible* mAcc; + uint32_t mRowIdx; + uint32_t mColIdx; + // The cell index of the previous implicit column header. + uint32_t mPrevColHeaderCellIdx; + friend class CachedTableAccessible; +}; + +/** + * TableAccessible implementation which builds and queries a cache. + */ +class CachedTableAccessible final : public TableAccessible { + public: + static CachedTableAccessible* GetFrom(Accessible* aAcc); + + /** + * This must be called whenever a table is destroyed or the structure of a + * table changes; e.g. cells wer added or removed. It can be called with + * either a table or a cell. + */ + static void Invalidate(Accessible* aAcc); + + virtual Accessible* Caption() const override; + virtual void Summary(nsString& aSummary) override; + + virtual uint32_t ColCount() const override { return mColCount; } + + virtual uint32_t RowCount() override { return mRowColToCellIdx.Length(); } + + virtual int32_t ColIndexAt(uint32_t aCellIdx) override { + if (aCellIdx < mCells.Length()) { + return static_cast<int32_t>(mCells[aCellIdx].mColIdx); + } + return -1; + } + + virtual int32_t RowIndexAt(uint32_t aCellIdx) override { + if (aCellIdx < mCells.Length()) { + return static_cast<int32_t>(mCells[aCellIdx].mRowIdx); + } + return -1; + } + + virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx, + int32_t* aColIdx) override { + if (aCellIdx < mCells.Length()) { + CachedTableCellAccessible& cell = mCells[aCellIdx]; + *aRowIdx = static_cast<int32_t>(cell.mRowIdx); + *aColIdx = static_cast<int32_t>(cell.mColIdx); + return; + } + *aRowIdx = -1; + *aColIdx = -1; + } + + virtual uint32_t ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) override { + int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx); + if (cellIdx == -1) { + return 0; + } + // Verify that the cell's Accessible is valid. + mCells[cellIdx].Acc(mAcc); + return mCells[cellIdx].ColExtent(); + } + + virtual uint32_t RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) override { + int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx); + if (cellIdx == -1) { + return 0; + } + // Verify that the cell's Accessible is valid. + mCells[cellIdx].Acc(mAcc); + return mCells[cellIdx].RowExtent(); + } + + virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) override { + if (aRowIdx < mRowColToCellIdx.Length()) { + auto& row = mRowColToCellIdx[aRowIdx]; + if (aColIdx < row.Length()) { + uint32_t cellIdx = row[aColIdx]; + if (cellIdx != kNoCellIdx) { + return static_cast<int32_t>(cellIdx); + } + } + } + return -1; + } + + virtual Accessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) override; + + virtual bool IsColSelected(uint32_t aColIdx) override { + bool selected = false; + for (uint32_t row = 0; row < RowCount(); ++row) { + selected = IsCellSelected(row, aColIdx); + if (!selected) { + break; + } + } + return selected; + } + + virtual bool IsRowSelected(uint32_t aRowIdx) override { + bool selected = false; + for (uint32_t col = 0; col < mColCount; ++col) { + selected = IsCellSelected(aRowIdx, col); + if (!selected) { + break; + } + } + return selected; + } + + virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override { + int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx); + if (cellIdx == -1) { + return false; + } + // Verify that the cell's Accessible is valid. + mCells[cellIdx].Acc(mAcc); + return mCells[cellIdx].Selected(); + } + + virtual uint32_t SelectedCellCount() override { + uint32_t count = 0; + for (auto& cell : mCells) { + // Verify that the cell's Accessible is valid. + cell.Acc(mAcc); + if (cell.Selected()) { + ++count; + } + } + return count; + } + + virtual uint32_t SelectedColCount() override { + uint32_t count = 0; + for (uint32_t col = 0; col < mColCount; ++col) { + if (IsColSelected(col)) { + ++count; + } + } + return count; + } + + virtual uint32_t SelectedRowCount() override { + uint32_t count = 0; + for (uint32_t row = 0; row < RowCount(); ++row) { + if (IsRowSelected(row)) { + ++count; + } + } + return count; + } + + virtual void SelectedCells(nsTArray<Accessible*>* aCells) override { + for (auto& cell : mCells) { + // Verify that the cell's Accessible is valid. + Accessible* acc = cell.Acc(mAcc); + if (cell.Selected()) { + aCells->AppendElement(acc); + } + } + } + + virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override { + for (uint32_t idx = 0; idx < mCells.Length(); ++idx) { + CachedTableCellAccessible& cell = mCells[idx]; + // Verify that the cell's Accessible is valid. + cell.Acc(mAcc); + if (cell.Selected()) { + aCells->AppendElement(idx); + } + } + } + + virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override { + for (uint32_t col = 0; col < mColCount; ++col) { + if (IsColSelected(col)) { + aCols->AppendElement(col); + } + } + } + + virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override { + for (uint32_t row = 0; row < RowCount(); ++row) { + if (IsRowSelected(row)) { + aRows->AppendElement(row); + } + } + } + + virtual Accessible* AsAccessible() override { return mAcc; } + + virtual bool IsProbablyLayoutTable() override; + + private: + explicit CachedTableAccessible(Accessible* aAcc); + + // Ensure that the given row exists in our data structure, creating array + // elements as needed. + void EnsureRow(uint32_t aRowIdx); + + // Ensure that the given row and column coordinate exists in our data + // structure, creating array elements as needed. A newly created coordinate + // will be set to kNoCellIdx. + void EnsureRowCol(uint32_t aRowIdx, uint32_t aColIdx); + + Accessible* mAcc; // The table Accessible. + // We track the column count because it might not be uniform across rows in + // malformed tables. + uint32_t mColCount = 0; + // An array of cell instances. A cell index is an index into this array. + nsTArray<CachedTableCellAccessible> mCells; + // Maps row and column coordinates to cell indices. + nsTArray<nsTArray<uint32_t>> mRowColToCellIdx; + // Maps Accessibles to cell indexes to facilitate retrieval of a cell + // instance from a cell Accessible. The Accessible* keys should only be used + // for lookup. They should not be dereferenced. + nsTHashMap<Accessible*, uint32_t> mAccToCellIdx; + uint64_t mCaptionAccID = 0; + + friend class CachedTableCellAccessible; +}; + +} // namespace mozilla::a11y + +#endif diff --git a/accessible/base/DocManager.cpp b/accessible/base/DocManager.cpp new file mode 100644 index 0000000000..b7a5203e40 --- /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 "DocAccessible-inl.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 "nsIWebProgress.h" +#include "nsCoreUtils.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()); +} + +LocalAccessible* DocManager::FindAccessibleInCache(nsINode* aNode) const { + for (const auto& docAccessible : mDocAccessibleCache.Values()) { + NS_ASSERTION(docAccessible, + "No doc accessible for the object in doc accessible cache!"); + + if (docAccessible) { + LocalAccessible* accessible = docAccessible->GetAccessible(aNode); + if (accessible) { + return accessible; + } + } + } + return nullptr; +} + +void DocManager::RemoveFromXPCDocumentCache(DocAccessible* aDocument, + bool aAllowServiceShutdown) { + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (xpcDoc) { + xpcDoc->Shutdown(); + mXPCDocumentCache.Remove(aDocument); + + if (aAllowServiceShutdown && !HasXPCDocuments()) { + MaybeShutdownAccService(nsAccessibilityService::eXPCOM); + } + } +} + +void DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument, + Document* aDOMDocument, + bool aAllowServiceShutdown) { + // 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, aAllowServiceShutdown); + 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; + + return mXPCDocumentCache.GetOrInsertNew(aDocument, aDocument); +} + +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); + sRemoteXPCDocumentCache->InsertOrUpdate(aDoc, RefPtr{doc}); + + return doc; +} + +#ifdef DEBUG +bool DocManager::IsProcessingRefreshDriverNotification() const { + for (const auto& entry : mDocAccessibleCache) { + DocAccessible* docAccessible = entry.GetWeak(); + 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) || !document->IsContentDocument()) { + 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) { + 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 (!nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocument) || + 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->GetWindowType() == widget::WindowType::Invisible) { + return nullptr; + } + + // Ignore documents without presshell. We must not ignore documents with no + // root frame because DOM focus can hit such documents and ignoring them would + // prevent a11y focus. + 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.InsertOrUpdate(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); +} + +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..94da5d0a24 --- /dev/null +++ b/accessible/base/DocManager.h @@ -0,0 +1,193 @@ +/* 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 LocalAccessible; +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. + */ + LocalAccessible* FindAccessibleInCache(nsINode* aNode) const; + + /** + * Called by document accessible when it gets shutdown. + * @param aAllowServiceShutdown true to shut down nsAccessibilityService + * if it is no longer required, false to prevent it. + */ + void NotifyOfDocumentShutdown(DocAccessible* aDocument, + dom::Document* aDOMDocument, + bool aAllowServiceShutdown = true); + + void RemoveFromXPCDocumentCache(DocAccessible* aDocument, + bool aAllowServiceShutdown = true); + + /** + * 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..cba5f2d9ba --- /dev/null +++ b/accessible/base/EmbeddedObjCollector.cpp @@ -0,0 +1,62 @@ +/* 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 "LocalAccessible.h" + +using namespace mozilla::a11y; + +uint32_t EmbeddedObjCollector::Count() { + EnsureNGetIndex(nullptr); + return mObjects.Length(); +} + +LocalAccessible* EmbeddedObjCollector::GetAccessibleAt(uint32_t aIndex) { + LocalAccessible* accessible = mObjects.SafeElementAt(aIndex, nullptr); + if (accessible) return accessible; + + return EnsureNGetObject(aIndex); +} + +LocalAccessible* EmbeddedObjCollector::EnsureNGetObject(uint32_t aIndex) { + uint32_t childCount = mRoot->ChildCount(); + while (mRootChildIdx < childCount) { + LocalAccessible* child = mRoot->LocalChildAt(mRootChildIdx++); + if (child->IsText()) continue; + + AppendObject(child); + if (mObjects.Length() - 1 == aIndex) return mObjects[aIndex]; + } + + return nullptr; +} + +int32_t EmbeddedObjCollector::EnsureNGetIndex(LocalAccessible* aAccessible) { + uint32_t childCount = mRoot->ChildCount(); + while (mRootChildIdx < childCount) { + LocalAccessible* child = mRoot->LocalChildAt(mRootChildIdx++); + if (child->IsText()) continue; + + AppendObject(child); + if (child == aAccessible) return mObjects.Length() - 1; + } + + return -1; +} + +int32_t EmbeddedObjCollector::GetIndexAt(LocalAccessible* aAccessible) { + if (aAccessible->mParent != mRoot) return -1; + + if (aAccessible->mIndexOfEmbeddedChild != -1) { + return aAccessible->mIndexOfEmbeddedChild; + } + + return !aAccessible->IsText() ? EnsureNGetIndex(aAccessible) : -1; +} + +void EmbeddedObjCollector::AppendObject(LocalAccessible* aAccessible) { + aAccessible->mIndexOfEmbeddedChild = mObjects.Length(); + mObjects.AppendElement(aAccessible); +} diff --git a/accessible/base/EmbeddedObjCollector.h b/accessible/base/EmbeddedObjCollector.h new file mode 100644 index 0000000000..a1d29e458f --- /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 LocalAccessible; + +/** + * 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(LocalAccessible* aAccessible); + + /** + * Return accessible count within the collection. + */ + uint32_t Count(); + + /** + * Return an accessible from the collection at the given index. + */ + LocalAccessible* GetAccessibleAt(uint32_t aIndex); + + protected: + /** + * Ensure accessible at the given index is stored and return it. + */ + LocalAccessible* EnsureNGetObject(uint32_t aIndex); + + /** + * Ensure index for the given accessible is stored and return it. + */ + int32_t EnsureNGetIndex(LocalAccessible* aAccessible); + + // Make sure it's used by LocalAccessible class only. + explicit EmbeddedObjCollector(LocalAccessible* aRoot) + : mRoot(aRoot), mRootChildIdx(0) {} + + /** + * Append the object to collection. + */ + void AppendObject(LocalAccessible* aAccessible); + + friend class LocalAccessible; + + LocalAccessible* mRoot; + uint32_t mRootChildIdx; + nsTArray<LocalAccessible*> mObjects; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/EventQueue.cpp b/accessible/base/EventQueue.cpp new file mode 100644 index 0000000000..8a5e22cd48 --- /dev/null +++ b/accessible/base/EventQueue.cpp @@ -0,0 +1,436 @@ +/* -*- 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 "LocalAccessible-inl.h" +#include "nsEventShell.h" +#include "DocAccessibleChild.h" +#include "nsTextEquivUtils.h" +#ifdef A11Y_LOG +# include "Logging.h" +#endif +#include "Relation.h" + +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!"); + + if (aEvent->mEventType == nsIAccessibleEvent::EVENT_FOCUS) { + mFocusEvent = aEvent; + return true; + } + + // 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)) { + PushNameOrDescriptionChange(aEvent); + } + return true; +} + +bool EventQueue::PushNameOrDescriptionChange(AccEvent* aOrigEvent) { + // Fire name/description change event on parent or related LocalAccessible + // being labelled/described given that this event hasn't been coalesced, the + // dependent's name/description was calculated from this subtree, and the + // subtree was changed. + LocalAccessible* target = aOrigEvent->mAccessible; + // If the text of a text leaf changed without replacing the leaf, the only + // event we get is text inserted on the container. In this case, we might + // need to fire a name change event on the target itself. + const bool maybeTargetNameChanged = + (aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED || + aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) && + nsTextEquivUtils::HasNameRule(target, eNameFromSubtreeRule); + const bool doName = target->HasNameDependent() || maybeTargetNameChanged; + const bool doDesc = target->HasDescriptionDependent(); + if (!doName && !doDesc) { + return false; + } + bool pushed = false; + bool nameCheckAncestor = true; + // Only continue traversing up the tree if it's possible that the parent + // LocalAccessible's name (or a LocalAccessible being labelled by this + // LocalAccessible or an ancestor) can depend on this LocalAccessible's name. + LocalAccessible* parent = target; + do { + // Test possible name dependent parent. + if (doName) { + if (nameCheckAncestor && (maybeTargetNameChanged || parent != target) && + nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeRule)) { + nsAutoString name; + ENameValueFlag nameFlag = parent->Name(name); + // If name is obtained from subtree, fire name change event. + // HTML file inputs always get part of their name from the subtree, even + // if the author provided a name. + if (nameFlag == eNameFromSubtree || parent->IsHTMLFileInput()) { + RefPtr<AccEvent> nameChangeEvent = + new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, parent); + pushed |= PushEvent(nameChangeEvent); + } + nameCheckAncestor = false; + } + + Relation rel = parent->RelationByType(RelationType::LABEL_FOR); + while (LocalAccessible* relTarget = rel.LocalNext()) { + RefPtr<AccEvent> nameChangeEvent = + new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, relTarget); + pushed |= PushEvent(nameChangeEvent); + } + } + + if (doDesc) { + Relation rel = parent->RelationByType(RelationType::DESCRIPTION_FOR); + while (LocalAccessible* relTarget = rel.LocalNext()) { + RefPtr<AccEvent> descChangeEvent = new AccEvent( + nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, relTarget); + pushed |= PushEvent(descChangeEvent); + } + } + + if (parent->IsDoc()) { + // Never cross document boundaries. + break; + } + parent = parent->LocalParent(); + } while (parent && + nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeIfReqRule)); + + return pushed; +} + +//////////////////////////////////////////////////////////////////////////////// +// 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<LocalAccessible*> 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); + nsTArray<uint64_t> selectedIDs; + nsTArray<uint64_t> unselectedIDs; + + uint32_t eventCount = events.Length(); +#ifdef A11Y_LOG + if ((eventCount > 0 || mFocusEvent) && logging::IsEnabled(logging::eEvents)) { + logging::MsgBegin("EVENTS", "events processing"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + if (mFocusEvent) { + // Always fire a pending focus event before all other events. We do this for + // two reasons: + // 1. It prevents extraneous screen reader speech if the name, states, etc. + // of the currently focused item change at the same time as another item is + // focused. If aria-activedescendant is used, even setting + // aria-activedescendant before changing other properties results in the + // property change events being queued before the focus event because we + // process aria-activedescendant async. + // 2. It improves perceived performance. Focus changes should be reported as + // soon as possible, so clients should handle focus events before they spend + // time on anything else. + RefPtr<AccEvent> event = std::move(mFocusEvent); + if (!event->mAccessible->IsDefunct()) { + FocusMgr()->ProcessFocusEvent(event); + } + } + + for (uint32_t idx = 0; idx < eventCount; idx++) { + AccEvent* event = events[idx]; + uint32_t eventType = event->mEventType; + LocalAccessible* target = event->GetAccessible(); + if (!target || target->IsDefunct()) { + continue; + } + + // Collect select changes + if (IPCAccessibilityActive()) { + if ((event->mEventRule == AccEvent::eDoNotEmit && + (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD || + eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE || + eventType == nsIAccessibleEvent::EVENT_SELECTION)) || + eventType == nsIAccessibleEvent::EVENT_SELECTION_WITHIN) { + // The selection even was either dropped or morphed to a + // selection-within. We need to collect the items from all these events + // and manually push their new state to the parent process. + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + LocalAccessible* item = selChangeEvent->mItem; + if (!item->IsDefunct()) { + uint64_t itemID = + item->IsDoc() ? 0 : reinterpret_cast<uint64_t>(item->UniqueID()); + bool selected = selChangeEvent->mSelChangeType == + AccSelChangeEvent::eSelectionAdd; + if (selected) { + selectedIDs.AppendElement(itemID); + } else { + unselectedIDs.AppendElement(itemID); + } + } + } + } + + if (event->mEventRule == AccEvent::eDoNotEmit) { + continue; + } + + // Dispatch caret moved and text selection change events. + if (eventType == nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED) { + SelectionMgr()->ProcessTextSelChangeEvent(event); + continue; + } + + // Fire selected state change events in support to selection events. + if (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, true, + event->mIsFromUserInput); + + } else if (eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, false, + event->mIsFromUserInput); + + } else if (eventType == 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; + } + } + + if (mDocument && IPCAccessibilityActive() && + (!selectedIDs.IsEmpty() || !unselectedIDs.IsEmpty())) { + DocAccessibleChild* ipcDoc = mDocument->IPCDoc(); + ipcDoc->SendSelectedAccessiblesChanged(selectedIDs, unselectedIDs); + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/base/EventQueue.h b/accessible/base/EventQueue.h new file mode 100644 index 0000000000..87bcf89340 --- /dev/null +++ b/accessible/base/EventQueue.h @@ -0,0 +1,83 @@ +/* -*- 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" +#include "mozilla/Assertions.h" + +namespace mozilla { +namespace a11y { + +class DocAccessible; + +/** + * Used to organize and coalesce pending events. + */ +class EventQueue { + protected: + explicit EventQueue(DocAccessible* aDocument) : mDocument(aDocument) { + MOZ_ASSERT(mDocument, + "There's no point creating an event queue for a null document"); + } + + /** + * Put an accessible event into the queue to process it later. + */ + bool PushEvent(AccEvent* aEvent); + + /** + * Puts name and/or description change events into the queue, if needed. + */ + bool PushNameOrDescriptionChange(AccEvent* aOrigEvent); + + /** + * 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; + + // Pending focus event. + RefPtr<AccEvent> mFocusEvent; +}; + +} // 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..29cf66e493 --- /dev/null +++ b/accessible/base/EventTree.cpp @@ -0,0 +1,99 @@ +/* -*- 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 "EmbeddedObjCollector.h" +#include "NotificationController.h" +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +#include "mozilla/UniquePtr.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TreeMutation class + +TreeMutation::TreeMutation(LocalAccessible* aParent, bool aNoEvents) + : mParent(aParent), + mStartIdx(UINT32_MAX), + mStateFlagsCopy(mParent->mStateFlags), + mQueueEvents(!aNoEvents) { +#ifdef DEBUG + mIsDone = false; +#endif + + mParent->mStateFlags |= LocalAccessible::eKidsMutating; +} + +TreeMutation::~TreeMutation() { + MOZ_ASSERT(mIsDone, "Done() must be called explicitly"); +} + +void TreeMutation::AfterInsertion(LocalAccessible* aChild) { + MOZ_ASSERT(aChild->LocalParent() == 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(LocalAccessible* aChild, bool aNoShutdown) { + MOZ_ASSERT(aChild->LocalParent() == 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 & LocalAccessible::eKidsMutating); + mParent->mStateFlags &= ~LocalAccessible::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]->mIndexOfEmbeddedChild = -1; + } + + for (uint32_t idx = 0; idx < length; idx++) { + mParent->mChildren[idx]->mStateFlags |= LocalAccessible::eGroupInfoDirty; + } + + mParent->mEmbeddedObjCollector = nullptr; + mParent->mStateFlags |= mStateFlagsCopy & LocalAccessible::eKidsMutating; + +#ifdef DEBUG + mIsDone = true; +#endif +} diff --git a/accessible/base/EventTree.h b/accessible/base/EventTree.h new file mode 100644 index 0000000000..bd9976b4fb --- /dev/null +++ b/accessible/base/EventTree.h @@ -0,0 +1,61 @@ +/* -*- 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 "LocalAccessible.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(LocalAccessible* aParent, bool aNoEvents = false); + ~TreeMutation(); + + void AfterInsertion(LocalAccessible* aChild); + void BeforeRemoval(LocalAccessible* aChild, bool aNoShutdown = false); + void Done(); + + private: + NotificationController* Controller() const { + return mParent->Document()->Controller(); + } + + LocalAccessible* mParent; + uint32_t mStartIdx; + uint32_t mStateFlagsCopy; + + /* + * True if mutation events should be queued. + */ + bool mQueueEvents; + +#ifdef DEBUG + bool mIsDone; +#endif +}; + +} // 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..3ef58dc4fe --- /dev/null +++ b/accessible/base/Filters.cpp @@ -0,0 +1,25 @@ +/* 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 "LocalAccessible-inl.h" +#include "States.h" + +using namespace mozilla::a11y; +using namespace mozilla::a11y::filters; + +uint32_t filters::GetSelected(LocalAccessible* aAccessible) { + if (aAccessible->State() & states::SELECTED) return eMatch | eSkipSubtree; + + return eSkip; +} + +uint32_t filters::GetSelectable(LocalAccessible* aAccessible) { + if (aAccessible->InteractiveState() & states::SELECTABLE) { + return eMatch | eSkipSubtree; + } + + return eSkip; +} diff --git a/accessible/base/Filters.h b/accessible/base/Filters.h new file mode 100644 index 0000000000..486beb5c83 --- /dev/null +++ b/accessible/base/Filters.h @@ -0,0 +1,36 @@ +/* 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 LocalAccessible; + +namespace filters { + +enum EResult { eSkip = 0, eMatch = 1, eSkipSubtree = 2 }; + +/** + * Return true if the traversed accessible complies with filter. + */ +typedef uint32_t (*FilterFuncPtr)(LocalAccessible*); + +/** + * Matches selected/selectable accessibles in subtree. + */ +uint32_t GetSelected(LocalAccessible* aAccessible); +uint32_t GetSelectable(LocalAccessible* 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..57cfd7034e --- /dev/null +++ b/accessible/base/FocusManager.cpp @@ -0,0 +1,462 @@ +/* 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 "LocalAccessible-inl.h" +#include "DocAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsEventShell.h" + +#include "nsFocusManager.h" +#include "mozilla/a11y/DocAccessibleParent.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() {} + +LocalAccessible* FocusManager::FocusedLocalAccessible() const { + MOZ_ASSERT(NS_IsMainThread()); + if (mActiveItem) { + if (mActiveItem->IsDefunct()) { + MOZ_ASSERT_UNREACHABLE("Stored active item is unbound from document"); + return nullptr; + } + + return mActiveItem; + } + + if (nsAccessibilityService::IsShutdown()) { + // We might try to get or create a DocAccessible below, which isn't safe (or + // useful) if the accessibility service is shutting down. + return nullptr; + } + + nsINode* focusedNode = FocusedDOMNode(); + if (focusedNode) { + DocAccessible* doc = + GetAccService()->GetDocAccessible(focusedNode->OwnerDoc()); + return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode) + : nullptr; + } + + return nullptr; +} + +Accessible* FocusManager::FocusedAccessible() const { +#if defined(ANDROID) + // It's not safe to call FocusedLocalAccessible() except on the main thread. + // Android might query RemoteAccessibles on the UI thread, which might call + // FocusedAccessible(). Never try to get the focused LocalAccessible in this + // case. + if (NS_IsMainThread()) { + if (Accessible* focusedAcc = FocusedLocalAccessible()) { + return focusedAcc; + } + } else { + nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns(); + } + return mFocusedRemoteDoc ? mFocusedRemoteDoc->GetFocusedAcc() : nullptr; +#else + if (Accessible* focusedAcc = FocusedLocalAccessible()) { + return focusedAcc; + } + + if (!XRE_IsParentProcess()) { + // DocAccessibleParent's don't exist in the content + // process, so we can't return anything useful if this + // is the case. + return nullptr; + } + + nsFocusManager* focusManagerDOM = nsFocusManager::GetFocusManager(); + if (!focusManagerDOM) { + return nullptr; + } + + // If we call GetFocusedBrowsingContext from the chrome process + // it returns the BrowsingContext for the focused _window_, which + // is not helpful here. Instead use GetFocusedBrowsingContextInChrome + // which returns the content BrowsingContext that has focus. + dom::BrowsingContext* focusedContext = + focusManagerDOM->GetFocusedBrowsingContextInChrome(); + + DocAccessibleParent* focusedDoc = + DocAccessibleParent::GetFrom(focusedContext); + return focusedDoc ? focusedDoc->GetFocusedAcc() : nullptr; +#endif // defined(ANDROID) +} + +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 LocalAccessible* aAccessible) const { + LocalAccessible* focus = FocusedLocalAccessible(); + if (!focus) return eNone; + + // If focused. + if (focus == aAccessible) return eFocused; + + // If contains the focus. + LocalAccessible* child = focus->LocalParent(); + while (child) { + if (child == aAccessible) return eContainsFocus; + + child = child->LocalParent(); + } + + // If contained by focus. + child = aAccessible->LocalParent(); + while (child) { + if (child == focus) return eContainedByFocus; + + child = child->LocalParent(); + } + + return eNone; +} + +bool FocusManager::WasLastFocused(const LocalAccessible* 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(LocalAccessible* 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) { + LocalAccessible* 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. + LocalAccessible* target = FocusedLocalAccessible(); + 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, + LocalAccessible* 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; + if (mActiveItem != aTarget) { + // This new focus overrides the stored active item, so clear the active + // item. Among other things, the old active item might die. + mActiveItem = nullptr; + } + +#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; + + LocalAccessible* 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; + + LocalAccessible* DOMFocus = + document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); + if (target != DOMFocus) return; + + LocalAccessible* 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. + LocalAccessible* 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; + + LocalAccessible* DOMFocus = + document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode); + if (target != DOMFocus) return; + + LocalAccessible* 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. + LocalAccessible* ARIAMenubar = nullptr; + for (LocalAccessible* parent = target->LocalParent(); parent; + parent = parent->LocalParent()) { + 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); + LocalAccessible* anchorJump = targetDocument->AnchorJump(); + if (anchorJump) { + if (target == targetDocument || target->IsNonInteractive()) { + // 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..7460a21f9a --- /dev/null +++ b/accessible/base/FocusManager.h @@ -0,0 +1,169 @@ +/* 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 Accessible; +class AccEvent; +class LocalAccessible; +class DocAccessible; +class DocAccessibleParent; + +/** + * Manage the accessible focus. Used to fire and process accessible events. + */ +class FocusManager { + public: + virtual ~FocusManager(); + + /** + * Return the currently focused LocalAccessible. If a remote document has + * focus, this will return null. + */ + LocalAccessible* FocusedLocalAccessible() const; + + /** + * Return the currently focused Accessible, local or remote. + */ + Accessible* FocusedAccessible() const; + + /** + * Return true if given accessible is focused. + */ + bool IsFocused(const Accessible* aAccessible) const { + return FocusedAccessible() == aAccessible; + } + + /** + * 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 LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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(LocalAccessible* 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, LocalAccessible* aTarget); + + /** + * Process DOM focus notification. + */ + void ProcessDOMFocus(nsINode* aTarget); + + /** + * Process the delayed accessible event. + * do. + */ + void ProcessFocusEvent(AccEvent* aEvent); + +#ifdef ANDROID + void SetFocusedRemoteDoc(DocAccessibleParent* aDoc) { + mFocusedRemoteDoc = aDoc; + } + bool IsFocusedRemoteDoc(DocAccessibleParent* aDoc) { + return mFocusedRemoteDoc == aDoc; + } +#endif + + protected: + FocusManager(); + + private: + FocusManager(const FocusManager&); + FocusManager& operator=(const FocusManager&); + + /** + * Return DOM document having DOM focus. + */ + dom::Document* FocusedDOMDocument() const; + + private: + RefPtr<LocalAccessible> mActiveItem; + RefPtr<LocalAccessible> mLastFocus; + RefPtr<LocalAccessible> mActiveARIAMenubar; +#ifdef ANDROID + DocAccessibleParent* mFocusedRemoteDoc = nullptr; +#endif +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/HTMLMarkupMap.h b/accessible/base/HTMLMarkupMap.h new file mode 100644 index 0000000000..b903097ea0 --- /dev/null +++ b/accessible/base/HTMLMarkupMap.h @@ -0,0 +1,444 @@ +/* -*- 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, LocalAccessible* aContext) -> LocalAccessible* { + // An anchor element without an href attribute and without a click + // listener should be a generic. + if (!aElement->HasAttr(nsGkAtoms::href) && + !nsCoreUtils::HasClickListener(aElement)) { + return new HyperTextAccessible(aElement, aContext->Document()); + } + // 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 HyperTextAccessible(aElement, aContext->Document()); + } + + return new HTMLLinkAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(abbr, New_HyperText, 0) + +MARKUPMAP(acronym, New_HyperText, 0) + +MARKUPMAP(address, New_HyperText, roles::GROUPING) + +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, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLButtonAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + caption, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + 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) + +MARKUPMAP(code, New_HyperText, roles::CODE) + +MARKUPMAP(dd, New_HTMLDtOrDd<HyperTextAccessible>, roles::DEFINITION) + +MARKUPMAP(del, New_HyperText, roles::CONTENT_DELETION) + +MARKUPMAP(details, New_HyperText, roles::DETAILS) + +MARKUPMAP(dfn, New_HyperText, roles::TERM) + +MARKUPMAP(dialog, New_HyperText, roles::DIALOG) + +MARKUPMAP( + div, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + // 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(nsGkAtoms::id)) { + return new HyperTextAccessible(aElement, aContext->Document()); + } + // Never create an accessible if the div is not display:block; or + // display:inline-block or the like. + nsIFrame* f = aElement->GetPrimaryFrame(); + if (!f || !f->IsBlockFrameOrSubclass()) { + 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. + // FIXME: This looks extremely incorrect in presence of shadow DOM, + // display: contents, and what not. + nsIContent* prevSibling = aElement->GetPreviousSibling(); + if (prevSibling) { + nsIFrame* prevSiblingFrame = prevSibling->GetPrimaryFrame(); + if (prevSiblingFrame && prevSiblingFrame->IsInlineOutside()) { + return new HyperTextAccessible(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 HyperTextAccessible(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 HyperTextAccessible(aElement, aContext->Document()); + } + } + } + return nullptr; + }, + roles::SECTION) + +MARKUPMAP( + dl, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::DEFINITION_LIST) + +MARKUPMAP(dt, New_HTMLDtOrDd<HTMLLIAccessible>, roles::TERM) + +MARKUPMAP(em, New_HyperText, roles::EMPHASIS) + +MARKUPMAP( + figcaption, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLFigcaptionAccessible(aElement, aContext->Document()); + }, + roles::CAPTION) + +MARKUPMAP( + figure, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLFigureAccessible(aElement, aContext->Document()); + }, + roles::FIGURE, Attr(xmlroles, figure)) + +MARKUPMAP( + fieldset, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLGroupboxAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + form, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLFormAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + footer, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLHeaderOrFooterAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + header, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + 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(hgroup, New_HyperText, roles::GROUPING) + +MARKUPMAP( + hr, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLHRAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + input, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + // 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) || + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::datetime_local, eIgnoreCase)) { + return new HTMLDateTimeAccessible<roles::DATE_EDITOR>( + aElement, aContext->Document()); + } + return nullptr; + }, + 0) + +MARKUPMAP(ins, New_HyperText, roles::CONTENT_INSERTION) + +MARKUPMAP( + label, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLLabelAccessible(aElement, aContext->Document()); + }, + roles::LABEL) + +MARKUPMAP( + legend, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLLegendAccessible(aElement, aContext->Document()); + }, + roles::LABEL) + +MARKUPMAP( + li, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + // 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( + menu, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +MARKUPMAP(nav, New_HyperText, roles::LANDMARK) + +MARKUPMAP( + ol, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +MARKUPMAP( + option, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLSelectOptionAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + optgroup, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLSelectOptGroupAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP( + output, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLOutputAccessible(aElement, aContext->Document()); + }, + roles::STATUSBAR, Attr(aria_live, polite)) + +MARKUPMAP(p, nullptr, roles::PARAGRAPH) + +MARKUPMAP( + progress, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLProgressAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(q, New_HyperText, 0) + +MARKUPMAP(s, New_HyperText, roles::CONTENT_DELETION) + +MARKUPMAP( + section, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLSectionAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(strong, New_HyperText, roles::STRONG) + +MARKUPMAP(sub, New_HyperText, roles::SUBSCRIPT) + +MARKUPMAP( + summary, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLSummaryAccessible(aElement, aContext->Document()); + }, + roles::SUMMARY) + +MARKUPMAP(sup, New_HyperText, roles::SUPERSCRIPT) + +MARKUPMAP( + table, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableAccessible(aElement, aContext->Document()); + }, + roles::TABLE) + +MARKUPMAP(time, New_HyperText, roles::TIME, Attr(xmlroles, time), + AttrFromDOM(datetime, datetime)) + +MARKUPMAP(tbody, nullptr, roles::GROUPING) + +MARKUPMAP( + td, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + if (!aContext->IsHTMLTableRow()) { + return nullptr; + } + if (aElement->HasAttr(nsGkAtoms::scope)) { + return new HTMLTableHeaderCellAccessible(aElement, + aContext->Document()); + } + return new HTMLTableCellAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(tfoot, nullptr, roles::GROUPING) + +MARKUPMAP( + th, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + if (!aContext->IsHTMLTableRow()) { + return nullptr; + } + return new HTMLTableHeaderCellAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(thead, nullptr, roles::GROUPING) + +MARKUPMAP( + tr, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + if (aContext->IsTableRow()) { + // A <tr> within a row isn't valid. + return nullptr; + } + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(aElement); + if (roleMapEntry && roleMapEntry->role != roles::NOTHING && + roleMapEntry->role != roles::ROW) { + // There is a valid ARIA role which isn't "row". Don't treat this as an + // HTML table row. + return nullptr; + } + // Check if this <tr> is within a table. We check the grandparent because + // it might be inside a rowgroup. We don't specifically check for an HTML + // table because there are cases where there is a <tr> inside a + // <div role="table"> such as Monorail. + if (aContext->IsTable() || + (aContext->LocalParent() && aContext->LocalParent()->IsTable())) { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + } + return nullptr; + }, + roles::ROW) + +MARKUPMAP( + ul, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLListAccessible(aElement, aContext->Document()); + }, + roles::LIST) + +MARKUPMAP( + meter, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLMeterAccessible(aElement, aContext->Document()); + }, + roles::METER) + +MARKUPMAP(search, New_HyperText, roles::LANDMARK) 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..67bf76ee5b --- /dev/null +++ b/accessible/base/Logging.cpp @@ -0,0 +1,992 @@ +/* -*- 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 "LocalAccessible-inl.h" +#include "AccEvent.h" +#include "DocAccessible.h" +#include "DocAccessible-inl.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" +#include "OuterDocAccessible.h" + +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIWebProgress.h" +#include "prenv.h" +#include "nsIDocShellTreeItem.h" +#include "mozilla/Maybe.h" +#include "mozilla/PresShell.h" +#include "mozilla/StackWalk.h" +#include "mozilla/ToString.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; + +MOZ_DEFINE_MALLOC_SIZE_OF(AccessibleLoggingMallocSizeOf) + +//////////////////////////////////////////////////////////////////////////////// +// 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}, + {"platforms", logging::ePlatforms}, + {"text", logging::eText}, + {"tree", logging::eTree}, + {"treeSize", logging::eTreeSize}, + + {"DOMEvents", logging::eDOMEvents}, + {"focus", logging::eFocus}, + {"selection", logging::eSelection}, + {"notifications", logging::eNotifications}, + + {"stack", logging::eStack}, + {"verbose", logging::eVerbose}, + {"cache", logging::eCache}}; + +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) { + nsIURI* uri = aDocumentNode->GetDocumentURI(); + if (uri) { + printf("uri: %s", uri->GetSpecOrDefault().get()); + } else { + printf("uri: null"); + } +} + +static void LogDocShellState(dom::Document* aDocumentNode) { + printf("docshell busy: "); + nsCOMPtr<nsIDocShell> docShell = aDocumentNode->GetDocShell(); + if (!docShell) { + printf("null docshell"); + return; + } + + nsAutoCString docShellBusy; + 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 = aDocumentNode->IsContentDocument(); + 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()); + if (!treeItem) { + printf("in-process docshell hierarchy, null docshell;"); + return; + } + 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", + nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocumentNode) + ? "" + : "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_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_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_LINK: + printf("link; "); + break; + case LOAD_REFRESH: + printf("refresh; "); + break; + case LOAD_REFRESH_REPLACE: + printf("refresh replace; "); + 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, + LocalAccessible* 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, + LocalAccessible* aTarget) { + SubMsgBegin(); + printf(" Caused by: %s\n", aCause); + AccessibleNNode("Item", aTarget); + SubMsgEnd(); +} + +void logging::ActiveWidget(LocalAccessible* 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(LocalAccessible* 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) { + LocalAccessible* acc = va_arg(vl, LocalAccessible*); + 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, LocalAccessible*)); + } + } 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, LocalAccessible* aAcc, + const char* aMsg2, nsINode* aNode) { + if (IsEnabledAll(logging::eTree | aExtraFlags)) { + MsgBegin("TREE", "%s; doc: %p", aMsg, aAcc ? aAcc->Document() : nullptr); + AccessibleInfo(aMsg1, aAcc); + LocalAccessible* 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, + LocalAccessible* 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->LocalChildAt(idx)); + } + MsgEnd(); + } +} + +void logging::Tree(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot, GetTreePrefix aPrefixFunc, + void* aGetTreePrefixData) { + logging::MsgBegin(aTitle, "%s", aMsgText); + + nsAutoString level; + LocalAccessible* root = aRoot; + do { + const char* prefix = + aPrefixFunc ? aPrefixFunc(aGetTreePrefixData, root) : ""; + printf("%s", NS_ConvertUTF16toUTF8(level).get()); + logging::AccessibleInfo(prefix, root); + if (root->LocalFirstChild() && !root->LocalFirstChild()->IsDoc()) { + level.AppendLiteral(u" "); + root = root->LocalFirstChild(); + 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->LocalParent())) { + 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::TreeSize(const char* aTitle, const char* aMsgText, + LocalAccessible* aRoot) { + logging::MsgBegin(aTitle, "%s", aMsgText); + logging::AccessibleInfo("Logging tree size from: ", aRoot); + size_t b = 0; + size_t n = 0; + LocalAccessible* root = aRoot; + do { + // Process the current acc + b += AccessibleLoggingMallocSizeOf(root); + n++; + + // Get next acc + if (root->LocalFirstChild() && !root->LocalFirstChild()->IsDoc()) { + root = root->LocalFirstChild(); + 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->LocalParent())) { + 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); + + printf("\nTree contains %zu accessibles and is %zu bytes\n", n, b); + 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, LocalAccessible* 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) { + Maybe<uint32_t> idxInParent = aNode->ComputeIndexInParentNode(); + nsAutoString nodeDesc; + DescribeNode(aNode, nodeDesc); + printf(" %s: %s, idx in parent %s\n", aDescr, + NS_ConvertUTF16toUTF8(nodeDesc).get(), ToString(idxInParent).c_str()); +} + +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, LocalAccessible* 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, + LocalAccessible* 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) { + LocalAccessible* 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"); + MozWalkTheStack(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..2a6a93faa9 --- /dev/null +++ b/accessible/base/Logging.h @@ -0,0 +1,236 @@ +/* -*- 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 LocalAccessible; +class DocAccessible; +class OuterDocAccessible; + +namespace logging { + +enum EModules { + eDocLoad = 1 << 0, + eDocCreate = 1 << 1, + eDocDestroy = 1 << 2, + eDocLifeCycle = eDocLoad | eDocCreate | eDocDestroy, + + eEvents = 1 << 3, + ePlatforms = 1 << 4, + eText = 1 << 5, + eTree = 1 << 6, + eTreeSize = 1 << 7, + + eDOMEvents = 1 << 8, + eFocus = 1 << 9, + eSelection = 1 << 10, + eNotifications = eDOMEvents | eSelection | eFocus, + + // extras + eStack = 1 << 11, + eVerbose = 1 << 12, + eCache = 1 << 13, +}; + +/** + * 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, + LocalAccessible* 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, LocalAccessible* aTarget); + +/** + * Log the active widget (submessage). + */ +void ActiveWidget(LocalAccessible* aWidget); + +/** + * Log the focus event was dispatched (submessage). + */ +void FocusDispatched(LocalAccessible* 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, + LocalAccessible* aAcc, const char* aMsg2, nsINode* aNode); +void TreeInfo(const char* aMsg, uint32_t aExtraFlags, LocalAccessible* aParent); + +/** + * Log the accessible/DOM tree. + */ +typedef const char* (*GetTreePrefix)(void* aData, LocalAccessible*); +void Tree(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot, + GetTreePrefix aPrefixFunc = nullptr, + void* aGetTreePrefixData = nullptr); +void DOMTree(const char* aTitle, const char* aMsgText, DocAccessible* aDoc); + +/** + * Log the tree size in bytes. + */ +void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot); + +/** + * 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, LocalAccessible* 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, LocalAccessible* aAccessible); +void AccessibleNNode(const char* aDescr, LocalAccessible* 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/MathMLMarkupMap.h b/accessible/base/MathMLMarkupMap.h new file mode 100644 index 0000000000..a03dccb358 --- /dev/null +++ b/accessible/base/MathMLMarkupMap.h @@ -0,0 +1,113 @@ +/* -*- 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(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_ROW) + +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, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE, AttrFromDOM(align, align), + AttrFromDOM(columnlines_, columnlines_), AttrFromDOM(rowlines_, rowlines_)) + +MARKUPMAP( + mlabeledtr_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_LABELED_ROW) + +MARKUPMAP( + mtr_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableRowAccessible(aElement, aContext->Document()); + }, + roles::MATHML_TABLE_ROW) + +MARKUPMAP( + mtd_, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new HTMLTableCellAccessible(aElement, aContext->Document()); + }, + 0) + +MARKUPMAP(maction_, New_HyperText, roles::MATHML_ACTION, + AttrFromDOM(actiontype_, actiontype_), + AttrFromDOM(selection_, selection_)) + +MARKUPMAP(merror_, New_HyperText, roles::MATHML_ERROR) + +MARKUPMAP(mstack_, New_HyperText, roles::MATHML_STACK, + AttrFromDOM(align, align), AttrFromDOM(position, position)) + +MARKUPMAP(mlongdiv_, New_HyperText, roles::MATHML_LONG_DIVISION, + AttrFromDOM(longdivstyle_, longdivstyle_)) + +MARKUPMAP(msgroup_, New_HyperText, roles::MATHML_STACK_GROUP, + AttrFromDOM(position, position), AttrFromDOM(shift_, shift_)) + +MARKUPMAP(msrow_, New_HyperText, roles::MATHML_STACK_ROW, + AttrFromDOM(position, position)) + +MARKUPMAP(mscarries_, New_HyperText, roles::MATHML_STACK_CARRIES, + AttrFromDOM(location_, location_), AttrFromDOM(position, position)) + +MARKUPMAP(mscarry_, New_HyperText, roles::MATHML_STACK_CARRY, + AttrFromDOM(crossout_, crossout_)) + +MARKUPMAP(msline_, New_HyperText, roles::MATHML_STACK_LINE, + AttrFromDOM(position, position)) diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp new file mode 100644 index 0000000000..63786861f1 --- /dev/null +++ b/accessible/base/NotificationController.cpp @@ -0,0 +1,1107 @@ +/* -*- 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 "LocalAccessible-inl.h" +#include "nsEventShell.h" +#include "TextLeafAccessible.h" +#include "TextUpdater.h" + +#include "nsIContentInlines.h" + +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccessibilityService.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) { + // Schedule initial accessible tree construction. + ScheduleProcessing(); +} + +NotificationController::~NotificationController() { + NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); + if (mDocument) { + Shutdown(); + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed"); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 (const auto& entry : tmp->mContentInsertions) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key"); + cb.NoteXPCOMChild(entry.GetKey()); + nsTArray<nsCOMPtr<nsIContent>>* list = entry.GetData().get(); + 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(mFocusEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: public + +void NotificationController::Shutdown() { + if (mObservingState != eNotObservingRefresh && + mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { + // Note, this was our last chance to unregister, since we're about to + // clear mPresShell further down in this function. + mObservingState = eNotObservingRefresh; + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed (and we just " + "passed our last change to unregister)"); + // Immediately null out mPresShell, to prevent us from being registered as a + // refresh observer again. + mPresShell = nullptr; + + // 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; + + mTextArray.Clear(); + mContentInsertions.Clear(); + mNotifications.Clear(); + mFocusEvent = nullptr; + mEvents.Clear(); + mRelocations.Clear(); +} + +void NotificationController::CoalesceHideEvent(AccHideEvent* aHideEvent) { + LocalAccessible* parent = aHideEvent->LocalParent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + if (parent->HideEventTarget()) { + DropMutationEvent(aHideEvent); + break; + } + + if (parent->ShowEventTarget()) { + AccShowEvent* showEvent = + downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ShowEvent)); + if (showEvent->EventGeneration() < aHideEvent->EventGeneration()) { + DropMutationEvent(aHideEvent); + break; + } + } + + parent = parent->LocalParent(); + } +} + +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; + } + + // Don't queue a hide event on an accessible that's already being moved. It + // or an ancestor should already have a hide event queued. + if (mDocument && + mDocument->IsAccessibleBeingMoved(aEvent->GetAccessible())) { + 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. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + CoalesceHideEvent(downcast_accEvent(aEvent)); + + // 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. + LocalAccessible* container = aEvent->GetAccessible()->LocalParent(); + 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 + // and/or description of dependent Accessibles may be changing. + if (PushNameOrDescriptionChange(aEvent)) { + ScheduleProcessing(); + } + } 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; + } + + LocalAccessible* target = aEvent->GetAccessible(); + 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->LocalParent() == mutEvent->LocalParent()) { + 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->LocalParent() == target->LocalParent()) { + 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) { + const uint32_t eventType = aEvent->GetEventType(); + MOZ_ASSERT(eventType != nsIAccessibleEvent::EVENT_INNER_REORDER, + "Inner reorder has already been dropped, cannot drop again"); + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + // We don't fully drop reorder events, we just change them to inner reorder + // events. + AccReorderEvent* reorderEvent = downcast_accEvent(aEvent); + + MOZ_ASSERT(reorderEvent); + reorderEvent->SetInner(); + return; + } + if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetShowEventTarget(false); + } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetHideEventTarget(false); + + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + MOZ_ASSERT(hideEvent); + + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible()); + } + } else { + MOZ_ASSERT_UNREACHABLE("Mutation event has non-mutation event type"); + } + + // 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) { + LocalAccessible* 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; + } + + LocalAccessible* parent = acc->LocalParent(); + if (parent && 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) { + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + 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->LocalParent(); + } + } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { + MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE, + "mutation event list has an invalid event"); + + AccHideEvent* hideEvent = downcast_accEvent(event); + CoalesceHideEvent(hideEvent); + } + + event = nextEvent; + } +} + +void NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) { + // Schedule child document binding to the tree. + mHangingChildDocuments.AppendElement(aDocument); + ScheduleProcessing(); +} + +void NotificationController::ScheduleContentInsertion( + LocalAccessible* aContainer, nsTArray<nsCOMPtr<nsIContent>>& aInsertions) { + if (!aInsertions.IsEmpty()) { + mContentInsertions.GetOrInsertNew(aContainer)->AppendElements(aInsertions); + ScheduleProcessing(); + } +} + +void NotificationController::ScheduleProcessing() { + // If notification flush isn't planned yet, start notification flush + // asynchronously (after style and layout). + // Note: the mPresShell null-check might be unnecessary; it's just to prevent + // a null-deref here, if we somehow get called after we've been shut down. + if (mObservingState == eNotObservingRefresh && mPresShell) { + 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 || + !mTextArray.IsEmpty() || + !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() { + // Firing an event can indirectly run script; e.g. an XPCOM event observer + // or querying a XUL interface. Further mutations might be queued as a result. + // It's important that the mutation queue and state bits from one tick don't + // interfere with the next tick. Otherwise, we can end up dropping events. + // Therefore: + // 1. Clear the state bits, which we only need for coalescence. + for (AccTreeMutationEvent* event = mFirstMutationEvent; event; + event = event->NextEvent()) { + LocalAccessible* acc = event->GetAccessible(); + acc->SetShowEventTarget(false); + acc->SetHideEventTarget(false); + acc->SetReorderEventTarget(false); + } + // 2. Keep the current queue locally, but clear the queue on the instance. + RefPtr<AccTreeMutationEvent> firstEvent = mFirstMutationEvent; + mFirstMutationEvent = mLastMutationEvent = nullptr; + mMutationMap.Clear(); + mEventGeneration = 0; + + // Group the show events by the parent of their target. + nsTHashMap<nsPtrHashKey<LocalAccessible>, nsTArray<AccTreeMutationEvent*>> + showEvents; + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) { + continue; + } + + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + showEvents.LookupOrInsert(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. We do this before firing any events + // because firing an event might indirectly run script which might alter the + // tree, breaking our sort. However, we don't actually fire the events yet. + 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 && (a == b || 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 && (a == b || aIdx != bIdx)); + return a == b; + } + }; + + nsTArray<AccTreeMutationEvent*>& events = iter.Data(); + events.Sort(AccIdxComparator()); + } + + // 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 = firstEvent; 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); + } + } + + // Fire the show events we sorted earlier. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + nsTArray<AccTreeMutationEvent*>& events = iter.Data(); + 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 (const uint32_t reorderType : {nsIAccessibleEvent::EVENT_INNER_REORDER, + nsIAccessibleEvent::EVENT_REORDER}) { + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != reorderType) { + continue; + } + + if (event->GetAccessible()->IsDefunct()) { + // An inner reorder target may have been hidden itself and no + // longer bound to the document. + MOZ_ASSERT(reorderType == nsIAccessibleEvent::EVENT_INNER_REORDER, + "An 'outer' reorder target should not be defunct"); + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + LocalAccessible* target = event->GetAccessible(); + target->Document()->MaybeNotifyOfValueChange(target); + if (!mDocument) { + return; + } + } + } + + // Our events are in a doubly linked list. Clear the pointers to reduce + // pressure on the cycle collector. Even though clearing the previous pointers + // removes cycles, this isn't enough. The cycle collector still gets bogged + // down when there are lots of mutation events if the next pointers aren't + // cleared. Even without the cycle collector, not clearing the next pointers + // potentially results in deep recursion because releasing each event releases + // its next event. + RefPtr<AccTreeMutationEvent> event = firstEvent; + while (event) { + RefPtr<AccTreeMutationEvent> next = event->NextEvent(); + event->SetNextEvent(nullptr); + event->SetPrevEvent(nullptr); + event = next; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: private + +void NotificationController::WillRefresh(mozilla::TimeStamp aTime) { + AUTO_PROFILER_MARKER_TEXT("NotificationController::WillRefresh", A11Y, {}, + ""_ns); + Telemetry::AutoTimer<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> timer; + // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. + + AUTO_PROFILER_LABEL("NotificationController::WillRefresh", A11Y); + + // If mDocument is null, the document accessible that this notification + // controller was created for is now shut down. This means we've lost our + // ability to unregister ourselves, which is bad. (However, it also shouldn't + // be logically possible for us to get here with a null mDocument; the only + // thing that clears that pointer is our Shutdown() method, which first + // unregisters and fatally asserts if that fails). + MOZ_RELEASE_ASSERT( + mDocument, + "The document was shut down while refresh observer is attached!"); + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + // Wait until an update, we have started, or an interruptible reflow is + // finished. We also check the existance of our pres context and root pres + // context, since if we can't reach either of these the frame tree is being + // destroyed. + nsPresContext* pc = mPresShell->GetPresContext(); + if (mObservingState == eRefreshProcessing || + mObservingState == eRefreshProcessingForUpdate || + mPresShell->IsReflowInterrupted() || !pc || !pc->GetRootPresContext()) { + 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 || ipc::ProcessChild::ExpectingShutdown()) { + 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)) { + // (1) If document is not bound to parent at this point, or + // (2) the PresShell is not initialized (and it isn't about:blank), + // then the document is not ready yet (process notifications later). + if (!mDocument->IsBoundToParent() || + (!mPresShell->DidInitialize() && + !mDocument->DocumentNode()->IsInitialDocument())) { + 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(); + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + NS_ASSERTION(mContentInsertions.Count() == 0, + "Pending content insertions while initial accessible tree " + "isn't created!"); + } + + mDocument->ProcessPendingUpdates(); + + // Process rendered text change notifications. Even though we want to process + // them in the order in which they were queued, we still want to avoid + // duplicates. + nsTHashSet<nsIContent*> textHash; + for (nsIContent* textNode : mTextArray) { + if (!textHash.EnsureInserted(textNode)) { + continue; // Already processed. + } + LocalAccessible* 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!"); + + LocalAccessible* container = + mDocument->AccessibleOrTrueContainer(containerNode, true); + if (container) { + nsTArray<nsCOMPtr<nsIContent>>* list = + mContentInsertions.GetOrInsertNew(container); + list->AppendElement(textNode); + } + } + } + textHash.Clear(); + mTextArray.Clear(); + + // Process content inserted notifications to update the tree. + // Processing an insertion can indirectly run script (e.g. querying a XUL + // interface), which might result in another insertion being queued. + // We don't want to lose any queued insertions if this happens. Therefore, we + // move the current insertions into a temporary data structure and process + // them from there. Any insertions queued during processing will get handled + // in subsequent refresh driver ticks. + const auto contentInsertions = std::move(mContentInsertions); + for (const auto& entry : contentInsertions) { + mDocument->ProcessContentInserted(entry.GetKey(), entry.GetData().get()); + if (!mDocument) { + return; + } + } + + // 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 = childDoc->DocumentNode()->GetEmbedderElement(); + if (ownerContent) { + LocalAccessible* 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 (ipc::ProcessChild::ExpectingShutdown()) { + 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; + + mDocument->SendAccessiblesWillMove(); + + // Send any queued cache updates before we fire any mutation events so the + // cache is up to date when mutation events are fired. We do this after + // insertions (but not their events) so that cache updates dependent on the + // tree work correctly; e.g. line start calculation. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + CoalesceMutationEvents(); + ProcessMutationEvents(); + + // When firing mutation events, mObservingState is set to + // eRefreshProcessing. Any calls to ScheduleProcessing() that + // occur before mObservingState is reset will be dropped because we only + // schedule a tick if mObservingState == eNotObservingRefresh. + // This sometimes results in our viewport cache being out-of-date after + // processing mutation events. Call ProcessQueuedCacheUpdates again to + // ensure it is updated. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + if (mDocument) { + mDocument->ClearMutationData(); + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + ProcessEventQueue(); + + if (IPCAccessibilityActive()) { + size_t newDocCount = newChildDocs.Length(); + for (size_t i = 0; i < newDocCount; i++) { + DocAccessible* childDoc = newChildDocs[i]; + if (childDoc->IsDefunct()) { + continue; + } + + LocalAccessible* parent = childDoc->LocalParent(); + 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(WrapNotNull(ipcDoc), id); + continue; + } + + ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager()); + childDoc->SetIPCDoc(ipcDoc); + + nsCOMPtr<nsIBrowserChild> browserChild = + do_GetInterface(mDocument->DocumentNode()->GetDocShell()); + if (browserChild) { + static_cast<BrowserChild*>(browserChild.get()) + ->SendPDocAccessibleConstructor( + ipcDoc, parentIPCDoc, id, + childDoc->DocumentNode()->GetBrowsingContext()); + } + } + } + + if (!mDocument) { + // A null mDocument means we've gotten a Shutdown() call (presumably via + // some script that we triggered above), and that means we're done here. + // Note: in this case, it's important that don't modify mObservingState; + // Shutdown() will have *unregistered* us as a refresh observer, and we + // don't want to mistakenly overwrite mObservingState and fool ourselves + // into thinking we've re-registered when we really haven't! + MOZ_ASSERT(mObservingState == eNotObservingRefresh, + "We've been shutdown, which means we should've been " + "unregistered as a refresh observer"); + return; + } + mObservingState = eRefreshObserving; + + // 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() && + !mFocusEvent && mEvents.IsEmpty() && mTextArray.IsEmpty() && + 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.InsertOrUpdate(addr, RefPtr{aEvent}); +} + +AccTreeMutationEvent* NotificationController::EventMap::GetEvent( + LocalAccessible* 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: + case nsIAccessibleEvent::EVENT_INNER_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..137963f117 --- /dev/null +++ b/accessible/base/NotificationController.h @@ -0,0 +1,396 @@ +/* -*- 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 "nsClassHashtable.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIFrame.h" +#include "nsRefreshObservers.h" +#include "nsTHashSet.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)(std::get<Indices>(mArgs)...); + } + + Class* mInstance; + Callback mCallback; + std::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(); + } + } + + /** + * 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"); + + mTextArray.AppendElement(aTextNode); + + ScheduleProcessing(); + } + + /** + * Pend accessible tree update for content insertion. + */ + void ScheduleContentInsertion(LocalAccessible* aContainer, + nsTArray<nsCOMPtr<nsIContent>>& aInsertions); + + /** + * Pend an accessible subtree relocation. + */ + void ScheduleRelocation(LocalAccessible* 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; + + private: + /** + * Remove a specific hide event if it should not be propagated. + */ + void CoalesceHideEvent(AccHideEvent* aHideEvent); + + /** + * 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<LocalAccessible>, + 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. + * When there are a lot of nearby text insertions (e.g. during a reflow), it + * is much more performant to process them in order because we then benefit + * from the layout line cursor. Therefore, we use an array here. + */ + nsTArray<nsCOMPtr<nsIContent>> mTextArray; + + /** + * 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<LocalAccessible>> mRelocations; + + /** + * 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(LocalAccessible* 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..146d9207cf --- /dev/null +++ b/accessible/base/Pivot.cpp @@ -0,0 +1,331 @@ +/* -*- 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 "LocalAccessible.h" +#include "RemoteAccessible.h" +#include "nsAccUtils.h" +#include "nsIAccessiblePivot.h" + +#include "mozilla/a11y/Accessible.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Pivot +//////////////////////////////////////////////////////////////////////////////// + +Pivot::Pivot(Accessible* aRoot) : mRoot(aRoot) { MOZ_COUNT_CTOR(Pivot); } + +Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot); } + +Accessible* Pivot::AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule, + uint16_t* aFilterResult) { + Accessible* matched = aAnchor; + *aFilterResult = aRule.Match(aAnchor); + + if (aAnchor && aAnchor != mRoot) { + for (Accessible* temp = aAnchor->Parent(); temp && temp != mRoot; + temp = temp->Parent()) { + uint16_t filtered = aRule.Match(temp); + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + *aFilterResult = filtered; + matched = temp; + } + } + } + + return matched; +} + +Accessible* Pivot::SearchBackward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent) { + // Initial position could be unset, in that case return null. + if (!aAnchor) { + return nullptr; + } + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + + Accessible* acc = AdjustStartPosition(aAnchor, aRule, &filtered); + + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return acc; + } + + while (acc && acc != mRoot) { + Accessible* parent = acc->Parent(); +#if defined(ANDROID) + MOZ_ASSERT( + acc->IsLocal() || (acc->IsRemote() && parent->IsRemote()), + "Pivot::SearchBackward climbed out of remote subtree in Android!"); +#endif + int32_t idxInParent = acc->IndexInParent(); + while (idxInParent > 0 && parent) { + acc = parent->ChildAt(--idxInParent); + if (!acc) { + continue; + } + + filtered = aRule.Match(acc); + + Accessible* lastChild = acc->LastChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + lastChild) { + parent = acc; + acc = lastChild; + idxInParent = acc->IndexInParent(); + filtered = aRule.Match(acc); + lastChild = acc->LastChild(); + } + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + acc = parent; + if (!acc) { + break; + } + + filtered = aRule.Match(acc); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + return nullptr; +} + +Accessible* Pivot::SearchForward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent) { + // Initial position could be not set, in that case begin search from root. + Accessible* acc = aAnchor ? aAnchor : mRoot; + + uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE; + acc = AdjustStartPosition(acc, aRule, &filtered); + if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) { + return acc; + } + + while (acc) { + Accessible* firstChild = acc->FirstChild(); + while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) && + firstChild) { + acc = firstChild; + filtered = aRule.Match(acc); + + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + firstChild = acc->FirstChild(); + } + + Accessible* sibling = nullptr; + Accessible* temp = acc; + do { + if (temp == mRoot) { + break; + } + + sibling = temp->NextSibling(); + + if (sibling) { + break; + } + temp = temp->Parent(); +#if defined(ANDROID) + MOZ_ASSERT( + acc->IsLocal() || (acc->IsRemote() && temp->IsRemote()), + "Pivot::SearchForward climbed out of remote subtree in Android!"); +#endif + + } while (temp); + + if (!sibling) { + break; + } + + acc = sibling; + filtered = aRule.Match(acc); + if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) { + return acc; + } + } + + return nullptr; +} + +Accessible* Pivot::Next(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchForward(aAnchor, aRule, aIncludeStart); +} + +Accessible* Pivot::Prev(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart) { + return SearchBackward(aAnchor, aRule, aIncludeStart); +} + +Accessible* Pivot::First(PivotRule& aRule) { + return SearchForward(mRoot, aRule, true); +} + +Accessible* Pivot::Last(PivotRule& aRule) { + Accessible* lastAcc = mRoot; + + // First go to the last accessible in pre-order + while (lastAcc && lastAcc->HasChildren()) { + lastAcc = lastAcc->LastChild(); + } + + // Search backwards from last accessible and find the last occurrence in the + // doc + return SearchBackward(lastAcc, aRule, true); +} + +Accessible* Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) { + Accessible* match = nullptr; + Accessible* child = + mRoot ? mRoot->ChildAtPoint(aX, aY, + Accessible::EWhichChildAtPoint::DeepestChild) + : nullptr; + while (child && (mRoot != child)) { + uint16_t filtered = aRule.Match(child); + + // Ignore any matching nodes that were below this one + if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) { + match = nullptr; + } + + // Match if no node below this is a match + if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && !match) { + LayoutDeviceIntRect childRect = child->IsLocal() + ? child->AsLocal()->Bounds() + : child->AsRemote()->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, + Accessible* aDirectDescendantsFrom) + : mRole(aRole), mDirectDescendantsFrom(aDirectDescendantsFrom) {} + +uint16_t PivotRoleRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (mDirectDescendantsFrom && (aAcc != 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 aAcc + // 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 (aAcc && aAcc->Role() == mRole) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + + return result; +} + +// State Rule + +PivotStateRule::PivotStateRule(uint64_t aState) : mState(aState) {} + +uint16_t PivotStateRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + + if (nsAccUtils::MustPrune(aAcc)) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (aAcc && (aAcc->State() & mState)) { + result = nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + return result; +} + +// LocalAccInSameDocRule + +uint16_t LocalAccInSameDocRule::Match(Accessible* aAcc) { + LocalAccessible* acc = aAcc ? aAcc->AsLocal() : nullptr; + if (!acc) { + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + if (acc->IsOuterDoc()) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + return nsIAccessibleTraversalRule::FILTER_MATCH; +} + +// Radio Button Name Rule + +PivotRadioNameRule::PivotRadioNameRule(const nsString& aName) : mName(aName) {} + +uint16_t PivotRadioNameRule::Match(Accessible* aAcc) { + uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; + RemoteAccessible* remote = aAcc->AsRemote(); + if (!remote) { + // We need the cache to be able to fetch the name attribute below. + return result; + } + + if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) { + result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (remote->IsHTMLRadioButton()) { + nsString currName = remote->GetCachedHTMLNameAttribute(); + if (!currName.IsEmpty() && mName.Equals(currName)) { + result |= nsIAccessibleTraversalRule::FILTER_MATCH; + } + } + + return result; +} + +// MustPruneSameDocRule + +uint16_t MustPruneSameDocRule::Match(Accessible* aAcc) { + if (!aAcc) { + return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) { + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + return nsIAccessibleTraversalRule::FILTER_MATCH; +} diff --git a/accessible/base/Pivot.h b/accessible/base/Pivot.h new file mode 100644 index 0000000000..bd2814f48e --- /dev/null +++ b/accessible/base/Pivot.h @@ -0,0 +1,141 @@ +/* -*- 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 "mozilla/a11y/Role.h" +#include "mozilla/dom/ChildIterator.h" + +namespace mozilla { +namespace a11y { + +class DocAccessible; +class Accessible; + +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(Accessible* aAcc) = 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(Accessible* 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. + Accessible* Next(Accessible* 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. + Accessible* Prev(Accessible* aAnchor, PivotRule& aRule, + bool aIncludeStart = false); + + // Return the first accessible within the root that matches the pivot rule. + Accessible* First(PivotRule& aRule); + + // Return the last accessible within the root that matches the pivot rule. + Accessible* Last(PivotRule& aRule); + + // Return the accessible at the given screen coordinate if it matches the + // pivot rule. + Accessible* AtPoint(int32_t aX, int32_t aY, PivotRule& aRule); + + private: + Accessible* AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule, + uint16_t* aFilterResult); + + // Search in preorder for the first accessible to match the rule. + Accessible* SearchForward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + // Reverse search in preorder for the first accessible to match the rule. + Accessible* SearchBackward(Accessible* aAnchor, PivotRule& aRule, + bool aSearchCurrent); + + Accessible* 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, Accessible* aDirectDescendantsFrom); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + role mRole; + Accessible* mDirectDescendantsFrom; +}; + +/** + * This rule matches accessibles with a given state. + */ +class PivotStateRule : public PivotRule { + public: + explicit PivotStateRule(uint64_t aState); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + uint64_t mState; +}; + +/** + * This rule matches any local LocalAccessible (i.e. not RemoteAccessible) in + * the same document as the anchor. That is, it includes any descendant + * OuterDocAccessible, but not its descendants. + */ +class LocalAccInSameDocRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override; +}; + +/** + * This rule matches remote radio button accessibles with the given name + * attribute. It assumes the cache is enabled. + */ +class PivotRadioNameRule : public PivotRule { + public: + explicit PivotRadioNameRule(const nsString& aName); + + virtual uint16_t Match(Accessible* aAcc) override; + + protected: + const nsString& mName; +}; + +/** + * This rule doesn't search iframes. Subtrees that should be + * pruned by way of nsAccUtils::MustPrune are also not searched. + */ + +class MustPruneSameDocRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override; +}; + +} // 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..23f214246f --- /dev/null +++ b/accessible/base/Platform.h @@ -0,0 +1,136 @@ +/* -*- 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" +#include "Units.h" + +#if defined(ANDROID) +# include "nsTArray.h" +# include "nsRect.h" +#endif + +#ifdef MOZ_WIDGET_COCOA +# include "mozilla/a11y/Role.h" +# include "nsTArray.h" +#endif + +namespace mozilla { +namespace a11y { + +class Accessible; +class RemoteAccessible; + +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) +/* + * 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 RemoteAccessible is created, so the platform may setup a + * wrapper for it, or take other action. + */ +void ProxyCreated(RemoteAccessible* aProxy); + +/** + * Called just before a RemoteAccessible is destroyed so its wrapper can be + * disposed of and other action taken. + */ +void ProxyDestroyed(RemoteAccessible*); + +/** + * Called when an event is fired on an Accessible so that platforms may fire + * events if appropriate. + */ +void PlatformEvent(Accessible* aTarget, uint32_t aEventType); +void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState, + bool aEnabled); + +void PlatformFocusEvent(Accessible* aTarget, + const LayoutDeviceIntRect& aCaretRect); +void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, int32_t aGranularity, + const LayoutDeviceIntRect& aCaretRect, + bool aFromUser); +void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aIsInsert, + bool aFromUser); +void PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent, + bool aInsert, bool aFromUser); +void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget, + uint32_t aType); + +#if defined(ANDROID) +void PlatformScrollingEvent(Accessible* aTarget, uint32_t aEventType, + uint32_t aScrollX, uint32_t aScrollY, + uint32_t aMaxScrollX, uint32_t aMaxScrollY); + +void PlatformAnnouncementEvent(Accessible* aTarget, + const nsAString& aAnnouncement, + uint16_t aPriority); + +bool LocalizeString(const nsAString& aToken, nsAString& aLocalized); +#endif + +#ifdef MOZ_WIDGET_COCOA +class TextRange; +void PlatformTextSelectionChangeEvent(Accessible* aTarget, + const nsTArray<TextRange>& aSelection); + +void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole, + uint8_t aRoleMapEntryIndex); +#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..24fceeab02 --- /dev/null +++ b/accessible/base/Relation.h @@ -0,0 +1,105 @@ +/* -*- 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; + } + + inline LocalAccessible* LocalNext() { + Accessible* next = Next(); + return next ? next->AsLocal() : nullptr; + } + + 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/RelationTypeGen.py b/accessible/base/RelationTypeGen.py new file mode 100644 index 0000000000..8d9a0f91bf --- /dev/null +++ b/accessible/base/RelationTypeGen.py @@ -0,0 +1,41 @@ +#!/usr/bin/env 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/. + +import re + + +def generate(relH, relIdl): + input = open(relIdl, "rt").read() + relations = re.findall( + r"const unsigned long RELATION_([A-Z_]+) = ([x0-9a-f]+);", input + ) + + relH.write( + "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n" + "/* Relations are defined in accessible/interfaces/nsIAccessibleRelation.idl */\n\n" + "#ifndef mozilla_a11y_relationtype_h_\n" + "#define mozilla_a11y_relationtype_h_\n\n" + "namespace mozilla {\n" + "namespace a11y {\n\n" + "enum class RelationType {\n" + ) + for name, num in relations: + relH.write(f" {name} = {num},\n") + lastName = relations[-1][0] + relH.write( + f" LAST = {lastName}\n" + "};\n\n" + "} // namespace a11y\n" + "} // namespace mozilla\n\n" + "#endif\n" + ) + + +# For debugging +if __name__ == "__main__": + import sys + + generate(sys.stdout, "accessible/interfaces/nsIAccessibleRelation.idl") diff --git a/accessible/base/RelationTypeMap.h b/accessible/base/RelationTypeMap.h new file mode 100644 index 0000000000..e819682368 --- /dev/null +++ b/accessible/base/RelationTypeMap.h @@ -0,0 +1,90 @@ +/* -*- 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) + +RELATIONTYPE(LINKS_TO, "links to", ATK_RELATION_NULL, NAVRELATION_LINKS_TO, + IA2_RELATION_NULL) diff --git a/accessible/base/RoleHGen.py b/accessible/base/RoleHGen.py new file mode 100644 index 0000000000..374d2f66a9 --- /dev/null +++ b/accessible/base/RoleHGen.py @@ -0,0 +1,42 @@ +#!/usr/bin/env 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/. + +import re + + +def generate(roleH, roleIdl): + input = open(roleIdl, "rt").read() + roles = re.findall(r"const unsigned long ROLE_([A-Z_]+) = (\d+);", input) + + roleH.write( + "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n" + "/* Roles are defined in accessible/interfaces/nsIAccessibleRole.idl */\n\n" + "#ifndef _role_h_\n" + "#define _role_h_\n\n" + "namespace mozilla {\n" + "namespace a11y {\n" + "namespace roles {\n\n" + "enum Role {\n" + ) + for name, num in roles: + roleH.write(f" {name} = {num},\n") + lastName = roles[-1][0] + roleH.write( + f" LAST_ROLE = {lastName}\n" + "};\n\n" + "} // namespace roles\n\n" + "typedef enum mozilla::a11y::roles::Role role;\n\n" + "} // namespace a11y\n" + "} // namespace mozilla\n\n" + "#endif\n" + ) + + +# For debugging +if __name__ == "__main__": + import sys + + generate(sys.stdout, "accessible/interfaces/nsIAccessibleRole.idl") diff --git a/accessible/base/RoleMap.h b/accessible/base/RoleMap.h new file mode 100644 index 0000000000..ce82000188 --- /dev/null +++ b/accessible/base/RoleMap.h @@ -0,0 +1,1546 @@ +/* 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, ariaRole, atkRole, macRole, macSubrole, msaaRole, ia2Role, nameRule) + */ + +ROLE(NOTHING, + "nothing", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CLIENT, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(MENUBAR, + "menubar", + nsGkAtoms::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", + nsGkAtoms::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(ALERT, + "alert", + nsGkAtoms::alert, + ATK_ROLE_ALERT, + NSAccessibilityGroupRole, + @"AXApplicationAlert", + ROLE_SYSTEM_ALERT, + ROLE_SYSTEM_ALERT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(INTERNAL_FRAME, + "internal frame", + nullptr, + ATK_ROLE_INTERNAL_FRAME, + NSAccessibilityScrollAreaRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_INTERNAL_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MENUPOPUP, + "menupopup", + nsGkAtoms::menu, + ATK_ROLE_MENU, + NSAccessibilityMenuRole, //The parent of menuitems. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUPOPUP, + ROLE_SYSTEM_MENUPOPUP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MENUITEM, + "menuitem", + nsGkAtoms::menuitem, + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(TOOLTIP, + "tooltip", + nsGkAtoms::tooltip, + ATK_ROLE_TOOL_TIP, + NSAccessibilityGroupRole, + @"AXUserInterfaceTooltip", + ROLE_SYSTEM_TOOLTIP, + ROLE_SYSTEM_TOOLTIP, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(APPLICATION, + "application", + nsGkAtoms::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", + nsGkAtoms::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", + nullptr, + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(DIALOG, + "dialog", + nsGkAtoms::dialog, + ATK_ROLE_DIALOG, + NSAccessibilityGroupRole, //There's a dialog subrole. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIALOG, + ROLE_SYSTEM_DIALOG, + java::SessionAccessibility::CLASSNAME_DIALOG, + eNoNameRule) + +ROLE(GROUPING, + "grouping", + nsGkAtoms::group, + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(SEPARATOR, + "separator", + nsGkAtoms::separator_, + ATK_ROLE_SEPARATOR, + NSAccessibilitySplitterRole, + @"AXContentSeparator", + ROLE_SYSTEM_SEPARATOR, + ROLE_SYSTEM_SEPARATOR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TOOLBAR, + "toolbar", + nsGkAtoms::toolbar, + ATK_ROLE_TOOL_BAR, + NSAccessibilityToolbarRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TOOLBAR, + ROLE_SYSTEM_TOOLBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(STATUSBAR, + "statusbar", + nsGkAtoms::status, + ATK_ROLE_STATUSBAR, + NSAccessibilityGroupRole, + @"AXApplicationStatus", + ROLE_SYSTEM_STATUSBAR, + ROLE_SYSTEM_STATUSBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TABLE, + "table", + nsGkAtoms::table, + ATK_ROLE_TABLE, + NSAccessibilityTableRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TABLE, + ROLE_SYSTEM_TABLE, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(COLUMNHEADER, + "columnheader", + nsGkAtoms::columnheader, + ATK_ROLE_COLUMN_HEADER, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COLUMNHEADER, + ROLE_SYSTEM_COLUMNHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(ROWHEADER, + "rowheader", + nsGkAtoms::rowheader, + ATK_ROLE_ROW_HEADER, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_ROWHEADER, + ROLE_SYSTEM_ROWHEADER, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(ROW, + "row", + nsGkAtoms::row, + ATK_ROLE_TABLE_ROW, + NSAccessibilityRowRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_ROW, + ROLE_SYSTEM_ROW, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(CELL, + "cell", + nsGkAtoms::cell, + ATK_ROLE_TABLE_CELL, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LINK, + "link", + nsGkAtoms::link, + ATK_ROLE_LINK, + NSAccessibilityLinkRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LINK, + ROLE_SYSTEM_LINK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(LIST, + "list", + nsGkAtoms::list_, + ATK_ROLE_LIST, + NSAccessibilityListRole, + NSAccessibilityContentListSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LISTITEM, + "listitem", + nsGkAtoms::listitem, + ATK_ROLE_LIST_ITEM, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(OUTLINE, + "outline", + nsGkAtoms::tree, + ATK_ROLE_TREE, + NSAccessibilityOutlineRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(OUTLINEITEM, + "outlineitem", + nsGkAtoms::treeitem, + ATK_ROLE_TREE_ITEM, + NSAccessibilityRowRole, + NSAccessibilityOutlineRowSubrole, + ROLE_SYSTEM_OUTLINEITEM, + ROLE_SYSTEM_OUTLINEITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(PAGETAB, + "pagetab", + nsGkAtoms::tab, + 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", + nsGkAtoms::tabpanel, + ATK_ROLE_SCROLL_PANE, + NSAccessibilityGroupRole, + @"AXTabPanel", + ROLE_SYSTEM_PROPERTYPAGE, + ROLE_SYSTEM_PROPERTYPAGE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GRAPHIC, + "graphic", + nsGkAtoms::image, + ATK_ROLE_IMAGE, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(STATICTEXT, + "statictext", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TEXT_LEAF, + "text leaf", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PUSHBUTTON, + "pushbutton", + nsGkAtoms::button, + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(CHECKBUTTON, + "checkbutton", + nsGkAtoms::checkbox, + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(RADIOBUTTON, + "radiobutton", + nsGkAtoms::radio, + 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", + nsGkAtoms::combobox, + ATK_ROLE_COMBO_BOX, + NSAccessibilityPopUpButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNameFromValueRule) + +ROLE(PROGRESSBAR, + "progressbar", + nsGkAtoms::progressbar, + ATK_ROLE_PROGRESS_BAR, + NSAccessibilityProgressIndicatorRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PROGRESSBAR, + ROLE_SYSTEM_PROGRESSBAR, + java::SessionAccessibility::CLASSNAME_PROGRESSBAR, + eNameFromValueRule) + +ROLE(SLIDER, + "slider", + nsGkAtoms::slider, + ATK_ROLE_SLIDER, + NSAccessibilitySliderRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SLIDER, + ROLE_SYSTEM_SLIDER, + java::SessionAccessibility::CLASSNAME_SEEKBAR, + eNameFromValueRule) + +ROLE(SPINBUTTON, + "spinbutton", + nsGkAtoms::spinbutton, + ATK_ROLE_SPIN_BUTTON, + NSAccessibilityIncrementorRole, //Subroles: Increment/Decrement. + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_SPINBUTTON, + ROLE_SYSTEM_SPINBUTTON, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(DIAGRAM, + "diagram", + nsGkAtoms::graphicsDocument, + ATK_ROLE_IMAGE, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_DIAGRAM, + ROLE_SYSTEM_DIAGRAM, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(ANIMATION, + "animation", + nsGkAtoms::marquee, + ATK_ROLE_ANIMATION, + NSAccessibilityUnknownRole, + @"AXApplicationMarquee", + ROLE_SYSTEM_ANIMATION, + ROLE_SYSTEM_ANIMATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(BUTTONDROPDOWN, + "buttondropdown", + nullptr, + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityPopUpButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BUTTONDROPDOWN, + ROLE_SYSTEM_BUTTONDROPDOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(BUTTONMENU, + "buttonmenu", + nsGkAtoms::button, + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityMenuButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_BUTTONMENU, + ROLE_SYSTEM_BUTTONMENU, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNameFromSubtreeRule) + +ROLE(WHITESPACE, + "whitespace", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_WHITESPACE, + ROLE_SYSTEM_WHITESPACE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(PAGETABLIST, + "pagetablist", + nsGkAtoms::tablist, + ATK_ROLE_PAGE_TAB_LIST, + NSAccessibilityTabGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PAGETABLIST, + ROLE_SYSTEM_PAGETABLIST, + java::SessionAccessibility::CLASSNAME_TABWIDGET, + eNoNameRule) + +ROLE(CANVAS, + "canvas", + nullptr, + ATK_ROLE_CANVAS, + NSAccessibilityImageRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRAPHIC, + IA2_ROLE_CANVAS, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(CHECK_MENU_ITEM, + "check menu item", + nsGkAtoms::menuitemcheckbox, + ATK_ROLE_CHECK_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_CHECK_MENU_ITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(DATE_EDITOR, + "date editor", + nullptr, + ATK_ROLE_DATE_EDITOR, + @"AXGroup", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_DATE_EDITOR, + java::SessionAccessibility::CLASSNAME_SPINNER, + eNoNameRule) + +ROLE(CHROME_WINDOW, + "chrome window", + nullptr, + ATK_ROLE_FRAME, + NSAccessibilityGroupRole, //Contains the main Firefox UI + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_APPLICATION, + IA2_ROLE_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(LABEL, + "label", + nullptr, + ATK_ROLE_LABEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + IA2_ROLE_LABEL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(PASSWORD_TEXT, + "password text", + nullptr, + ATK_ROLE_PASSWORD_TEXT, + NSAccessibilityTextFieldRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNoNameRule) + +ROLE(RADIO_MENU_ITEM, + "radio menu item", + nsGkAtoms::menuitemradio, + ATK_ROLE_RADIO_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + IA2_ROLE_RADIO_MENU_ITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(TEXT_CONTAINER, + "text container", + nsGkAtoms::generic, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TOGGLE_BUTTON, + "toggle button", + nsGkAtoms::button, + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + NSAccessibilityToggleSubrole, + ROLE_SYSTEM_PUSHBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + java::SessionAccessibility::CLASSNAME_TOGGLEBUTTON, + eNameFromSubtreeRule) + +ROLE(TREE_TABLE, + "tree table", + nsGkAtoms::treegrid, + ATK_ROLE_TREE_TABLE, + NSAccessibilityTableRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_OUTLINE, + ROLE_SYSTEM_OUTLINE, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNoNameRule) + +ROLE(PARAGRAPH, + "paragraph", + nsGkAtoms::paragraph, + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_PARAGRAPH, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(ENTRY, + "entry", + nsGkAtoms::textbox, + ATK_ROLE_ENTRY, + NSAccessibilityTextFieldRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_TEXT, + ROLE_SYSTEM_TEXT, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(CAPTION, + "caption", + nsGkAtoms::caption, + ATK_ROLE_CAPTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_CAPTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(NON_NATIVE_DOCUMENT, + "non-native document", + nsGkAtoms::document, + ATK_ROLE_DOCUMENT_FRAME, + NSAccessibilityGroupRole, + @"AXDocument", + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(HEADING, + "heading", + nsGkAtoms::heading, + ATK_ROLE_HEADING, + @"AXHeading", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_HEADING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(SECTION, + "section", + nsGkAtoms::generic, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_SECTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(FORM, + "form", + nsGkAtoms::form, + ATK_ROLE_FORM, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_FORM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(APP_ROOT, + "app root", + nullptr, + 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", + nsGkAtoms::menuitem, + ATK_ROLE_MENU, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_MENUITEM, + ROLE_SYSTEM_MENUITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(COMBOBOX_LIST, + "combobox list", + nsGkAtoms::listbox, + ATK_ROLE_MENU, + NSAccessibilityMenuRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(COMBOBOX_OPTION, + "combobox option", + nsGkAtoms::option, + ATK_ROLE_MENU_ITEM, + NSAccessibilityMenuItemRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_MENUITEM, + eNameFromSubtreeRule) + +ROLE(IMAGE_MAP, + "image map", + nsGkAtoms::img, + ATK_ROLE_IMAGE, + @"AXImageMap", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GRAPHIC, + ROLE_SYSTEM_GRAPHIC, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNoNameRule) + +ROLE(OPTION, + "listbox option", + nsGkAtoms::option, + ATK_ROLE_LIST_ITEM, + NSAccessibilityStaticTextRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(RICH_OPTION, + "listbox rich option", + nullptr, + ATK_ROLE_LIST_ITEM, + NSAccessibilityRowRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(LISTBOX, + "listbox", + nsGkAtoms::listbox, + ATK_ROLE_LIST_BOX, + NSAccessibilityListRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNoNameRule) + +ROLE(FLAT_EQUATION, + "flat equation", + nsGkAtoms::math, + ATK_ROLE_UNKNOWN, + NSAccessibilityUnknownRole, + @"AXDocumentMath", + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(GRID_CELL, + "gridcell", + nsGkAtoms::gridcell, + ATK_ROLE_TABLE_CELL, + NSAccessibilityCellRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CELL, + ROLE_SYSTEM_CELL, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(NOTE, + "note", + nsGkAtoms::note_, + ATK_ROLE_COMMENT, + NSAccessibilityGroupRole, + @"AXDocumentNote", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_NOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(FIGURE, + "figure", + nsGkAtoms::figure, + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CHECK_RICH_OPTION, + "check rich option", + nullptr, + ATK_ROLE_CHECK_BOX, + NSAccessibilityCheckBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_CHECKBUTTON, + ROLE_SYSTEM_CHECKBUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(DEFINITION_LIST, + "definitionlist", + nullptr, + ATK_ROLE_LIST, + NSAccessibilityListRole, + @"AXDescriptionList", + ROLE_SYSTEM_LIST, + ROLE_SYSTEM_LIST, + java::SessionAccessibility::CLASSNAME_LISTVIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TERM, + "term", + nsGkAtoms::term, + ATK_ROLE_DESCRIPTION_TERM, + NSAccessibilityGroupRole, + @"AXTerm", + ROLE_SYSTEM_LISTITEM, + ROLE_SYSTEM_LISTITEM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(DEFINITION, + "definition", + nsGkAtoms::definition, + ATK_ROLE_PARAGRAPH, + NSAccessibilityGroupRole, + @"AXDescription", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_PARAGRAPH, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(KEY, + "key", + nullptr, + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(SWITCH, + "switch", + nsGkAtoms::svgSwitch, + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + NSAccessibilitySwitchSubrole, + ROLE_SYSTEM_CHECKBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + java::SessionAccessibility::CLASSNAME_CHECKBOX, + eNameFromSubtreeRule) + +ROLE(MATHML_MATH, + "math", + nsGkAtoms::math, + ATK_ROLE_MATH, + NSAccessibilityGroupRole, + @"AXDocumentMath", + ROLE_SYSTEM_EQUATION, + ROLE_SYSTEM_EQUATION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_IDENTIFIER, + "mathml identifier", + nullptr, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathIdentifier", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_NUMBER, + "mathml number", + nullptr, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathNumber", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_OPERATOR, + "mathml operator", + nullptr, + 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. + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_TEXT, + "mathml text", + nullptr, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXMathRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_STRING_LITERAL, + "mathml string literal", + nullptr, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeRule) + +ROLE(MATHML_GLYPH, + "mathml glyph", + nullptr, + ATK_ROLE_IMAGE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_IMAGE, + eNameFromSubtreeRule) + +ROLE(MATHML_ROW, + "mathml row", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_FRACTION, + "mathml fraction", + nullptr, + ATK_ROLE_MATH_FRACTION, + NSAccessibilityGroupRole, + @"AXMathFraction", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SQUARE_ROOT, + "mathml square root", + nullptr, + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + @"AXMathSquareRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ROOT, + "mathml root", + nullptr, + ATK_ROLE_MATH_ROOT, + NSAccessibilityGroupRole, + @"AXMathRoot", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ENCLOSED, + "mathml enclosed", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STYLE, + "mathml style", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUB, + "mathml sub", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUP, + "mathml sup", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_SUB_SUP, + "mathml sub sup", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathSubscriptSuperscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_UNDER, + "mathml under", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_OVER, + "mathml over", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_UNDER_OVER, + "mathml under over", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathUnderOver", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_MULTISCRIPTS, + "mathml multiscripts", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathMultiscript", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_TABLE, + "mathml table", + nullptr, + ATK_ROLE_TABLE, + NSAccessibilityGroupRole, + @"AXMathTable", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_GRIDVIEW, + eNoNameRule) + +ROLE(MATHML_LABELED_ROW, + "mathml labeled row", + nullptr, + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_TABLE_ROW, + "mathml table row", + nullptr, + ATK_ROLE_TABLE_ROW, + NSAccessibilityGroupRole, + @"AXMathTableRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_CELL, + "mathml cell", + nullptr, + ATK_ROLE_TABLE_CELL, + NSAccessibilityGroupRole, + @"AXMathTableCell", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ACTION, + "mathml action", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_ERROR, + "mathml error", + nullptr, + ATK_ROLE_SECTION, + NSAccessibilityGroupRole, + @"AXMathRow", + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK, + "mathml stack", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_LONG_DIVISION, + "mathml long division", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_GROUP, + "mathml stack group", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_ROW, + "mathml stack row", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_CARRIES, + "mathml stack carries", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_CARRY, + "mathml stack carry", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MATHML_STACK_LINE, + "mathml stack line", + nullptr, + ATK_ROLE_UNKNOWN, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + 0, + IA2_ROLE_UNKNOWN, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(RADIO_GROUP, + "grouping", + nsGkAtoms::radiogroup, + ATK_ROLE_PANEL, + NSAccessibilityRadioGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(TEXT, + "text", + nsGkAtoms::generic, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(DETAILS, + "details", + nsGkAtoms::group, + ATK_ROLE_PANEL, + NSAccessibilityGroupRole, + @"AXDetails", + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(SUMMARY, + "summary", + nullptr, + ATK_ROLE_PUSH_BUTTON, + NSAccessibilityButtonRole, + @"AXSummary", + ROLE_SYSTEM_PUSHBUTTON, + ROLE_SYSTEM_PUSHBUTTON, + java::SessionAccessibility::CLASSNAME_BUTTON, + eNameFromSubtreeRule) + +ROLE(LANDMARK, + "landmark", + nullptr, + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_LANDMARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(NAVIGATION, + "navigation", + nullptr, + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkNavigation", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_LANDMARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(FOOTNOTE, + "footnote", + nullptr, + ATK_ROLE_FOOTNOTE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_FOOTNOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(ARTICLE, + "article", + nsGkAtoms::article, + ATK_ROLE_ARTICLE, + NSAccessibilityGroupRole, + @"AXDocumentArticle", + ROLE_SYSTEM_DOCUMENT, + ROLE_SYSTEM_DOCUMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(REGION, + "region", + nsGkAtoms::region, + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkRegion", + ROLE_SYSTEM_GROUPING, + 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", + nsGkAtoms::combobox, + ATK_ROLE_COMBO_BOX, + NSAccessibilityComboBoxRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_COMBOBOX, + ROLE_SYSTEM_COMBOBOX, + java::SessionAccessibility::CLASSNAME_EDITTEXT, + eNameFromValueRule) + +ROLE(BLOCKQUOTE, + "blockquote", + nsGkAtoms::blockquote, + ATK_ROLE_BLOCK_QUOTE, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_BLOCK_QUOTE, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CONTENT_DELETION, + "content deletion", + nsGkAtoms::deletion, + ATK_ROLE_CONTENT_DELETION, + NSAccessibilityGroupRole, + @"AXDeleteStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_CONTENT_DELETION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(CONTENT_INSERTION, + "content insertion", + nsGkAtoms::insertion, + ATK_ROLE_CONTENT_INSERTION, + NSAccessibilityGroupRole, + @"AXInsertStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_CONTENT_INSERTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(FORM_LANDMARK, + "form", + nsGkAtoms::form, + ATK_ROLE_LANDMARK, + NSAccessibilityGroupRole, + @"AXLandmarkForm", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_FORM, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(MARK, + "mark", + nsGkAtoms::mark, + ATK_ROLE_MARK, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_MARK, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(SUGGESTION, + "suggestion", + nsGkAtoms::suggestion, + ATK_ROLE_SUGGESTION, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_SUGGESTION, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(COMMENT, + "comment", + nsGkAtoms::comment, + ATK_ROLE_COMMENT, + NSAccessibilityGroupRole, + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + IA2_ROLE_COMMENT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(CODE, + "code", + nsGkAtoms::code, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXCodeStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TIME_EDITOR, + "time editor", + nullptr, + ATK_ROLE_PANEL, + @"AXTimeField", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(LISTITEM_MARKER, + "list item marker", + nullptr, + ATK_ROLE_UNKNOWN, + @"AXListMarker", + NSAccessibilityUnknownSubrole, + ROLE_SYSTEM_STATICTEXT, + ROLE_SYSTEM_STATICTEXT, + java::SessionAccessibility::CLASSNAME_VIEW, + eNoNameRule) + +ROLE(METER, + "meter", + nsGkAtoms::meter, + ATK_ROLE_LEVEL_BAR, + NSAccessibilityLevelIndicatorRole, + @"AXMeter", + ROLE_SYSTEM_PROGRESSBAR, + ROLE_SYSTEM_PROGRESSBAR, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromValueRule) + +ROLE(SUBSCRIPT, + "subscript", + nsGkAtoms::subscript, + ATK_ROLE_SUBSCRIPT, + NSAccessibilityGroupRole, + @"AXSubscriptStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(SUPERSCRIPT, + "superscript", + nsGkAtoms::superscript, + ATK_ROLE_SUPERSCRIPT, + NSAccessibilityGroupRole, + @"AXSuperscriptStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(EMPHASIS, + "emphasis", + nsGkAtoms::emphasis, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXEmphasisStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(STRONG, + "strong", + nsGkAtoms::strong, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXStrongStyleGroup", + ROLE_SYSTEM_GROUPING, + IA2_ROLE_TEXT_FRAME, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +ROLE(TIME, + "time", + nsGkAtoms::time, + ATK_ROLE_STATIC, + NSAccessibilityGroupRole, + @"AXTimeGroup", + ROLE_SYSTEM_GROUPING, + ROLE_SYSTEM_GROUPING, + java::SessionAccessibility::CLASSNAME_VIEW, + eNameFromSubtreeIfReqRule) + +// clang-format on diff --git a/accessible/base/SelectionManager.cpp b/accessible/base/SelectionManager.cpp new file mode 100644 index 0000000000..97721bb439 --- /dev/null +++ b/accessible/base/SelectionManager.cpp @@ -0,0 +1,246 @@ +/* -*- 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 "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "nsEventShell.h" +#include "nsFrameSelection.h" +#include "TextLeafRange.h" + +#include "mozilla/PresShell.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, int32_t aGranularity) + : mSel(aSel), mReason(aReason), mGranularity(aGranularity) {} + + RefPtr<Selection> mSel; + int16_t mReason; + int32_t mGranularity; + + 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); + + if (mCurrCtrlNormalSel) { + if (mCurrCtrlNormalSel->GetPresShell() == aPresShell) { + // Remove 'this' registered as selection listener for the normal selection + // if we are removing listeners for its PresShell. + mCurrCtrlNormalSel->RemoveSelectionListener(this); + mCurrCtrlNormalSel = nullptr; + } + } + + if (mCurrCtrlSpellSel) { + if (mCurrCtrlSpellSel->GetPresShell() == aPresShell) { + // Remove 'this' registered as selection listener for the spellcheck + // selection if we are removing listeners for its PresShell. + mCurrCtrlSpellSel->RemoveSelectionListener(this); + mCurrCtrlSpellSel = nullptr; + } + } +} + +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. Also, it must not + // be a collapsed selection on the container of a focused text field, since + // the text field has an independent selection and will thus fire its own + // selection events. + AccTextSelChangeEvent* event = downcast_accEvent(aEvent); + if (!event->IsCaretMoveOnly() && + !(event->mSel->IsCollapsed() && event->mSel != mCurrCtrlNormalSel && + FocusMgr() && FocusMgr()->FocusedLocalAccessible() && + FocusMgr()->FocusedLocalAccessible()->IsTextField())) { + 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(), + caretCntr->IsCaretAtEndOfLine(), + event->GetGranularity(), aEvent->FromUserInput()); + nsEventShell::FireEvent(caretMoveEvent); + } +} + +NS_IMETHODIMP +SelectionManager::NotifySelectionChanged(dom::Document* aDocument, + Selection* aSelection, int16_t aReason, + int32_t aAmount) { + 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, aAmount); + 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, aSelData->mGranularity); + 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); + } +} + +void SelectionManager::SpellCheckRangeChanged(const nsRange& aRange) { + // Events are fired in SelectionManager::NotifySelectionChanged. This is only + // used to push cache updates. + if (IPCAccessibilityActive()) { + dom::Document* doc = aRange.GetStartContainer()->OwnerDoc(); + MOZ_ASSERT(doc); + TextLeafPoint::UpdateCachedSpellingError(doc, aRange); + } +} + +SelectionManager::~SelectionManager() = default; diff --git a/accessible/base/SelectionManager.h b/accessible/base/SelectionManager.h new file mode 100644 index 0000000000..1dba086036 --- /dev/null +++ b/accessible/base/SelectionManager.h @@ -0,0 +1,141 @@ +/* -*- 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" + +class nsRange; + +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; + } + + /** + * Called by mozInlineSpellChecker when a spell check range is added/removed. + * nsISelectionListener isn't sufficient for spelling errors, since it only + * tells us that there was a change, not which range changed. We don't want + * to unnecessarily push a cache update for all Accessibles in the entire + * selection. + */ + void SpellCheckRangeChanged(const nsRange& aRange); + + ~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..5588568d89 --- /dev/null +++ b/accessible/base/States.h @@ -0,0 +1,305 @@ +/* -*- 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 + +/** + * States that must be calculated by RemoteAccessible and are thus not cached. + */ +const uint64_t kRemoteCalculatedStates = + states::FOCUSED | states::INVISIBLE | states::OFFSCREEN | states::ENABLED | + states::SENSITIVE | states::COLLAPSED | states::OPAQUE1; + +} // 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..2673e9684d --- /dev/null +++ b/accessible/base/StyleInfo.cpp @@ -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/. */ + +#include "StyleInfo.h" + +#include "nsStyleConsts.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +void StyleInfo::FormatColor(const nscolor& aValue, nsAString& aFormattedValue) { + // Combine the string like rgb(R, G, B) from nscolor. + // FIXME: What about the alpha channel? + 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(')'); +} + +already_AddRefed<nsAtom> StyleInfo::TextDecorationStyleToAtom( + StyleTextDecorationStyle aValue) { + // TODO: When these are enum classes that rust also understands we should just + // make an FFI call here. + // TODO: These should probably be static atoms. + switch (aValue) { + case StyleTextDecorationStyle::None: + return NS_Atomize("-moz-none"); + case StyleTextDecorationStyle::Solid: + return NS_Atomize("solid"); + case StyleTextDecorationStyle::Double: + return NS_Atomize("double"); + case StyleTextDecorationStyle::Dotted: + return NS_Atomize("dotted"); + case StyleTextDecorationStyle::Dashed: + return NS_Atomize("dashed"); + case StyleTextDecorationStyle::Wavy: + return NS_Atomize("wavy"); + default: + MOZ_ASSERT_UNREACHABLE("Unknown decoration style"); + break; + } + + return nullptr; +} diff --git a/accessible/base/StyleInfo.h b/accessible/base/StyleInfo.h new file mode 100644 index 0000000000..e078a86834 --- /dev/null +++ b/accessible/base/StyleInfo.h @@ -0,0 +1,36 @@ +/* -*- 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/AlreadyAddRefed.h" +#include "nsStringFwd.h" +#include "nsColor.h" + +class nsAtom; + +namespace mozilla { + +enum class StyleTextDecorationStyle : uint8_t; + +namespace dom { +class Element; +} // namespace dom +namespace a11y { + +class StyleInfo { + public: + static void FormatColor(const nscolor& aValue, nsAString& aFormattedValue); + static already_AddRefed<nsAtom> TextDecorationStyleToAtom( + StyleTextDecorationStyle aValue); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextAttrs.cpp b/accessible/base/TextAttrs.cpp new file mode 100644 index 0000000000..e1ca62e549 --- /dev/null +++ b/accessible/base/TextAttrs.cpp @@ -0,0 +1,816 @@ +/* -*- 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 "AccAttributes.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "StyleInfo.h" + +#include "gfxTextRun.h" +#include "nsFontMetrics.h" +#include "nsLayoutUtils.h" +#include "nsContainerFrame.h" +#include "HyperTextAccessible.h" +#include "mozilla/AppUnits.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// TextAttrsMgr +//////////////////////////////////////////////////////////////////////////////// + +void TextAttrsMgr::GetAttributes(AccAttributes* aAttributes, + uint32_t* aStartOffset, uint32_t* aEndOffset) { + // 1. Hyper text accessible must be specified always. + // 2. Offset accessible must be specified in + // the case of text attributes. Result hyper text offsets are optional if you + // just want the attributes for a single text Accessible. + // 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) || + (!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()) { + if (!aStartOffset) { + return; + } + for (int32_t childIdx = mOffsetAccIdx - 1; childIdx >= 0; childIdx--) { + LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(childIdx); + if (currAcc->IsText()) break; + + (*aStartOffset)--; + } + + uint32_t childCount = mHyperTextAcc->ChildCount(); + for (uint32_t childIdx = mOffsetAccIdx + 1; childIdx < childCount; + childIdx++) { + LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(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(); + 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, hyperTextElm, offsetNode); + + 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 (aStartOffset) { + 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--) { + LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(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++) { + LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(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(LocalAccessible* aAccessible, + nsString* aValue) { + nsCoreUtils::GetLanguageFor(aAccessible->GetContent(), mRootContent, *aValue); + return !aValue->IsEmpty(); +} + +void TextAttrsMgr::LangTextAttr::ExposeValue(AccAttributes* aAttributes, + const nsString& aValue) { + RefPtr<nsAtom> lang = NS_Atomize(aValue); + aAttributes->SetAttribute(nsGkAtoms::language, lang); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* aAccessible, + uint32_t* aValue) { + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + return elm ? GetValue(elm, aValue) : false; +} + +void TextAttrsMgr::InvalidTextAttr::ExposeValue(AccAttributes* aAttributes, + const uint32_t& aValue) { + switch (aValue) { + case eFalse: + aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::_false); + break; + + case eGrammar: + aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::grammar); + break; + + case eSpelling: + aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling); + break; + + case eTrue: + aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::_true); + 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 = nsAccUtils::FindARIAAttrValueIn( + elm->AsElement(), 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(LocalAccessible* 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(AccAttributes* aAttributes, + const nscolor& aValue) { + aAttributes->SetAttribute(nsGkAtoms::backgroundColor, Color{aValue}); +} + +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(LocalAccessible* 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(AccAttributes* aAttributes, + const nscolor& aValue) { + aAttributes->SetAttribute(nsGkAtoms::color, Color{aValue}); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* 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(AccAttributes* aAttributes, + const nsString& aValue) { + RefPtr<nsAtom> family = NS_Atomize(aValue); + aAttributes->SetAttribute(nsGkAtoms::font_family, family); +} + +bool TextAttrsMgr::FontFamilyTextAttr::GetFontFamily(nsIFrame* aFrame, + nsString& aFamily) { + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f); + + gfxFontGroup* fontGroup = fm->GetThebesFontGroup(); + RefPtr<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(LocalAccessible* 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(AccAttributes* 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. + FontSize fontSize{NS_lround(px * 3 / 4)}; + + aAttributes->SetAttribute(nsGkAtoms::font_size, fontSize); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* 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( + AccAttributes* aAttributes, const FontSlantStyle& aValue) { + if (aValue.IsNormal()) { + aAttributes->SetAttribute(nsGkAtoms::font_style, nsGkAtoms::normal); + } else if (aValue.IsItalic()) { + RefPtr<nsAtom> atom = NS_Atomize("italic"); + aAttributes->SetAttribute(nsGkAtoms::font_style, atom); + } else { + nsAutoCString s; + aValue.ToString(s); + nsString wide; + CopyUTF8toUTF16(s, wide); + aAttributes->SetAttribute(nsGkAtoms::font_style, std::move(wide)); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* 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(AccAttributes* aAttributes, + const FontWeight& aValue) { + int value = aValue.ToIntRounded(); + aAttributes->SetAttribute(nsGkAtoms::fontWeight, value); +} + +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(); + RefPtr<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. (Simply returns false on any + // platforms that don't use the multi-strike synthetic bolding.) + if (font->ApplySyntheticBold()) { + 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, LocalAccessible* 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( + LocalAccessible* aAccessible, bool* aValue) { + return *aValue = (aAccessible->NativeRole() == roles::STATICTEXT); +} + +void TextAttrsMgr::AutoGeneratedTextAttr::ExposeValue( + AccAttributes* aAttributes, const bool& aValue) { + aAttributes->SetAttribute(nsGkAtoms::auto_generated, aValue); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(LocalAccessible* 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( + AccAttributes* aAttributes, const TextDecorValue& aValue) { + if (aValue.IsUnderline()) { + RefPtr<nsAtom> underlineStyle = + StyleInfo::TextDecorationStyleToAtom(aValue.Style()); + aAttributes->SetAttribute(nsGkAtoms::textUnderlineStyle, underlineStyle); + + aAttributes->SetAttribute(nsGkAtoms::textUnderlineColor, + Color{aValue.Color()}); + return; + } + + if (aValue.IsLineThrough()) { + RefPtr<nsAtom> lineThroughStyle = + StyleInfo::TextDecorationStyleToAtom(aValue.Style()); + aAttributes->SetAttribute(nsGkAtoms::textLineThroughStyle, + lineThroughStyle); + + aAttributes->SetAttribute(nsGkAtoms::textLineThroughColor, + Color{aValue.Color()}); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TextPosTextAttr +//////////////////////////////////////////////////////////////////////////////// + +TextAttrsMgr::TextPosTextAttr::TextPosTextAttr(nsIFrame* aRootFrame, + nsIFrame* aFrame, + nsIContent* aRootElm, + nsIContent* aElm) + : TTextAttr<Maybe<TextPosValue>>(!aFrame && !aElm), mRootElm(aRootElm) { + // Get the text-position values for the roots and children. + // If we find an ARIA text-position value on a DOM element - searching up + // from the supplied root DOM element - use the associated frame as the root + // frame. This ensures that we're using the proper root frame for comparison. + nsIFrame* ariaFrame = nullptr; + Maybe<TextPosValue> rootAria = GetAriaTextPosValue(aRootElm, ariaFrame); + if (rootAria && ariaFrame) { + aRootFrame = ariaFrame; + } + Maybe<TextPosValue> rootLayout = GetLayoutTextPosValue(aRootFrame); + Maybe<TextPosValue> childLayout; + Maybe<TextPosValue> childAria; + if (aFrame) { + childLayout = GetLayoutTextPosValue(aFrame); + } + if (aElm) { + childAria = GetAriaTextPosValue(aElm); + } + + // Aria values take precedence over layout values. + mIsRootDefined = rootAria || rootLayout; + mRootNativeValue = rootAria ? rootAria : rootLayout; + mIsDefined = childAria || childLayout; + mNativeValue = childAria ? childAria : childLayout; + + // If there's no child text-position information from ARIA, and the child + // layout info is equivalent to the root layout info (i.e., it's inherited), + // then we should prefer the root information. + if (!childAria && childLayout == rootLayout) { + mIsDefined = false; + } +} + +bool TextAttrsMgr::TextPosTextAttr::GetValueFor(LocalAccessible* aAccessible, + Maybe<TextPosValue>* aValue) { + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent()); + if (elm) { + nsIFrame* frame = elm->GetPrimaryFrame(); + if (frame) { + Maybe<TextPosValue> layoutValue = GetLayoutTextPosValue(frame); + Maybe<TextPosValue> ariaValue = GetAriaTextPosValue(elm); + + *aValue = ariaValue ? ariaValue : layoutValue; + return aValue->isSome(); + } + } + return false; +} + +void TextAttrsMgr::TextPosTextAttr::ExposeValue( + AccAttributes* aAttributes, const Maybe<TextPosValue>& aValue) { + if (aValue.isNothing()) { + return; + } + + RefPtr<nsAtom> atom = nullptr; + switch (*aValue) { + case eTextPosBaseline: + atom = nsGkAtoms::baseline; + break; + + case eTextPosSub: + atom = nsGkAtoms::sub; + break; + + case eTextPosSuper: + atom = NS_Atomize("super"); + break; + } + + if (atom) { + aAttributes->SetAttribute(nsGkAtoms::textPosition, atom); + } +} + +Maybe<TextAttrsMgr::TextPosValue> +TextAttrsMgr::TextPosTextAttr::GetAriaTextPosValue(nsIContent* aElm) const { + nsIFrame* ariaFrame = nullptr; + return GetAriaTextPosValue(aElm, ariaFrame); +} + +Maybe<TextAttrsMgr::TextPosValue> +TextAttrsMgr::TextPosTextAttr::GetAriaTextPosValue(nsIContent* aElm, + nsIFrame*& ariaFrame) const { + // Search for the superscript and subscript roles that imply text-position. + const nsIContent* elm = aElm; + do { + if (elm->IsElement()) { + const mozilla::dom::Element* domElm = elm->AsElement(); + static const dom::Element::AttrValuesArray tokens[] = { + nsGkAtoms::subscript, nsGkAtoms::superscript, nullptr}; + const int32_t valueIdx = domElm->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::role, tokens, eCaseMatters); + ariaFrame = domElm->GetPrimaryFrame(); + if (valueIdx == 0) { + return Some(eTextPosSub); + } + if (valueIdx == 1) { + return Some(eTextPosSuper); + } + } + } while ((elm = elm->GetParent()) && elm != mRootElm); + + ariaFrame = nullptr; + return Nothing{}; +} + +Maybe<TextAttrsMgr::TextPosValue> +TextAttrsMgr::TextPosTextAttr::GetLayoutTextPosValue(nsIFrame* aFrame) const { + const auto& verticalAlign = aFrame->StyleDisplay()->mVerticalAlign; + if (verticalAlign.IsKeyword()) { + switch (verticalAlign.AsKeyword()) { + case StyleVerticalAlignKeyword::Baseline: + return Some(eTextPosBaseline); + case StyleVerticalAlignKeyword::Sub: + return Some(eTextPosSub); + case StyleVerticalAlignKeyword::Super: + return Some(eTextPosSuper); + // No good guess for the rest, so do not expose value of text-position + // attribute. + default: + return Nothing{}; + } + } + + const auto& length = verticalAlign.AsLength(); + if (length.ConvertsToPercentage()) { + const float percentValue = length.ToPercentage(); + return percentValue > 0 ? Some(eTextPosSuper) + : (percentValue < 0 ? Some(eTextPosSub) + : Some(eTextPosBaseline)); + } + + if (length.ConvertsToLength()) { + const nscoord coordValue = length.ToLength(); + return coordValue > 0 + ? Some(eTextPosSuper) + : (coordValue < 0 ? Some(eTextPosSub) : Some(eTextPosBaseline)); + } + + if (const nsIContent* content = aFrame->GetContent()) { + if (content->IsHTMLElement(nsGkAtoms::sup)) return Some(eTextPosSuper); + if (content->IsHTMLElement(nsGkAtoms::sub)) return Some(eTextPosSub); + } + + return Nothing{}; +} diff --git a/accessible/base/TextAttrs.h b/accessible/base/TextAttrs.h new file mode 100644 index 0000000000..031e114273 --- /dev/null +++ b/accessible/base/TextAttrs.h @@ -0,0 +1,440 @@ +/* -*- 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 nsIContent; +class nsDeviceContext; + +namespace mozilla { +namespace a11y { + +class AccAttributes; +class LocalAccessible; +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, + LocalAccessible* 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(AccAttributes* 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: + LocalAccessible* 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(AccAttributes* aAttributes, + bool aIncludeDefAttrValue) = 0; + + /** + * Return true if the text attribute value on the given element equals with + * predefined attribute value. + */ + virtual bool Equal(LocalAccessible* 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(AccAttributes* 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(LocalAccessible* 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(AccAttributes* aAttributes, const T& aValue) = 0; + + // Return native value for the given DOM element. + virtual bool GetValueFor(LocalAccessible* 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(LocalAccessible* aAccessible, + nsString* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + uint32_t* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + nscolor* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + nscolor* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + nsString* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + nscoord* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aContent, + mozilla::FontSlantStyle* aValue) override; + virtual void ExposeValue(AccAttributes* 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(LocalAccessible* aAccessible, + mozilla::FontWeight* aValue) override; + virtual void ExposeValue(AccAttributes* 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, + LocalAccessible* aAccessible); + virtual ~AutoGeneratedTextAttr() {} + + protected: + // TextAttr + virtual bool GetValueFor(LocalAccessible* aAccessible, + bool* aValue) override; + virtual void ExposeValue(AccAttributes* 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{StyleTextDecorationStyle::None} {} + explicit TextDecorValue(nsIFrame* aFrame); + + nscolor Color() const { return mColor; } + mozilla::StyleTextDecorationStyle 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) const { + return mColor == aValue.mColor && mLine == aValue.mLine && + mStyle == aValue.mStyle; + } + bool operator!=(const TextDecorValue& aValue) const { + return !(*this == aValue); + } + + private: + nscolor mColor; + mozilla::StyleTextDecorationLine mLine; + mozilla::StyleTextDecorationStyle mStyle; + }; + + class TextDecorTextAttr : public TTextAttr<TextDecorValue> { + public: + TextDecorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame); + virtual ~TextDecorTextAttr() {} + + protected: + // TextAttr + virtual bool GetValueFor(LocalAccessible* aAccessible, + TextDecorValue* aValue) override; + virtual void ExposeValue(AccAttributes* aAttributes, + const TextDecorValue& aValue) override; + }; + + /** + * Class is used for the work with "text-position" text attribute. + */ + + enum TextPosValue { eTextPosBaseline, eTextPosSub, eTextPosSuper }; + + class TextPosTextAttr : public TTextAttr<Maybe<TextPosValue>> { + public: + TextPosTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame, + nsIContent* aRootElm, nsIContent* aElm); + virtual ~TextPosTextAttr() {} + + protected: + // TextAttr + virtual bool GetValueFor(LocalAccessible* aAccessible, + Maybe<TextPosValue>* aValue) override; + virtual void ExposeValue(AccAttributes* aAttributes, + const Maybe<TextPosValue>& aValue) override; + + private: + Maybe<TextPosValue> GetAriaTextPosValue(nsIContent* aElm) const; + Maybe<TextPosValue> GetAriaTextPosValue(nsIContent* aElm, + nsIFrame*& ariaFrame) const; + Maybe<TextPosValue> GetLayoutTextPosValue(nsIFrame* aFrame) const; + nsIContent* mRootElm; + }; + +}; // TextAttrMgr + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/TextLeafRange.cpp b/accessible/base/TextLeafRange.cpp new file mode 100644 index 0000000000..9b658e12af --- /dev/null +++ b/accessible/base/TextLeafRange.cpp @@ -0,0 +1,1990 @@ +/* -*- 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 "TextLeafRange.h" + +#include "HyperTextAccessible-inl.h" +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/CacheConstants.h" +#include "mozilla/a11y/DocAccessible.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/LocalAccessible.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/Casting.h" +#include "mozilla/dom/CharacterData.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/PresShell.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/intl/WordBreaker.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/TextEditor.h" +#include "nsAccUtils.h" +#include "nsBlockFrame.h" +#include "nsFrameSelection.h" +#include "nsIAccessiblePivot.h" +#include "nsILineIterator.h" +#include "nsINode.h" +#include "nsRange.h" +#include "nsStyleStructInlines.h" +#include "nsTArray.h" +#include "nsTextFrame.h" +#include "nsUnicharUtils.h" +#include "Pivot.h" +#include "TextAttrs.h" + +using mozilla::intl::WordBreaker; +using FindWordOptions = mozilla::intl::WordBreaker::FindWordOptions; + +namespace mozilla::a11y { + +/*** Helpers ***/ + +/** + * These two functions convert between rendered and content text offsets. + * When text DOM nodes are rendered, the rendered text often does not contain + * all the whitespace from the source. For example, by default, the text + * "a b" will be rendered as "a b"; i.e. multiple spaces are compressed to + * one. TextLeafAccessibles contain rendered text, but when we query layout, we + * need to provide offsets into the original content text. Similarly, layout + * returns content offsets, but we need to convert them to rendered offsets to + * map them to TextLeafAccessibles. + */ + +static int32_t RenderedToContentOffset(LocalAccessible* aAcc, + uint32_t aRenderedOffset) { + nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame()); + if (!frame) { + MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(), + "No text frame because this is a XUL label[value] text leaf or " + "a BR element."); + return static_cast<int32_t>(aRenderedOffset); + } + + if (frame->StyleText()->WhiteSpaceIsSignificant() && + frame->StyleText()->NewlineIsSignificant(frame)) { + // Spaces and new lines aren't altered, so the content and rendered offsets + // are the same. This happens in pre-formatted text and text fields. + return static_cast<int32_t>(aRenderedOffset); + } + + nsIFrame::RenderedText text = + frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1, + nsIFrame::TextOffsetType::OffsetsInRenderedText, + nsIFrame::TrailingWhitespace::DontTrim); + return text.mOffsetWithinNodeText; +} + +static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc, + int32_t aContentOffset) { + nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame()); + if (!frame) { + MOZ_ASSERT(!aAcc->HasOwnContent(), + "No text frame because this is a XUL label[value] text leaf."); + return aContentOffset; + } + + if (frame->StyleText()->WhiteSpaceIsSignificant() && + frame->StyleText()->NewlineIsSignificant(frame)) { + // Spaces and new lines aren't altered, so the content and rendered offsets + // are the same. This happens in pre-formatted text and text fields. + return aContentOffset; + } + + nsIFrame::RenderedText text = + frame->GetRenderedText(aContentOffset, aContentOffset + 1, + nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + return text.mOffsetWithinNodeRenderedText; +} + +class LeafRule : public PivotRule { + public: + explicit LeafRule(bool aIgnoreListItemMarker) + : mIgnoreListItemMarker(aIgnoreListItemMarker) {} + + virtual uint16_t Match(Accessible* aAcc) override { + if (aAcc->IsOuterDoc()) { + // Treat an embedded doc as a single character in this document, but do + // not descend inside it. + return nsIAccessibleTraversalRule::FILTER_MATCH | + nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; + } + + if (mIgnoreListItemMarker && aAcc->Role() == roles::LISTITEM_MARKER) { + // Ignore list item markers if configured to do so. + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + // We deliberately include Accessibles such as empty input elements and + // empty containers, as these can be at the start of a line. + if (!aAcc->HasChildren()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } + + private: + bool mIgnoreListItemMarker; +}; + +static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) { + for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) { + if (HyperTextAccessible* ht = acc->AsHyperText()) { + return ht; + } + } + return nullptr; +} + +static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false, + bool aIgnoreListItemMarker = false) { + MOZ_ASSERT(aOrigin); + Accessible* doc = nsAccUtils::DocumentFor(aOrigin); + Pivot pivot(doc); + auto rule = LeafRule(aIgnoreListItemMarker); + Accessible* leaf = pivot.Next(aOrigin, rule); + if (aIsEditable && leaf) { + return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE) + ? leaf + : nullptr; + } + return leaf; +} + +static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false, + bool aIgnoreListItemMarker = false) { + MOZ_ASSERT(aOrigin); + Accessible* doc = nsAccUtils::DocumentFor(aOrigin); + Pivot pivot(doc); + auto rule = LeafRule(aIgnoreListItemMarker); + Accessible* leaf = pivot.Prev(aOrigin, rule); + if (aIsEditable && leaf) { + return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE) + ? leaf + : nullptr; + } + return leaf; +} + +static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) { + dom::HTMLInputElement* input = + dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent()); + if (!input) { + if (LocalAccessible* parent = aAcc->LocalParent()) { + input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent()); + } + } + + if (input) { + // If this is a single line input (or a leaf of an input) we want to return + // the top frame of the input element and not the text leaf's frame because + // the leaf may be inside of an embedded block frame in the input's shadow + // DOM that we aren't interested in. + return input->GetPrimaryFrame(); + } + + return aAcc->GetFrame(); +} + +static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) { + if (aAcc->NativeRole() == roles::LISTITEM_MARKER) { + // A bullet always starts a line. + return true; + } + // Splitting of content across lines is handled by layout. + // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame + // on its line. However, we can't use that because the first frame on a line + // might not be included in the a11y tree; e.g. an empty span, or space + // in the DOM after a line break which is stripped when rendered. Instead, we + // get the line number for this Accessible's frame and the line number for the + // previous leaf Accessible's frame and compare them. + Accessible* prev = PrevLeaf(aAcc); + LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr; + if (!prevLocal) { + // There's nothing before us, so this is the start of the first line. + return true; + } + if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) { + // If there is a bullet immediately before us and we're inside the same + // list item, this is not the start of a line. + LocalAccessible* listItem = prevLocal->LocalParent(); + MOZ_ASSERT(listItem); + LocalAccessible* doc = listItem->Document(); + MOZ_ASSERT(doc); + for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc; + parent = parent->LocalParent()) { + if (parent == listItem) { + return false; + } + } + } + + nsIFrame* thisFrame = GetFrameInBlock(aAcc); + if (!thisFrame) { + return false; + } + + nsIFrame* prevFrame = GetFrameInBlock(prevLocal); + if (!prevFrame) { + return false; + } + + auto [thisBlock, thisLineFrame] = thisFrame->GetContainingBlockForLine( + /* aLockScroll */ false); + if (!thisBlock) { + // We couldn't get the containing block for this frame. In that case, we + // play it safe and assume this is the beginning of a new line. + return true; + } + + // The previous leaf might cross lines. We want to compare against the last + // line. + prevFrame = prevFrame->LastContinuation(); + auto [prevBlock, prevLineFrame] = prevFrame->GetContainingBlockForLine( + /* aLockScroll */ false); + if (thisBlock != prevBlock) { + // If the blocks are different, that means there's nothing before us on the + // same line, so we're at the start. + return true; + } + if (nsBlockFrame* block = do_QueryFrame(thisBlock)) { + // If we have a block frame, it's faster for us to use + // BlockInFlowLineIterator because it uses the line cursor. + bool found = false; + block->SetupLineCursorForQuery(); + nsBlockInFlowLineIterator prevIt(block, prevLineFrame, &found); + if (!found) { + // Error; play it safe. + return true; + } + found = false; + nsBlockInFlowLineIterator thisIt(block, thisLineFrame, &found); + // if the lines are different, that means there's nothing before us on the + // same line, so we're at the start. + return !found || prevIt.GetLine() != thisIt.GetLine(); + } + AutoAssertNoDomMutations guard; + nsILineIterator* it = prevBlock->GetLineIterator(); + MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible"); + int32_t prevLineNum = it->FindLineContaining(prevLineFrame); + if (prevLineNum < 0) { + // Error; play it safe. + return true; + } + int32_t thisLineNum = it->FindLineContaining(thisLineFrame, prevLineNum); + // if the blocks and line numbers are different, that means there's nothing + // before us on the same line, so we're at the start. + return thisLineNum != prevLineNum; +} + +/** + * There are many kinds of word break, but we only need to treat punctuation and + * space specially. + */ +enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther }; + +static WordBreakClass GetWordBreakClass(char16_t aChar) { + // Based on IsSelectionInlineWhitespace and IsSelectionNewline in + // layout/generic/nsTextFrame.cpp. + const char16_t kCharNbsp = 0xA0; + switch (aChar) { + case ' ': + case kCharNbsp: + case '\t': + case '\f': + case '\n': + case '\r': + return eWbcSpace; + default: + break; + } + return mozilla::IsPunctuationForWordSelect(aChar) ? eWbcPunct : eWbcOther; +} + +/** + * Words can cross Accessibles. To work out whether we're at the start of a + * word, we might have to check the previous leaf. This class handles querying + * the previous WordBreakClass, crossing Accessibles if necessary. + */ +class PrevWordBreakClassWalker { + public: + PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText, + int32_t aOffset) + : mAcc(aAcc), mText(aText), mOffset(aOffset) { + mClass = GetWordBreakClass(mText.CharAt(mOffset)); + } + + WordBreakClass CurClass() { return mClass; } + + Maybe<WordBreakClass> PrevClass() { + for (;;) { + if (!PrevChar()) { + return Nothing(); + } + WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset)); + if (curClass != mClass) { + mClass = curClass; + return Some(curClass); + } + } + MOZ_ASSERT_UNREACHABLE(); + return Nothing(); + } + + bool IsStartOfGroup() { + if (!PrevChar()) { + // There are no characters before us. + return true; + } + WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset)); + // We wanted to peek at the previous character, not really move to it. + ++mOffset; + return curClass != mClass; + } + + private: + bool PrevChar() { + if (mOffset > 0) { + --mOffset; + return true; + } + if (!mAcc) { + // PrevChar was called already and failed. + return false; + } + mAcc = PrevLeaf(mAcc); + if (!mAcc) { + return false; + } + mText.Truncate(); + mAcc->AppendTextTo(mText); + mOffset = static_cast<int32_t>(mText.Length()) - 1; + return true; + } + + Accessible* mAcc; + nsAutoString mText; + int32_t mOffset; + WordBreakClass mClass; +}; + +/** + * WordBreaker breaks at all space, punctuation, etc. We want to emulate + * layout, so that's not what we want. This function determines whether this + * is acceptable as the start of a word for our purposes. + */ +static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText, + int32_t aOffset) { + PrevWordBreakClassWalker walker(aAcc, aText, aOffset); + if (!walker.IsStartOfGroup()) { + // If we're not at the start of a WordBreaker group, this can't be the + // start of a word. + return false; + } + WordBreakClass curClass = walker.CurClass(); + if (curClass == eWbcSpace) { + // Space isn't the start of a word. + return false; + } + Maybe<WordBreakClass> prevClass = walker.PrevClass(); + if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) { + // Punctuation isn't the start of a word (unless it is after space). + return false; + } + if (!prevClass || prevClass.value() != eWbcPunct) { + // If there's nothing before this or the group before this isn't + // punctuation, this is the start of a word. + return true; + } + // At this point, we know the group before this is punctuation. + if (!StaticPrefs::layout_word_select_stop_at_punctuation()) { + // When layout.word_select.stop_at_punctuation is false (defaults to true), + // if there is punctuation before this, this is not the start of a word. + return false; + } + Maybe<WordBreakClass> prevPrevClass = walker.PrevClass(); + if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) { + // If there is punctuation before this and space (or nothing) before the + // punctuation, this is not the start of a word. + return false; + } + return true; +} + +class BlockRule : public PivotRule { + public: + virtual uint16_t Match(Accessible* aAcc) override { + if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block || + aAcc->IsHTMLListItem() || aAcc->IsTableRow() || aAcc->IsTableCell()) { + return nsIAccessibleTraversalRule::FILTER_MATCH; + } + return nsIAccessibleTraversalRule::FILTER_IGNORE; + } +}; + +/** + * Find spelling error DOM ranges overlapping the requested LocalAccessible and + * offsets. This includes ranges that begin or end outside of the given + * LocalAccessible. Note that the offset arguments are rendered offsets, but + * because the returned ranges are DOM ranges, those offsets are content + * offsets. See the documentation for dom::Selection::GetRangesForIntervalArray + * for information about the aAllowAdjacent argument. + */ +static nsTArray<nsRange*> FindDOMSpellingErrors(LocalAccessible* aAcc, + int32_t aRenderedStart, + int32_t aRenderedEnd, + bool aAllowAdjacent = false) { + if (!aAcc->IsTextLeaf() || !aAcc->HasOwnContent()) { + return {}; + } + nsIFrame* frame = aAcc->GetFrame(); + RefPtr<nsFrameSelection> frameSel = + frame ? frame->GetFrameSelection() : nullptr; + dom::Selection* domSel = + frameSel ? frameSel->GetSelection(SelectionType::eSpellCheck) : nullptr; + if (!domSel) { + return {}; + } + nsINode* node = aAcc->GetNode(); + uint32_t contentStart = RenderedToContentOffset(aAcc, aRenderedStart); + uint32_t contentEnd = + aRenderedEnd == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT + ? dom::CharacterData::FromNode(node)->TextLength() + : RenderedToContentOffset(aAcc, aRenderedEnd); + nsTArray<nsRange*> domRanges; + domSel->GetDynamicRangesForIntervalArray(node, contentStart, node, contentEnd, + aAllowAdjacent, &domRanges); + return domRanges; +} + +/** + * Given two DOM nodes get DOM Selection object that is common + * to both of them. + */ +static dom::Selection* GetDOMSelection(const nsIContent* aStartContent, + const nsIContent* aEndContent) { + nsIFrame* startFrame = aStartContent->GetPrimaryFrame(); + const nsFrameSelection* startFrameSel = + startFrame ? startFrame->GetConstFrameSelection() : nullptr; + nsIFrame* endFrame = aEndContent->GetPrimaryFrame(); + const nsFrameSelection* endFrameSel = + endFrame ? endFrame->GetConstFrameSelection() : nullptr; + + if (startFrameSel != endFrameSel) { + // Start and end point don't share the same selection state. + // This could happen when both points aren't in the same editable. + return nullptr; + } + + return startFrameSel ? startFrameSel->GetSelection(SelectionType::eNormal) + : nullptr; +} + +std::pair<nsIContent*, int32_t> TextLeafPoint::ToDOMPoint( + bool aIncludeGenerated) const { + if (!(*this) || !mAcc->IsLocal()) { + MOZ_ASSERT_UNREACHABLE("Invalid point"); + return {nullptr, 0}; + } + + nsIContent* content = mAcc->AsLocal()->GetContent(); + nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr; + MOZ_ASSERT(frame); + + if (!aIncludeGenerated && frame && frame->IsGeneratedContentFrame()) { + // List markers accessibles represent the generated content element, + // before/after text accessibles represent the child text nodes. + auto generatedElement = content->IsGeneratedContentContainerForMarker() + ? content + : content->GetParentElement(); + auto parent = generatedElement ? generatedElement->GetParent() : nullptr; + MOZ_ASSERT(parent); + if (parent) { + if (generatedElement->IsGeneratedContentContainerForAfter()) { + // Use the end offset of the parent element for trailing generated + // content. + return {parent, parent->GetChildCount()}; + } + + if (generatedElement->IsGeneratedContentContainerForBefore() || + generatedElement->IsGeneratedContentContainerForMarker()) { + // Use the start offset of the parent element for leading generated + // content. + return {parent, 0}; + } + + MOZ_ASSERT_UNREACHABLE("Unknown generated content type!"); + } + } + + if (!mAcc->IsTextLeaf() && !mAcc->IsHTMLBr() && !mAcc->HasChildren()) { + // If this is not a text leaf it can be an empty editable container, + // whitespace, or an empty doc. In any case, the offset inside should be 0. + MOZ_ASSERT(mOffset == 0); + + if (RefPtr<TextControlElement> textControlElement = + TextControlElement::FromNodeOrNull(content)) { + // This is an empty input, use the shadow root's element. + if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) { + if (textEditor->IsEmpty()) { + MOZ_ASSERT(mOffset == 0); + return {textEditor->GetRoot(), 0}; + } + } + } + + return {content, 0}; + } + + return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)}; +} + +/*** TextLeafPoint ***/ + +TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) { + if (!aAcc) { + // Construct an invalid point. + mAcc = nullptr; + mOffset = 0; + return; + } + + // Even though an OuterDoc contains a document, we treat it as a leaf because + // we don't want to move into another document. + if (aOffset != nsIAccessibleText::TEXT_OFFSET_CARET && !aAcc->IsOuterDoc() && + aAcc->HasChildren()) { + // Find a leaf. This might not necessarily be a TextLeafAccessible; it + // could be an empty container. + auto GetChild = [&aOffset](Accessible* acc) -> Accessible* { + if (acc->IsOuterDoc()) { + return nullptr; + } + return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT + ? acc->FirstChild() + : acc->LastChild(); + }; + + for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) { + mAcc = acc; + } + mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT + ? 0 + : nsAccUtils::TextLength(mAcc); + return; + } + mAcc = aAcc; + mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT + ? aOffset + : nsAccUtils::TextLength(mAcc); +} + +bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const { + if (mAcc == aPoint.mAcc) { + return mOffset < aPoint.mOffset; + } + return mAcc->IsBefore(aPoint.mAcc); +} + +bool TextLeafPoint::operator<=(const TextLeafPoint& aPoint) const { + return *this == aPoint || *this < aPoint; +} + +bool TextLeafPoint::IsDocEdge(nsDirection aDirection) const { + if (aDirection == eDirPrevious) { + return mOffset == 0 && !PrevLeaf(mAcc); + } + + return mOffset == static_cast<int32_t>(nsAccUtils::TextLength(mAcc)) && + !NextLeaf(mAcc); +} + +bool TextLeafPoint::IsLeafAfterListItemMarker() const { + Accessible* prev = PrevLeaf(mAcc); + return prev && prev->Role() == roles::LISTITEM_MARKER && + prev->Parent()->IsAncestorOf(mAcc); +} + +bool TextLeafPoint::IsEmptyLastLine() const { + if (mAcc->IsHTMLBr() && mOffset == 1) { + return true; + } + if (!mAcc->IsTextLeaf()) { + return false; + } + if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) { + return false; + } + nsAutoString text; + mAcc->AppendTextTo(text, mOffset - 1, 1); + return text.CharAt(0) == '\n'; +} + +char16_t TextLeafPoint::GetChar() const { + nsAutoString text; + mAcc->AppendTextTo(text, mOffset, 1); + return text.CharAt(0); +} + +TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc( + bool aIncludeOrigin) const { + LocalAccessible* acc = mAcc->AsLocal(); + MOZ_ASSERT(acc); + if (mOffset == 0) { + if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) { + return *this; + } + return TextLeafPoint(); + } + nsIFrame* frame = acc->GetFrame(); + if (!frame) { + // This can happen if this is an empty element with display: contents. In + // that case, this Accessible contains no lines. + return TextLeafPoint(); + } + if (!frame->IsTextFrame()) { + if (IsLocalAccAtLineStart(acc)) { + return TextLeafPoint(acc, 0); + } + return TextLeafPoint(); + } + // Each line of a text node is rendered as a continuation frame. Get the + // continuation containing the origin. + int32_t origOffset = mOffset; + origOffset = RenderedToContentOffset(acc, origOffset); + nsTextFrame* continuation = nullptr; + int32_t unusedOffsetInContinuation = 0; + frame->GetChildFrameContainingOffset( + origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation); + MOZ_ASSERT(continuation); + int32_t lineStart = continuation->GetContentOffset(); + if (!aIncludeOrigin && lineStart > 0 && lineStart == origOffset) { + // A line starts at the origin, but the caller doesn't want this included. + // Go back one more. + continuation = continuation->GetPrevContinuation(); + MOZ_ASSERT(continuation); + lineStart = continuation->GetContentOffset(); + } + MOZ_ASSERT(lineStart >= 0); + if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) { + // This is the first line of this text node, but there is something else + // on the same line before this text node, so don't return this as a line + // start. + return TextLeafPoint(); + } + lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart)); + return TextLeafPoint(acc, lineStart); +} + +TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc( + bool aIncludeOrigin) const { + LocalAccessible* acc = mAcc->AsLocal(); + MOZ_ASSERT(acc); + if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) { + return *this; + } + nsIFrame* frame = acc->GetFrame(); + if (!frame) { + // This can happen if this is an empty element with display: contents. In + // that case, this Accessible contains no lines. + return TextLeafPoint(); + } + if (!frame->IsTextFrame()) { + // There can't be multiple lines in a non-text leaf. + return TextLeafPoint(); + } + // Each line of a text node is rendered as a continuation frame. Get the + // continuation containing the origin. + int32_t origOffset = mOffset; + origOffset = RenderedToContentOffset(acc, origOffset); + nsTextFrame* continuation = nullptr; + int32_t unusedOffsetInContinuation = 0; + frame->GetChildFrameContainingOffset( + origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation); + MOZ_ASSERT(continuation); + if ( + // A line starts at the origin and the caller wants this included. + aIncludeOrigin && continuation->GetContentOffset() == origOffset && + // If this is the first line of this text node (offset 0), don't treat it + // as a line start if there's something else on the line before this text + // node. + !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) { + return *this; + } + continuation = continuation->GetNextContinuation(); + if (!continuation) { + return TextLeafPoint(); + } + int32_t lineStart = continuation->GetContentOffset(); + lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart)); + return TextLeafPoint(acc, lineStart); +} + +TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc( + nsDirection aDirection, bool aIncludeOrigin) const { + RemoteAccessible* acc = mAcc->AsRemote(); + MOZ_ASSERT(acc); + auto lines = acc->GetCachedTextLines(); + if (!lines) { + return TextLeafPoint(); + } + size_t index; + // If BinarySearch returns true, mOffset is in the array and index points at + // it. If BinarySearch returns false, mOffset is not in the array and index + // points at the next line start after mOffset. + if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) { + if (aIncludeOrigin) { + return *this; + } + if (aDirection == eDirNext) { + // We don't want to include the origin. Get the next line start. + ++index; + } + } + MOZ_ASSERT(index <= lines->Length()); + if ((aDirection == eDirNext && index == lines->Length()) || + (aDirection == eDirPrevious && index == 0)) { + return TextLeafPoint(); + } + // index points at the line start after mOffset. + if (aDirection == eDirPrevious) { + --index; + } + return TextLeafPoint(mAcc, lines->ElementAt(index)); +} + +TextLeafPoint TextLeafPoint::FindLineStartSameAcc( + nsDirection aDirection, bool aIncludeOrigin, + bool aIgnoreListItemMarker) const { + TextLeafPoint boundary; + if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 && + IsLeafAfterListItemMarker()) { + // If: + // (1) we are ignoring list markers + // (2) we should include origin + // (3) we are at the start of a leaf that follows a list item marker + // ...then return this point. + return *this; + } + + if (mAcc->IsLocal()) { + boundary = aDirection == eDirNext + ? FindNextLineStartSameLocalAcc(aIncludeOrigin) + : FindPrevLineStartSameLocalAcc(aIncludeOrigin); + } else { + boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin); + } + + if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary && + mOffset != 0 && IsLeafAfterListItemMarker()) { + // If: + // (1) we are ignoring list markers + // (2) we are searching backwards in accessible + // (3) we did not find a line start before this point + // (4) we are in a leaf that follows a list item marker + // ...then return the first point in this accessible. + boundary = TextLeafPoint(mAcc, 0); + } + + return boundary; +} + +TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc( + bool aIncludeOrigin) const { + if (mOffset == 0 && !aIncludeOrigin) { + // We can't go back any further and the caller doesn't want the origin + // included, so there's nothing more to do. + return TextLeafPoint(); + } + nsAutoString text; + mAcc->AppendTextTo(text); + TextLeafPoint lineStart = *this; + if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 && + text.CharAt(0) == '\n')) { + // We're not interested in a line that starts here, either because + // aIncludeOrigin is false or because we're at the end of a line break + // node. + --lineStart.mOffset; + } + // A word never starts with a line feed character. If there are multiple + // consecutive line feed characters and we're after the first of them, the + // previous line start will be a line feed character. Skip this and any prior + // consecutive line feed first. + for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n'; + --lineStart.mOffset) { + } + if (lineStart.mOffset < 0) { + // There's no line start for our purposes. + lineStart = TextLeafPoint(); + } else { + lineStart = + lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true); + } + // Keep walking backward until we find an acceptable word start. + intl::WordRange word; + if (mOffset == 0) { + word.mBegin = 0; + } else if (mOffset == static_cast<int32_t>(text.Length())) { + word = WordBreaker::FindWord( + text, mOffset - 1, + StaticPrefs::layout_word_select_stop_at_punctuation() + ? FindWordOptions::StopAtPunctuation + : FindWordOptions::None); + } else { + word = WordBreaker::FindWord( + text, mOffset, + StaticPrefs::layout_word_select_stop_at_punctuation() + ? FindWordOptions::StopAtPunctuation + : FindWordOptions::None); + } + for (;; word = WordBreaker::FindWord( + text, word.mBegin - 1, + StaticPrefs::layout_word_select_stop_at_punctuation() + ? FindWordOptions::StopAtPunctuation + : FindWordOptions::None)) { + if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) { + // A word possibly starts at the origin, but the caller doesn't want this + // included. + MOZ_ASSERT(word.mBegin != 0); + continue; + } + if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) { + // A line start always starts a new word. + return lineStart; + } + if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) { + break; + } + if (word.mBegin == 0) { + // We can't go back any further. + if (lineStart) { + // A line start always starts a new word. + return lineStart; + } + return TextLeafPoint(); + } + } + return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin)); +} + +TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc( + bool aIncludeOrigin) const { + nsAutoString text; + mAcc->AppendTextTo(text); + int32_t wordStart = mOffset; + if (aIncludeOrigin) { + if (wordStart == 0) { + if (IsAcceptableWordStart(mAcc, text, 0)) { + return *this; + } + } else { + // The origin might start a word, so search from just before it. + --wordStart; + } + } + TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin); + if (lineStart) { + // A word never starts with a line feed character. If there are multiple + // consecutive line feed characters, lineStart will point at the second of + // them. Skip this and any subsequent consecutive line feed. + for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) && + text.CharAt(lineStart.mOffset) == '\n'; + ++lineStart.mOffset) { + } + if (lineStart.mOffset == static_cast<int32_t>(text.Length())) { + // There's no line start for our purposes. + lineStart = TextLeafPoint(); + } + } + // Keep walking forward until we find an acceptable word start. + intl::WordBreakIteratorUtf16 wordBreakIter(text); + int32_t previousPos = wordStart; + Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart); + for (;;) { + if (!nextBreak || *nextBreak == text.Length()) { + if (lineStart) { + // A line start always starts a new word. + return lineStart; + } + if (StaticPrefs::layout_word_select_stop_at_punctuation()) { + // If layout.word_select.stop_at_punctuation is true, we have to look + // for punctuation class since it may not break state in UAX#29. + for (int32_t i = previousPos + 1; + i < static_cast<int32_t>(text.Length()); i++) { + if (IsAcceptableWordStart(mAcc, text, i)) { + return TextLeafPoint(mAcc, i); + } + } + } + return TextLeafPoint(); + } + wordStart = AssertedCast<int32_t>(*nextBreak); + if (lineStart && wordStart > lineStart.mOffset) { + // A line start always starts a new word. + return lineStart; + } + if (IsAcceptableWordStart(mAcc, text, wordStart)) { + break; + } + + if (StaticPrefs::layout_word_select_stop_at_punctuation()) { + // If layout.word_select.stop_at_punctuation is true, we have to look + // for punctuation class since it may not break state in UAX#29. + for (int32_t i = previousPos + 1; i < wordStart; i++) { + if (IsAcceptableWordStart(mAcc, text, i)) { + return TextLeafPoint(mAcc, i); + } + } + } + previousPos = wordStart; + nextBreak = wordBreakIter.Next(); + } + return TextLeafPoint(mAcc, wordStart); +} + +bool TextLeafPoint::IsCaretAtEndOfLine() const { + MOZ_ASSERT(IsCaret()); + if (LocalAccessible* acc = mAcc->AsLocal()) { + HyperTextAccessible* ht = HyperTextFor(acc); + if (!ht) { + return false; + } + // Use HyperTextAccessible::IsCaretAtEndOfLine. Eventually, we'll want to + // move that code into TextLeafPoint, but existing code depends on it living + // in HyperTextAccessible (including caret events). + return ht->IsCaretAtEndOfLine(); + } + return mAcc->AsRemote()->Document()->IsCaretAtEndOfLine(); +} + +TextLeafPoint TextLeafPoint::ActualizeCaret(bool aAdjustAtEndOfLine) const { + MOZ_ASSERT(IsCaret()); + HyperTextAccessibleBase* ht; + int32_t htOffset; + if (LocalAccessible* acc = mAcc->AsLocal()) { + // Use HyperTextAccessible::CaretOffset. Eventually, we'll want to move + // that code into TextLeafPoint, but existing code depends on it living in + // HyperTextAccessible (including caret events). + ht = HyperTextFor(acc); + if (!ht) { + return TextLeafPoint(); + } + htOffset = ht->CaretOffset(); + if (htOffset == -1) { + return TextLeafPoint(); + } + } else { + // Ideally, we'd cache the caret as a leaf, but our events are based on + // HyperText for now. + std::tie(ht, htOffset) = mAcc->AsRemote()->Document()->GetCaret(); + if (!ht) { + return TextLeafPoint(); + } + } + if (aAdjustAtEndOfLine && htOffset > 0 && IsCaretAtEndOfLine()) { + // It is the same character offset when the caret is visually at the very + // end of a line or the start of a new line (soft line break). Getting text + // at the line should provide the line with the visual caret. Otherwise, + // screen readers will announce the wrong line as the user presses up or + // down arrow and land at the end of a line. + --htOffset; + } + return ht->ToTextLeafPoint(htOffset); +} + +TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType, + nsDirection aDirection, + BoundaryFlags aFlags) const { + if (IsCaret()) { + if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) { + if (IsCaretAtEndOfLine()) { + // The caret is at the end of the line. Return no character. + return ActualizeCaret(/* aAdjustAtEndOfLine */ false); + } + } + return ActualizeCaret().FindBoundary( + aBoundaryType, aDirection, aFlags & BoundaryFlags::eIncludeOrigin); + } + + bool inEditableAndStopInIt = (aFlags & BoundaryFlags::eStopInEditable) && + mAcc->Parent() && + (mAcc->Parent()->State() & states::EDITABLE); + if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) { + return FindLineEnd(aDirection, + inEditableAndStopInIt + ? aFlags + : (aFlags & ~BoundaryFlags::eStopInEditable)); + } + if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) { + return FindWordEnd(aDirection, + inEditableAndStopInIt + ? aFlags + : (aFlags & ~BoundaryFlags::eStopInEditable)); + } + if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START || + aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) && + (aFlags & BoundaryFlags::eIncludeOrigin) && aDirection == eDirPrevious && + IsEmptyLastLine()) { + // If we're at an empty line at the end of an Accessible, we don't want to + // walk into the previous line. For example, this can happen if the caret + // is positioned on an empty line at the end of a textarea. + return *this; + } + bool includeOrigin = !!(aFlags & BoundaryFlags::eIncludeOrigin); + bool ignoreListItemMarker = !!(aFlags & BoundaryFlags::eIgnoreListItemMarker); + Accessible* lastAcc = nullptr; + for (TextLeafPoint searchFrom = *this; searchFrom; + searchFrom = searchFrom.NeighborLeafPoint( + aDirection, inEditableAndStopInIt, ignoreListItemMarker)) { + lastAcc = searchFrom.mAcc; + if (ignoreListItemMarker && searchFrom == *this && + searchFrom.mAcc->Role() == roles::LISTITEM_MARKER) { + continue; + } + TextLeafPoint boundary; + // Search for the boundary within the current Accessible. + switch (aBoundaryType) { + case nsIAccessibleText::BOUNDARY_CHAR: + if (includeOrigin) { + boundary = searchFrom; + } else if (aDirection == eDirPrevious && searchFrom.mOffset > 0) { + boundary.mAcc = searchFrom.mAcc; + boundary.mOffset = searchFrom.mOffset - 1; + } else if (aDirection == eDirNext && + searchFrom.mOffset + 1 < + static_cast<int32_t>( + nsAccUtils::TextLength(searchFrom.mAcc))) { + boundary.mAcc = searchFrom.mAcc; + boundary.mOffset = searchFrom.mOffset + 1; + } + break; + case nsIAccessibleText::BOUNDARY_WORD_START: + if (aDirection == eDirPrevious) { + boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin); + } else { + boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin); + } + break; + case nsIAccessibleText::BOUNDARY_LINE_START: + boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin, + ignoreListItemMarker); + break; + case nsIAccessibleText::BOUNDARY_PARAGRAPH: + boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin, + ignoreListItemMarker); + break; + default: + MOZ_ASSERT_UNREACHABLE(); + break; + } + if (boundary) { + return boundary; + } + + // The start/end of the Accessible might be a boundary. If so, we must stop + // on it. + includeOrigin = true; + } + + MOZ_ASSERT(lastAcc); + // No further leaf was found. Use the start/end of the first/last leaf. + return TextLeafPoint( + lastAcc, aDirection == eDirPrevious + ? 0 + : static_cast<int32_t>(nsAccUtils::TextLength(lastAcc))); +} + +TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection, + BoundaryFlags aFlags) const { + if (aDirection == eDirPrevious && IsEmptyLastLine()) { + // If we're at an empty line at the end of an Accessible, we don't want to + // walk into the previous line. For example, this can happen if the caret + // is positioned on an empty line at the end of a textarea. + // Because we want the line end, we must walk back to the line feed + // character. + return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + aFlags & ~BoundaryFlags::eIncludeOrigin); + } + if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) { + return *this; + } + if (aDirection == eDirPrevious && !(aFlags & BoundaryFlags::eIncludeOrigin)) { + // If there is a line feed immediately before us, return that. + TextLeafPoint prevChar = + FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + aFlags & ~BoundaryFlags::eIncludeOrigin); + if (prevChar.IsLineFeedChar()) { + return prevChar; + } + } + TextLeafPoint searchFrom = *this; + if (aDirection == eDirNext && IsLineFeedChar()) { + // If we search for the next line start from a line feed, we'll get the + // character immediately following the line feed. We actually want the + // next line start after that. Skip the line feed. + searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext, + aFlags & ~BoundaryFlags::eIncludeOrigin); + } + TextLeafPoint lineStart = searchFrom.FindBoundary( + nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aFlags); + if (aDirection == eDirNext && IsEmptyLastLine()) { + // There is a line feed immediately before us, but that's actually the end + // of the previous line, not the end of our empty line. Don't walk back. + return lineStart; + } + // If there is a line feed before this line start (at the end of the previous + // line), we must return that. + TextLeafPoint prevChar = + lineStart.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + aFlags & ~BoundaryFlags::eIncludeOrigin); + if (prevChar && prevChar.IsLineFeedChar()) { + return prevChar; + } + return lineStart; +} + +bool TextLeafPoint::IsSpace() const { + return GetWordBreakClass(GetChar()) == eWbcSpace; +} + +TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection, + BoundaryFlags aFlags) const { + char16_t origChar = GetChar(); + const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace; + bool prevIsSpace = false; + if (aDirection == eDirPrevious || + ((aFlags & BoundaryFlags::eIncludeOrigin) && origIsSpace) || !origChar) { + TextLeafPoint prev = + FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + aFlags & ~BoundaryFlags::eIncludeOrigin); + if (aDirection == eDirPrevious && prev == *this) { + return *this; // Can't go any further. + } + prevIsSpace = prev.IsSpace(); + if ((aFlags & BoundaryFlags::eIncludeOrigin) && + (origIsSpace || IsDocEdge(eDirNext)) && !prevIsSpace) { + // The origin is space or end of document, but the previous + // character is not. This means we're at the end of a word. + return *this; + } + } + TextLeafPoint boundary = *this; + if (aDirection == eDirPrevious && !prevIsSpace) { + // If there isn't space immediately before us, first find the start of the + // previous word. + boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, + eDirPrevious, aFlags); + } else if (aDirection == eDirNext && + (origIsSpace || (!origChar && prevIsSpace))) { + // We're within the space at the end of the word. Skip over the space. We + // can do that by searching for the next word start. + boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext, + aFlags & ~BoundaryFlags::eIncludeOrigin); + if (boundary.IsSpace()) { + // The next word starts with a space. This can happen if there is a space + // after or at the start of a block element. + return boundary; + } + } + if (aDirection == eDirNext) { + BoundaryFlags flags = aFlags; + if (IsDocEdge(eDirPrevious)) { + // If this is the start of the doc don't be inclusive in the word-start + // search because there is no preceding block where this could be a + // word-end for. + flags &= ~BoundaryFlags::eIncludeOrigin; + } + boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, + eDirNext, flags); + } + // At this point, boundary is either the start of a word or at a space. A + // word ends at the beginning of consecutive space. Therefore, skip back to + // the start of any space before us. + TextLeafPoint prev = boundary; + for (;;) { + prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, + aFlags & ~BoundaryFlags::eIncludeOrigin); + if (prev == boundary) { + break; // Can't go any further. + } + if (!prev.IsSpace()) { + break; + } + boundary = prev; + } + return boundary; +} + +TextLeafPoint TextLeafPoint::FindParagraphSameAcc( + nsDirection aDirection, bool aIncludeOrigin, + bool aIgnoreListItemMarker) const { + if (aIncludeOrigin && IsDocEdge(eDirPrevious)) { + // The top of the document is a paragraph boundary. + return *this; + } + + if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 && + IsLeafAfterListItemMarker()) { + // If we are in a list item and the previous sibling is + // a bullet, the 0 offset in this leaf is a line start. + return *this; + } + + if (mAcc->IsTextLeaf() && + // We don't want to copy strings unnecessarily. See below for the context + // of these individual conditions. + ((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext || + mOffset >= 2)) { + // If there is a line feed, a new paragraph begins after it. + nsAutoString text; + mAcc->AppendTextTo(text); + if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') { + return TextLeafPoint(mAcc, mOffset); + } + int32_t lfOffset = -1; + if (aDirection == eDirNext) { + lfOffset = text.FindChar('\n', mOffset); + } else if (mOffset >= 2) { + // A line feed at mOffset - 1 means the origin begins a new paragraph, + // but we already handled aIncludeOrigin above. Therefore, we search from + // mOffset - 2. + lfOffset = text.RFindChar('\n', mOffset - 2); + } + if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) { + return TextLeafPoint(mAcc, lfOffset + 1); + } + } + + if (aIgnoreListItemMarker && mOffset > 0 && aDirection == eDirPrevious && + IsLeafAfterListItemMarker()) { + // No line breaks were found in the preceding text to this offset. + // If we are in a list item and the previous sibling is + // a bullet, the 0 offset in this leaf is a line start. + return TextLeafPoint(mAcc, 0); + } + + // Check whether this Accessible begins a paragraph. + if ((!aIncludeOrigin && mOffset == 0) || + (aDirection == eDirNext && mOffset > 0)) { + // The caller isn't interested in whether this Accessible begins a + // paragraph. + return TextLeafPoint(); + } + Accessible* prevLeaf = PrevLeaf(mAcc); + BlockRule blockRule; + Pivot pivot(nsAccUtils::DocumentFor(mAcc)); + Accessible* prevBlock = pivot.Prev(mAcc, blockRule); + // Check if we're the first leaf after a block element. + if (prevBlock) { + if ( + // If there's no previous leaf, we must be the first leaf after the + // block. + !prevLeaf || + // A block can be a leaf; e.g. an empty div or paragraph. + prevBlock == prevLeaf) { + return TextLeafPoint(mAcc, 0); + } + if (prevBlock->IsAncestorOf(mAcc)) { + // We're inside the block. + if (!prevBlock->IsAncestorOf(prevLeaf)) { + // The previous leaf isn't inside the block. That means we're the first + // leaf in the block. + return TextLeafPoint(mAcc, 0); + } + } else { + // We aren't inside the block, so the block ends before us. + if (prevBlock->IsAncestorOf(prevLeaf)) { + // The previous leaf is inside the block. That means we're the first + // leaf after the block. This case is necessary because a block causes a + // paragraph break both before and after it. + return TextLeafPoint(mAcc, 0); + } + } + } + if (!prevLeaf || prevLeaf->IsHTMLBr()) { + // We're the first leaf after a line break or the start of the document. + return TextLeafPoint(mAcc, 0); + } + if (prevLeaf->IsTextLeaf()) { + // There's a text leaf before us. Check if it ends with a line feed. + nsAutoString text; + prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1); + if (text.CharAt(0) == '\n') { + return TextLeafPoint(mAcc, 0); + } + } + return TextLeafPoint(); +} + +bool TextLeafPoint::IsInSpellingError() const { + if (LocalAccessible* acc = mAcc->AsLocal()) { + auto domRanges = FindDOMSpellingErrors(acc, mOffset, mOffset + 1); + // If there is a spelling error overlapping this character, we're in a + // spelling error. + return !domRanges.IsEmpty(); + } + + RemoteAccessible* acc = mAcc->AsRemote(); + MOZ_ASSERT(acc); + if (!acc->mCachedFields) { + return false; + } + auto spellingErrors = acc->mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::SpellingErrors); + if (!spellingErrors) { + return false; + } + size_t index; + const bool foundOrigin = BinarySearch( + *spellingErrors, 0, spellingErrors->Length(), mOffset, &index); + // In spellingErrors, even indices are start offsets, odd indices are end + // offsets. + const bool foundStart = index % 2 == 0; + if (foundOrigin) { + // mOffset is a spelling error boundary. If it's a start offset, we're in a + // spelling error. + return foundStart; + } + // index points at the next spelling error boundary after mOffset. + if (index == 0) { + return false; // No spelling errors before mOffset. + } + if (foundStart) { + // We're not in a spelling error because it starts after mOffset. + return false; + } + // A spelling error ends after mOffset. + return true; +} + +TextLeafPoint TextLeafPoint::FindSpellingErrorSameAcc( + nsDirection aDirection, bool aIncludeOrigin) const { + if (!aIncludeOrigin && mOffset == 0 && aDirection == eDirPrevious) { + return TextLeafPoint(); + } + if (LocalAccessible* acc = mAcc->AsLocal()) { + // We want to find both start and end points, so we pass true for + // aAllowAdjacent. + auto domRanges = + aDirection == eDirNext + ? FindDOMSpellingErrors(acc, mOffset, + nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT, + /* aAllowAdjacent */ true) + : FindDOMSpellingErrors(acc, 0, mOffset, + /* aAllowAdjacent */ true); + nsINode* node = acc->GetNode(); + if (aDirection == eDirNext) { + for (nsRange* domRange : domRanges) { + if (domRange->GetStartContainer() == node) { + int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( + acc, static_cast<int32_t>(domRange->StartOffset()))); + if ((aIncludeOrigin && matchOffset == mOffset) || + matchOffset > mOffset) { + return TextLeafPoint(mAcc, matchOffset); + } + } + if (domRange->GetEndContainer() == node) { + int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( + acc, static_cast<int32_t>(domRange->EndOffset()))); + if ((aIncludeOrigin && matchOffset == mOffset) || + matchOffset > mOffset) { + return TextLeafPoint(mAcc, matchOffset); + } + } + } + } else { + for (nsRange* domRange : Reversed(domRanges)) { + if (domRange->GetEndContainer() == node) { + int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( + acc, static_cast<int32_t>(domRange->EndOffset()))); + if ((aIncludeOrigin && matchOffset == mOffset) || + matchOffset < mOffset) { + return TextLeafPoint(mAcc, matchOffset); + } + } + if (domRange->GetStartContainer() == node) { + int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset( + acc, static_cast<int32_t>(domRange->StartOffset()))); + if ((aIncludeOrigin && matchOffset == mOffset) || + matchOffset < mOffset) { + return TextLeafPoint(mAcc, matchOffset); + } + } + } + } + return TextLeafPoint(); + } + + RemoteAccessible* acc = mAcc->AsRemote(); + MOZ_ASSERT(acc); + if (!acc->mCachedFields) { + return TextLeafPoint(); + } + auto spellingErrors = acc->mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::SpellingErrors); + if (!spellingErrors) { + return TextLeafPoint(); + } + size_t index; + if (BinarySearch(*spellingErrors, 0, spellingErrors->Length(), mOffset, + &index)) { + // mOffset is in spellingErrors. + if (aIncludeOrigin) { + return *this; + } + if (aDirection == eDirNext) { + // We don't want the origin, so move to the next spelling error boundary + // after mOffset. + ++index; + } + } + // index points at the next spelling error boundary after mOffset. + if (aDirection == eDirNext) { + if (spellingErrors->Length() == index) { + return TextLeafPoint(); // No spelling error boundary after us. + } + return TextLeafPoint(mAcc, (*spellingErrors)[index]); + } + if (index == 0) { + return TextLeafPoint(); // No spelling error boundary before us. + } + // Decrement index so it points at a spelling error boundary before mOffset. + --index; + if ((*spellingErrors)[index] == -1) { + MOZ_ASSERT(index == 0); + // A spelling error starts before mAcc. + return TextLeafPoint(); + } + return TextLeafPoint(mAcc, (*spellingErrors)[index]); +} + +TextLeafPoint TextLeafPoint::NeighborLeafPoint( + nsDirection aDirection, bool aIsEditable, + bool aIgnoreListItemMarker) const { + Accessible* acc = aDirection == eDirPrevious + ? PrevLeaf(mAcc, aIsEditable, aIgnoreListItemMarker) + : NextLeaf(mAcc, aIsEditable, aIgnoreListItemMarker); + if (!acc) { + return TextLeafPoint(); + } + + return TextLeafPoint( + acc, aDirection == eDirPrevious + ? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1 + : 0); +} + +LayoutDeviceIntRect TextLeafPoint::ComputeBoundsFromFrame() const { + LocalAccessible* local = mAcc->AsLocal(); + MOZ_ASSERT(local, "Can't compute bounds in frame from non-local acc"); + nsIFrame* frame = local->GetFrame(); + MOZ_ASSERT(frame, "No frame found for acc!"); + + if (!frame || !frame->IsTextFrame()) { + return local->Bounds(); + } + + // Substring must be entirely within the same text node. + MOZ_ASSERT(frame->IsPrimaryFrame(), + "Cannot compute content offset on non-primary frame"); + nsIFrame::RenderedText text = frame->GetRenderedText( + mOffset, mOffset + 1, nsIFrame::TextOffsetType::OffsetsInRenderedText, + nsIFrame::TrailingWhitespace::DontTrim); + int32_t contentOffset = text.mOffsetWithinNodeText; + int32_t contentOffsetInFrame; + // Get the right frame continuation -- not really a child, but a sibling of + // the primary frame passed in + nsresult rv = frame->GetChildFrameContainingOffset( + contentOffset, true, &contentOffsetInFrame, &frame); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + // Start with this frame's screen rect, which we will shrink based on + // the char we care about within it. + nsRect frameScreenRect = frame->GetScreenRectInAppUnits(); + + // Add the point where the char starts to the frameScreenRect + nsPoint frameTextStartPoint; + rv = frame->GetPointFromOffset(contentOffset, &frameTextStartPoint); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + // Use the next offset to calculate the width + // XXX(morgan) does this work for vertical text? + nsPoint frameTextEndPoint; + rv = frame->GetPointFromOffset(contentOffset + 1, &frameTextEndPoint); + NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect()); + + frameScreenRect.SetRectX( + frameScreenRect.X() + + std::min(frameTextStartPoint.x, frameTextEndPoint.x), + mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x)); + + nsPresContext* presContext = local->Document()->PresContext(); + return LayoutDeviceIntRect::FromAppUnitsToNearest( + frameScreenRect, presContext->AppUnitsPerDevPixel()); +} + +/* static */ +nsTArray<int32_t> TextLeafPoint::GetSpellingErrorOffsets( + LocalAccessible* aAcc) { + nsINode* node = aAcc->GetNode(); + auto domRanges = FindDOMSpellingErrors( + aAcc, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + // Our offsets array will contain two offsets for each range: one for the + // start, one for the end. That is, the array is of the form: + // [r1start, r1end, r2start, r2end, ...] + nsTArray<int32_t> offsets(domRanges.Length() * 2); + for (nsRange* domRange : domRanges) { + if (domRange->GetStartContainer() == node) { + offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset( + aAcc, static_cast<int32_t>(domRange->StartOffset())))); + } else { + // This range overlaps aAcc, but starts before it. + // This can only happen for the first range. + MOZ_ASSERT(domRange == *domRanges.begin() && offsets.IsEmpty()); + // Using -1 here means this won't be treated as the start of a spelling + // error range, while still indicating that we're within a spelling error. + offsets.AppendElement(-1); + } + if (domRange->GetEndContainer() == node) { + offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset( + aAcc, static_cast<int32_t>(domRange->EndOffset())))); + } else { + // This range overlaps aAcc, but ends after it. + // This can only happen for the last range. + MOZ_ASSERT(domRange == *domRanges.rbegin()); + // We don't append -1 here because this would just make things harder for + // a binary search. + } + } + return offsets; +} + +/* static */ +void TextLeafPoint::UpdateCachedSpellingError(dom::Document* aDocument, + const nsRange& aRange) { + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (!docAcc) { + return; + } + LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer()); + LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer()); + if (!startAcc || !endAcc) { + return; + } + for (Accessible* acc = startAcc; acc; acc = NextLeaf(acc)) { + if (acc->IsTextLeaf()) { + docAcc->QueueCacheUpdate(acc->AsLocal(), CacheDomain::Spelling); + } + if (acc == endAcc) { + // Subtle: We check this here rather than in the loop condition because + // we want to include endAcc but stop once we reach it. Putting it in the + // loop condition would mean we stop at endAcc, but we would also exclude + // it; i.e. we wouldn't push the cache for it. + break; + } + } +} + +already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc( + bool aIncludeDefaults) const { + LocalAccessible* acc = mAcc->AsLocal(); + MOZ_ASSERT(acc); + MOZ_ASSERT(acc->IsText()); + // TextAttrsMgr wants a HyperTextAccessible. + LocalAccessible* parent = acc->LocalParent(); + HyperTextAccessible* hyperAcc = parent->AsHyperText(); + MOZ_ASSERT(hyperAcc); + RefPtr<AccAttributes> attributes = new AccAttributes(); + if (hyperAcc) { + TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc, + acc ? acc->IndexInParent() : -1); + mgr.GetAttributes(attributes, nullptr, nullptr); + } + return attributes.forget(); +} + +already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes( + bool aIncludeDefaults) const { + if (!mAcc->IsText()) { + return nullptr; + } + RefPtr<AccAttributes> attrs; + if (mAcc->IsLocal()) { + attrs = GetTextAttributesLocalAcc(aIncludeDefaults); + } else { + attrs = new AccAttributes(); + if (aIncludeDefaults) { + Accessible* parent = mAcc->Parent(); + if (parent && parent->IsRemote() && parent->IsHyperText()) { + if (auto defAttrs = parent->AsRemote()->GetCachedTextAttributes()) { + defAttrs->CopyTo(attrs); + } + } + } + if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) { + thisAttrs->CopyTo(attrs); + } + } + if (IsInSpellingError()) { + attrs->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling); + } + return attrs.forget(); +} + +TextLeafPoint TextLeafPoint::FindTextAttrsStart(nsDirection aDirection, + bool aIncludeOrigin) const { + if (IsCaret()) { + return ActualizeCaret().FindTextAttrsStart(aDirection, aIncludeOrigin); + } + const bool isRemote = mAcc->IsRemote(); + RefPtr<const AccAttributes> lastAttrs = + isRemote ? mAcc->AsRemote()->GetCachedTextAttributes() + : GetTextAttributesLocalAcc(); + if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) { + // Even when searching forward, the only way to know whether the origin is + // the start of a text attrs run is to compare with the previous sibling. + // Anything other than text breaks an attrs run. + TextLeafPoint point; + point.mAcc = mAcc->PrevSibling(); + if (!point.mAcc || !point.mAcc->IsText()) { + return *this; + } + // For RemoteAccessible, we can get attributes from the cache without any + // calculation or copying. + RefPtr<const AccAttributes> attrs = + isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes() + : point.GetTextAttributesLocalAcc(); + if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) { + return *this; + } + } + TextLeafPoint lastPoint = *this; + for (;;) { + if (TextLeafPoint spelling = lastPoint.FindSpellingErrorSameAcc( + aDirection, aIncludeOrigin && lastPoint.mAcc == mAcc)) { + // A spelling error starts or ends somewhere in the Accessible we're + // considering. This causes an attribute change, so return that point. + return spelling; + } + TextLeafPoint point; + point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling() + : lastPoint.mAcc->PrevSibling(); + if (!point.mAcc || !point.mAcc->IsText()) { + break; + } + RefPtr<const AccAttributes> attrs = + isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes() + : point.GetTextAttributesLocalAcc(); + if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) { + // The attributes change here. If we're moving forward, we want to + // return this point. If we're moving backward, we've now moved before + // the start of the attrs run containing the origin, so return that start + // point; i.e. the start of the last Accessible we hit. + if (aDirection == eDirPrevious) { + point = lastPoint; + point.mOffset = 0; + } + if (!aIncludeOrigin && point == *this) { + MOZ_ASSERT(aDirection == eDirPrevious); + // The origin is the start of an attrs run, but the caller doesn't want + // the origin included. + continue; + } + return point; + } + lastPoint = point; + if (aDirection == eDirPrevious) { + // On the next iteration, we want to search for spelling errors from the + // end of this Accessible. + lastPoint.mOffset = + static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc)); + } + lastAttrs = attrs; + } + // We couldn't move any further. Use the start/end. + return TextLeafPoint( + lastPoint.mAcc, + aDirection == eDirPrevious + ? 0 + : static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc))); +} + +LayoutDeviceIntRect TextLeafPoint::CharBounds() { + if (mAcc && !mAcc->IsText()) { + // If we're dealing with an empty container, return the + // accessible's non-text bounds. + return mAcc->Bounds(); + } + + if (!mAcc || (mAcc->IsRemote() && !mAcc->AsRemote()->mCachedFields)) { + return LayoutDeviceIntRect(); + } + + if (LocalAccessible* local = mAcc->AsLocal()) { + if (!local->IsTextLeaf() || nsAccUtils::TextLength(local) == 0) { + // Empty content, use our own bounds to at least get x,y coordinates + return local->Bounds(); + } + + if (mOffset >= 0 && + static_cast<uint32_t>(mOffset) >= nsAccUtils::TextLength(local)) { + // It's valid for a caller to query the length because the caret might be + // at the end of editable text. In that case, we should just silently + // return. However, we assert that the offset isn't greater than the + // length. + NS_ASSERTION( + static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local), + "Wrong in offset"); + return LayoutDeviceIntRect(); + } + + LayoutDeviceIntRect bounds = ComputeBoundsFromFrame(); + + // This document may have a resolution set, we will need to multiply + // the document-relative coordinates by that value and re-apply the doc's + // screen coordinates. + nsPresContext* presContext = local->Document()->PresContext(); + nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); + LayoutDeviceIntRect orgRectPixels = + LayoutDeviceIntRect::FromAppUnitsToNearest( + rootFrame->GetScreenRectInAppUnits(), + presContext->AppUnitsPerDevPixel()); + bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y()); + bounds.ScaleRoundOut(presContext->PresShell()->GetResolution()); + bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y()); + return bounds; + } + + RemoteAccessible* remote = mAcc->AsRemote(); + nsRect charBounds = remote->GetCachedCharRect(mOffset); + if (!charBounds.IsEmpty()) { + return remote->BoundsWithOffset(Some(charBounds)); + } + + return LayoutDeviceIntRect(); +} + +bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) { + if (mAcc && !mAcc->IsText()) { + // If we're dealing with an empty embedded object, use the + // accessible's non-text bounds. + return mAcc->Bounds().Contains(aX, aY); + } + + return CharBounds().Contains(aX, aY); +} + +bool TextLeafRange::Crop(Accessible* aContainer) { + TextLeafPoint containerStart(aContainer, 0); + TextLeafPoint containerEnd(aContainer, + nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + + if (mEnd < containerStart || containerEnd < mStart) { + // The range ends before the container, or starts after it. + return false; + } + + if (mStart < containerStart) { + // If range start is before container start, adjust range start to + // start of container. + mStart = containerStart; + } + + if (containerEnd < mEnd) { + // If range end is after container end, adjust range end to end of + // container. + mEnd = containerEnd; + } + + return true; +} + +LayoutDeviceIntRect TextLeafRange::Bounds() const { + if (mEnd == mStart || mEnd < mStart) { + return LayoutDeviceIntRect(); + } + + bool locatedFinalLine = false; + TextLeafPoint currPoint = mStart; + LayoutDeviceIntRect result = currPoint.CharBounds(); + + // Union the first and last chars of each line to create a line rect. Then, + // union the lines together. + while (!locatedFinalLine) { + // Fetch the last point in the current line by getting the + // start of the next line and going back one char. We don't + // use BOUNDARY_LINE_END here because it is equivalent to LINE_START when + // the line doesn't end with a line feed character. + TextLeafPoint lineStartPoint = currPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_LINE_START, eDirNext); + TextLeafPoint lastPointInLine = lineStartPoint.FindBoundary( + nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); + // If currPoint is the end of the document, lineStartPoint will be equal + // to currPoint and we would be in an endless loop. + if (lineStartPoint == currPoint || mEnd <= lastPointInLine) { + lastPointInLine = mEnd; + locatedFinalLine = true; + } + + LayoutDeviceIntRect currLine = currPoint.CharBounds(); + currLine.UnionRect(currLine, lastPointInLine.CharBounds()); + result.UnionRect(result, currLine); + + currPoint = lineStartPoint; + } + + return result; +} + +bool TextLeafRange::SetSelection(int32_t aSelectionNum) const { + if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) { + return false; + } + + if (mStart.mAcc->IsRemote()) { + DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document(); + if (doc != mEnd.mAcc->AsRemote()->Document()) { + return false; + } + + Unused << doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset, + mEnd.mAcc->ID(), mEnd.mOffset, + aSelectionNum); + return true; + } + + bool reversed = mEnd < mStart; + auto [startContent, startContentOffset] = + !reversed ? mStart.ToDOMPoint(false) : mEnd.ToDOMPoint(false); + auto [endContent, endContentOffset] = + !reversed ? mEnd.ToDOMPoint(false) : mStart.ToDOMPoint(false); + + if (!startContent || !endContent) { + return false; + } + + RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent); + if (!domSel) { + return false; + } + + uint32_t rangeCount = domSel->RangeCount(); + RefPtr<nsRange> domRange = nullptr; + if (aSelectionNum == static_cast<int32_t>(rangeCount) || aSelectionNum < 0) { + domRange = nsRange::Create(startContent); + } else { + domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum)); + } + if (!domRange) { + return false; + } + + domRange->SetStart(startContent, startContentOffset); + domRange->SetEnd(endContent, endContentOffset); + + // 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(*domRange, + IgnoreErrors()); + } + + IgnoredErrorResult err; + domSel->AddRangeAndSelectFramesAndNotifyListeners(*domRange, 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 TextLeafRange::ScrollIntoView(uint32_t aScrollType) const { + if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) { + return; + } + + if (mStart.mAcc->IsRemote()) { + DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document(); + if (doc != mEnd.mAcc->AsRemote()->Document()) { + // Can't scroll range that spans docs. + return; + } + + Unused << doc->SendScrollTextLeafRangeIntoView( + mStart.mAcc->ID(), mStart.mOffset, mEnd.mAcc->ID(), mEnd.mOffset, + aScrollType); + return; + } + + auto [startContent, startContentOffset] = mStart.ToDOMPoint(); + auto [endContent, endContentOffset] = mEnd.ToDOMPoint(); + + if (!startContent || !endContent) { + return; + } + + ErrorResult er; + RefPtr<nsRange> domRange = nsRange::Create(startContent, startContentOffset, + endContent, endContentOffset, er); + if (er.Failed()) { + return; + } + + nsCoreUtils::ScrollSubstringTo(mStart.mAcc->AsLocal()->GetFrame(), domRange, + aScrollType); +} + +TextLeafRange::Iterator TextLeafRange::Iterator::BeginIterator( + const TextLeafRange& aRange) { + Iterator result(aRange); + + result.mSegmentStart = aRange.mStart; + if (aRange.mStart.mAcc == aRange.mEnd.mAcc) { + result.mSegmentEnd = aRange.mEnd; + } else { + result.mSegmentEnd = TextLeafPoint( + aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + } + + return result; +} + +TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator( + const TextLeafRange& aRange) { + Iterator result(aRange); + + result.mSegmentEnd = TextLeafPoint(); + result.mSegmentStart = TextLeafPoint(); + + return result; +} + +TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() { + if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) { + mSegmentEnd = TextLeafPoint(); + mSegmentStart = TextLeafPoint(); + return *this; + } + + if (Accessible* nextLeaf = NextLeaf(mSegmentEnd.mAcc)) { + mSegmentStart = TextLeafPoint(nextLeaf, 0); + if (nextLeaf == mRange.mEnd.mAcc) { + mSegmentEnd = TextLeafPoint(nextLeaf, mRange.mEnd.mOffset); + } else { + mSegmentEnd = + TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); + } + } else { + mSegmentEnd = TextLeafPoint(); + mSegmentStart = TextLeafPoint(); + } + + return *this; +} + +} // namespace mozilla::a11y diff --git a/accessible/base/TextLeafRange.h b/accessible/base/TextLeafRange.h new file mode 100644 index 0000000000..23fea2ecfb --- /dev/null +++ b/accessible/base/TextLeafRange.h @@ -0,0 +1,360 @@ +/* -*- 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_TextLeafRange_h__ +#define mozilla_a11y_TextLeafRange_h__ + +#include <stdint.h> + +#include "AccAttributes.h" +#include "nsDirection.h" +#include "nsIAccessibleText.h" + +class nsRange; + +namespace mozilla { +namespace dom { +class Document; +} + +namespace a11y { +class Accessible; +class LocalAccessible; + +/** + * Represents a point within accessible text. + * This is stored as a leaf Accessible and an offset into that Accessible. + * For an empty Accessible, the offset will always be 0. + * This will eventually replace TextPoint. Unlike TextPoint, this does not + * use HyperTextAccessible offsets. + */ +class TextLeafPoint final { + public: + TextLeafPoint(Accessible* aAcc, int32_t aOffset); + + /** + * Constructs an invalid TextPoint (mAcc is null). + * A TextLeafPoint in this state will evaluate to false. + * mAcc can be set later. Alternatively, this can be used to indicate an error + * (e.g. if a requested point couldn't be found). + */ + TextLeafPoint() : mAcc(nullptr), mOffset(0) {} + + /** + * Construct a TextLeafPoint representing the caret. + * The actual offset used for the caret differs depending on whether the + * caret is at the end of a line and the query being made. Thus, mOffset on + * the returned TextLeafPoint is not a valid offset. + */ + static TextLeafPoint GetCaret(Accessible* aAcc) { + return TextLeafPoint(aAcc, nsIAccessibleText::TEXT_OFFSET_CARET); + } + + Accessible* mAcc; + int32_t mOffset; + + bool operator==(const TextLeafPoint& aPoint) const { + return mAcc == aPoint.mAcc && mOffset == aPoint.mOffset; + } + + bool operator!=(const TextLeafPoint& aPoint) const { + return !(*this == aPoint); + } + + bool operator<(const TextLeafPoint& aPoint) const; + + bool operator<=(const TextLeafPoint& aPoint) const; + + /** + * A valid TextLeafPoint evaluates to true. An invalid TextLeafPoint + * evaluates to false. + */ + explicit operator bool() const { return !!mAcc; } + + bool IsCaret() const { + return mOffset == nsIAccessibleText::TEXT_OFFSET_CARET; + } + + bool IsCaretAtEndOfLine() const; + + /** + * Get a TextLeafPoint at the actual caret offset. + * This should only be called on a TextLeafPoint created with GetCaret. + * If aAdjustAtEndOfLine is true, the point will be adjusted if the caret is + * at the end of a line so that word and line boundaries can be calculated + * correctly. + */ + TextLeafPoint ActualizeCaret(bool aAdjustAtEndOfLine = true) const; + + enum class BoundaryFlags : uint32_t { + eDefaultBoundaryFlags = 0, + // Return point unchanged if it is at the given boundary type. + eIncludeOrigin = 1 << 0, + // If current point is in editable, return point within samme editable. + eStopInEditable = 1 << 1, + // Skip over list items in searches and don't consider them line or + // paragraph starts. + eIgnoreListItemMarker = 1 << 2, + }; + + /** + * Find a boundary (word start, line start, etc.) in a specific direction. + * If no boundary is found, the start/end of the document is returned + * (depending on the direction). + */ + TextLeafPoint FindBoundary( + AccessibleTextBoundary aBoundaryType, nsDirection aDirection, + BoundaryFlags aFlags = BoundaryFlags::eDefaultBoundaryFlags) const; + + /** + * These two functions find a line start boundary within the same + * LocalAccessible as this. That is, they do not cross Accessibles. If no + * boundary is found, an invalid TextLeafPoint is returned. + * These are used by FindBoundary. Most callers will want FindBoundary + * instead. + */ + TextLeafPoint FindPrevLineStartSameLocalAcc(bool aIncludeOrigin) const; + TextLeafPoint FindNextLineStartSameLocalAcc(bool aIncludeOrigin) const; + + /** + * These two functions find a word start boundary within the same + * Accessible as this. That is, they do not cross Accessibles. If no + * boundary is found, an invalid TextLeafPoint is returned. + * These are used by FindBoundary. Most callers will want FindBoundary + * instead. + */ + TextLeafPoint FindPrevWordStartSameAcc(bool aIncludeOrigin) const; + TextLeafPoint FindNextWordStartSameAcc(bool aIncludeOrigin) const; + + /** + * Get the text attributes at this point. + * If aIncludeDefaults is true, default attributes on the HyperTextAccessible + * will be included. + */ + already_AddRefed<AccAttributes> GetTextAttributes( + bool aIncludeDefaults = true) const; + + /** + * Get Get the text attributes at this point in a LocalAccessible. + * This is used by GetTextAttributes. Most callers will want GetTextAttributes + * instead. + */ + already_AddRefed<AccAttributes> GetTextAttributesLocalAcc( + bool aIncludeDefaults = true) const; + + /** + * Get the offsets of all spelling errors in a given LocalAccessible. This + * should only be used when pushing the cache. Most callers will want + * FindTextAttrsStart instead. + */ + static nsTArray<int32_t> GetSpellingErrorOffsets(LocalAccessible* aAcc); + + /** + * Queue a cache update for a spelling error in a given DOM range. + */ + static void UpdateCachedSpellingError(dom::Document* aDocument, + const nsRange& aRange); + + /** + * Find the start of a run of text attributes in a specific direction. + * A text attributes run is a span of text where the attributes are the same. + * If no boundary is found, the start/end of the container is returned + * (depending on the direction). + * If aIncludeorigin is true and this is at a boundary, this will be + * returned unchanged. + */ + TextLeafPoint FindTextAttrsStart(nsDirection aDirection, + bool aIncludeOrigin = false) const; + + /** + * Returns a rect (in dev pixels) describing position and size of + * the character at mOffset in mAcc. This rect is screen-relative. + * This function only works on remote accessibles, and assumes caching + * is enabled. + */ + LayoutDeviceIntRect CharBounds(); + + /** + * Returns true if the given point (in screen coords) is contained + * in the char bounds of the current TextLeafPoint. Returns false otherwise. + * If the current point is an empty container, we use the acc's bounds instead + * of char bounds. Because this depends on CharBounds, this function only + * works on remote accessibles, and assumes caching is enabled. + */ + bool ContainsPoint(int32_t aX, int32_t aY); + + bool IsLineFeedChar() const { return GetChar() == '\n'; } + + bool IsSpace() const; + + bool IsParagraphStart(bool aIgnoreListItemMarker = false) const { + return mOffset == 0 && + FindParagraphSameAcc(eDirPrevious, true, aIgnoreListItemMarker); + } + + /** + * Translate given TextLeafPoint into a DOM point. + */ + MOZ_CAN_RUN_SCRIPT std::pair<nsIContent*, int32_t> ToDOMPoint( + bool aIncludeGenerated = true) const; + + private: + bool IsEmptyLastLine() const; + + bool IsDocEdge(nsDirection aDirection) const; + + bool IsLeafAfterListItemMarker() const; + + char16_t GetChar() const; + + TextLeafPoint FindLineStartSameRemoteAcc(nsDirection aDirection, + bool aIncludeOrigin) const; + + /** + * Helper which just calls the appropriate function based on whether mAcc + *is local or remote. + */ + TextLeafPoint FindLineStartSameAcc(nsDirection aDirection, + bool aIncludeOrigin, + bool aIgnoreListItemMarker = false) const; + + TextLeafPoint FindLineEnd(nsDirection aDirection, BoundaryFlags aFlags) const; + TextLeafPoint FindWordEnd(nsDirection aDirection, BoundaryFlags aFlags) const; + + TextLeafPoint FindParagraphSameAcc(nsDirection aDirection, + bool aIncludeOrigin, + bool aIgnoreListItemMarker = false) const; + + bool IsInSpellingError() const; + + /** + * Find a spelling error boundary in the same Accessible. This function + * searches for either start or end points, since either means a change in + * text attributes. + */ + TextLeafPoint FindSpellingErrorSameAcc(nsDirection aDirection, + bool aIncludeOrigin) const; + + // Return the point immediately succeeding or preceding this leaf depending + // on given direction. + TextLeafPoint NeighborLeafPoint(nsDirection aDirection, bool aIsEditable, + bool aIgnoreListItemMarker) const; + + /** + * This function assumes mAcc is a LocalAccessible. + * It iterates the continuations of mAcc's primary frame until it locates + * the continuation containing mOffset (a rendered offset). It then uses + * GetScreenRectInAppUnits to compute screen coords for the frame, resizing + * such that the resulting rect contains only one character. + */ + LayoutDeviceIntRect ComputeBoundsFromFrame() const; +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(TextLeafPoint::BoundaryFlags) + +/** + * Represents a range of accessible text. + * This will eventually replace TextRange. + */ +class TextLeafRange final { + public: + TextLeafRange(const TextLeafPoint& aStart, const TextLeafPoint& aEnd) + : mStart(aStart), mEnd(aEnd) {} + explicit TextLeafRange(const TextLeafPoint& aStart) + : mStart(aStart), mEnd(aStart) {} + explicit TextLeafRange() {} + + /** + * A valid TextLeafRange evaluates to true. An invalid TextLeafRange + * evaluates to false. + */ + explicit operator bool() const { return !!mStart && !!mEnd; } + + bool operator!=(const TextLeafRange& aOther) const { + return mEnd != aOther.mEnd || mStart != aOther.mStart; + } + + bool operator==(const TextLeafRange& aOther) const { + return mEnd == aOther.mEnd && mStart == aOther.mStart; + } + + TextLeafPoint Start() const { return mStart; } + void SetStart(const TextLeafPoint& aStart) { mStart = aStart; } + TextLeafPoint End() const { return mEnd; } + void SetEnd(const TextLeafPoint& aEnd) { mEnd = aEnd; } + + bool Crop(Accessible* aContainer); + + /** + * Returns a union rect (in dev pixels) of all character bounds in this range. + * This rect is screen-relative and inclusive of mEnd. This function only + * works on remote accessibles, and assumes caching is enabled. + */ + LayoutDeviceIntRect Bounds() const; + + /** + * Set range as DOM selection. + * aSelectionNum is the selection index to use. If aSelectionNum is + * out of bounds for current selection ranges, or is -1, a new selection + * range is created. + */ + MOZ_CAN_RUN_SCRIPT bool SetSelection(int32_t aSelectionNum) const; + + MOZ_CAN_RUN_SCRIPT void ScrollIntoView(uint32_t aScrollType) const; + + private: + TextLeafPoint mStart; + TextLeafPoint mEnd; + + public: + /** + * A TextLeafRange iterator will iterate through single leaf segments of the + * given range. + */ + + class Iterator { + public: + Iterator(Iterator&& aOther) + : mRange(aOther.mRange), + mSegmentStart(aOther.mSegmentStart), + mSegmentEnd(aOther.mSegmentEnd) {} + + static Iterator BeginIterator(const TextLeafRange& aRange); + + static Iterator EndIterator(const TextLeafRange& aRange); + + Iterator& operator++(); + + bool operator!=(const Iterator& aOther) const { + return mRange != aOther.mRange || mSegmentStart != aOther.mSegmentStart || + mSegmentEnd != aOther.mSegmentEnd; + } + + TextLeafRange operator*() { + return TextLeafRange(mSegmentStart, mSegmentEnd); + } + + private: + explicit Iterator(const TextLeafRange& aRange) : mRange(aRange) {} + + Iterator() = delete; + Iterator(const Iterator&) = delete; + Iterator& operator=(const Iterator&) = delete; + Iterator& operator=(const Iterator&&) = delete; + + const TextLeafRange& mRange; + TextLeafPoint mSegmentStart; + TextLeafPoint mSegmentEnd; + }; + + Iterator begin() const { return Iterator::BeginIterator(*this); } + Iterator end() const { return Iterator::EndIterator(*this); } +}; + +} // 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..3c53bd5038 --- /dev/null +++ b/accessible/base/TextRange-inl.h @@ -0,0 +1,25 @@ +/* -*- 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" + +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..15ff8bd05a --- /dev/null +++ b/accessible/base/TextRange.cpp @@ -0,0 +1,376 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TextRange-inl.h" + +#include "LocalAccessible-inl.h" +#include "HyperTextAccessible-inl.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/dom/Selection.h" +#include "nsAccUtils.h" + +namespace mozilla { +namespace a11y { + +/** + * Returns a text point for aAcc within aContainer. + */ +static void ToTextPoint(Accessible* aAcc, Accessible** aContainer, + int32_t* aOffset, bool aIsBefore = true) { + if (aAcc->IsHyperText()) { + *aContainer = aAcc; + *aOffset = + aIsBefore + ? 0 + : static_cast<int32_t>(aAcc->AsHyperTextBase()->CharacterCount()); + return; + } + + Accessible* child = nullptr; + Accessible* parent = aAcc; + do { + child = parent; + parent = parent->Parent(); + } while (parent && !parent->IsHyperText()); + + if (parent) { + *aContainer = parent; + *aOffset = parent->AsHyperTextBase()->GetChildOffset( + child->IndexInParent() + static_cast<int32_t>(!aIsBefore)); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(Accessible* aRoot, Accessible* aStartContainer, + int32_t aStartOffset, Accessible* aEndContainer, + int32_t aEndOffset) + : mRoot(aRoot), + mStartContainer(aStartContainer), + mEndContainer(aEndContainer), + mStartOffset(aStartOffset), + mEndOffset(aEndOffset) {} + +bool TextRange::Crop(Accessible* aContainer) { + uint32_t boundaryPos = 0, containerPos = 0; + AutoTArray<Accessible*, 30> boundaryParents, containerParents; + + // Crop the start boundary. + Accessible* container = nullptr; + HyperTextAccessibleBase* startHyper = mStartContainer->AsHyperTextBase(); + Accessible* boundary = startHyper->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. + ToTextPoint(aContainer, &mStartContainer, &mStartOffset); + } else { + // The start boundary and the container are siblings. + container = aContainer; + } + } else { + // 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()) { + ToTextPoint(container, &mStartContainer, &mStartOffset); + } + } + + boundaryParents.SetLengthAndRetainStorage(0); + containerParents.SetLengthAndRetainStorage(0); + } + + HyperTextAccessibleBase* endHyper = mEndContainer->AsHyperTextBase(); + boundary = endHyper->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) { + ToTextPoint(aContainer, &mEndContainer, &mEndOffset, false); + } else { + container = aContainer; + } + } else { + boundary = boundaryParents[boundaryPos]; + container = containerParents[containerPos]; + } + + if (!container) { + return true; + } + + if (boundary->IndexInParent() < container->IndexInParent()) { + return !!(mRoot = nullptr); + } + + if (boundary->IndexInParent() > container->IndexInParent()) { + ToTextPoint(container, &mEndContainer, &mEndOffset, false); + } + + return true; +} + +/** + * 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 { + MOZ_ASSERT(mRoot->IsLocal(), "Not supported for RemoteAccessible"); + bool reversed = EndPoint() < StartPoint(); + if (aReversed) { + *aReversed = reversed; + } + + HyperTextAccessible* startHyper = mStartContainer->AsLocal()->AsHyperText(); + HyperTextAccessible* endHyper = mEndContainer->AsLocal()->AsHyperText(); + DOMPoint startPoint = reversed ? endHyper->OffsetToDOMPoint(mEndOffset) + : startHyper->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 ? startHyper->OffsetToDOMPoint(mStartOffset) + : endHyper->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()); + + const uint32_t rangeCount = aSelection->RangeCount(); + for (const uint32_t idx : IntegerRange(rangeCount)) { + MOZ_ASSERT(aSelection->RangeCount() == rangeCount); + const nsRange* DOMRange = aSelection->GetRangeAt(idx); + MOZ_ASSERT(DOMRange); + 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(Accessible* aRoot, Accessible* aStartContainer, + int32_t aStartOffset, Accessible* aEndContainer, + int32_t aEndOffset) { + mRoot = aRoot; + mStartContainer = aStartContainer; + mEndContainer = aEndContainer; + mStartOffset = aStartOffset; + mEndOffset = aEndOffset; +} + +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..121dbe8399 --- /dev/null +++ b/accessible/base/TextRange.h @@ -0,0 +1,164 @@ +/* -*- 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 "nsTArray.h" + +class nsRange; + +namespace mozilla { +namespace dom { +class Selection; +} // namespace dom +namespace a11y { + +class Accessible; +class LocalAccessible; + +/** + * A text point (HyperText + offset), represents a boundary of text range. + * In new code, This should only be used when you explicitly need to deal with + * HyperText containers and offsets, including embedded objects; e.g. for + * IAccessible2 and ATK. Otherwise, use TextLeafPoint instead. + */ +struct TextPoint final { + TextPoint(Accessible* aContainer, int32_t aOffset) + : mContainer(aContainer), mOffset(aOffset) {} + TextPoint(const TextPoint& aPoint) + : mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) {} + + Accessible* 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 HyperText range within the text control or document. + * In new code, This should only be used when you explicitly need to deal with + * HyperText containers and offsets, including embedded objects; e.g. for + * IAccessible2 and ATK. Otherwise, use TextLeafRange instead. + */ +class TextRange final { + public: + TextRange(Accessible* aRoot, Accessible* aStartContainer, + int32_t aStartOffset, Accessible* 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; + } + + Accessible* Root() { return mRoot; } + Accessible* StartContainer() const { return mStartContainer; } + int32_t StartOffset() const { return mStartOffset; } + Accessible* 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; + + /** + * Crops the range if it overlaps the given accessible element boundaries, + * returns true if the range was cropped successfully. + */ + bool Crop(Accessible* aContainer); + + /** + * 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(Accessible* aContainer, int32_t aOffset) { + mStartContainer = aContainer; + mStartOffset = aOffset; + } + void SetEndPoint(Accessible* 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(Accessible* aRoot, Accessible* aStartContainer, int32_t aStartOffset, + Accessible* aEndContainer, int32_t aEndOffset); + + /** + * 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; + + Accessible* mRoot; + Accessible* mStartContainer; + Accessible* 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..29f8ac11f3 --- /dev/null +++ b/accessible/base/TextUpdater.cpp @@ -0,0 +1,215 @@ +/* -*- 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 "CacheConstants.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); + aDocument->QueueCacheUpdate(aTextLeaf, CacheDomain::Text); + } +} + +void TextUpdater::DoUpdate(const nsAString& aNewText, const nsAString& aOldText, + uint32_t aSkipStart) { + LocalAccessible* parent = mTextLeaf->LocalParent(); + 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!"); + + // Don't bother diffing if the hypertext isn't editable. Diffing non-editable + // text can lead to weird screen reader results with live regions, e.g., + // changing "text" to "testing" might read the diff "s ing" when we'd really + // just like to hear "testing." + if (!mHyperText->IsEditable()) { + // Fire text change event for removal. + RefPtr<AccEvent> textRemoveEvent = + new AccTextChangeEvent(mHyperText, mTextOffset, aOldText, false); + mDocument->FireDelayedEvent(textRemoveEvent); + + // Fire text change event for insertion if there's text to insert. + if (!aNewText.IsEmpty()) { + RefPtr<AccEvent> textInsertEvent = + new AccTextChangeEvent(mHyperText, mTextOffset, aNewText, true); + mDocument->FireDelayedEvent(textInsertEvent); + } + + mDocument->MaybeNotifyOfValueChange(mHyperText); + + // Update the text. + mTextLeaf->SetText(aNewText); + return; + } + + 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..ff314bd83c --- /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 "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(LocalAccessible* 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(LocalAccessible* 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); } + +LocalAccessible* TreeWalker::Scope(nsIContent* aAnchorNode) { + Reset(); + + mAnchorNode = aAnchorNode; + + mFlags |= eScoped; + + bool skipSubtree = false; + LocalAccessible* 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. + LocalAccessible* child = mDoc->GetAccessible(childNode); + if (child && child->IsRelocated()) { + MOZ_ASSERT( + !(mFlags & eScoped), + "Walker should not be scoped when seeking into relocated children"); + if (child->LocalParent() != mContext) { + return false; + } + + LocalAccessible* 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); + + MOZ_ASSERT_UNREACHABLE("because the do-while loop never breaks"); +} + +LocalAccessible* TreeWalker::Next() { + if (mStateStack.IsEmpty()) { + if (mPhase == eAtEnd) { + return nullptr; + } + + if (mPhase == eAtDOM || mPhase == eAtARIAOwns) { + if (!(mFlags & eScoped)) { + mPhase = eAtARIAOwns; + LocalAccessible* 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; + LocalAccessible* 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(); +} + +LocalAccessible* 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; + LocalAccessible* 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; +} + +LocalAccessible* TreeWalker::AccessibleFor(nsIContent* aNode, uint32_t aFlags, + bool* aSkipSubtree) { + // Ignore the accessible and its subtree if it was repositioned by means + // of aria-owns. + LocalAccessible* 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..9093d8187f --- /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 LocalAccessible; +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(LocalAccessible* 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(LocalAccessible* 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. + */ + LocalAccessible* 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. + */ + LocalAccessible* Next(); + LocalAccessible* Prev(); + + LocalAccessible* 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. + */ + LocalAccessible* 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; + LocalAccessible* 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..4687e85814 --- /dev/null +++ b/accessible/base/XULMap.h @@ -0,0 +1,115 @@ +/* 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, XULMenuitemAccessible) +XULMAP_TYPE(menubar, XULMenubarAccessible) +XULMAP_TYPE(menucaption, XULMenuitemAccessible) +XULMAP_TYPE(menuitem, XULMenuitemAccessible) +XULMAP_TYPE(menulist, XULComboboxAccessible) +XULMAP_TYPE(menuseparator, XULMenuSeparatorAccessible) +XULMAP_TYPE(notification, XULAlertAccessible) +XULMAP_TYPE(radio, XULRadioButtonAccessible) +XULMAP_TYPE(radiogroup, XULRadioGroupAccessible) +XULMAP_TYPE(richlistbox, XULListboxAccessible) +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(treecols, XULTreeColumAccessible) +XULMAP_TYPE(toolbar, XULToolbarAccessible) +XULMAP_TYPE(toolbarbutton, XULToolbarButtonAccessible) + +XULMAP(description, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + if (aElement->ClassList()->Contains(u"tooltip-label"_ns)) { + // FIXME(emilio): Why this special case? + return nullptr; + } + + return new XULLabelAccessible(aElement, aContext->Document()); + }) + +XULMAP(tooltip, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + return new XULTooltipAccessible(aElement, aContext->Document()); + }) + +XULMAP(label, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + if (aElement->ClassList()->Contains(u"text-link"_ns)) { + return new XULLinkAccessible(aElement, aContext->Document()); + } + return new XULLabelAccessible(aElement, aContext->Document()); + }) + +XULMAP(image, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + // Don't include nameless images in accessible tree. + if (!aElement->HasAttr(nsGkAtoms::tooltiptext)) { + return nullptr; + } + + return new ImageAccessible(aElement, aContext->Document()); + }) + +XULMAP(menupopup, [](Element* aElement, LocalAccessible* aContext) { + return CreateMenupopupAccessible(aElement, aContext); +}) + +XULMAP(panel, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + 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(tree, + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { + 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 XULTreeGridAccessible(aElement, aContext->Document(), + treeFrame); + }) diff --git a/accessible/base/moz.build b/accessible/base/moz.build new file mode 100644 index 0000000000..b65c90ceba --- /dev/null +++ b/accessible/base/moz.build @@ -0,0 +1,122 @@ +# -*- 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/. + +GeneratedFile( + "RelationType.h", + script="/accessible/base/RelationTypeGen.py", + entry_point="generate", + inputs=["/accessible/interfaces/nsIAccessibleRelation.idl"], +) +GeneratedFile( + "Role.h", + script="/accessible/base/RoleHGen.py", + entry_point="generate", + inputs=["/accessible/interfaces/nsIAccessibleRole.idl"], +) + +EXPORTS += ["AccEvent.h", "nsAccessibilityService.h"] + +EXPORTS.mozilla.a11y += [ + "!RelationType.h", + "!Role.h", + "AccAttributes.h", + "AccGroupInfo.h", + "AccTypes.h", + "CacheConstants.h", + "DocManager.h", + "FocusManager.h", + "IDSet.h", + "Platform.h", + "SelectionManager.h", + "States.h", +] + +if CONFIG["MOZ_DEBUG"]: + EXPORTS.mozilla.a11y += [ + "Logging.h", + ] + +UNIFIED_SOURCES += [ + "AccAttributes.cpp", + "AccEvent.cpp", + "AccGroupInfo.cpp", + "AccIterator.cpp", + "ARIAMap.cpp", + "ARIAStateMap.cpp", + "Asserts.cpp", + "CachedTableAccessible.cpp", + "DocManager.cpp", + "EmbeddedObjCollector.cpp", + "EventQueue.cpp", + "EventTree.cpp", + "Filters.cpp", + "FocusManager.cpp", + "NotificationController.cpp", + "nsAccessibilityService.cpp", + "nsAccUtils.cpp", + "nsCoreUtils.cpp", + "nsEventShell.cpp", + "nsTextEquivUtils.cpp", + "Pivot.cpp", + "SelectionManager.cpp", + "StyleInfo.cpp", + "TextAttrs.cpp", + "TextLeafRange.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", +] + +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", + "/gfx/cairo/cairo/src", + ] +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") diff --git a/accessible/base/nsAccCache.h b/accessible/base/nsAccCache.h new file mode 100644 index 0000000000..db0c877035 --- /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_ + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible 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..82af56348f --- /dev/null +++ b/accessible/base/nsAccUtils.cpp @@ -0,0 +1,626 @@ +/* -*- 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 "AccAttributes.h" +#include "ARIAMap.h" +#include "nsCoreUtils.h" +#include "nsGenericHTMLElement.h" +#include "DocAccessible.h" +#include "DocAccessibleParent.h" +#include "HyperTextAccessible.h" +#include "nsIAccessibleTypes.h" +#include "mozilla/a11y/Role.h" +#include "States.h" +#include "TextLeafAccessible.h" + +#include "nsIBaseWindow.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIDOMXULContainerElement.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ElementInternals.h" +#include "nsAccessibilityService.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +void nsAccUtils::SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel, + int32_t aSetSize, int32_t aPosInSet) { + nsAutoString value; + + if (aLevel) { + aAttributes->SetAttribute(nsGkAtoms::level, aLevel); + } + + if (aSetSize && aPosInSet) { + aAttributes->SetAttribute(nsGkAtoms::posinset, aPosInSet); + aAttributes->SetAttribute(nsGkAtoms::setsize, aSetSize); + } +} + +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(AccAttributes* aAttributes, + Accessible* aStartAcc) { + nsAutoString live, relevant, busy; + nsStaticAtom* role = nullptr; + Maybe<bool> atomic; + for (Accessible* acc = aStartAcc; acc; acc = acc->Parent()) { + // We only want the nearest value for each attribute. If we already got a + // value, don't bother fetching it from further ancestors. + const bool wasLiveEmpty = live.IsEmpty(); + acc->LiveRegionAttributes(wasLiveEmpty ? &live : nullptr, + relevant.IsEmpty() ? &relevant : nullptr, + atomic ? nullptr : &atomic, + busy.IsEmpty() ? &busy : nullptr); + if (wasLiveEmpty) { + const nsRoleMapEntry* roleMap = acc->ARIARoleMap(); + if (live.IsEmpty()) { + // aria-live wasn't explicitly set. See if an aria-live value is implied + // by an ARIA role or markup element. + if (roleMap) { + GetLiveAttrValue(roleMap->liveAttRule, live); + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + acc, nsGkAtoms::aria_live)) { + value->ToString(live); + } + } + if (!live.IsEmpty() && roleMap && + roleMap->roleAtom != nsGkAtoms::_empty) { + role = roleMap->roleAtom; + } + } + if (acc->IsDoc()) { + break; + } + } + if (!live.IsEmpty()) { + aAttributes->SetAttribute(nsGkAtoms::containerLive, std::move(live)); + } + if (role) { + aAttributes->SetAttribute(nsGkAtoms::containerLiveRole, std::move(role)); + } + if (!relevant.IsEmpty()) { + aAttributes->SetAttribute(nsGkAtoms::containerRelevant, + std::move(relevant)); + } + if (atomic) { + aAttributes->SetAttribute(nsGkAtoms::containerAtomic, *atomic); + } + if (!busy.IsEmpty()) { + aAttributes->SetAttribute(nsGkAtoms::containerBusy, std::move(busy)); + } +} + +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 (auto* htmlElement = nsGenericHTMLElement::FromNode(element); + htmlElement && !element->HasAttr(aAtom)) { + const auto* defaults = GetARIADefaults(htmlElement); + if (!defaults) { + return false; + } + return HasDefinedARIAToken(defaults, aAtom); + } + return HasDefinedARIAToken(&element->GetAttrs(), aAtom); +} + +bool nsAccUtils::HasDefinedARIAToken(const AttrArray* aAttrs, nsAtom* aAtom) { + return aAttrs->HasAttr(aAtom) && + !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_empty, + eCaseMatters) && + !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_undefined, + eCaseMatters); +} + +nsStaticAtom* nsAccUtils::NormalizeARIAToken(const AttrArray* aAttrs, + nsAtom* aAttr) { + if (!HasDefinedARIAToken(aAttrs, aAttr)) { + return nsGkAtoms::_empty; + } + + if (aAttr == nsGkAtoms::aria_current) { + static AttrArray::AttrValuesArray tokens[] = { + nsGkAtoms::page, nsGkAtoms::step, nsGkAtoms::location_, + nsGkAtoms::date, nsGkAtoms::time, nsGkAtoms::_true, + nullptr}; + int32_t idx = + aAttrs->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; + } + + static AttrArray::AttrValuesArray tokens[] = { + nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::mixed, nullptr}; + int32_t idx = + aAttrs->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters); + if (idx >= 0) { + return tokens[idx]; + } + + return nullptr; +} + +nsStaticAtom* nsAccUtils::NormalizeARIAToken(dom::Element* aElement, + nsAtom* aAttr) { + if (auto* htmlElement = nsGenericHTMLElement::FromNode(aElement); + htmlElement && !aElement->HasAttr(aAttr)) { + const auto* defaults = GetARIADefaults(htmlElement); + if (!defaults) { + return nsGkAtoms::_empty; + } + return NormalizeARIAToken(defaults, aAttr); + } + return NormalizeARIAToken(&aElement->GetAttrs(), aAttr); +} + +Accessible* nsAccUtils::GetSelectableContainer(const Accessible* aAccessible, + uint64_t aState) { + if (!aAccessible) return nullptr; + + if (!(aState & states::SELECTABLE)) return nullptr; + MOZ_ASSERT(!aAccessible->IsDoc()); + + const Accessible* parent = aAccessible; + while ((parent = parent->Parent()) && !parent->IsSelect()) { + if (parent->IsDoc() || parent->Role() == roles::PANE) { + return nullptr; + } + } + return const_cast<Accessible*>(parent); +} + +LocalAccessible* nsAccUtils::GetSelectableContainer( + LocalAccessible* aAccessible, uint64_t aState) { + Accessible* selectable = + GetSelectableContainer(static_cast<Accessible*>(aAccessible), aState); + return selectable ? selectable->AsLocal() : nullptr; +} + +bool nsAccUtils::IsDOMAttrTrue(const LocalAccessible* aAccessible, + nsAtom* aAttr) { + dom::Element* el = aAccessible->Elm(); + return el && ARIAAttrValueIs(el, aAttr, nsGkAtoms::_true, eCaseMatters); +} + +Accessible* nsAccUtils::TableFor(Accessible* aAcc) { + if (!aAcc || + (!aAcc->IsTable() && !aAcc->IsTableRow() && !aAcc->IsTableCell())) { + return nullptr; + } + Accessible* table = aAcc; + for (; table && !table->IsTable(); table = table->Parent()) { + } + // We don't assert (table && table->IsTable()) here because + // it's possible for this tree walk to yield no table at all + // ex. because a table part has been moved in the tree + // using aria-owns. + return table; +} + +LocalAccessible* nsAccUtils::TableFor(LocalAccessible* aRow) { + Accessible* table = TableFor(static_cast<Accessible*>(aRow)); + return table ? table->AsLocal() : nullptr; +} + +HyperTextAccessible* nsAccUtils::GetTextContainer(nsINode* aNode) { + // Get text accessible containing the result node. + DocAccessible* doc = GetAccService()->GetDocAccessible(aNode->OwnerDoc()); + LocalAccessible* accessible = + doc ? doc->GetAccessibleOrContainer(aNode) : nullptr; + if (!accessible) return nullptr; + + do { + HyperTextAccessible* textAcc = accessible->AsHyperText(); + if (textAcc) return textAcc; + + accessible = accessible->LocalParent(); + } while (accessible); + + return nullptr; +} + +LayoutDeviceIntPoint nsAccUtils::ConvertToScreenCoords( + int32_t aX, int32_t aY, uint32_t aCoordinateType, Accessible* aAccessible) { + LayoutDeviceIntPoint coords(aX, aY); + + switch (aCoordinateType) { + // Regardless of coordinate type, the coords returned + // are in dev pixels. + case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE: + break; + + case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: { + coords += GetScreenCoordsForWindow(aAccessible); + 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) { + // Regardless of coordinate type, the values returned for + // aX and aY are in dev pixels. + case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE: + break; + + case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: { + LayoutDeviceIntPoint coords = GetScreenCoordsForWindow(aAccessible); + *aX -= coords.x; + *aY -= coords.y; + break; + } + + case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: { + LayoutDeviceIntPoint coords = GetScreenCoordsForParent(aAccessible); + *aX -= coords.x; + *aY -= coords.y; + break; + } + + default: + MOZ_ASSERT_UNREACHABLE("invalid coord type!"); + } +} + +LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForParent( + Accessible* aAccessible) { + if (!aAccessible) return LayoutDeviceIntPoint(); + + if (Accessible* parent = aAccessible->Parent()) { + LayoutDeviceIntRect parentBounds = parent->Bounds(); + // The rect returned from Bounds() is already in dev + // pixels, so we don't need to do any conversion here. + return parentBounds.TopLeft(); + } + + return LayoutDeviceIntPoint(); +} + +LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForWindow( + Accessible* aAccessible) { + LayoutDeviceIntPoint coords(0, 0); + a11y::LocalAccessible* localAcc = aAccessible->AsLocal(); + if (!localAcc) { + localAcc = aAccessible->AsRemote()->OuterDocOfRemoteBrowser(); + if (!localAcc) { + // This could be null if the tab is closing but the document is still + // being shut down. + return coords; + } + } + + nsCOMPtr<nsIDocShellTreeItem> treeItem( + nsCoreUtils::GetDocShellFor(localAcc->GetNode())); + 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.value, + &coords.y.value); // in device pixels + } + + return coords; +} + +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(LocalAccessible* 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++) { + LocalAccessible* child = aAccessible->LocalChildAt(childIdx); + if (child && child->IsText()) { + foundText = true; + break; + } + } + + return !foundText || aAccessible->IsHyperText(); +} +#endif + +uint32_t nsAccUtils::TextLength(Accessible* aAccessible) { + if (!aAccessible->IsText()) { + return 1; + } + + if (LocalAccessible* localAcc = aAccessible->AsLocal()) { + TextLeafAccessible* textLeaf = localAcc->AsTextLeaf(); + if (textLeaf) { + return textLeaf->Text().Length(); + } + } else if (aAccessible->IsText()) { + RemoteAccessible* remoteAcc = aAccessible->AsRemote(); + MOZ_ASSERT(remoteAcc); + return remoteAcc->GetCachedTextLength(); + } + + // For list bullets (or anything other accessible which would compute its own + // text. They don't have their own frame. + // XXX In the future, list bullets may have frame and anon content, so + // we should be able to remove this at that point + nsAutoString text; + aAccessible->AppendTextTo(text); // Get all the text + return text.Length(); +} + +bool nsAccUtils::MustPrune(Accessible* aAccessible) { + MOZ_ASSERT(aAccessible); + roles::Role role = aAccessible->Role(); + + if (role == roles::SLIDER || role == roles::PROGRESSBAR) { + // Always prune the tree for sliders and progressbars, as it doesn't make + // sense for either to have descendants. Per the ARIA spec, children of + // these elements are presentational. They also confuse 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::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::IsARIALive(const LocalAccessible* 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)) { + GetARIAAttr(ancestor->AsElement(), nsGkAtoms::aria_live, live); + } else if (role) { + GetLiveAttrValue(role->liveAttRule, live); + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + ancestor, nsGkAtoms::aria_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; +} + +Accessible* nsAccUtils::DocumentFor(Accessible* aAcc) { + if (!aAcc) { + return nullptr; + } + if (LocalAccessible* localAcc = aAcc->AsLocal()) { + return localAcc->Document(); + } + return aAcc->AsRemote()->Document(); +} + +Accessible* nsAccUtils::GetAccessibleByID(Accessible* aDoc, uint64_t aID) { + if (!aDoc) { + return nullptr; + } + if (LocalAccessible* localAcc = aDoc->AsLocal()) { + if (DocAccessible* doc = localAcc->AsDoc()) { + if (!aID) { + // GetAccessibleByUniqueID doesn't treat 0 as the document. + return aDoc; + } + return doc->GetAccessibleByUniqueID( + reinterpret_cast<void*>(static_cast<uintptr_t>(aID))); + } + } else if (DocAccessibleParent* doc = aDoc->AsRemote()->AsDoc()) { + return doc->GetAccessible(aID); + } + return nullptr; +} + +void nsAccUtils::DocumentURL(Accessible* aDoc, nsAString& aURL) { + MOZ_ASSERT(aDoc && aDoc->IsDoc()); + if (LocalAccessible* localAcc = aDoc->AsLocal()) { + return localAcc->AsDoc()->URL(aURL); + } + return aDoc->AsRemote()->AsDoc()->URL(aURL); +} + +void nsAccUtils::DocumentMimeType(Accessible* aDoc, nsAString& aMimeType) { + MOZ_ASSERT(aDoc && aDoc->IsDoc()); + if (LocalAccessible* localAcc = aDoc->AsLocal()) { + return localAcc->AsDoc()->MimeType(aMimeType); + } + return aDoc->AsRemote()->AsDoc()->MimeType(aMimeType); +} + +// ARIA Accessibility Default Accessors +const AttrArray* nsAccUtils::GetARIADefaults(dom::Element* aElement) { + auto* element = nsGenericHTMLElement::FromNode(aElement); + if (!element) { + return nullptr; + } + auto* internals = element->GetInternals(); + if (!internals) { + return nullptr; + } + return &internals->GetAttrs(); +} + +bool nsAccUtils::HasARIAAttr(dom::Element* aElement, const nsAtom* aName) { + if (aElement->HasAttr(aName)) { + return true; + } + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return false; + } + return defaults->HasAttr(aName); +} + +bool nsAccUtils::GetARIAAttr(dom::Element* aElement, const nsAtom* aName, + nsAString& aResult) { + if (aElement->GetAttr(aName, aResult)) { + return true; + } + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return false; + } + return defaults->GetAttr(aName, aResult); +} + +const nsAttrValue* nsAccUtils::GetARIAAttr(dom::Element* aElement, + const nsAtom* aName) { + if (const auto* val = aElement->GetParsedAttr(aName, kNameSpaceID_None)) { + return val; + } + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return nullptr; + } + return defaults->GetAttr(aName, kNameSpaceID_None); +} + +bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, + const nsAString& aValue, + nsCaseTreatment aCaseSensitive) { + if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) { + return true; + } + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return false; + } + return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue, + aCaseSensitive); +} + +bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, + const nsAtom* aValue, + nsCaseTreatment aCaseSensitive) { + if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) { + return true; + } + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return false; + } + return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue, + aCaseSensitive); +} + +int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement, + const nsAtom* aName, + AttrArray::AttrValuesArray* aValues, + nsCaseTreatment aCaseSensitive) { + int32_t index = aElement->FindAttrValueIn(kNameSpaceID_None, aName, aValues, + aCaseSensitive); + if (index == AttrArray::ATTR_MISSING) { + const auto* defaults = GetARIADefaults(aElement); + if (!defaults) { + return index; + } + index = defaults->FindAttrValueIn(kNameSpaceID_None, aName, aValues, + aCaseSensitive); + } + return index; +} diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h new file mode 100644 index 0000000000..ed51eef709 --- /dev/null +++ b/accessible/base/nsAccUtils.h @@ -0,0 +1,299 @@ +/* -*- 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/LocalAccessible.h" +#include "mozilla/a11y/DocManager.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: + /** + * Set group attributes ('level', 'setsize', 'posinset'). + */ + static void SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel, + int32_t aSetSize, int32_t aPosInSet); + + /** + * 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 aStartAcc Accessible to start from + */ + static void SetLiveContainerAttributes(AccAttributes* aAttributes, + Accessible* aStartAcc); + + /** + * 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); + static bool HasDefinedARIAToken(const AttrArray* aAttrs, nsAtom* aAtom); + + /** + * 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(const AttrArray* aAttrs, + nsAtom* aAttr); + 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(const Accessible* aAccessible, + uint64_t aState); + static LocalAccessible* GetSelectableContainer(LocalAccessible* aAccessible, + uint64_t aState); + + /** + * Return a text container accessible for the given node. + */ + static HyperTextAccessible* GetTextContainer(nsINode* aNode); + + static Accessible* TableFor(Accessible* aRow); + static LocalAccessible* TableFor(LocalAccessible* aRow); + + static const LocalAccessible* TableFor(const LocalAccessible* aAcc) { + return TableFor(const_cast<LocalAccessible*>(aAcc)); + } + + /** + * Return true if the DOM node of a given accessible has a given attribute + * with a value of "true". + */ + static bool IsDOMAttrTrue(const LocalAccessible* aAccessible, nsAtom* aAttr); + + /** + * Return true if the DOM node of given accessible has aria-selected="true" + * attribute. + */ + static inline bool IsARIASelected(const LocalAccessible* 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 LocalAccessible* aAccessible) { + return IsDOMAttrTrue(aAccessible, nsGkAtoms::aria_multiselectable); + } + + /** + * Converts the given coordinates to coordinates relative screen. + * + * @param aX [in] the given x coord in dev pixels + * @param aY [in] the given y coord in dev pixels + * @param aCoordinateType [in] specifies coordinates origin (refer to + * nsIAccessibleCoordinateType) + * @param aAccessible [in] the accessible if coordinates are given + * relative it. + * @return converted coordinates + */ + static LayoutDeviceIntPoint 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 in dev pixels + * @param aY [in, out] the given y coord in dev pixels + * @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 screen-relative coordinates (in dev pixels) for the parent of the + * given accessible. + * + * @param [in] aAccessible the accessible + */ + static LayoutDeviceIntPoint GetScreenCoordsForParent(Accessible* aAccessible); + + /** + * Returns coordinates in device pixels relative screen for the top level + * window. + * + * @param aAccessible the acc hosted in the window. + */ + static mozilla::LayoutDeviceIntPoint GetScreenCoordsForWindow( + mozilla::a11y::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(LocalAccessible* 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 extraState ? extraState : aState; + } + + /** + * Return true if the given accessible can't have children. Used when exposing + * to platform accessibility APIs, should the children be pruned off? + */ + static bool MustPrune(Accessible* aAccessible); + + /** + * 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 LocalAccessible* aAccessible); + + /** + * Get the document Accessible which owns a given Accessible. + * This function is needed because there is no unified base class for local + * and remote documents. + * If aAcc is null, null will be returned. + */ + static Accessible* DocumentFor(Accessible* aAcc); + + /** + * Get an Accessible in a given document by its unique id. + * An Accessible's id can be obtained using Accessible::ID. + * This function is needed because there is no unified base class for local + * and remote documents. + * If aDoc is nul, null will be returned. + */ + static Accessible* GetAccessibleByID(Accessible* aDoc, uint64_t aID); + + /** + * Get the URL for a given document. + * This function is needed because there is no unified base class for local + * and remote documents. + */ + static void DocumentURL(Accessible* aDoc, nsAString& aURL); + + /** + * Get the mime type for a given document. + * This function is needed because there is no unified base class for local + * and remote documents. + */ + static void DocumentMimeType(Accessible* aDoc, nsAString& aMimeType); + + /** + * Accessors for element attributes that are aware of CustomElement ARIA + * accessibility defaults. If the element does not have the provided + * attribute defined directly on it, we will then attempt to fetch the + * default instead. + */ + static const AttrArray* GetARIADefaults(dom::Element* aElement); + static bool HasARIAAttr(dom::Element* aElement, const nsAtom* aName); + static bool GetARIAAttr(dom::Element* aElement, const nsAtom* aName, + nsAString& aResult); + static const nsAttrValue* GetARIAAttr(dom::Element* aElement, + const nsAtom* aName); + static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, + const nsAString& aValue, + nsCaseTreatment aCaseSensitive); + static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName, + const nsAtom* aValue, + nsCaseTreatment aCaseSensitive); + static int32_t FindARIAAttrValueIn(dom::Element* aElement, + const nsAtom* aName, + AttrArray::AttrValuesArray* aValues, + nsCaseTreatment aCaseSensitive); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp new file mode 100644 index 0000000000..c31dd666ce --- /dev/null +++ b/accessible/base/nsAccessibilityService.cpp @@ -0,0 +1,1933 @@ +/* -*- 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 "ARIAGridAccessible.h" +#include "ARIAMap.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "FocusManager.h" +#include "HTMLCanvasAccessible.h" +#include "HTMLElementAccessibles.h" +#include "HTMLImageMapAccessible.h" +#include "HTMLLinkAccessible.h" +#include "HTMLListAccessible.h" +#include "HTMLSelectAccessible.h" +#include "HTMLTableAccessible.h" +#include "HyperTextAccessible.h" +#include "RootAccessible.h" +#include "nsAccUtils.h" +#include "nsArrayUtils.h" +#include "nsAttrName.h" +#include "nsDOMTokenList.h" +#include "nsCRT.h" +#include "nsEventShell.h" +#include "nsGkAtoms.h" +#include "nsIFrameInlines.h" +#include "nsServiceManagerUtils.h" +#include "nsTextFormatter.h" +#include "OuterDocAccessible.h" +#include "mozilla/a11y/Role.h" +#ifdef MOZ_ACCESSIBILITY_ATK +# include "RootAccessibleWrap.h" +#endif +#include "States.h" +#include "Statistics.h" +#include "TextLeafAccessible.h" +#include "xpcAccessibleApplication.h" + +#ifdef XP_WIN +# include "mozilla/a11y/Compatibility.h" +# include "mozilla/StaticPtr.h" +#endif + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +#include "nsExceptionHandler.h" +#include "nsImageFrame.h" +#include "nsIObserverService.h" +#include "nsMenuPopupFrame.h" +#include "nsLayoutUtils.h" +#include "nsTreeBodyFrame.h" +#include "nsTreeUtils.h" +#include "mozilla/a11y/AccTypes.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/ProfilerMarkers.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Services.h" + +#include "XULAlertAccessible.h" +#include "XULComboboxAccessible.h" +#include "XULElementAccessibles.h" +#include "XULFormControlAccessible.h" +#include "XULListboxAccessible.h" +#include "XULMenuAccessible.h" +#include "XULTabAccessible.h" +#include "XULTreeGridAccessible.h" + +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 +//////////////////////////////////////////////////////////////////////////////// + +/** + * If the element has an ARIA attribute that requires a specific Accessible + * class, create and return it. Otherwise, return null. + */ +static LocalAccessible* MaybeCreateSpecificARIAAccessible( + const nsRoleMapEntry* aRoleMapEntry, const LocalAccessible* aContext, + nsIContent* aContent, DocAccessible* aDocument) { + if (aRoleMapEntry && aRoleMapEntry->accTypes & eTableCell) { + if (aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th) && + aContext->IsHTMLTableRow()) { + // Don't use ARIAGridCellAccessible for a valid td/th because + // HTMLTableCellAccessible can provide additional info; e.g. row/col span + // from the layout engine. + return nullptr; + } + // A cell must be in a row. + const Accessible* parent = aContext; + if (parent->IsGeneric()) { + parent = parent->GetNonGenericParent(); + } + if (!parent || parent->Role() != roles::ROW) { + return nullptr; + } + // That row must be in a table, though there may be an intervening rowgroup. + parent = parent->GetNonGenericParent(); + if (!parent) { + return nullptr; + } + if (!parent->IsTable() && parent->Role() == roles::GROUPING) { + parent = parent->GetNonGenericParent(); + if (!parent) { + return nullptr; + } + } + if (parent->IsTable()) { + return new ARIAGridCellAccessible(aContent, aDocument); + } + } + return nullptr; +} + +/** + * Return true if the element has an attribute (ARIA, title, or relation) that + * requires the creation of an Accessible for the element. + */ +static bool AttributesMustBeAccessible(nsIContent* aContent, + DocAccessible* aDocument) { + 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 element must be a generic Accessible, even if it has been + * marked presentational with role="presentation", etc. MustBeAccessible causes + * an Accessible to be created as if it weren't marked presentational at all; + * e.g. <table role="presentation" tabindex="0"> will expose roles::TABLE and + * support TableAccessible. In contrast, this function causes a generic + * Accessible to be created; e.g. <table role="presentation" style="position: + * fixed;"> will expose roles::TEXT_CONTAINER and will not support + * TableAccessible. This is necessary in certain cases for the + * RemoteAccessible cache. + */ +static bool MustBeGenericAccessible(nsIContent* aContent, + DocAccessible* aDocument) { + if (aContent->IsInNativeAnonymousSubtree() || aContent->IsSVGElement()) { + // We should not force create accs for anonymous content. + // This is an issue for inputs, which have an intermediate + // container with relevant overflow styling between the input + // and its internal input content. + // We should also avoid this for SVG elements (ie. `<foreignobject>`s + // which have default overflow:hidden styling). + return false; + } + nsIFrame* frame = aContent->GetPrimaryFrame(); + MOZ_ASSERT(frame); + nsAutoCString overflow; + frame->Style()->GetComputedPropertyValue(eCSSProperty_overflow, overflow); + // If the frame has been transformed, and the content has any children, we + // should create an Accessible so that we can account for the transform when + // calculating the Accessible's bounds using the parent process cache. + // Ditto for content which is position: fixed or sticky or has overflow + // styling (auto, scroll, hidden). + // However, don't do this for XUL widgets, as this breaks XUL a11y code + // expectations in some cases. XUL widgets are only used in the parent + // process and can't be cached anyway. + return !aContent->IsXULElement() && + ((aContent->HasChildren() && frame->IsTransformed()) || + frame->IsStickyPositioned() || + (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && + nsLayoutUtils::IsReallyFixedPos(frame)) || + overflow.Equals("auto"_ns) || overflow.Equals("scroll"_ns) || + overflow.Equals("hidden"_ns)); +} + +/** + * Return true if the element must be accessible. + */ +static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) { + nsIFrame* frame = aContent->GetPrimaryFrame(); + MOZ_ASSERT(frame); + // This document might be invisible when it first loads. Therefore, we must + // check focusability irrespective of visibility here. Otherwise, we might not + // create Accessibles for some focusable elements; e.g. a span with only a + // tabindex. Elements that are invisible within this document are excluded + // earlier in CreateAccessible. + if (frame->IsFocusable(/* aWithMouse */ false, + /* aCheckVisibility */ false)) { + return true; + } + + return AttributesMustBeAccessible(aContent, aDocument); +} + +bool nsAccessibilityService::ShouldCreateImgAccessible( + mozilla::dom::Element* aElement, DocAccessible* aDocument) { + // The element must have a layout frame for us to proceed. If there is no + // frame, the image is likely hidden. + nsIFrame* frame = aElement->GetPrimaryFrame(); + if (!frame) { + return false; + } + + // If the element is not an img, and also not an embedded image via embed or + // object, then we should not create an accessible. + if (!aElement->IsHTMLElement(nsGkAtoms::img) && + ((!aElement->IsHTMLElement(nsGkAtoms::embed) && + !aElement->IsHTMLElement(nsGkAtoms::object)) || + frame->AccessibleType() != AccType::eImageType)) { + return false; + } + + nsAutoString newAltText; + const bool hasAlt = aElement->GetAttr(nsGkAtoms::alt, newAltText); + if (!hasAlt || !newAltText.IsEmpty()) { + // If there is no alt attribute, we should create an accessible. The + // author may have missed the attribute, and the AT may want to provide a + // name. If there is alt text, we should create an accessible. + return true; + } + + if (newAltText.IsEmpty() && (nsCoreUtils::HasClickListener(aElement) || + MustBeAccessible(aElement, aDocument))) { + // If there is empty alt text, but there is a click listener for this img, + // or if it otherwise must be an accessible (e.g., if it has an aria-label + // attribute), we should create an accessible. + return true; + } + + // Otherwise, no alt text means we should not create an accessible. + return false; +} + +/** + * Return true if the SVG element should be accessible + */ +static bool MustSVGElementBeAccessible(nsIContent* aContent, + DocAccessible* aDocument) { + // 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 MustBeAccessible(aContent, aDocument); +} + +/** + * Used by XULMap.h to map both menupopup and popup elements + */ +LocalAccessible* CreateMenupopupAccessible(Element* aElement, + LocalAccessible* 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()); +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible constructors + +static LocalAccessible* New_HyperText(Element* aElement, + LocalAccessible* aContext) { + return new HyperTextAccessible(aElement, aContext->Document()); +} + +template <typename AccClass> +static LocalAccessible* New_HTMLDtOrDd(Element* aElement, + LocalAccessible* 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 MarkupMapInfo sHTMLMarkupMapList[] = { +#include "HTMLMarkupMap.h" +}; + +static const MarkupMapInfo sMathMLMarkupMapList[] = { +#include "MathMLMarkupMap.h" +}; + +#undef MARKUPMAP + +#define XULMAP(atom, ...) {nsGkAtoms::atom, __VA_ARGS__}, + +#define XULMAP_TYPE(atom, new_type) \ + XULMAP( \ + atom, \ + [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { \ + return new new_type(aElement, aContext->Document()); \ + }) + +static const XULMarkupMapInfo sXULMarkupMapList[] = { +#include "XULMap.h" +}; + +#undef XULMAP_TYPE +#undef XULMAP + +#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() + : mHTMLMarkupMap(ArrayLength(sHTMLMarkupMapList)), + mMathMLMarkupMap(ArrayLength(sMathMLMarkupMapList)), + mXULMarkupMap(ArrayLength(sXULMarkupMapList)) {} + +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)); + nsIContent* content(nsIContent::FromEventTargetOrNull(target)); + if (!content || !content->IsHTMLElement()) { + continue; + } + + uint32_t changeCount; + change->GetCountOfEventListenerChangesAffectingAccessibility(&changeCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (changeCount) { + Document* ownerDoc = content->OwnerDoc(); + DocAccessible* document = GetExistingDocAccessible(ownerDoc); + + if (document) { + LocalAccessible* acc = document->GetAccessible(content); + if (!acc && (content == document->GetContent() || + content == document->DocumentNode()->GetRootElement())) { + acc = document; + } + if (!acc && content->IsElement() && + content->AsElement()->IsHTMLElement(nsGkAtoms::area)) { + // For area accessibles, we have to recreate the entire image map, + // since the image map accessible manages the tree itself. The click + // listener change may require us to update the role for the + // accessible associated with the area element. + LocalAccessible* areaAcc = + document->GetAccessibleEvenIfNotInMap(content); + if (areaAcc && areaAcc->LocalParent()) { + document->RecreateAccessible(areaAcc->LocalParent()->GetContent()); + } + } + if (!acc && nsCoreUtils::HasClickListener(content)) { + // Create an accessible for a inaccessible element having click event + // handler. + document->ContentInserted(content, content->GetNextSibling()); + } else if (acc) { + if ((acc->IsHTMLLink() && !acc->AsHTMLLink()->IsLinked()) || + (content->IsElement() && + content->AsElement()->IsHTMLElement(nsGkAtoms::a) && + !acc->IsHTMLLink())) { + // An HTML link without an href attribute should have a generic + // role, unless it has a click listener. Since we might have gained + // or lost a click listener here, recreate the accessible so that we + // can create the correct type of accessible. If it was a link, it + // may no longer be one. If it wasn't, it may become one. + document->RecreateAccessible(content); + } + + // A click listener change might mean losing or gaining an action. + document->QueueCacheUpdate(acc, CacheDomain::Actions); + } + } + } + } + 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) { + return; + } + DocAccessible* document = GetDocAccessible(documentNode); + if (!document) { + return; + } + // If the document has focus when we get this notification, ensure that + // we fire a start scrolling event. + const Accessible* focusedAcc = FocusedAccessible(); + if (focusedAcc && + (focusedAcc == document || focusedAcc->IsNonInteractive())) { + LocalAccessible* targetAcc = document->GetAccessible(aTargetNode); + if (targetAcc) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START, + targetAcc); + document->SetAnchorJump(nullptr); + } else { + // We can't find the target accessible in the document yet. Set the + // anchor jump so that we can fire the scrolling start event later. + document->SetAnchorJump(aTargetNode); + } + } else { + document->SetAnchorJump(aTargetNode); + } +} + +void nsAccessibilityService::FireAccessibleEvent(uint32_t aEvent, + LocalAccessible* aTarget) { + nsEventShell::FireEvent(aEvent, aTarget); +} + +void nsAccessibilityService::NotifyOfPossibleBoundsChange( + mozilla::PresShell* aPresShell, nsIContent* aContent) { + if (IPCAccessibilityActive()) { + DocAccessible* document = aPresShell->GetDocAccessible(); + if (document) { + // DocAccessible::GetAccessible() won't return the document if a root + // element like body is passed. + LocalAccessible* accessible = aContent == document->GetContent() + ? document + : document->GetAccessible(aContent); + if (accessible) { + document->QueueCacheUpdate(accessible, CacheDomain::Bounds); + } + } + } +} + +void nsAccessibilityService::NotifyOfComputedStyleChange( + mozilla::PresShell* aPresShell, nsIContent* aContent) { + DocAccessible* document = aPresShell->GetDocAccessible(); + if (!document) { + return; + } + + // DocAccessible::GetAccessible() won't return the document if a root + // element like body is passed. + LocalAccessible* accessible = aContent == document->GetContent() + ? document + : document->GetAccessible(aContent); + if (!accessible && aContent && aContent->HasChildren() && + !aContent->IsInNativeAnonymousSubtree()) { + // If the content has children and its frame has a transform, create an + // Accessible so that we can account for the transform when calculating + // the Accessible's bounds using the parent process cache. Ditto for + // position: fixed/sticky and content with overflow styling (hidden, auto, + // scroll) + if (const nsIFrame* frame = aContent->GetPrimaryFrame()) { + const auto& disp = *frame->StyleDisplay(); + if (disp.HasTransform(frame) || + disp.mPosition == StylePositionProperty::Fixed || + disp.mPosition == StylePositionProperty::Sticky || + disp.IsScrollableOverflow()) { + document->ContentInserted(aContent, aContent->GetNextSibling()); + } + } + } else if (accessible && IPCAccessibilityActive()) { + accessible->MaybeQueueCacheUpdateForStyleChanges(); + } +} + +void nsAccessibilityService::NotifyOfResolutionChange( + mozilla::PresShell* aPresShell, float aResolution) { + DocAccessible* document = aPresShell->GetDocAccessible(); + if (document && document->IPCDoc()) { + AutoTArray<mozilla::a11y::CacheData, 1> data; + RefPtr<AccAttributes> fields = new AccAttributes(); + fields->SetAttribute(CacheKey::Resolution, aResolution); + data.AppendElement(mozilla::a11y::CacheData(0, fields)); + document->IPCDoc()->SendCache(CacheUpdateType::Update, data); + } +} + +void nsAccessibilityService::NotifyOfDevPixelRatioChange( + mozilla::PresShell* aPresShell, int32_t aAppUnitsPerDevPixel) { + DocAccessible* document = aPresShell->GetDocAccessible(); + if (document && document->IPCDoc()) { + AutoTArray<mozilla::a11y::CacheData, 1> data; + RefPtr<AccAttributes> fields = new AccAttributes(); + fields->SetAttribute(CacheKey::AppUnitsPerDevPixel, aAppUnitsPerDevPixel); + data.AppendElement(mozilla::a11y::CacheData(0, fields)); + document->IPCDoc()->SendCache(CacheUpdateType::Update, data); + } +} + +LocalAccessible* 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; +} + +void nsAccessibilityService::NotifyOfTabPanelVisibilityChange( + PresShell* aPresShell, Element* aPanel, bool aNowVisible) { + MOZ_ASSERT(aPanel->GetParent()->IsXULElement(nsGkAtoms::tabpanels)); + + DocAccessible* document = GetDocAccessible(aPresShell); + if (!document) { + return; + } + + if (LocalAccessible* acc = document->GetAccessible(aPanel)) { + RefPtr<AccEvent> event = + new AccStateChangeEvent(acc, states::OFFSCREEN, aNowVisible); + document->FireDelayedEvent(event); + } +} + +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->GetParentNode()); + 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::ScheduleAccessibilitySubtreeUpdate( + PresShell* aPresShell, nsIContent* aContent) { + DocAccessible* document = GetDocAccessible(aPresShell); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "schedule update; doc: %p", document); + logging::Node("content node", aContent); + logging::MsgEnd(); + } +#endif + + if (document) { + document->ScheduleTreeUpdate(aContent); + } +} + +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::TableLayoutGuessMaybeChanged( + PresShell* aPresShell, nsIContent* aContent) { + if (DocAccessible* document = GetDocAccessible(aPresShell)) { + if (LocalAccessible* acc = document->GetAccessible(aContent)) { + if (LocalAccessible* table = nsAccUtils::TableFor(acc)) { + document->QueueCacheUpdate(table, CacheDomain::Table); + } + } + } +} + +void nsAccessibilityService::ComboboxOptionMaybeChanged( + PresShell* aPresShell, nsIContent* aMutatingNode) { + DocAccessible* document = GetDocAccessible(aPresShell); + if (!document) { + return; + } + + for (nsIContent* cur = aMutatingNode; cur; cur = cur->GetParent()) { + if (cur->IsHTMLElement(nsGkAtoms::option)) { + if (LocalAccessible* accessible = document->GetAccessible(cur)) { + document->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, + accessible); + break; + } + if (cur->IsHTMLElement(nsGkAtoms::select)) { + break; + } + } + } +} + +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) { + LocalAccessible* 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) { + LocalAccessible* accessible = document->GetAccessible(aContent); + if (accessible) { + document->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, + accessible); + } + } +} + +void nsAccessibilityService::UpdateImageMap(nsImageFrame* aImageFrame) { + PresShell* presShell = aImageFrame->PresShell(); + DocAccessible* document = GetDocAccessible(presShell); + if (document) { + LocalAccessible* 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) { + LocalAccessible* 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, ariaRole, 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::EXPANDABLE) { + stringStates->Add(u"expandable"_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) { + static_assert( + 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 + +LocalAccessible* nsAccessibilityService::CreateAccessible( + nsINode* aNode, LocalAccessible* 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. + nsIFrame* frame = content->GetPrimaryFrame(); + if (frame) { + // If invisible or inert, we don't create an accessible, but we don't mark + // it with aIsSubtreeHidden = true, since visibility: hidden frame allows + // visible elements in subtree, and inert elements allow non-inert + // elements. + if (!frame->StyleVisibility()->IsVisible() || frame->StyleUI()->IsInert()) { + return nullptr; + } + } else if (nsCoreUtils::CanCreateAccessibleWithoutFrame(content)) { + // display:contents element doesn't have a frame, but retains the + // semantics. All its children are unaffected. + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(content->AsElement()); + RefPtr<LocalAccessible> newAcc = MaybeCreateSpecificARIAAccessible( + roleMapEntry, aContext, content, document); + const MarkupMapInfo* markupMap = nullptr; + if (!newAcc) { + markupMap = GetMarkupMapInfoFor(content); + if (markupMap && markupMap->new_func) { + newAcc = markupMap->new_func(content->AsElement(), aContext); + } + } + + // Check whether this element has an ARIA role or attribute that requires + // us to create an Accessible. + const bool hasNonPresentationalARIARole = + roleMapEntry && !roleMapEntry->Is(nsGkAtoms::presentation) && + !roleMapEntry->Is(nsGkAtoms::none); + if (!newAcc && (hasNonPresentationalARIARole || + AttributesMustBeAccessible(content, document))) { + newAcc = new HyperTextAccessible(content, document); + } + + // If there's still no Accessible but we do have an entry in the markup + // map for this non-presentational element, create a generic + // HyperTextAccessible. + if (!newAcc && markupMap && + (!roleMapEntry || hasNonPresentationalARIARole)) { + newAcc = new HyperTextAccessible(content, document); + } + + if (newAcc) { + document->BindToDocument(newAcc, roleMapEntry); + } + return newAcc; + } else { + if (aIsSubtreeHidden) { + *aIsSubtreeHidden = true; + } + return nullptr; + } + + if (frame->IsHiddenByContentVisibilityOnAnyAncestor( + nsIFrame::IncludeContentVisibility::Hidden)) { + if (aIsSubtreeHidden) { + *aIsSubtreeHidden = true; + } + return nullptr; + } + + if (nsMenuPopupFrame* popupFrame = do_QueryFrame(frame)) { + // Hidden tooltips and panels don't create accessibles in the whole subtree. + // Showing them gets handled by RootAccessible::ProcessDOMEvent. + if (content->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) { + nsPopupState popupState = popupFrame->PopupState(); + if (popupState == ePopupHiding || popupState == ePopupInvisible || + popupState == ePopupClosed) { + if (aIsSubtreeHidden) { + *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<LocalAccessible> 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); + MOZ_ASSERT(newAcc, "Accessible not created for text node!"); + 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 HyperTextAccessible(content, document); + document->BindToDocument(newAcc, aria::GetRoleMap(content->AsElement())); + return newAcc; + } + + const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(content->AsElement()); + + if (roleMapEntry && (roleMapEntry->Is(nsGkAtoms::presentation) || + roleMapEntry->Is(nsGkAtoms::none))) { + if (MustBeAccessible(content, document)) { + // If the element is focusable, a global ARIA attribute is applied to it + // or it is referenced by an ARIA relationship, then treat + // role="presentation" on the element as if the role is not there. + roleMapEntry = nullptr; + } else if (MustBeGenericAccessible(content, document)) { + // Clear roleMapEntry so that we use the generic role specified below. + // Otherwise, we'd expose roles::NOTHING as specified for presentation in + // ARIAMap. + roleMapEntry = nullptr; + newAcc = new EnumRoleHyperTextAccessible<roles::TEXT_CONTAINER>(content, + document); + } else { + return nullptr; + } + } + + // We should always use OuterDocAccessible for OuterDocs, even if there's a + // specific ARIA class we would otherwise use. + if (!newAcc && frame->AccessibleType() != eOuterDocType) { + newAcc = MaybeCreateSpecificARIAAccessible(roleMapEntry, aContext, content, + document); + } + + if (!newAcc && content->IsHTMLElement()) { // HTML accessibles + // Prefer to use markup to decide if and what kind of accessible to + // create, + const MarkupMapInfo* 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); + } + + // 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()) { + if (content->IsXULElement(nsGkAtoms::panel)) { + // We filter here instead of in the XUL map because + // if we filter there and return null, we still end up + // creating a generic accessible at the end of this function. + // Doing the filtering here ensures we never create accessibles + // for panels whose popups aren't visible. + nsMenuPopupFrame* popupFrame = do_QueryFrame(frame); + if (!popupFrame) { + return nullptr; + } + + nsPopupState popupState = popupFrame->PopupState(); + if (popupState == ePopupHiding || popupState == ePopupInvisible || + popupState == ePopupClosed) { + return nullptr; + } + } + + // 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); + } + + // Any XUL/flex 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(); + // FIXME(emilio): Why only these frame types? + if (frameType == LayoutFrameType::FlexContainer || + frameType == LayoutFrameType::Scroll) { + newAcc = new XULTabpanelAccessible(content, document); + } + } + } + + if (!newAcc) { + if (content->IsSVGElement()) { + if (content->IsSVGGeometryElement() || + content->IsSVGElement(nsGkAtoms::image)) { + // Shape elements: rect, circle, ellipse, line, path, polygon, + // and polyline. 'use' and 'text' graphic elements require + // special support. + if (MustSVGElementBeAccessible(content, document)) { + newAcc = new EnumRoleAccessible<roles::GRAPHIC>(content, document); + } + } else if (content->IsSVGElement(nsGkAtoms::text)) { + newAcc = new HyperTextAccessible(content->AsElement(), document); + } else if (content->IsSVGElement(nsGkAtoms::svg)) { + // An <svg> element could contain <foreignObject>, which contains HTML + // but does not normally create its own Accessible. This means that the + // <svg> Accessible could have TextLeafAccessible children, so it must + // be a HyperTextAccessible. + newAcc = + new EnumRoleHyperTextAccessible<roles::DIAGRAM>(content, document); + } else if (content->IsSVGElement(nsGkAtoms::g) && + MustSVGElementBeAccessible(content, document)) { + // <g> can also contain <foreignObject>. + newAcc = + new EnumRoleHyperTextAccessible<roles::GROUPING>(content, document); + } else if (content->IsSVGElement(nsGkAtoms::a)) { + newAcc = new HTMLLinkAccessible(content, document); + } + + } else if (content->IsMathMLElement()) { + const MarkupMapInfo* markupMap = + mMathMLMarkupMap.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); + } + } else if (content->IsGeneratedContentContainerForMarker()) { + if (aContext->IsHTMLListItem()) { + newAcc = new HTMLListBulletAccessible(content, document); + } + if (aIsSubtreeHidden) { + *aIsSubtreeHidden = true; + } + } + } + + // 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() || content->IsMathMLElement() || + content->IsSVGElement(nsGkAtoms::foreignObject)) { + // Interesting container which may have selectable text and/or embedded + // objects. + newAcc = new HyperTextAccessible(content, document); + } else { // XUL, other SVG, etc. + // Interesting generic non-HTML container + newAcc = new AccessibleWrap(content, document); + } + } else if (!newAcc && MustBeGenericAccessible(content, document)) { + newAcc = new EnumRoleHyperTextAccessible<roles::TEXT_CONTAINER>(content, + document); + } + + if (newAcc) { + document->BindToDocument(newAcc, roleMapEntry); + } + return newAcc; +} + +#if defined(ANDROID) +# include "mozilla/Monitor.h" +# include "mozilla/Maybe.h" + +static Maybe<Monitor> sAndroidMonitor; + +mozilla::Monitor& nsAccessibilityService::GetAndroidMonitor() { + if (!sAndroidMonitor.isSome()) { + sAndroidMonitor.emplace("nsAccessibility::sAndroidMonitor"); + } + + return *sAndroidMonitor; +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// nsAccessibilityService private + +bool nsAccessibilityService::Init() { + AUTO_PROFILER_MARKER_TEXT("nsAccessibilityService::Init", A11Y, {}, ""_ns); + // DO NOT ADD CODE ABOVE HERE: THIS CODE IS MEASURING TIMINGS. + + // 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.InsertOrUpdate(sHTMLMarkupMapList[i].tag, + &sHTMLMarkupMapList[i]); + } + for (const auto& info : sMathMLMarkupMapList) { + mMathMLMarkupMap.InsertOrUpdate(info.tag, &info); + } + + for (uint32_t i = 0; i < ArrayLength(sXULMarkupMapList); i++) { + mXULMarkupMap.InsertOrUpdate(sXULMarkupMapList[i].tag, + &sXULMarkupMapList[i]); + } + +#ifdef A11Y_LOG + logging::CheckEnv(); +#endif + + gAccessibilityService = this; + NS_ADDREF(gAccessibilityService); // will release in Shutdown() + + if (XRE_IsParentProcess()) { + gApplicationAccessible = new ApplicationAccessibleWrap(); + } else { + gApplicationAccessible = new ApplicationAccessible(); + } + + NS_ADDREF(gApplicationAccessible); // will release in Shutdown() + gApplicationAccessible->Init(); + + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::Accessibility, + "Active"_ns); + + // 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(); + + if (XRE_IsParentProcess()) PlatformShutdown(); + + gApplicationAccessible->Shutdown(); + NS_RELEASE(gApplicationAccessible); + gApplicationAccessible = nullptr; + + NS_IF_RELEASE(gXPCApplicationAccessible); + gXPCApplicationAccessible = nullptr; + +#if defined(ANDROID) + // Don't allow the service to shut down while an a11y request is being handled + // in the UI thread, as the request may depend on state from the service. + MonitorAutoLock mal(GetAndroidMonitor()); +#endif + NS_RELEASE(gAccessibilityService); + gAccessibilityService = nullptr; + + if (observerService) { + static const char16_t kShutdownIndicator[] = {'0', 0}; + observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown", + kShutdownIndicator); + } +} + +already_AddRefed<LocalAccessible> +nsAccessibilityService::CreateAccessibleByFrameType(nsIFrame* aFrame, + nsIContent* aContent, + LocalAccessible* aContext) { + DocAccessible* document = aContext->Document(); + + RefPtr<LocalAccessible> 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 HyperTextAccessible(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: + case eHTMLTableCellType: + // We handle markup and ARIA tables elsewhere. If we reach here, this is + // a CSS table part. Just create a generic text container. + newAcc = new HyperTextAccessible(aContent, document); + break; + case eHTMLTableRowType: + // This is a CSS table row. Don't expose it at all. + 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 HyperTextAccessible(aContent, document); + } + break; + } + case eImageType: + if (aContent->IsElement() && + ShouldCreateImgAccessible(aContent->AsElement(), document)) { + newAcc = new ImageAccessible(aContent, document); + } + break; + case eOuterDocType: + newAcc = new OuterDocAccessible(aContent, document); + break; + case eTextLeafType: + newAcc = new TextLeafAccessible(aContent, document); + break; + default: + MOZ_ASSERT(false); + break; + } + + return newAcc.forget(); +} + +void nsAccessibilityService::MarkupAttributes( + Accessible* aAcc, AccAttributes* aAttributes) const { + const mozilla::a11y::MarkupMapInfo* markupMap = GetMarkupMapInfoFor(aAcc); + if (!markupMap) return; + + dom::Element* el = aAcc->IsLocal() ? aAcc->AsLocal()->Elm() : nullptr; + for (uint32_t i = 0; i < ArrayLength(markupMap->attrs); i++) { + const MarkupAttrInfo* info = markupMap->attrs + i; + if (!info->name) break; + + if (info->DOMAttrName) { + if (!el) { + // XXX Expose DOM attributes for cached RemoteAccessibles. + continue; + } + if (info->DOMAttrValue) { + if (el->AttrValueIs(kNameSpaceID_None, info->DOMAttrName, + info->DOMAttrValue, eCaseMatters)) { + aAttributes->SetAttribute(info->name, info->DOMAttrValue); + } + continue; + } + + nsString value; + el->GetAttr(info->DOMAttrName, value); + + if (!value.IsEmpty()) { + aAttributes->SetAttribute(info->name, std::move(value)); + } + + continue; + } + + aAttributes->SetAttribute(info->name, info->value); + } +} + +LocalAccessible* 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( + LocalAccessible* 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()); +} + +const mozilla::a11y::MarkupMapInfo* nsAccessibilityService::GetMarkupMapInfoFor( + Accessible* aAcc) const { + if (LocalAccessible* localAcc = aAcc->AsLocal()) { + return localAcc->HasOwnContent() + ? GetMarkupMapInfoFor(localAcc->GetContent()) + : nullptr; + } + // XXX For now, we assume all RemoteAccessibles are HTML elements. This + // isn't strictly correct, but as far as current callers are concerned, + // this doesn't matter. If that changes in future, we could expose the + // element type via AccGenericType. + return mHTMLMarkupMap.Get(aAcc->TagName()); +} + +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, + "LocalAccessible 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..0b3f172f89 --- /dev/null +++ b/accessible/base/nsAccessibilityService.h @@ -0,0 +1,492 @@ +/* -*- 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 "nsAtomHashKeys.h" +#include "nsIContent.h" +#include "nsIObserver.h" +#include "nsIAccessibleEvent.h" +#include "nsIEventListenerService.h" +#include "nsXULAppAPI.h" +#include "xpcAccessibilityService.h" + +class nsImageFrame; +class nsIArray; +class nsITreeView; + +namespace mozilla { + +class PresShell; +class Monitor; +namespace dom { +class DOMStringList; +class Element; +} // namespace dom + +namespace a11y { + +class AccAttributes; +class Accessible; +class ApplicationAccessible; +class xpcAccessibleApplication; + +/** + * Return focus manager. + */ +FocusManager* FocusMgr(); + +/** + * Return selection manager. + */ +SelectionManager* SelectionMgr(); + +/** + * Returns the application accessible. + */ +ApplicationAccessible* ApplicationAcc(); +xpcAccessibleApplication* XPCApplicationAcc(); + +typedef LocalAccessible*(New_Accessible)(mozilla::dom::Element* aElement, + LocalAccessible* aContext); + +// These fields are not `nsStaticAtom* const` because MSVC doesn't like it. +struct MarkupAttrInfo { + nsStaticAtom* name; + nsStaticAtom* value; + + nsStaticAtom* DOMAttrName; + nsStaticAtom* DOMAttrValue; +}; + +struct MarkupMapInfo { + nsStaticAtom* const tag; + New_Accessible* new_func; + a11y::role role; + MarkupAttrInfo attrs[4]; +}; + +struct XULMarkupMapInfo { + nsStaticAtom* const tag; + New_Accessible* new_func; +}; + +/** + * 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::LocalAccessible LocalAccessible; + typedef mozilla::a11y::DocAccessible DocAccessible; + + // nsIListenerChangeListener + NS_IMETHOD ListenersChanged(nsIArray* aEventChanges) override; + + protected: + ~nsAccessibilityService(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOBSERVER + + LocalAccessible* GetRootDocumentAccessible(mozilla::PresShell* aPresShell, + bool aCanCreate); + + /** + * Adds/remove ATK root accessible for gtk+ native window to/from children + * of the application accessible. + */ + LocalAccessible* AddNativeRootAccessible(void* aAtkAccessible); + void RemoveNativeRootAccessible(LocalAccessible* 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 new content is + * inserted. + */ + void ContentRangeInserted(mozilla::PresShell* aPresShell, + nsIContent* aStartChild, nsIContent* aEndChild); + + /** + * Triggers a re-evaluation of the a11y tree of aContent after the next + * refresh. This is important because whether we create accessibles may + * depend on the frame tree / style. + */ + void ScheduleAccessibilitySubtreeUpdate(mozilla::PresShell* aPresShell, + nsIContent* aStartChild); + + /** + * Notification used to update the accessible tree when content is removed. + */ + void ContentRemoved(mozilla::PresShell* aPresShell, nsIContent* aChild); + + /** + * Notification used to invalidate the isLayoutTable cache. + */ + void TableLayoutGuessMaybeChanged(mozilla::PresShell* aPresShell, + nsIContent* aContent); + + /** + * Notifies when a combobox <option> text or label changes. + */ + void ComboboxOptionMaybeChanged(mozilla::PresShell*, + nsIContent* aMutatingNode); + + 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 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, LocalAccessible* aTarget); + + void NotifyOfPossibleBoundsChange(mozilla::PresShell* aPresShell, + nsIContent* aContent); + + void NotifyOfComputedStyleChange(mozilla::PresShell* aPresShell, + nsIContent* aContent); + + void NotifyOfTabPanelVisibilityChange(mozilla::PresShell* aPresShell, + mozilla::dom::Element* aPanel, + bool aVisible); + + void NotifyOfResolutionChange(mozilla::PresShell* aPresShell, + float aResolution); + + void NotifyOfDevPixelRatioChange(mozilla::PresShell* aPresShell, + int32_t aAppUnitsPerDevPixel); + + // nsAccessibiltiyService + + /** + * Return true if accessibility service has been shutdown. + */ + static bool IsShutdown() { return gConsumers == 0; }; + + /** + * Return true if there should be an image accessible for the given element. + */ + static bool ShouldCreateImgAccessible(mozilla::dom::Element* aElement, + DocAccessible* aDocument); + + /** + * 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 + */ + LocalAccessible* CreateAccessible(nsINode* aNode, LocalAccessible* aContext, + bool* aIsSubtreeHidden = nullptr); + + mozilla::a11y::role MarkupRole(const nsIContent* aContent) const { + const mozilla::a11y::MarkupMapInfo* markupMap = + GetMarkupMapInfoFor(aContent); + 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. This can be + * called with either an nsIContent or an Accessible. + */ + template <typename T> + nsStaticAtom* MarkupAttribute(T aSource, nsStaticAtom* aAtom) const { + const mozilla::a11y::MarkupMapInfo* markupMap = + GetMarkupMapInfoFor(aSource); + 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(mozilla::a11y::Accessible* aAcc, + mozilla::a11y::AccAttributes* 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, + }; + +#if defined(ANDROID) + static mozilla::Monitor& GetAndroidMonitor(); +#endif + + 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<LocalAccessible> CreateAccessibleByFrameType( + nsIFrame* aFrame, nsIContent* aContent, LocalAccessible* 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; + + // Can be weak because all atoms are known static + using MarkupMap = nsTHashMap<nsAtom*, const mozilla::a11y::MarkupMapInfo*>; + MarkupMap mHTMLMarkupMap; + MarkupMap mMathMLMarkupMap; + + const mozilla::a11y::MarkupMapInfo* GetMarkupMapInfoFor( + const nsIContent* aContent) const { + if (aContent->IsHTMLElement()) { + return mHTMLMarkupMap.Get(aContent->NodeInfo()->NameAtom()); + } + if (aContent->IsMathMLElement()) { + return mMathMLMarkupMap.Get(aContent->NodeInfo()->NameAtom()); + } + // This function can be called by MarkupAttribute, etc. which might in turn + // be called on a XUL, SVG, etc. element. For example, this can happen + // with nsAccUtils::SetLiveContainerAttributes. + return nullptr; + } + + const mozilla::a11y::MarkupMapInfo* GetMarkupMapInfoFor( + mozilla::a11y::Accessible* aAcc) const; + + nsTHashMap<nsAtom*, const mozilla::a11y::XULMarkupMapInfo*> mXULMarkupMap; + + 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 + "focus", // EVENT_FOCUS + "state change", // EVENT_STATE_CHANGE + "name changed", // EVENT_NAME_CHANGE + "description change", // EVENT_DESCRIPTION_CHANGE + "value change", // EVENT_VALUE_CHANGE + "selection", // EVENT_SELECTION + "selection add", // EVENT_SELECTION_ADD + "selection remove", // EVENT_SELECTION_REMOVE + "selection within", // EVENT_SELECTION_WITHIN + "alert", // EVENT_ALERT + "menu start", // EVENT_MENU_START + "menu end", // EVENT_MENU_END + "menupopup start", // EVENT_MENUPOPUP_START + "menupopup end", // EVENT_MENUPOPUP_END + "dragdrop start", // EVENT_DRAGDROP_START + "scrolling start", // EVENT_SCROLLING_START + "scrolling end", // EVENT_SCROLLING_END + "document load complete", // EVENT_DOCUMENT_LOAD_COMPLETE + "document reload", // EVENT_DOCUMENT_RELOAD + "document load stopped", // EVENT_DOCUMENT_LOAD_STOPPED + "text attribute changed", // EVENT_TEXT_ATTRIBUTE_CHANGED + "text caret moved", // EVENT_TEXT_CARET_MOVED + "text inserted", // EVENT_TEXT_INSERTED + "text removed", // EVENT_TEXT_REMOVED + "text selection changed", // EVENT_TEXT_SELECTION_CHANGED + "window activate", // EVENT_WINDOW_ACTIVATE + "window deactivate", // EVENT_WINDOW_DEACTIVATE + "window maximize", // EVENT_WINDOW_MAXIMIZE + "window minimize", // EVENT_WINDOW_MINIMIZE + "window restore", // EVENT_WINDOW_RESTORE + "object attribute changed", // EVENT_OBJECT_ATTRIBUTE_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 + "inner reorder", // EVENT_INNER_REORDER +}; + +#endif diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp new file mode 100644 index 0000000000..80739bb401 --- /dev/null +++ b/accessible/base/nsCoreUtils.cpp @@ -0,0 +1,622 @@ +/* -*- 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 "nsAttrValue.h" +#include "nsIAccessibleTypes.h" + +#include "mozilla/dom/Document.h" +#include "nsAccUtils.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 "nsIContentInlines.h" +#include "nsTreeColumns.h" +#include "mozilla/dom/DocumentInlines.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; + +using mozilla::a11y::nsAccUtils; + +//////////////////////////////////////////////////////////////////////////////// +// 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.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); + + // 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 LayoutDeviceIntPoint& aPoint) { + nsIScrollableFrame* scrollableFrame = do_QueryFrame(aScrollableFrame); + if (!scrollableFrame) return; + + nsPoint point = LayoutDeviceIntPoint::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 = WhereToScroll::Start; + whenY = WhenToScroll::Always; + whereX = WhereToScroll::Start; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT: + whereY = WhereToScroll::End; + whenY = WhenToScroll::Always; + whereX = WhereToScroll::End; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE: + whereY = WhereToScroll::Start; + whenY = WhenToScroll::Always; + whereX = WhereToScroll::Nearest; + whenX = WhenToScroll::IfNotFullyVisible; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_EDGE: + whereY = WhereToScroll::End; + whenY = WhenToScroll::Always; + whereX = WhereToScroll::Nearest; + whenX = WhenToScroll::IfNotFullyVisible; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_LEFT_EDGE: + whereY = WhereToScroll::Nearest; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = WhereToScroll::Start; + whenX = WhenToScroll::Always; + break; + case nsIAccessibleScrollType::SCROLL_TYPE_RIGHT_EDGE: + whereY = WhereToScroll::Nearest; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = WhereToScroll::End; + whenX = WhenToScroll::Always; + break; + default: + whereY = WhereToScroll::Center; + whenY = WhenToScroll::IfNotFullyVisible; + whereX = WhereToScroll::Center; + whenX = WhenToScroll::IfNotFullyVisible; + } + *aVertical = ScrollAxis(whereY, whenY); + *aHorizontal = ScrollAxis(whereX, whenX); +} + +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::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(nsGkAtoms::id, aID); +} + +bool nsCoreUtils::GetUIntAttr(nsIContent* aContent, nsAtom* aAttr, + int32_t* aUInt) { + if (!aContent->IsElement()) { + return false; + } + return GetUIntAttrValue(nsAccUtils::GetARIAAttr(aContent->AsElement(), aAttr), + aUInt); +} + +bool nsCoreUtils::GetUIntAttrValue(const nsAttrValue* aVal, int32_t* aUInt) { + if (!aVal) { + return false; + } + nsAutoString value; + aVal->ToString(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(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(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) { + auto* element = Element::FromNodeOrNull(aContent); + return element && element->IsDisplayContents(); +} + +bool nsCoreUtils::CanCreateAccessibleWithoutFrame(nsIContent* aContent) { + auto* element = Element::FromNodeOrNull(aContent); + if (!element) { + return false; + } + if (!element->HasServoData() || Servo_Element_IsDisplayNone(element)) { + // Out of the flat tree or in a display: none subtree. + return false; + } + + // If we aren't display: contents or option/optgroup we can't create an + // accessible without frame. Our select combobox code relies on the latter. + if (!element->IsDisplayContents() && + !element->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup)) { + return false; + } + + // Even if we're display: contents or optgroups, we might not be able to + // create an accessible if we're in a content-visibility: hidden subtree. + // + // To check that, find the closest ancestor element with a frame. + for (nsINode* ancestor = element->GetFlattenedTreeParentNode(); + ancestor && ancestor->IsContent(); + ancestor = ancestor->GetFlattenedTreeParentNode()) { + if (nsIFrame* f = ancestor->AsContent()->GetPrimaryFrame()) { + if (f->HidesContent(nsIFrame::IncludeContentVisibility::Hidden) || + f->IsHiddenByContentVisibilityOnAnyAncestor( + nsIFrame::IncludeContentVisibility::Hidden)) { + return false; + } + break; + } + } + + return true; +} + +bool nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors( + const Document* aDocument) { + const Document* parent = aDocument; + do { + if (!parent->IsVisible()) { + return false; + } + } while ((parent = parent->GetInProcessParentDocument())); + return true; +} diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h new file mode 100644 index 0000000000..2c3e7330ff --- /dev/null +++ b/accessible/base/nsCoreUtils.h @@ -0,0 +1,329 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsCoreUtils_h_ +#define nsCoreUtils_h_ + +#include "AttrArray.h" +#include "mozilla/EventForwards.h" +#include "nsCaseTreatment.h" +#include "nsIAccessibleEvent.h" +#include "nsIContent.h" +#include "mozilla/FlushType.h" +#include "mozilla/PresShellForwards.h" + +#include "nsPoint.h" +#include "nsTArray.h" +#include "Units.h" + +class nsAttrValue; +class nsGenericHTMLElement; +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 (in dev pixels) + */ + static void ScrollFrameToPoint(nsIFrame* aScrollableFrame, nsIFrame* aFrame, + const mozilla::LayoutDeviceIntPoint& 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); + + /** + * 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 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); + static bool GetUIntAttrValue(const nsAttrValue* aVal, 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); + static bool CanCreateAccessibleWithoutFrame(nsIContent* aContent); + + /** + * Return whether the document and all its in-process ancestors are visible in + * the sense of pageshow / hide. + */ + static bool IsDocumentVisibleConsideringInProcessAncestors( + const Document* aDocument); +}; + +#endif diff --git a/accessible/base/nsEventShell.cpp b/accessible/base/nsEventShell.cpp new file mode 100644 index 0000000000..fcdc954919 --- /dev/null +++ b/accessible/base/nsEventShell.cpp @@ -0,0 +1,81 @@ +/* -*- 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 "AccAttributes.h" + +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/DOMStringList.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// nsEventShell +//////////////////////////////////////////////////////////////////////////////// + +void nsEventShell::FireEvent(AccEvent* aEvent) { + if (!aEvent || aEvent->mEventRule == AccEvent::eDoNotEmit) return; + + LocalAccessible* 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()); + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_STATE_CHANGE) { + AccStateChangeEvent* event = downcast_accEvent(aEvent); + RefPtr<dom::DOMStringList> stringStates = + GetAccService()->GetStringStates(event->GetState()); + nsAutoString state; + stringStates->Item(0, state); + logging::MsgEntry("state: %s = %s", NS_ConvertUTF16toUTF8(state).get(), + event->IsStateEnabled() ? "true" : "false"); + } + logging::AccessibleInfo("target", aEvent->GetAccessible()); + logging::MsgEnd(); + } +#endif + + accessible->HandleAccEvent(aEvent); + aEvent->mEventRule = AccEvent::eDoNotEmit; + + sEventTargetNode = nullptr; +} + +void nsEventShell::FireEvent(uint32_t aEventType, LocalAccessible* aAccessible, + EIsFromUserInput aIsFromUserInput) { + NS_ENSURE_TRUE_VOID(aAccessible); + + RefPtr<AccEvent> event = + new AccEvent(aEventType, aAccessible, aIsFromUserInput); + + FireEvent(event); +} + +void nsEventShell::GetEventAttributes(nsINode* aNode, + AccAttributes* aAttributes) { + if (aNode != sEventTargetNode) return; + + aAttributes->SetAttribute(nsGkAtoms::eventFromInput, sEventFromUserInput); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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..ff2e062750 --- /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; +} + +/** + * 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::LocalAccessible* aAccessible, + mozilla::a11y::EIsFromUserInput aIsFromUserInput = + mozilla::a11y::eAutoDetect); + + /** + * Fire state change event. + */ + static void FireEvent(mozilla::a11y::LocalAccessible* 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, + mozilla::a11y::AccAttributes* 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..d95229c1dc --- /dev/null +++ b/accessible/base/nsTextEquivUtils.cpp @@ -0,0 +1,360 @@ +/* -*- 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 "LocalAccessible-inl.h" +#include "AccIterator.h" +#include "nsCoreUtils.h" +#include "mozilla/dom/ChildIterator.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 LocalAccessible* 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 LocalAccessible* 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 += ' '; + + if (refContent->IsHTMLElement(nsGkAtoms::slot)) printf("jtd idref slot\n"); + nsresult rv = + AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult nsTextEquivUtils::AppendTextEquivFromContent( + const LocalAccessible* aInitiatorAcc, nsIContent* aContent, + nsAString* aString) { + // Prevent recursion which can cause infinite loops. + if (sInitiatorAcc) return NS_OK; + + sInitiatorAcc = aInitiatorAcc; + + nsresult rv = NS_ERROR_FAILURE; + if (LocalAccessible* accessible = + aInitiatorAcc->Document()->GetAccessible(aContent)) { + rv = AppendFromAccessible(accessible, aString); + } else { + // The given content is invisible or otherwise inaccessible, so use the DOM + // subtree. + rv = AppendFromDOMNode(aContent, aString); + } + + sInitiatorAcc = nullptr; + return rv; +} + +nsresult nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent* aContent, + nsAString* aString) { + if (aContent->IsText()) { + 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); + } + } + + 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; +} + +nsresult nsTextEquivUtils::AppendFromDOMChildren(nsIContent* aContent, + nsAString* aString) { + auto iter = + dom::AllChildrenIterator(aContent, nsIContent::eAllChildren, true); + while (nsIContent* childContent = iter.GetNextChild()) { + nsresult rv = AppendFromDOMNode(childContent, aString); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// 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->ChildAt(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? + bool isHTMLBlock = false; + if (aAccessible->IsLocal() && aAccessible->AsLocal()->IsContent()) { + nsIContent* content = aAccessible->AsLocal()->GetContent(); + nsresult rv = AppendTextEquivFromTextContent(content, aString); + if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return rv; + if (!content->IsText()) { + nsIFrame* frame = content->GetPrimaryFrame(); + if (frame) { + // If this is 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(' ')); + } + } + } + } + } + + 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); + if (isHTMLBlock) { + aString->Append(char16_t(' ')); + } + return NS_OK; + } + + if (!isEmptyTextEquiv && isHTMLBlock) { + aString->Append(char16_t(' ')); + } + 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; + + for (Accessible* next = aAccessible->NextSibling(); next; + next = next->NextSibling()) { + if (!IsWhitespaceLeaf(next)) { + for (Accessible* prev = aAccessible->PrevSibling(); prev; + prev = prev->PrevSibling()) { + if (!IsWhitespaceLeaf(prev)) { + aAccessible->Value(text); + + return AppendString(aString, text) ? NS_OK + : NS_OK_NO_NAME_CLAUSE_HANDLED; + } + } + } + } + + return NS_OK_NO_NAME_CLAUSE_HANDLED; +} + +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->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style)) { + // The text within these elements is never meant for users. + return NS_OK; + } + + if (aContent->IsXULElement()) { + nsAutoString textEquivalent; + if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL)) { + aContent->AsElement()->GetAttr(nsGkAtoms::value, textEquivalent); + } else { + aContent->AsElement()->GetAttr(nsGkAtoms::label, textEquivalent); + } + + if (textEquivalent.IsEmpty()) { + aContent->AsElement()->GetAttr(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, ariaRole, 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, LocalAccessible::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; +} + +bool nsTextEquivUtils::IsWhitespaceLeaf(Accessible* aAccessible) { + if (!aAccessible || !aAccessible->IsTextLeaf()) { + return false; + } + + nsAutoString name; + aAccessible->Name(name); + return nsCoreUtils::IsWhitespaceString(name); +} diff --git a/accessible/base/nsTextEquivUtils.h b/accessible/base/nsTextEquivUtils.h new file mode 100644 index 0000000000..525727b102 --- /dev/null +++ b/accessible/base/nsTextEquivUtils.h @@ -0,0 +1,182 @@ +/* -*- 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 "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/Role.h" + +class nsIContent; + +namespace mozilla { +namespace a11y { +class LocalAccessible; +} +} // namespace mozilla + +/** + * 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::LocalAccessible LocalAccessible; + 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 LocalAccessible* 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 LocalAccessible* 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 LocalAccessible* 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); + + /** + * Iterates DOM children and calculates text equivalent from each child node. + * Then, appends found text to the given string. + * + * @param aContent [in] the node to fetch DOM children from + * @param aString [in, out] the string + */ + static nsresult AppendFromDOMChildren(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); + + /** + * 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); + + /** + * Returns true if a given accessible is a text leaf containing only + * whitespace. + */ + static bool IsWhitespaceLeaf(Accessible* aAccessible); +}; + +#endif |