summaryrefslogtreecommitdiffstats
path: root/accessible/base
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/base')
-rw-r--r--accessible/base/ARIAMap.cpp1687
-rw-r--r--accessible/base/ARIAMap.h335
-rw-r--r--accessible/base/ARIAStateMap.cpp334
-rw-r--r--accessible/base/ARIAStateMap.h66
-rw-r--r--accessible/base/AccAttributes.cpp270
-rw-r--r--accessible/base/AccAttributes.h293
-rw-r--r--accessible/base/AccEvent.cpp256
-rw-r--r--accessible/base/AccEvent.h562
-rw-r--r--accessible/base/AccGroupInfo.cpp397
-rw-r--r--accessible/base/AccGroupInfo.h101
-rw-r--r--accessible/base/AccIterator.cpp360
-rw-r--r--accessible/base/AccIterator.h328
-rw-r--r--accessible/base/AccTypes.h98
-rw-r--r--accessible/base/Asserts.cpp29
-rw-r--r--accessible/base/CacheConstants.h255
-rw-r--r--accessible/base/CachedTableAccessible.cpp429
-rw-r--r--accessible/base/CachedTableAccessible.h294
-rw-r--r--accessible/base/DocManager.cpp562
-rw-r--r--accessible/base/DocManager.h193
-rw-r--r--accessible/base/EmbeddedObjCollector.cpp62
-rw-r--r--accessible/base/EmbeddedObjCollector.h68
-rw-r--r--accessible/base/EventQueue.cpp436
-rw-r--r--accessible/base/EventQueue.h83
-rw-r--r--accessible/base/EventTree.cpp99
-rw-r--r--accessible/base/EventTree.h61
-rw-r--r--accessible/base/Filters.cpp25
-rw-r--r--accessible/base/Filters.h36
-rw-r--r--accessible/base/FocusManager.cpp462
-rw-r--r--accessible/base/FocusManager.h169
-rw-r--r--accessible/base/HTMLMarkupMap.h444
-rw-r--r--accessible/base/IDSet.h129
-rw-r--r--accessible/base/Logging.cpp992
-rw-r--r--accessible/base/Logging.h236
-rw-r--r--accessible/base/MathMLMarkupMap.h113
-rw-r--r--accessible/base/NotificationController.cpp1107
-rw-r--r--accessible/base/NotificationController.h396
-rw-r--r--accessible/base/Pivot.cpp331
-rw-r--r--accessible/base/Pivot.h141
-rw-r--r--accessible/base/Platform.h136
-rw-r--r--accessible/base/Relation.h105
-rw-r--r--accessible/base/RelationTypeGen.py41
-rw-r--r--accessible/base/RelationTypeMap.h90
-rw-r--r--accessible/base/RoleHGen.py42
-rw-r--r--accessible/base/RoleMap.h1546
-rw-r--r--accessible/base/SelectionManager.cpp246
-rw-r--r--accessible/base/SelectionManager.h141
-rw-r--r--accessible/base/States.h305
-rw-r--r--accessible/base/Statistics.h42
-rw-r--r--accessible/base/StyleInfo.cpp50
-rw-r--r--accessible/base/StyleInfo.h36
-rw-r--r--accessible/base/TextAttrs.cpp816
-rw-r--r--accessible/base/TextAttrs.h440
-rw-r--r--accessible/base/TextLeafRange.cpp1990
-rw-r--r--accessible/base/TextLeafRange.h360
-rw-r--r--accessible/base/TextRange-inl.h25
-rw-r--r--accessible/base/TextRange.cpp376
-rw-r--r--accessible/base/TextRange.h164
-rw-r--r--accessible/base/TextUpdater.cpp215
-rw-r--r--accessible/base/TextUpdater.h95
-rw-r--r--accessible/base/TreeWalker.cpp348
-rw-r--r--accessible/base/TreeWalker.h142
-rw-r--r--accessible/base/XULMap.h115
-rw-r--r--accessible/base/moz.build122
-rw-r--r--accessible/base/nsAccCache.h24
-rw-r--r--accessible/base/nsAccUtils.cpp626
-rw-r--r--accessible/base/nsAccUtils.h299
-rw-r--r--accessible/base/nsAccessibilityService.cpp1933
-rw-r--r--accessible/base/nsAccessibilityService.h492
-rw-r--r--accessible/base/nsCoreUtils.cpp622
-rw-r--r--accessible/base/nsCoreUtils.h329
-rw-r--r--accessible/base/nsEventShell.cpp81
-rw-r--r--accessible/base/nsEventShell.h66
-rw-r--r--accessible/base/nsTextEquivUtils.cpp360
-rw-r--r--accessible/base/nsTextEquivUtils.h182
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